astro-dev-edit 0.11.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/LICENSE +21 -0
- package/README.md +125 -0
- package/package.json +52 -0
- package/src/client/admin-bar.ts +622 -0
- package/src/client/api.ts +370 -0
- package/src/client/classify-cache.ts +61 -0
- package/src/client/css-inspect.ts +345 -0
- package/src/client/editors/asset-picker.ts +155 -0
- package/src/client/editors/body-editor.ts +419 -0
- package/src/client/editors/collections-panel.ts +1532 -0
- package/src/client/editors/copy-panel.ts +73 -0
- package/src/client/editors/drawer.ts +95 -0
- package/src/client/editors/entry.ts +433 -0
- package/src/client/editors/expression.ts +77 -0
- package/src/client/editors/fields.ts +309 -0
- package/src/client/editors/image.ts +268 -0
- package/src/client/editors/markup-insert.ts +73 -0
- package/src/client/editors/markup.ts +125 -0
- package/src/client/editors/media-grid.ts +326 -0
- package/src/client/editors/media-modal.ts +588 -0
- package/src/client/editors/notice.ts +160 -0
- package/src/client/editors/peek.ts +135 -0
- package/src/client/editors/settings-panel.ts +457 -0
- package/src/client/editors/source-popup.ts +166 -0
- package/src/client/editors/text.ts +105 -0
- package/src/client/editors/unsplash-pane.ts +317 -0
- package/src/client/element-context.ts +308 -0
- package/src/client/features.ts +81 -0
- package/src/client/focus.ts +166 -0
- package/src/client/group.ts +186 -0
- package/src/client/highlight.ts +146 -0
- package/src/client/hover.ts +485 -0
- package/src/client/icons.ts +160 -0
- package/src/client/markdown.ts +319 -0
- package/src/client/overlay.ts +466 -0
- package/src/client/page-source.ts +143 -0
- package/src/client/router.ts +198 -0
- package/src/client/shadow.ts +111 -0
- package/src/client/source-map.ts +150 -0
- package/src/client/state.ts +153 -0
- package/src/client/styles.ts +3485 -0
- package/src/client/tree-model.ts +45 -0
- package/src/client/tree.ts +366 -0
- package/src/client/ui.ts +987 -0
- package/src/client/unsplash-search.ts +250 -0
- package/src/index.ts +299 -0
- package/src/patcher/astro.ts +792 -0
- package/src/patcher/content-config.ts +1035 -0
- package/src/patcher/dotenv.ts +121 -0
- package/src/patcher/expression-trace.ts +326 -0
- package/src/patcher/frontmatter.ts +249 -0
- package/src/patcher/registry.ts +11 -0
- package/src/patcher/types.ts +32 -0
- package/src/server/annotate.ts +173 -0
- package/src/server/assets.ts +167 -0
- package/src/server/collection-entries.ts +91 -0
- package/src/server/content-config.ts +210 -0
- package/src/server/editor.ts +15 -0
- package/src/server/entry-detect.ts +110 -0
- package/src/server/entry-resolve-routes.ts +218 -0
- package/src/server/entry-routes.ts +304 -0
- package/src/server/inspect-locate.ts +81 -0
- package/src/server/inspect-routes.ts +94 -0
- package/src/server/middleware.ts +480 -0
- package/src/server/options.ts +778 -0
- package/src/server/page-source-routes.ts +71 -0
- package/src/server/paths.ts +219 -0
- package/src/server/private-files.ts +116 -0
- package/src/server/route-manifest.ts +200 -0
- package/src/server/router.ts +94 -0
- package/src/server/schema-introspect.ts +233 -0
- package/src/server/schema-routes.ts +808 -0
- package/src/server/settings-routes.ts +246 -0
- package/src/server/settings.ts +382 -0
- package/src/server/text-writes.ts +105 -0
- package/src/server/unsplash-routes.ts +515 -0
- package/src/server/zod-adapt.ts +239 -0
- package/src/shared/asset-path.ts +132 -0
- package/src/shared/protocol.ts +935 -0
- package/src/shared/slug.ts +17 -0
- package/src/shared/unsplash.ts +51 -0
|
@@ -0,0 +1,3485 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The overlay's one stylesheet, adopted by the shadow root (shadow.ts).
|
|
3
|
+
*
|
|
4
|
+
* Two jobs:
|
|
5
|
+
*
|
|
6
|
+
* 1. **Close the inheritance leak.** Selectors do not cross a shadow boundary,
|
|
7
|
+
* but inheritance does — a host page's `body { font-family }` and `color`
|
|
8
|
+
* reach every node in here unless stopped. `:host { all: initial }` stops
|
|
9
|
+
* them, once, and the declarations after it establish what the overlay
|
|
10
|
+
* inherits instead.
|
|
11
|
+
*
|
|
12
|
+
* 2. **Publish the tokens as custom properties.** Custom properties *do* cross
|
|
13
|
+
* the boundary, which makes them the overlay's theming API: a user writes
|
|
14
|
+
* `astro-dev-edit { --atx-card: #101014 }` in their own CSS and it lands.
|
|
15
|
+
* See docs/STYLING.md.
|
|
16
|
+
*
|
|
17
|
+
* **The variable block is generated from `ui.ts`'s tokens, never hand-written.**
|
|
18
|
+
* `tests/contrast.test.ts` imports `COLOR` and is the only thing standing
|
|
19
|
+
* between a token and an illegible panel; a hand-maintained CSS copy could
|
|
20
|
+
* drift and the test would still pass while the UI was wrong. Naming rule:
|
|
21
|
+
* camelCase to kebab, prefixed — `mutedFg` → `--atx-muted-fg`.
|
|
22
|
+
*
|
|
23
|
+
* `overlayCss()` is a **function**, not a constant, because ui.ts → shadow.ts →
|
|
24
|
+
* styles.ts → ui.ts is an import cycle: reading `COLOR` at module-evaluation
|
|
25
|
+
* time would hit the temporal dead zone. Reading it at first mount does not.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { BAR_CHIP, BAR_CHIP_HOVER, CHECKER, COLOR, FONT, RADIUS, Z, hexToRgba, lift } from './ui.ts';
|
|
29
|
+
|
|
30
|
+
const kebab = (k: string): string => k.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`);
|
|
31
|
+
|
|
32
|
+
function vars(prefix: string, tokens: Record<string, string>): string {
|
|
33
|
+
return Object.entries(tokens)
|
|
34
|
+
.map(([k, v]) => ` --atx-${prefix}${kebab(k)}: ${v};`)
|
|
35
|
+
.join('\n');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** The admin bar's height. Mirrors BAR_H in admin-bar.ts, which reports the
|
|
39
|
+
* same number to setChromeInset so docked surfaces keep clear of it. */
|
|
40
|
+
const BAR_H = 36;
|
|
41
|
+
|
|
42
|
+
/** The checkbox tick, as a mask rather than a glyph: the shape comes from here
|
|
43
|
+
* and the colour from whatever `background` the rule sets, so the tick tracks
|
|
44
|
+
* `primaryFg` without a second copy of the path living in CSS. Lucide's
|
|
45
|
+
* `check`, at the stroke weight a 12px box needs to stay crisp. */
|
|
46
|
+
const CHECK_MASK =
|
|
47
|
+
"url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='3.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20 6 9 17l-5-5'/%3E%3C/svg%3E\") center / contain no-repeat";
|
|
48
|
+
|
|
49
|
+
/** Lucide's `chevron-down` as a `background-image`, in a colour baked in at
|
|
50
|
+
* stylesheet-build time. A select cannot carry a pseudo-element and a mask
|
|
51
|
+
* would clip the control itself, so the arrow has to be a background — which
|
|
52
|
+
* means it cannot read a custom property, hence the argument. */
|
|
53
|
+
function chevronUrl(color: string): string {
|
|
54
|
+
const svg =
|
|
55
|
+
`<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' ` +
|
|
56
|
+
`stroke='${color}' stroke-width='2' stroke-linecap='round' ` +
|
|
57
|
+
`stroke-linejoin='round'><path d='m6 9.5 6 6 6-6'/></svg>`;
|
|
58
|
+
return `url("data:image/svg+xml,${encodeURIComponent(svg)}")`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function overlayCss(): string {
|
|
62
|
+
return `
|
|
63
|
+
:host {
|
|
64
|
+
/* The guard. Everything the host page could inherit into the overlay stops
|
|
65
|
+
here; the declarations below are what the overlay inherits instead. */
|
|
66
|
+
all: initial;
|
|
67
|
+
|
|
68
|
+
/* No box at all — the host cannot shift the page's layout, and every
|
|
69
|
+
absolutely-positioned child still resolves against the initial containing
|
|
70
|
+
block, exactly as it did as a child of <body>. */
|
|
71
|
+
display: contents;
|
|
72
|
+
|
|
73
|
+
${vars('', COLOR)}
|
|
74
|
+
${vars('radius-', RADIUS)}
|
|
75
|
+
${vars('font-', FONT)}
|
|
76
|
+
|
|
77
|
+
font: 400 14px/1.45 var(--atx-font-ui);
|
|
78
|
+
color: var(--atx-foreground);
|
|
79
|
+
cursor: auto;
|
|
80
|
+
/* Light type on a near-black ground blooms under subpixel rendering, which
|
|
81
|
+
is what reads as fringed or crunchy. Grayscale antialiasing on both
|
|
82
|
+
engines, plus kerning and the discretionary pairs, is the whole of it;
|
|
83
|
+
everything inside the root inherits all four. */
|
|
84
|
+
-webkit-font-smoothing: antialiased;
|
|
85
|
+
-moz-osx-font-smoothing: grayscale;
|
|
86
|
+
text-rendering: optimizeLegibility;
|
|
87
|
+
font-kerning: normal;
|
|
88
|
+
text-align: left;
|
|
89
|
+
direction: ltr;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/* Form controls carry their own UA font reset, so the smoothing and kerning
|
|
93
|
+
set on :host stop at their boundary and they render a shade crunchier than
|
|
94
|
+
the prose beside them. Restating the four here is the only way across. */
|
|
95
|
+
input,
|
|
96
|
+
textarea,
|
|
97
|
+
select,
|
|
98
|
+
button {
|
|
99
|
+
-webkit-font-smoothing: inherit;
|
|
100
|
+
-moz-osx-font-smoothing: inherit;
|
|
101
|
+
text-rendering: inherit;
|
|
102
|
+
font-kerning: inherit;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/* ── Shells ────────────────────────────────────────────────────────────────
|
|
106
|
+
The panel, drawer, backdrop and tab strip built by ui.ts. What stays inline
|
|
107
|
+
at those call sites is only what cannot be known here: the computed stacking
|
|
108
|
+
layer (Z_MODAL + n), a caller's width or height override, and the sized-panel
|
|
109
|
+
branch, which is a [data-sized] flag rather than an inline display so the
|
|
110
|
+
layout lives in one place. */
|
|
111
|
+
|
|
112
|
+
.atx-panel {
|
|
113
|
+
position: fixed;
|
|
114
|
+
left: 50%;
|
|
115
|
+
top: 50%;
|
|
116
|
+
transform: translate(-50%, -50%);
|
|
117
|
+
width: min(420px, 92vw);
|
|
118
|
+
box-sizing: border-box;
|
|
119
|
+
overflow: hidden;
|
|
120
|
+
border: 1px solid transparent;
|
|
121
|
+
border-radius: var(--atx-radius-xl);
|
|
122
|
+
background: var(--atx-card);
|
|
123
|
+
color: var(--atx-foreground);
|
|
124
|
+
/* A ring separates the surfaces; the shadow only lifts the panel off the
|
|
125
|
+
page behind it. Two layers rather than one border, because this floats
|
|
126
|
+
over content the overlay does not control and a hairline alone would
|
|
127
|
+
disappear against a light page. */
|
|
128
|
+
box-shadow: 0 0 0 1px var(--atx-border), 0 8px 28px rgba(0, 0, 0, 0.4);
|
|
129
|
+
font: 400 14px var(--atx-font-ui);
|
|
130
|
+
/* Edit mode sets a crosshair cursor on the whole page; our UI is not a
|
|
131
|
+
click-to-edit surface, so restore normal per-element cursors. */
|
|
132
|
+
cursor: auto;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/* A sized panel lays its title/body/foot out as a column so the body is the
|
|
136
|
+
only part that grows; the default auto-height panel is unaffected. */
|
|
137
|
+
.atx-panel[data-sized] {
|
|
138
|
+
display: flex;
|
|
139
|
+
flex-direction: column;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.atx-panel-title {
|
|
143
|
+
display: flex;
|
|
144
|
+
align-items: center;
|
|
145
|
+
gap: 8px;
|
|
146
|
+
padding: 16px 20px;
|
|
147
|
+
border-bottom: 1px solid var(--atx-border);
|
|
148
|
+
font: 500 14px var(--atx-font-ui);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.atx-panel-heading {
|
|
152
|
+
flex: 1;
|
|
153
|
+
min-width: 0;
|
|
154
|
+
overflow: hidden;
|
|
155
|
+
text-overflow: ellipsis;
|
|
156
|
+
white-space: nowrap;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.atx-panel-body {
|
|
160
|
+
padding: 20px;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/* A body whose content brings its own edges — the source peek's code pane
|
|
164
|
+
runs to the panel's sides. */
|
|
165
|
+
.atx-panel-body[data-flush] {
|
|
166
|
+
padding: 0;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/* In a sized panel the body absorbs the leftover height and scrolls;
|
|
170
|
+
'min-height: 0' is what lets a flex child actually shrink to do that. */
|
|
171
|
+
.atx-panel[data-sized] .atx-panel-body {
|
|
172
|
+
flex: 1 1 auto;
|
|
173
|
+
min-height: 0;
|
|
174
|
+
overflow-y: auto;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/* Same band as the drawer's: a rule and a ground one step down from the panel,
|
|
178
|
+
so the row that completes the modal is plainly not part of its content. */
|
|
179
|
+
.atx-panel-foot {
|
|
180
|
+
display: flex;
|
|
181
|
+
align-items: center;
|
|
182
|
+
justify-content: flex-end;
|
|
183
|
+
gap: 8px;
|
|
184
|
+
padding: 16px 20px;
|
|
185
|
+
border-top: 1px solid var(--atx-border);
|
|
186
|
+
border-radius: 0 0 var(--atx-radius-xl) var(--atx-radius-xl);
|
|
187
|
+
background: var(--atx-background);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
.atx-panel[data-sized] .atx-panel-foot {
|
|
191
|
+
flex: 0 0 auto;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
.atx-drawer {
|
|
195
|
+
position: fixed;
|
|
196
|
+
right: 0;
|
|
197
|
+
top: 0;
|
|
198
|
+
display: flex;
|
|
199
|
+
flex-direction: column;
|
|
200
|
+
/* Half the screen, but never narrower than the classic 440px drawer and
|
|
201
|
+
never wider than the viewport allows on small screens. */
|
|
202
|
+
height: 100vh;
|
|
203
|
+
width: min(max(440px, 50vw), 94vw);
|
|
204
|
+
box-sizing: border-box;
|
|
205
|
+
border-left: 1px solid var(--atx-border);
|
|
206
|
+
/* The scrolling body is the canvas, one step darker than the cards on it --
|
|
207
|
+
which is the whole reason a card reads as a bounded thing here. The
|
|
208
|
+
title bar and the footer band paint themselves back up to the card
|
|
209
|
+
surface, so the drawer reads as chrome around content. */
|
|
210
|
+
background: var(--atx-background);
|
|
211
|
+
color: var(--atx-foreground);
|
|
212
|
+
box-shadow: -4px 0 16px rgba(0, 0, 0, 0.32);
|
|
213
|
+
font: 400 14px var(--atx-font-ui);
|
|
214
|
+
cursor: auto;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/* The drawer's title bar is a card header: a grid so the name and the line
|
|
218
|
+
under it both stop short of the corner action instead of running under it,
|
|
219
|
+
and so the action stays pinned to the top-right however tall the text
|
|
220
|
+
column grows. Two columns only when there is something to put in the
|
|
221
|
+
second -- [data-action], set by group.ts::cardHead. */
|
|
222
|
+
.atx-drawer-title {
|
|
223
|
+
display: grid;
|
|
224
|
+
grid-template-columns: 1fr;
|
|
225
|
+
align-items: start;
|
|
226
|
+
gap: 2px 12px;
|
|
227
|
+
flex: 0 0 auto;
|
|
228
|
+
padding: 16px 20px;
|
|
229
|
+
border-bottom: 1px solid var(--atx-border);
|
|
230
|
+
background: var(--atx-card);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/* The second column appears only once a caller has actually put something in
|
|
234
|
+
the action slot. :has() rather than a flag the caller has to remember to
|
|
235
|
+
set: the slot is filled after the drawer is built, and a header that
|
|
236
|
+
reserved the column unconditionally would pull the title short of an edge
|
|
237
|
+
with nothing at it. */
|
|
238
|
+
.atx-drawer-title:has([data-actions] > *) {
|
|
239
|
+
grid-template-columns: 1fr auto;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/* The name and anything qualifying it. min-width: 0 so the title inside can
|
|
243
|
+
still ellipsize -- a grid item defaults to min-content, which would let the
|
|
244
|
+
name push the header wider instead of shortening. */
|
|
245
|
+
.atx-drawer-name {
|
|
246
|
+
display: flex;
|
|
247
|
+
align-items: center;
|
|
248
|
+
gap: 8px;
|
|
249
|
+
min-width: 0;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/* 16px/500 is the only type at that size in a drawer, which is what makes it
|
|
253
|
+
read as the name of the thing rather than as the first of the labels. */
|
|
254
|
+
.atx-drawer-title-text {
|
|
255
|
+
min-width: 0;
|
|
256
|
+
overflow: hidden;
|
|
257
|
+
text-overflow: ellipsis;
|
|
258
|
+
white-space: nowrap;
|
|
259
|
+
color: var(--atx-foreground);
|
|
260
|
+
font: 500 16px/1.4 var(--atx-font-ui);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/* The line under the name: which file, which collection, what the caveat is.
|
|
264
|
+
Muted ink at body size -- separated from the title by weight and colour,
|
|
265
|
+
not by shrinking it into small print. */
|
|
266
|
+
.atx-drawer-subtitle {
|
|
267
|
+
min-width: 0;
|
|
268
|
+
overflow: hidden;
|
|
269
|
+
text-overflow: ellipsis;
|
|
270
|
+
white-space: nowrap;
|
|
271
|
+
color: var(--atx-muted-fg);
|
|
272
|
+
font: 400 14px/1.4 var(--atx-font-ui);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/* Spans both rows and sits at the top of them, so a one-line and a two-line
|
|
276
|
+
header put their action in the same place. */
|
|
277
|
+
.atx-drawer-actions {
|
|
278
|
+
display: flex;
|
|
279
|
+
gap: 6px;
|
|
280
|
+
grid-column: 2;
|
|
281
|
+
grid-row: 1 / span 2;
|
|
282
|
+
align-self: start;
|
|
283
|
+
justify-self: end;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/* A stack of cards, not a form. The gap is the only spacing the body owns;
|
|
287
|
+
each card brings its own padding, which is what keeps a card's edge a real
|
|
288
|
+
boundary rather than a line drawn through continuous content. */
|
|
289
|
+
.atx-drawer-body {
|
|
290
|
+
display: flex;
|
|
291
|
+
flex-direction: column;
|
|
292
|
+
gap: 16px;
|
|
293
|
+
flex: 1 1 auto;
|
|
294
|
+
padding: 16px;
|
|
295
|
+
overflow-y: auto;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/* A band, not a strip of the body: a rule above it and a ground half a step
|
|
299
|
+
off the drawer's own are what separate "the form" from "what you do with
|
|
300
|
+
it", and are the reason the footer survives being scrolled up to. */
|
|
301
|
+
.atx-drawer-foot {
|
|
302
|
+
display: flex;
|
|
303
|
+
align-items: center;
|
|
304
|
+
justify-content: flex-end;
|
|
305
|
+
gap: 8px;
|
|
306
|
+
flex: 0 0 auto;
|
|
307
|
+
padding: 16px 20px;
|
|
308
|
+
border-top: 1px solid var(--atx-border);
|
|
309
|
+
background: var(--atx-card);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/* The destructive action goes to the far end, away from the pair the user is
|
|
313
|
+
actually choosing between. Cancel and Save are one decision; Delete is a
|
|
314
|
+
different one, and putting all three in a row invites the wrong click. */
|
|
315
|
+
.atx-drawer-foot > .atx-btn-destructive:first-child,
|
|
316
|
+
.atx-panel-foot > .atx-btn-destructive:first-child {
|
|
317
|
+
margin-right: auto;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
.atx-backdrop {
|
|
321
|
+
position: fixed;
|
|
322
|
+
inset: 0;
|
|
323
|
+
background: rgba(0, 0, 0, 0.4);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/* == Grouping =============================================================
|
|
327
|
+
group.ts. The structural vocabulary every panel builds its content from:
|
|
328
|
+
a card is a bounded concern, a field group is a run of controls answering
|
|
329
|
+
one question, an item is one row of a list, and a separator is the smaller
|
|
330
|
+
claim a card makes about how far apart two things are.
|
|
331
|
+
|
|
332
|
+
These carry no colour of their own beyond a surface and a rule. What they
|
|
333
|
+
own is *distance* -- which is the part a flat run of controls gets wrong
|
|
334
|
+
however well each control is styled. */
|
|
335
|
+
|
|
336
|
+
/* The edge is a 1px inset shadow rather than a border, so a card can sit
|
|
337
|
+
flush inside a padded body without its own border-box changing the width
|
|
338
|
+
its children get. Same trick shadcn uses, same reason. */
|
|
339
|
+
.atx-card {
|
|
340
|
+
display: flex;
|
|
341
|
+
flex-direction: column;
|
|
342
|
+
gap: 16px;
|
|
343
|
+
padding: 16px 0 0;
|
|
344
|
+
border-radius: var(--atx-radius-xl);
|
|
345
|
+
background: var(--atx-card);
|
|
346
|
+
box-shadow: 0 0 0 1px var(--atx-border);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/* Horizontal padding lives on the header, the body and the footer rather
|
|
350
|
+
than on the card, which is what lets the footer band and a full-bleed
|
|
351
|
+
list run edge to edge inside it. */
|
|
352
|
+
.atx-card-head {
|
|
353
|
+
display: grid;
|
|
354
|
+
grid-template-columns: 1fr;
|
|
355
|
+
align-items: start;
|
|
356
|
+
gap: 2px 12px;
|
|
357
|
+
padding: 0 16px;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
.atx-card-head[data-action] {
|
|
361
|
+
grid-template-columns: 1fr auto;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
.atx-card-title {
|
|
365
|
+
min-width: 0;
|
|
366
|
+
color: var(--atx-foreground);
|
|
367
|
+
font: 500 16px/1.4 var(--atx-font-ui);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
.atx-card-desc {
|
|
371
|
+
min-width: 0;
|
|
372
|
+
color: var(--atx-muted-fg);
|
|
373
|
+
font: 400 14px/1.45 var(--atx-font-ui);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/* Spans both rows and pins to the top-right, so a card with a description and
|
|
377
|
+
one without put their action in the same place. */
|
|
378
|
+
.atx-card-action {
|
|
379
|
+
grid-column: 2;
|
|
380
|
+
grid-row: 1 / span 2;
|
|
381
|
+
align-self: start;
|
|
382
|
+
justify-self: end;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
.atx-card-body {
|
|
386
|
+
padding: 0 16px;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/* The card's own footer band. Matches the drawer's for the same reason: the
|
|
390
|
+
action that completes a thing sits below a rule, on a ground half a step
|
|
391
|
+
off the surface it completes. */
|
|
392
|
+
.atx-card-foot {
|
|
393
|
+
display: flex;
|
|
394
|
+
align-items: center;
|
|
395
|
+
gap: 8px;
|
|
396
|
+
padding: 16px;
|
|
397
|
+
border-top: 1px solid var(--atx-border);
|
|
398
|
+
border-radius: 0 0 var(--atx-radius-xl) var(--atx-radius-xl);
|
|
399
|
+
background: var(--atx-background);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
.atx-card-foot > .atx-btn-destructive:first-child {
|
|
403
|
+
margin-right: auto;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/* Trailing padding on the last slot, so the card's bottom matches the 16px it
|
|
407
|
+
opens with. A footer brings its own, hence the reset. */
|
|
408
|
+
.atx-card > :last-child {
|
|
409
|
+
padding-bottom: 16px;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
.atx-card > .atx-card-foot:last-child {
|
|
413
|
+
padding-bottom: 16px;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/* One question's worth of controls. 20px between fields is wide enough that
|
|
417
|
+
the label of the next one is plainly a new field rather than a second line
|
|
418
|
+
of the last one's help text -- which is the whole job. */
|
|
419
|
+
.atx-field-group {
|
|
420
|
+
display: flex;
|
|
421
|
+
flex-direction: column;
|
|
422
|
+
gap: 20px;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
.atx-sep {
|
|
426
|
+
height: 1px;
|
|
427
|
+
flex: 0 0 auto;
|
|
428
|
+
background: var(--atx-border);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/* A footnote to the title beside it, so it never takes the slack and never
|
|
432
|
+
wraps: whatever it qualifies is the thing allowed to ellipsize.
|
|
433
|
+
|
|
434
|
+
Borrowed from the outline button -- the same 1px input border over the same
|
|
435
|
+
faint fill -- because a chip that reads as chrome sits quietly beside a name,
|
|
436
|
+
where a tone-coloured outline shouted the qualifier louder than the thing it
|
|
437
|
+
qualifies. Muted ink, not foreground: this is text being read, not pressed.
|
|
438
|
+
Radius stays "full", which is the ladder's rule for a badge and the one thing
|
|
439
|
+
keeping it from being mistaken for the button it is coloured like. */
|
|
440
|
+
.atx-badge {
|
|
441
|
+
flex: 0 0 auto;
|
|
442
|
+
padding: 2px 8px;
|
|
443
|
+
border: 1px solid var(--atx-input);
|
|
444
|
+
border-radius: var(--atx-radius-full);
|
|
445
|
+
background: var(--atx-control-bg);
|
|
446
|
+
color: var(--atx-muted-fg);
|
|
447
|
+
font: 500 12px/1.4 var(--atx-font-ui);
|
|
448
|
+
white-space: nowrap;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/* The one tone that departs from chrome: something the reader has to act on. */
|
|
452
|
+
.atx-badge[data-tone='warn'] {
|
|
453
|
+
border-color: var(--atx-warning);
|
|
454
|
+
color: var(--atx-warning);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/* == Items ================================================================
|
|
458
|
+
One row: an optional 16px media slot, a text column that takes the slack,
|
|
459
|
+
and actions pinned right. Every list in the overlay is built from this
|
|
460
|
+
rather than each panel growing its own row. */
|
|
461
|
+
|
|
462
|
+
/* A list, not a stack of tiles. Rows sit flush and a 1px rule separates them:
|
|
463
|
+
the row already carries a transparent 1px border, so the rule is a colour
|
|
464
|
+
change and costs no layout shift. Gaps would make each row read as its own
|
|
465
|
+
object, which is exactly what a list is not. */
|
|
466
|
+
.atx-item-group {
|
|
467
|
+
display: flex;
|
|
468
|
+
flex-direction: column;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
.atx-item-group > .atx-item + .atx-item {
|
|
472
|
+
border-top-color: var(--atx-border);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/* Edge to edge in the card body holding it, so the rules reach the card's own
|
|
476
|
+
edges and a hover fill covers the whole row rather than an inset tile. The
|
|
477
|
+
16px it cancels is .atx-card-body's, put back on each row. */
|
|
478
|
+
.atx-item-group[data-bleed] {
|
|
479
|
+
margin: 0 -16px;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
.atx-item-group[data-bleed] > .atx-item {
|
|
483
|
+
padding-right: 16px;
|
|
484
|
+
padding-left: 16px;
|
|
485
|
+
border-right: 0;
|
|
486
|
+
border-left: 0;
|
|
487
|
+
border-radius: 0;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/* .atx-card carries no bottom padding, so a bleed list that ends the card
|
|
491
|
+
also ends the card: its last row has to pick up the card's own corners or a
|
|
492
|
+
square hover fill spills past them. Selector says exactly that condition --
|
|
493
|
+
the card can't be given overflow:hidden instead, which would clip a row's
|
|
494
|
+
focus ring. */
|
|
495
|
+
.atx-card-body:last-child > .atx-item-group[data-bleed]:last-child > .atx-item:last-child {
|
|
496
|
+
border-bottom-right-radius: var(--atx-radius-xl);
|
|
497
|
+
border-bottom-left-radius: var(--atx-radius-xl);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
.atx-item {
|
|
501
|
+
display: flex;
|
|
502
|
+
align-items: center;
|
|
503
|
+
gap: 10px;
|
|
504
|
+
padding: 10px 12px;
|
|
505
|
+
border: 1px solid transparent;
|
|
506
|
+
border-radius: var(--atx-radius-lg);
|
|
507
|
+
color: var(--atx-foreground);
|
|
508
|
+
font: 400 14px var(--atx-font-ui);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/* A filled tile, for a row that stands alone. A row inside a list stays
|
|
512
|
+
transparent: there, the list is the object and painting every row makes it
|
|
513
|
+
read as a stack of separate cards. */
|
|
514
|
+
.atx-item[data-variant='muted'] {
|
|
515
|
+
border-color: var(--atx-border);
|
|
516
|
+
background: var(--atx-elevated);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
.atx-item-media {
|
|
520
|
+
display: flex;
|
|
521
|
+
align-items: center;
|
|
522
|
+
justify-content: center;
|
|
523
|
+
width: 16px;
|
|
524
|
+
height: 16px;
|
|
525
|
+
flex: 0 0 auto;
|
|
526
|
+
color: var(--atx-muted-fg);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
.atx-item-content {
|
|
530
|
+
display: flex;
|
|
531
|
+
flex-direction: column;
|
|
532
|
+
gap: 2px;
|
|
533
|
+
min-width: 0;
|
|
534
|
+
flex: 1 1 0;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
.atx-item-title {
|
|
538
|
+
overflow: hidden;
|
|
539
|
+
text-overflow: ellipsis;
|
|
540
|
+
white-space: nowrap;
|
|
541
|
+
font: 500 14px/1.4 var(--atx-font-ui);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/* 12px here, unlike a field's help text at 14px. A description under a row
|
|
545
|
+
title is an attribute of the row -- a count, a path, a date -- not prose
|
|
546
|
+
the user has to read, and at 14px it competes with the title. */
|
|
547
|
+
.atx-item-desc {
|
|
548
|
+
overflow: hidden;
|
|
549
|
+
text-overflow: ellipsis;
|
|
550
|
+
white-space: nowrap;
|
|
551
|
+
color: var(--atx-muted-fg);
|
|
552
|
+
font: 500 12px/1.5 var(--atx-font-ui);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
.atx-item-actions {
|
|
556
|
+
display: flex;
|
|
557
|
+
align-items: center;
|
|
558
|
+
gap: 8px;
|
|
559
|
+
flex: 0 0 auto;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/* Tabs are keyed off their ARIA roles, not their classes: buildTabs names
|
|
563
|
+
those per caller (atx-<prefix>-tab), so there is no single class to match,
|
|
564
|
+
and the roles are already there and already correct. Selected state reads
|
|
565
|
+
aria-selected for the same reason it is set at all — one source of truth
|
|
566
|
+
for "this tab is active", instead of a class shadowing an attribute. */
|
|
567
|
+
/* A segmented control: the strip is the recessed track, and the selected tab
|
|
568
|
+
is a raised card sitting in it. The alternative — a filled accent on the
|
|
569
|
+
selected tab — reads as a call to action, which a tab is not. */
|
|
570
|
+
[role='tablist'] {
|
|
571
|
+
display: inline-flex;
|
|
572
|
+
align-items: center;
|
|
573
|
+
align-self: flex-start;
|
|
574
|
+
gap: 2px;
|
|
575
|
+
width: fit-content;
|
|
576
|
+
max-width: 100%;
|
|
577
|
+
flex: 0 0 auto;
|
|
578
|
+
padding: 3px;
|
|
579
|
+
border-radius: var(--atx-radius-lg);
|
|
580
|
+
background: var(--atx-input-bg);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
[role='tab'] {
|
|
584
|
+
height: 26px;
|
|
585
|
+
padding: 0 12px;
|
|
586
|
+
border: 1px solid transparent;
|
|
587
|
+
border-radius: var(--atx-radius-md);
|
|
588
|
+
background: transparent;
|
|
589
|
+
color: var(--atx-muted-fg);
|
|
590
|
+
font: 500 13px var(--atx-font-ui);
|
|
591
|
+
white-space: nowrap;
|
|
592
|
+
outline: none;
|
|
593
|
+
cursor: pointer;
|
|
594
|
+
transition: background 150ms, color 150ms;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
[role='tab']:hover {
|
|
598
|
+
color: var(--atx-foreground);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
[role='tab'][aria-selected='true'] {
|
|
602
|
+
background: var(--atx-accent);
|
|
603
|
+
color: var(--atx-foreground);
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
[data-tabhost] {
|
|
607
|
+
flex: 1 1 auto;
|
|
608
|
+
min-width: 0;
|
|
609
|
+
min-height: 0;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
/* == Controls ==============================================================
|
|
613
|
+
Buttons, form controls, the toast, the save veil and the pill button. Three
|
|
614
|
+
of these are keyed off a data attribute rather than a class, because the
|
|
615
|
+
class is chosen by the caller and there is no shared one to match:
|
|
616
|
+
[data-input] for anything built by inputEl(), [data-pill] for pillButton's
|
|
617
|
+
translucent chrome, [data-dimmed] for a button switched off through
|
|
618
|
+
setButtonEnabled. */
|
|
619
|
+
|
|
620
|
+
/* Every button in the overlay, keyed off the five variant classes. A bare
|
|
621
|
+
.atx-btn rule cannot be the base: four leaves borrow the atx-btn hook
|
|
622
|
+
without a variant and carry their own box, and handing them this padding
|
|
623
|
+
and radius would resize them.
|
|
624
|
+
|
|
625
|
+
32px, and on the same radius rung as a form control, so a button beside a
|
|
626
|
+
field lines up and reads as part of the same object rather than as a
|
|
627
|
+
different kind of thing parked next to it. What separates the two is fill
|
|
628
|
+
and weight, not shape. */
|
|
629
|
+
.atx-btn-default,
|
|
630
|
+
.atx-btn-secondary,
|
|
631
|
+
.atx-btn-outline,
|
|
632
|
+
.atx-btn-ghost,
|
|
633
|
+
.atx-btn-destructive {
|
|
634
|
+
display: inline-flex;
|
|
635
|
+
align-items: center;
|
|
636
|
+
justify-content: center;
|
|
637
|
+
gap: 6px;
|
|
638
|
+
height: 32px;
|
|
639
|
+
padding: 0 10px;
|
|
640
|
+
box-sizing: border-box;
|
|
641
|
+
border-radius: var(--atx-radius-lg);
|
|
642
|
+
font: 500 14px/1 var(--atx-font-ui);
|
|
643
|
+
white-space: nowrap;
|
|
644
|
+
outline: none;
|
|
645
|
+
transition: background 150ms, border-color 150ms, color 150ms, box-shadow 150ms;
|
|
646
|
+
cursor: pointer;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
/* One size down, for a button that is not the point of the surface it sits on:
|
|
650
|
+
a header's corner action, a row's own control. Smaller in every dimension
|
|
651
|
+
at once -- height, type, gap and radius -- because a button that only loses
|
|
652
|
+
height reads as a squashed full-size button rather than as a lesser one. */
|
|
653
|
+
.atx-btn-sm {
|
|
654
|
+
height: 28px;
|
|
655
|
+
padding: 0 10px;
|
|
656
|
+
gap: 4px;
|
|
657
|
+
border-radius: var(--atx-radius-md);
|
|
658
|
+
font-size: 12.8px;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
/* Icon-only: a square, so the glyph sits in the middle of it rather than in
|
|
662
|
+
the middle of a label-shaped box that has no label. */
|
|
663
|
+
.atx-btn-icon {
|
|
664
|
+
width: 28px;
|
|
665
|
+
height: 28px;
|
|
666
|
+
padding: 0;
|
|
667
|
+
border-radius: var(--atx-radius-md);
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
/* An icon inside a button is sized to the type, not to the button, and sits
|
|
671
|
+
6px from its label -- the gap above. */
|
|
672
|
+
.atx-btn-default > .atx-ico,
|
|
673
|
+
.atx-btn-secondary > .atx-ico,
|
|
674
|
+
.atx-btn-outline > .atx-ico,
|
|
675
|
+
.atx-btn-ghost > .atx-ico,
|
|
676
|
+
.atx-btn-destructive > .atx-ico {
|
|
677
|
+
width: 16px;
|
|
678
|
+
height: 16px;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
/* The press. Cheap, and it is most of what makes a button feel like one. */
|
|
682
|
+
.atx-btn-default:active:not(:disabled),
|
|
683
|
+
.atx-btn-secondary:active:not(:disabled),
|
|
684
|
+
.atx-btn-outline:active:not(:disabled),
|
|
685
|
+
.atx-btn-ghost:active:not(:disabled),
|
|
686
|
+
.atx-btn-destructive:active:not(:disabled) {
|
|
687
|
+
transform: translateY(1px);
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
/* The one emphatic fill. Near-white with dark ink, because on a near-black
|
|
691
|
+
overlay the loudest thing available is light rather than hue. */
|
|
692
|
+
.atx-btn-default {
|
|
693
|
+
border: 1px solid transparent;
|
|
694
|
+
background: var(--atx-primary);
|
|
695
|
+
color: var(--atx-primary-fg);
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
.atx-btn-default:hover:not(:disabled) {
|
|
699
|
+
background: ${hexToRgba(COLOR.primary, 0.9)};
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
.atx-btn-secondary {
|
|
703
|
+
border: 1px solid transparent;
|
|
704
|
+
background: var(--atx-elevated);
|
|
705
|
+
color: var(--atx-foreground);
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
.atx-btn-secondary:hover:not(:disabled) {
|
|
709
|
+
background: ${lift(COLOR.elevated, 10)};
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
/* A body and an edge, which is what shadcn's dark outline button actually is:
|
|
713
|
+
bg-input/30 inside border-input, not a line around nothing. Both halves
|
|
714
|
+
matter and the fill matters more -- with a transparent body this read as
|
|
715
|
+
bare text beside the filled confirm it is paired with, even though the two
|
|
716
|
+
boxes measure the same 32px.
|
|
717
|
+
|
|
718
|
+
The edge is the input token (15%), not border (10%): a separator may sit
|
|
719
|
+
under the 3:1 non-text floor, and a control the user has to see to operate
|
|
720
|
+
is not. Both earlier passes here were wrong in opposite directions -- 40%
|
|
721
|
+
read as a boxed-in field, 10% read as a hairline. */
|
|
722
|
+
.atx-btn-outline {
|
|
723
|
+
border: 1px solid var(--atx-input);
|
|
724
|
+
background: var(--atx-control-bg);
|
|
725
|
+
color: var(--atx-foreground);
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
.atx-btn-outline:hover:not(:disabled) {
|
|
729
|
+
background: var(--atx-accent);
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
/* The quiet variant, and the only one with no boundary at rest -- so it is
|
|
733
|
+
restricted to a control that sits *inside* another surface and is read as
|
|
734
|
+
part of it: a header's corner action, a toolbar key, a back link. A footer
|
|
735
|
+
action is never ghost. A footer is a band of decisions with nothing else in
|
|
736
|
+
it, and a label floating in one with no box and no edge does not read as a
|
|
737
|
+
button at all; that is what the outline variant is for.
|
|
738
|
+
|
|
739
|
+
Full-strength ink, not muted. Muted is for text being *read*, and a control
|
|
740
|
+
painted in it looks disabled before it looks quiet. */
|
|
741
|
+
.atx-btn-ghost {
|
|
742
|
+
border: 1px solid transparent;
|
|
743
|
+
background: transparent;
|
|
744
|
+
color: var(--atx-foreground);
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
.atx-btn-ghost:hover:not(:disabled) {
|
|
748
|
+
background: var(--atx-accent);
|
|
749
|
+
color: var(--atx-foreground);
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
/* margin-right: auto pushes a destructive button to the far left of a flex
|
|
753
|
+
footer, away from the safe actions.
|
|
754
|
+
|
|
755
|
+
Deliberately an outline where shadcn fills it: a footer that puts a solid
|
|
756
|
+
red beside the solid confirm button reads as two equal calls to action when
|
|
757
|
+
only one of them is the thing the user came to do. The edge is the danger
|
|
758
|
+
colour at 40%, so it states itself without shouting. */
|
|
759
|
+
.atx-btn-destructive {
|
|
760
|
+
margin-right: auto;
|
|
761
|
+
border: 1px solid ${hexToRgba(COLOR.destructive, 0.4)};
|
|
762
|
+
background: transparent;
|
|
763
|
+
color: var(--atx-destructive);
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
.atx-btn-destructive:hover:not(:disabled) {
|
|
767
|
+
border-color: ${hexToRgba(COLOR.destructive, 0.7)};
|
|
768
|
+
background: ${hexToRgba(COLOR.destructive, 0.1)};
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
/* One dimmed population, not two. setButtonEnabled dims through [data-dimmed]
|
|
772
|
+
and a dozen callers set .disabled directly; a button that is off should look
|
|
773
|
+
off however it got there, or it reads as broken rather than unavailable. */
|
|
774
|
+
[data-dimmed],
|
|
775
|
+
button:disabled,
|
|
776
|
+
[data-input]:disabled {
|
|
777
|
+
opacity: 0.5;
|
|
778
|
+
cursor: not-allowed;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
/* -- Focus -----------------------------------------------------------------
|
|
782
|
+
:focus-visible, never :focus -- the ring is for the keyboard, and painting
|
|
783
|
+
it on every mouse click is what makes people turn focus indicators off.
|
|
784
|
+
|
|
785
|
+
It carries more weight here than in most designs. A control's resting border
|
|
786
|
+
is transparent, so the ring is not a nicety on top of an outline that is
|
|
787
|
+
already there: it *is* the non-text indication. That is why it is 3px, why
|
|
788
|
+
the ring token is held to 3:1 on every surface, and why the border it paints
|
|
789
|
+
belongs to the same rule. */
|
|
790
|
+
.atx-btn-default:focus-visible,
|
|
791
|
+
.atx-btn-secondary:focus-visible,
|
|
792
|
+
.atx-btn-outline:focus-visible,
|
|
793
|
+
.atx-btn-ghost:focus-visible,
|
|
794
|
+
.atx-btn-destructive:focus-visible,
|
|
795
|
+
[data-input]:focus-visible,
|
|
796
|
+
[role='tab']:focus-visible {
|
|
797
|
+
border-color: var(--atx-ring);
|
|
798
|
+
box-shadow: 0 0 0 3px ${hexToRgba(COLOR.ring, 0.3)};
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
.atx-btn-destructive:focus-visible {
|
|
802
|
+
border-color: var(--atx-destructive);
|
|
803
|
+
box-shadow: 0 0 0 3px ${hexToRgba(COLOR.destructive, 0.4)};
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
/* -- Form controls ---------------------------------------------------------
|
|
807
|
+
Inputs, textareas and selects, keyed off [data-input] rather than a class
|
|
808
|
+
because every caller names its own (atx-field-input, atx-collections-input,
|
|
809
|
+
atx-settings-key ...) and there is no shared class to match.
|
|
810
|
+
|
|
811
|
+
**A control has no border at rest.** Its edge is where the fill stops, and
|
|
812
|
+
the 1px border is transparent, held in reserve for focus and for an invalid
|
|
813
|
+
value -- which is why those two states read as strongly as they do. Adding a
|
|
814
|
+
solid resting outline is the obvious change to make here, and it is the one
|
|
815
|
+
that would undo the look; see COLOR.input for the trade that buys.
|
|
816
|
+
|
|
817
|
+
color-scheme: dark keeps the browser's own chrome -- the date picker's
|
|
818
|
+
calendar popup, number spinners -- light rather than a near-invisible dark
|
|
819
|
+
glyph on a dark field. */
|
|
820
|
+
[data-input] {
|
|
821
|
+
width: 100%;
|
|
822
|
+
min-height: 32px;
|
|
823
|
+
padding: 4px 10px;
|
|
824
|
+
box-sizing: border-box;
|
|
825
|
+
border: 1px solid transparent;
|
|
826
|
+
border-radius: var(--atx-radius-lg);
|
|
827
|
+
background: var(--atx-input-bg);
|
|
828
|
+
color: var(--atx-foreground);
|
|
829
|
+
font: 400 14px/1.45 var(--atx-font-ui);
|
|
830
|
+
outline: none;
|
|
831
|
+
transition: color 200ms, background 200ms, border-color 200ms, box-shadow 200ms;
|
|
832
|
+
color-scheme: dark;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
/* field-sizing grows the box with the prose in it; min-height is the floor for
|
|
836
|
+
browsers without it, so the fallback is a fixed textarea rather than a
|
|
837
|
+
collapsed one. The extra horizontal padding keeps the text clear of the
|
|
838
|
+
corner curve, which a 64px-tall box has plenty of. */
|
|
839
|
+
textarea[data-input] {
|
|
840
|
+
min-height: 64px;
|
|
841
|
+
padding: 8px 12px;
|
|
842
|
+
line-height: 1.55;
|
|
843
|
+
resize: vertical;
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
/* The browser's own dropdown arrow is drawn hard against the right edge, at
|
|
847
|
+
whatever weight and size the platform picked, so it sits outside our padding
|
|
848
|
+
and looks nothing like the 16px icons everywhere else in the overlay. We
|
|
849
|
+
draw our own instead: appearance:none removes theirs, and the chevron is a
|
|
850
|
+
background image inset 10px from the edge with the text padded clear of it.
|
|
851
|
+
|
|
852
|
+
The glyph's colour is baked in from COLOR.mutedFg rather than read from
|
|
853
|
+
--atx-muted-fg, because a data: URI cannot see a custom property. It is the
|
|
854
|
+
one token a theme override will not move; it is chrome on a control, not
|
|
855
|
+
content, so the trade is worth the alignment. */
|
|
856
|
+
select[data-input] {
|
|
857
|
+
height: 32px;
|
|
858
|
+
padding-right: 32px;
|
|
859
|
+
appearance: none;
|
|
860
|
+
-webkit-appearance: none;
|
|
861
|
+
background-image: ${chevronUrl(COLOR.mutedFg)};
|
|
862
|
+
background-repeat: no-repeat;
|
|
863
|
+
background-position: right 10px center;
|
|
864
|
+
background-size: 16px 16px;
|
|
865
|
+
cursor: pointer;
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
select[data-input]:hover:not(:disabled) {
|
|
869
|
+
background-image: ${chevronUrl(COLOR.foreground)};
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
/* The fill is the control, so the fill is what lifts under the pointer.
|
|
873
|
+
background-color, not the shorthand: the shorthand would drop the chevron a
|
|
874
|
+
select paints as its background-image. */
|
|
875
|
+
[data-input]:hover:not(:disabled):not(:focus) {
|
|
876
|
+
background-color: rgb(255 255 255 / 0.11);
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
[data-input]::placeholder {
|
|
880
|
+
color: var(--atx-muted-fg);
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
/* The selection inside a field is the loud colour, so a selected run reads as
|
|
884
|
+
selected against a surface that is itself already light. */
|
|
885
|
+
[data-input]::selection {
|
|
886
|
+
background: var(--atx-primary);
|
|
887
|
+
color: var(--atx-primary-fg);
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
/* Set by fields.ts alongside the error line, so the boundary and the message
|
|
891
|
+
appear together -- and so a screen reader is told, which the red border on
|
|
892
|
+
its own never did. This is one of the two states the reserved border exists
|
|
893
|
+
for. */
|
|
894
|
+
[data-input][aria-invalid='true'] {
|
|
895
|
+
border-color: var(--atx-destructive);
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
[data-input][aria-invalid='true']:focus-visible {
|
|
899
|
+
border-color: var(--atx-destructive);
|
|
900
|
+
box-shadow: 0 0 0 3px ${hexToRgba(COLOR.destructive, 0.4)};
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
/* -- Checkbox --------------------------------------------------------------
|
|
904
|
+
Drawn rather than tinted. accent-color can only recolour the browser's own
|
|
905
|
+
box, and the browser's box is a 2px-radius outlined square: the wrong shape
|
|
906
|
+
whatever colour it is. appearance:none takes the painting and leaves every
|
|
907
|
+
keyboard and assistive behaviour exactly where it was.
|
|
908
|
+
|
|
909
|
+
Unchecked it is the full-strength input fill with no border, matching the
|
|
910
|
+
larger controls; checked it is the same near-white as the confirm button,
|
|
911
|
+
with the tick masked out of it. */
|
|
912
|
+
input[type='checkbox'] {
|
|
913
|
+
appearance: none;
|
|
914
|
+
-webkit-appearance: none;
|
|
915
|
+
display: inline-grid;
|
|
916
|
+
place-content: center;
|
|
917
|
+
width: 16px;
|
|
918
|
+
height: 16px;
|
|
919
|
+
margin: 0;
|
|
920
|
+
flex: 0 0 auto;
|
|
921
|
+
box-sizing: border-box;
|
|
922
|
+
border: 1px solid transparent;
|
|
923
|
+
border-radius: 4px;
|
|
924
|
+
background: var(--atx-input);
|
|
925
|
+
outline: none;
|
|
926
|
+
transition: background 150ms, border-color 150ms, box-shadow 150ms;
|
|
927
|
+
cursor: pointer;
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
input[type='checkbox']::before {
|
|
931
|
+
content: '';
|
|
932
|
+
width: 12px;
|
|
933
|
+
height: 12px;
|
|
934
|
+
background: var(--atx-primary-fg);
|
|
935
|
+
transform: scale(0);
|
|
936
|
+
transition: transform 120ms ease-out;
|
|
937
|
+
-webkit-mask: ${CHECK_MASK};
|
|
938
|
+
mask: ${CHECK_MASK};
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
input[type='checkbox']:checked {
|
|
942
|
+
border-color: var(--atx-primary);
|
|
943
|
+
background: var(--atx-primary);
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
input[type='checkbox']:checked::before {
|
|
947
|
+
transform: scale(1);
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
input[type='checkbox']:focus-visible {
|
|
951
|
+
border-color: var(--atx-ring);
|
|
952
|
+
box-shadow: 0 0 0 3px ${hexToRgba(COLOR.ring, 0.3)};
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
/* == Switch =================================================================
|
|
956
|
+
ui.ts::switchControl. The same input[type=checkbox] underneath, repainted as
|
|
957
|
+
a track and a thumb — so every rule above has to be overridden here, and the
|
|
958
|
+
selector carries the type and the attribute as well as the class to outrank
|
|
959
|
+
it rather than relying on source order.
|
|
960
|
+
|
|
961
|
+
Geometry is shadcn's, measured off the live reference: a 32x18 track and 14px
|
|
962
|
+
of travel. The thumb is 14px inset 2px rather than the reference's 16px inset
|
|
963
|
+
1px -- at this track height a 1px inset survives on the flat top and bottom
|
|
964
|
+
but is eaten by the pill's corner curve at each end, so the checked thumb
|
|
965
|
+
reads as touching the track. 2px is the smallest inset that still shows at
|
|
966
|
+
the ends, and the travel is unchanged (32 - 14 - 2 - 2). The colours are this overlay's own: the
|
|
967
|
+
unchecked track is the full-strength input fill every small control is made
|
|
968
|
+
of, and the checked track is the near-white primary that the confirm button
|
|
969
|
+
and a ticked checkbox already wear -- the brand purple the reference uses
|
|
970
|
+
here is spoken for, and means "this element on your page is editable". */
|
|
971
|
+
.atx-switch-row {
|
|
972
|
+
display: inline-flex;
|
|
973
|
+
align-items: center;
|
|
974
|
+
gap: 8px;
|
|
975
|
+
flex: 0 0 auto;
|
|
976
|
+
cursor: pointer;
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
.atx-switch-label {
|
|
980
|
+
color: var(--atx-muted-fg);
|
|
981
|
+
font: 500 12px/1.4 var(--atx-font-ui);
|
|
982
|
+
user-select: none;
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
input[type='checkbox'].atx-switch {
|
|
986
|
+
position: relative;
|
|
987
|
+
display: inline-flex;
|
|
988
|
+
place-content: unset;
|
|
989
|
+
align-items: center;
|
|
990
|
+
width: 32px;
|
|
991
|
+
height: 18px;
|
|
992
|
+
padding: 0;
|
|
993
|
+
border-radius: var(--atx-radius-full);
|
|
994
|
+
background: var(--atx-input);
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
/* The thumb. ::before is the checkbox's tick slot, reused — there is one
|
|
998
|
+
pseudo-element to spend and a switch has one moving part. */
|
|
999
|
+
input[type='checkbox'].atx-switch::before {
|
|
1000
|
+
content: '';
|
|
1001
|
+
position: absolute;
|
|
1002
|
+
/* Against the padding box, inside the 1px transparent border the shared
|
|
1003
|
+
checkbox rule gives every box: 1px here is the 2px the thumb reads as. */
|
|
1004
|
+
left: 1px;
|
|
1005
|
+
width: 14px;
|
|
1006
|
+
height: 14px;
|
|
1007
|
+
border-radius: var(--atx-radius-full);
|
|
1008
|
+
background: var(--atx-foreground);
|
|
1009
|
+
transform: none;
|
|
1010
|
+
transition: transform 150ms ease-out, background 150ms;
|
|
1011
|
+
-webkit-mask: none;
|
|
1012
|
+
mask: none;
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
input[type='checkbox'].atx-switch:checked {
|
|
1016
|
+
border-color: transparent;
|
|
1017
|
+
background: var(--atx-primary);
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
/* Travel = track - thumb - both insets. Moving the thumb rather than animating
|
|
1021
|
+
left keeps it on the compositor. */
|
|
1022
|
+
input[type='checkbox'].atx-switch:checked::before {
|
|
1023
|
+
background: var(--atx-primary-fg);
|
|
1024
|
+
transform: translateX(14px);
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
/* Disabled is handled here rather than inherited: the shared [data-input]
|
|
1028
|
+
baseline this control opts out of is what usually carries it. The label dims
|
|
1029
|
+
with the track, so the pair reads as one thing switched off. */
|
|
1030
|
+
input[type='checkbox'].atx-switch:disabled {
|
|
1031
|
+
opacity: 0.5;
|
|
1032
|
+
cursor: not-allowed;
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
.atx-switch-row:has(input:disabled) {
|
|
1036
|
+
cursor: not-allowed;
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
.atx-switch-row:has(input:disabled) .atx-switch-label {
|
|
1040
|
+
opacity: 0.5;
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
[data-pill] {
|
|
1044
|
+
display: inline-flex;
|
|
1045
|
+
align-items: center;
|
|
1046
|
+
justify-content: center;
|
|
1047
|
+
gap: 4px;
|
|
1048
|
+
margin-left: 8px;
|
|
1049
|
+
padding: 2px 7px;
|
|
1050
|
+
border: none;
|
|
1051
|
+
border-radius: var(--atx-radius-sm);
|
|
1052
|
+
background: rgba(255, 255, 255, 0.14);
|
|
1053
|
+
color: var(--atx-foreground);
|
|
1054
|
+
font: 600 11px var(--atx-font-ui);
|
|
1055
|
+
cursor: pointer;
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
[data-pill]:hover {
|
|
1059
|
+
background: rgba(255, 255, 255, 0.28);
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
/* Optical centring, which flexbox cannot do for text — see pillButton in
|
|
1063
|
+
ui.ts for why an 11px lowercase label reads as sitting low without it. */
|
|
1064
|
+
.atx-pill-label {
|
|
1065
|
+
position: relative;
|
|
1066
|
+
top: -1.5px;
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
.atx-toast {
|
|
1070
|
+
position: fixed;
|
|
1071
|
+
left: 50%;
|
|
1072
|
+
transform: translateX(-50%);
|
|
1073
|
+
padding: 10px 18px;
|
|
1074
|
+
border-radius: var(--atx-radius-lg);
|
|
1075
|
+
font: 500 14px var(--atx-font-ui);
|
|
1076
|
+
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.32);
|
|
1077
|
+
opacity: 0;
|
|
1078
|
+
transition: opacity 120ms;
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
.atx-toast[data-shown] {
|
|
1082
|
+
opacity: 1;
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
.atx-toast-ok {
|
|
1086
|
+
background: var(--atx-success);
|
|
1087
|
+
color: var(--atx-foreground);
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
.atx-toast-err {
|
|
1091
|
+
background: var(--atx-destructive);
|
|
1092
|
+
color: var(--atx-primary-fg);
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
/* The save veil's rect is measured off a host element, so its geometry is the
|
|
1096
|
+
one thing that stays inline. */
|
|
1097
|
+
.atx-veil {
|
|
1098
|
+
position: fixed;
|
|
1099
|
+
display: flex;
|
|
1100
|
+
align-items: center;
|
|
1101
|
+
justify-content: center;
|
|
1102
|
+
border-radius: var(--atx-radius-sm);
|
|
1103
|
+
background: ${hexToRgba(COLOR.brand, 0.12)};
|
|
1104
|
+
pointer-events: all;
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
.atx-veil-chip {
|
|
1108
|
+
padding: 2px 8px;
|
|
1109
|
+
border-radius: var(--atx-radius-full);
|
|
1110
|
+
background: var(--atx-brand);
|
|
1111
|
+
color: var(--atx-foreground);
|
|
1112
|
+
font: 600 11px var(--atx-font-ui);
|
|
1113
|
+
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3);
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
/* == Hover pill and outline ================================================
|
|
1117
|
+
hover.ts. The outline and the pill are shown by a [data-on] flag rather than
|
|
1118
|
+
an inline display. The verdict colours the outline alone -- its border and
|
|
1119
|
+
fill stay inline, because that colour is chosen per element at hover time.
|
|
1120
|
+
The pill carries no verdict colour: it says the word, the box round the
|
|
1121
|
+
element carries the hue. */
|
|
1122
|
+
|
|
1123
|
+
/* The measured rect is inset by OUTLINE_GAP in hover.ts so the border clears
|
|
1124
|
+
the element's own edge instead of sitting flush against its ink. */
|
|
1125
|
+
.atx-outline {
|
|
1126
|
+
position: fixed;
|
|
1127
|
+
z-index: ${Z};
|
|
1128
|
+
display: none;
|
|
1129
|
+
border: 2px solid var(--atx-brand);
|
|
1130
|
+
border-radius: var(--atx-radius-md);
|
|
1131
|
+
background: ${hexToRgba(COLOR.brand, 0.08)};
|
|
1132
|
+
pointer-events: none;
|
|
1133
|
+
transition: all 60ms ease-out;
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
.atx-outline[data-on] {
|
|
1137
|
+
display: block;
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
/* The pill is interactive: hovering it keeps it open, and its "open source"
|
|
1141
|
+
button jumps to the element's source in the editor. */
|
|
1142
|
+
.atx-tooltip {
|
|
1143
|
+
position: fixed;
|
|
1144
|
+
z-index: ${Z + 1};
|
|
1145
|
+
display: none;
|
|
1146
|
+
/* Column so the loc line and the class/ID chips row stack; each row sizes
|
|
1147
|
+
to its own content (flex-start) rather than stretching to the widest. */
|
|
1148
|
+
flex-direction: column;
|
|
1149
|
+
align-items: flex-start;
|
|
1150
|
+
/* Concentric with the pill buttons inside it: their 6px radius plus the 4px
|
|
1151
|
+
of padding around them is the 10px this curve needs to read as wrapping
|
|
1152
|
+
them rather than being clipped by them. The left padding is bigger because
|
|
1153
|
+
what sits there is a text label, which has no box of its own to inset. */
|
|
1154
|
+
padding: 4px 4px 4px 10px;
|
|
1155
|
+
border-radius: var(--atx-radius-lg);
|
|
1156
|
+
background: var(--atx-card);
|
|
1157
|
+
color: var(--atx-foreground);
|
|
1158
|
+
font: 500 12px/1.4 var(--atx-font-mono);
|
|
1159
|
+
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
|
|
1160
|
+
white-space: nowrap;
|
|
1161
|
+
pointer-events: auto;
|
|
1162
|
+
cursor: default;
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
.atx-tooltip[data-on] {
|
|
1166
|
+
display: flex;
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
.atx-tooltip-row {
|
|
1170
|
+
display: flex;
|
|
1171
|
+
align-items: center;
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
/* The label is clickable too — the whole file:loc line opens the in-browser
|
|
1175
|
+
source peek (the "open" button next to it is the editor jump). */
|
|
1176
|
+
.atx-tooltip-label {
|
|
1177
|
+
cursor: pointer;
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
.atx-tooltip-label:hover {
|
|
1181
|
+
text-decoration: underline;
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
/* The verdict sits in a fixed-width slot: the pill must not resize
|
|
1185
|
+
(distracting) when "loading…" upgrades to the real verdict. 8ch fits the
|
|
1186
|
+
longest words in the pill's monospace font; shorter verdicts leave a little
|
|
1187
|
+
slack instead of shrinking the pill. */
|
|
1188
|
+
.atx-tooltip-verdict {
|
|
1189
|
+
display: inline-block;
|
|
1190
|
+
width: 8ch;
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
.atx-tooltip-chips {
|
|
1194
|
+
display: none;
|
|
1195
|
+
flex-wrap: wrap;
|
|
1196
|
+
gap: 4px;
|
|
1197
|
+
max-width: 340px;
|
|
1198
|
+
margin-top: 5px;
|
|
1199
|
+
padding-top: 5px;
|
|
1200
|
+
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
|
1201
|
+
white-space: normal;
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
.atx-tooltip-chips[data-on] {
|
|
1205
|
+
display: flex;
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
.atx-tooltip-chip {
|
|
1209
|
+
padding: 1px 6px;
|
|
1210
|
+
border: 1px solid transparent;
|
|
1211
|
+
border-radius: var(--atx-radius-sm);
|
|
1212
|
+
background: rgba(255, 255, 255, 0.09);
|
|
1213
|
+
color: var(--atx-muted-fg);
|
|
1214
|
+
font: 500 11px var(--atx-font-mono);
|
|
1215
|
+
cursor: default;
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
/* Hovering a chip also pops its rules card; that half stays in JS. */
|
|
1219
|
+
.atx-tooltip-chip:hover {
|
|
1220
|
+
border-color: var(--atx-brand);
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
/* == Element tree ==========================================================
|
|
1224
|
+
tree.ts. Its panel, its edge tab and the locked-selection outline are all
|
|
1225
|
+
shown by [data-on]; what stays inline is the panel's top/bottom (the admin
|
|
1226
|
+
bar reserves a strip of one edge and reports it through onChromeInset), a
|
|
1227
|
+
row's indent, and the selection's measured rect. */
|
|
1228
|
+
|
|
1229
|
+
.atx-tree {
|
|
1230
|
+
position: fixed;
|
|
1231
|
+
left: 5px;
|
|
1232
|
+
/* Top/bottom rather than a height: the admin bar reserves a strip of one
|
|
1233
|
+
edge, and the panel must never sit under it. Both are overwritten inline
|
|
1234
|
+
as the inset changes. */
|
|
1235
|
+
top: 5px;
|
|
1236
|
+
bottom: 5px;
|
|
1237
|
+
z-index: ${Z + 3};
|
|
1238
|
+
display: none;
|
|
1239
|
+
flex-direction: column;
|
|
1240
|
+
width: min(320px, 90vw);
|
|
1241
|
+
box-sizing: border-box;
|
|
1242
|
+
border: 1px solid var(--atx-border);
|
|
1243
|
+
border-radius: var(--atx-radius-xl);
|
|
1244
|
+
background: rgba(0, 0, 0, 0.9);
|
|
1245
|
+
color: var(--atx-muted-fg);
|
|
1246
|
+
box-shadow: 0 8px 28px rgba(0, 0, 0, 0.4);
|
|
1247
|
+
font: 400 12px/1.5 var(--atx-font-mono);
|
|
1248
|
+
/* Edit mode sets a page-wide crosshair; the panel is not click-to-edit. */
|
|
1249
|
+
cursor: auto;
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
.atx-tree[data-on] {
|
|
1253
|
+
display: flex;
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
.atx-tree-title {
|
|
1257
|
+
display: flex;
|
|
1258
|
+
align-items: center;
|
|
1259
|
+
gap: 8px;
|
|
1260
|
+
flex: 0 0 auto;
|
|
1261
|
+
padding: 14px 16px;
|
|
1262
|
+
border-bottom: 1px solid var(--atx-border);
|
|
1263
|
+
color: var(--atx-foreground);
|
|
1264
|
+
font: 500 14px var(--atx-font-ui);
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
.atx-tree-title-text {
|
|
1268
|
+
flex: 1 1 auto;
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
.atx-tree-close {
|
|
1272
|
+
display: inline-flex;
|
|
1273
|
+
align-items: center;
|
|
1274
|
+
justify-content: center;
|
|
1275
|
+
flex: 0 0 auto;
|
|
1276
|
+
width: 24px;
|
|
1277
|
+
height: 24px;
|
|
1278
|
+
padding: 0;
|
|
1279
|
+
border: none;
|
|
1280
|
+
border-radius: var(--atx-radius-md);
|
|
1281
|
+
background: transparent;
|
|
1282
|
+
color: var(--atx-muted-fg);
|
|
1283
|
+
cursor: pointer;
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
.atx-tree-close:hover {
|
|
1287
|
+
background: var(--atx-accent);
|
|
1288
|
+
color: var(--atx-foreground);
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
/* The edge tab that reopens the tree. Only meaningful while editing — outside
|
|
1292
|
+
edit mode the tree has nothing live to point at. */
|
|
1293
|
+
.atx-tree-tab {
|
|
1294
|
+
position: fixed;
|
|
1295
|
+
left: 0;
|
|
1296
|
+
top: 50%;
|
|
1297
|
+
transform: translateY(-50%);
|
|
1298
|
+
z-index: ${Z + 3};
|
|
1299
|
+
display: none;
|
|
1300
|
+
align-items: center;
|
|
1301
|
+
justify-content: center;
|
|
1302
|
+
width: 24px;
|
|
1303
|
+
height: 64px;
|
|
1304
|
+
padding: 0;
|
|
1305
|
+
border: none;
|
|
1306
|
+
border-right: 1px solid rgba(255, 255, 255, 0.1);
|
|
1307
|
+
border-radius: 0 7px 7px 0;
|
|
1308
|
+
background: ${hexToRgba(COLOR.glass, 0.88)};
|
|
1309
|
+
backdrop-filter: blur(10px);
|
|
1310
|
+
color: var(--atx-brand-text);
|
|
1311
|
+
box-shadow: 2px 0 14px rgba(0, 0, 0, 0.3);
|
|
1312
|
+
cursor: pointer;
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
.atx-tree-tab[data-on] {
|
|
1316
|
+
display: flex;
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
.atx-tree-tab:hover {
|
|
1320
|
+
color: var(--atx-foreground);
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
.atx-tree-body {
|
|
1324
|
+
flex: 1 1 auto;
|
|
1325
|
+
padding: 6px 0;
|
|
1326
|
+
overflow-y: auto;
|
|
1327
|
+
overflow-x: hidden;
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
.atx-tree-selection {
|
|
1331
|
+
position: fixed;
|
|
1332
|
+
z-index: ${Z};
|
|
1333
|
+
display: none;
|
|
1334
|
+
border: 2px solid var(--atx-brand);
|
|
1335
|
+
border-radius: var(--atx-radius-md);
|
|
1336
|
+
box-shadow: 0 0 0 2px ${COLOR.brand}44, 0 0 12px ${COLOR.brand}66;
|
|
1337
|
+
pointer-events: none;
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
.atx-tree-selection[data-on] {
|
|
1341
|
+
display: block;
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
/* A row's indent is its depth, so padding-left stays inline. */
|
|
1345
|
+
.atx-tree-row {
|
|
1346
|
+
display: flex;
|
|
1347
|
+
align-items: center;
|
|
1348
|
+
gap: 5px;
|
|
1349
|
+
padding: 3px 10px 3px 0;
|
|
1350
|
+
border-radius: var(--atx-radius-md);
|
|
1351
|
+
background: transparent;
|
|
1352
|
+
color: var(--atx-muted-fg);
|
|
1353
|
+
outline: none;
|
|
1354
|
+
outline-offset: -1px;
|
|
1355
|
+
white-space: nowrap;
|
|
1356
|
+
cursor: pointer;
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
/* Surface shift, not a tint — every colour in the tree means "this is where
|
|
1360
|
+
your element is", so hover must not borrow one. */
|
|
1361
|
+
.atx-tree-row:hover:not([data-state]) {
|
|
1362
|
+
background: var(--atx-accent);
|
|
1363
|
+
color: var(--atx-foreground);
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
/* Hover-active reads as a dashed ring so it never looks like the solid locked
|
|
1367
|
+
selection, even when both land on the same row. The brand tint here is one
|
|
1368
|
+
of the places the purple keeps its meaning — it points at your page. */
|
|
1369
|
+
.atx-tree-row[data-state='active'] {
|
|
1370
|
+
background: ${hexToRgba(COLOR.brand, 0.16)};
|
|
1371
|
+
color: var(--atx-brand-text);
|
|
1372
|
+
outline: 1px dashed var(--atx-brand);
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
.atx-tree-row[data-state='selected'] {
|
|
1376
|
+
background: var(--atx-brand);
|
|
1377
|
+
color: var(--atx-foreground);
|
|
1378
|
+
outline: none;
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
.atx-tree-chevron {
|
|
1382
|
+
display: inline-flex;
|
|
1383
|
+
align-items: center;
|
|
1384
|
+
justify-content: center;
|
|
1385
|
+
flex: 0 0 auto;
|
|
1386
|
+
width: 12px;
|
|
1387
|
+
color: var(--atx-muted-fg);
|
|
1388
|
+
cursor: pointer;
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
.atx-tree-chevron[data-leaf] {
|
|
1392
|
+
cursor: default;
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
/* The tag is what the row *is*; the preview and the loc are what it happens to
|
|
1396
|
+
contain and where it happens to live. Full-strength ink on the tag against
|
|
1397
|
+
the row's muted default is what makes a long tree scannable by shape -- the
|
|
1398
|
+
angle brackets included, since they are what say "element" at a glance. */
|
|
1399
|
+
.atx-tree-tag {
|
|
1400
|
+
flex: 0 0 auto;
|
|
1401
|
+
color: var(--atx-foreground);
|
|
1402
|
+
font-weight: 600;
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
.atx-tree-preview {
|
|
1406
|
+
flex: 0 1 auto;
|
|
1407
|
+
overflow: hidden;
|
|
1408
|
+
text-overflow: ellipsis;
|
|
1409
|
+
color: var(--atx-muted-fg);
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
.atx-tree-loc {
|
|
1413
|
+
flex: 0 0 auto;
|
|
1414
|
+
margin-left: auto;
|
|
1415
|
+
padding-left: 10px;
|
|
1416
|
+
color: var(--atx-faint-fg);
|
|
1417
|
+
font-size: 11px;
|
|
1418
|
+
font-variant-numeric: tabular-nums;
|
|
1419
|
+
text-decoration: underline dotted transparent;
|
|
1420
|
+
text-underline-offset: 2px;
|
|
1421
|
+
cursor: pointer;
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
.atx-tree-loc:hover {
|
|
1425
|
+
color: var(--atx-brand-text);
|
|
1426
|
+
text-decoration-color: var(--atx-brand-text);
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
.atx-tree-empty {
|
|
1430
|
+
padding: 14px;
|
|
1431
|
+
color: var(--atx-muted-fg);
|
|
1432
|
+
font: 400 13px var(--atx-font-ui);
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
/* == Admin bar =============================================================
|
|
1436
|
+
admin-bar.ts. Three state flags drive everything the bar does with itself:
|
|
1437
|
+
[data-edge] which edge it is docked to, [data-open] whether it is revealed,
|
|
1438
|
+
and [data-docked] whether it is in the mode that never retracts (pinned, or
|
|
1439
|
+
edit mode, where the bar carries the save state and the way out and so must
|
|
1440
|
+
never be off-screen). What stays inline is the menu's measured position and
|
|
1441
|
+
the save button's colours, which come from a caller-supplied paint hook
|
|
1442
|
+
rather than a fixed vocabulary. */
|
|
1443
|
+
|
|
1444
|
+
.atx-bar {
|
|
1445
|
+
position: fixed;
|
|
1446
|
+
left: 0;
|
|
1447
|
+
right: 0;
|
|
1448
|
+
height: ${BAR_H}px;
|
|
1449
|
+
z-index: ${Z + 4};
|
|
1450
|
+
display: flex;
|
|
1451
|
+
align-items: center;
|
|
1452
|
+
gap: 6px;
|
|
1453
|
+
padding: 0 8px;
|
|
1454
|
+
box-sizing: border-box;
|
|
1455
|
+
/* A bar spans its edge from corner to corner, so it has no corners of its
|
|
1456
|
+
own to round. Stated rather than left to the default, because every other
|
|
1457
|
+
surface in here carries a radius and this one must not pick one up. */
|
|
1458
|
+
border-radius: 0;
|
|
1459
|
+
/* Opaque enough to guarantee the bar's own contrast. At 0.78 a white page
|
|
1460
|
+
showed through to an effective #494853, which dropped the hint and menu
|
|
1461
|
+
inks to ~3:1; the blur still reads as glass at 0.94, and over a dark page
|
|
1462
|
+
(the common case) the two are indistinguishable. */
|
|
1463
|
+
background: ${hexToRgba(COLOR.glass, 0.94)};
|
|
1464
|
+
backdrop-filter: blur(12px) saturate(1.3);
|
|
1465
|
+
color: var(--atx-foreground);
|
|
1466
|
+
font: 500 13px var(--atx-font-ui);
|
|
1467
|
+
transform: none;
|
|
1468
|
+
opacity: 1;
|
|
1469
|
+
pointer-events: auto;
|
|
1470
|
+
transition: opacity 140ms ease, transform 220ms cubic-bezier(0.4, 0, 0.2, 1);
|
|
1471
|
+
/* Edit mode sets a page-wide crosshair; the bar is not click-to-edit. */
|
|
1472
|
+
cursor: auto;
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
.atx-bar[data-edge='top'] {
|
|
1476
|
+
top: 0;
|
|
1477
|
+
border-bottom: 1px solid rgba(255, 255, 255, 0.09);
|
|
1478
|
+
box-shadow: 0 6px 22px rgba(0, 0, 0, 0.26);
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
.atx-bar[data-edge='bottom'] {
|
|
1482
|
+
bottom: 0;
|
|
1483
|
+
border-top: 1px solid rgba(255, 255, 255, 0.09);
|
|
1484
|
+
box-shadow: 0 -6px 22px rgba(0, 0, 0, 0.26);
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
/* Resting but never in the way — and still readable. At 0.5 the bar's own
|
|
1488
|
+
labels composited down to 2.8:1 against a white page, so the resting bar was
|
|
1489
|
+
the least legible thing the overlay drew. 0.72 keeps it recessive without
|
|
1490
|
+
going back there.
|
|
1491
|
+
|
|
1492
|
+
The resting bar sits near the AA line, and only the resting bar. Over a
|
|
1493
|
+
near-white page a label straight on the bar surface computes to ~5.9:1, and
|
|
1494
|
+
the three that sit inside a chip to ~4.8:1 — the chip's white tint lifts the
|
|
1495
|
+
surface under the ink, which is what makes those three the worst case.
|
|
1496
|
+
Approaching the bar or entering edit mode takes it to opacity 1 and 11:1 or
|
|
1497
|
+
better, which is every state a user reads it in for longer than a glance.
|
|
1498
|
+
|
|
1499
|
+
Those are computed figures: they model opacity as the group buffer it is,
|
|
1500
|
+
but not backdrop-filter's saturate pass, so treat them as ±0.3 and
|
|
1501
|
+
re-measure in the browser — docs/VERIFICATION.md carries that check.
|
|
1502
|
+
Recessive-until-touched is the point of the surface, so the number stays
|
|
1503
|
+
where it is. Tracked as a deferral on the roadmap board, not a bug, and it
|
|
1504
|
+
is why tests/contrast.test.ts pins the tokens rather than this composite,
|
|
1505
|
+
which depends on the host page. */
|
|
1506
|
+
.atx-bar[data-docked]:not([data-open]) {
|
|
1507
|
+
opacity: 0.72;
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
/* Unpinned and unrevealed: off the edge entirely, and not swallowing clicks on
|
|
1511
|
+
the strip it used to cover. */
|
|
1512
|
+
.atx-bar:not([data-docked]):not([data-open]) {
|
|
1513
|
+
opacity: 0;
|
|
1514
|
+
pointer-events: none;
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
.atx-bar:not([data-docked]):not([data-open])[data-edge='top'] {
|
|
1518
|
+
transform: translateY(-100%);
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
.atx-bar:not([data-docked]):not([data-open])[data-edge='bottom'] {
|
|
1522
|
+
transform: translateY(100%);
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
/* The sliver left behind by a retracted bar, so the edge is still findable. */
|
|
1526
|
+
.atx-hairline {
|
|
1527
|
+
position: fixed;
|
|
1528
|
+
left: 0;
|
|
1529
|
+
right: 0;
|
|
1530
|
+
height: 3px;
|
|
1531
|
+
z-index: ${Z + 3};
|
|
1532
|
+
display: none;
|
|
1533
|
+
background: linear-gradient(90deg, transparent, ${COLOR.brand}88, transparent);
|
|
1534
|
+
pointer-events: none;
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
.atx-hairline[data-on] {
|
|
1538
|
+
display: block;
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
.atx-hairline[data-edge='top'] {
|
|
1542
|
+
top: 0;
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
.atx-hairline[data-edge='bottom'] {
|
|
1546
|
+
bottom: 0;
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
.atx-hairline-nub {
|
|
1550
|
+
position: absolute;
|
|
1551
|
+
left: 50%;
|
|
1552
|
+
transform: translateX(-50%);
|
|
1553
|
+
width: 54px;
|
|
1554
|
+
height: 3px;
|
|
1555
|
+
background: var(--atx-brand);
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
.atx-hairline[data-edge='top'] .atx-hairline-nub {
|
|
1559
|
+
top: 0;
|
|
1560
|
+
border-radius: 0 0 3px 3px;
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
.atx-hairline[data-edge='bottom'] .atx-hairline-nub {
|
|
1564
|
+
bottom: 0;
|
|
1565
|
+
border-radius: 3px 3px 0 0;
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
.atx-bar-group {
|
|
1569
|
+
display: flex;
|
|
1570
|
+
align-items: center;
|
|
1571
|
+
gap: 6px;
|
|
1572
|
+
min-width: 0;
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
.atx-bar-group-right {
|
|
1576
|
+
margin-left: auto;
|
|
1577
|
+
min-width: auto;
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
/* The launcher glyph — the one piece of bar furniture that keeps the brand
|
|
1581
|
+
colour, because a product needs one place to be itself. */
|
|
1582
|
+
.atx-bar-brand {
|
|
1583
|
+
display: flex;
|
|
1584
|
+
align-items: center;
|
|
1585
|
+
justify-content: center;
|
|
1586
|
+
flex: 0 0 auto;
|
|
1587
|
+
width: 28px;
|
|
1588
|
+
height: 28px;
|
|
1589
|
+
padding: 0;
|
|
1590
|
+
border: none;
|
|
1591
|
+
border-radius: var(--atx-radius-md);
|
|
1592
|
+
background: var(--atx-brand);
|
|
1593
|
+
color: var(--atx-foreground);
|
|
1594
|
+
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.1);
|
|
1595
|
+
cursor: pointer;
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
.atx-bar-brand:hover {
|
|
1599
|
+
background: ${lift(COLOR.brand)};
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
.atx-bar-sep {
|
|
1603
|
+
flex: 0 0 auto;
|
|
1604
|
+
width: 1px;
|
|
1605
|
+
height: 18px;
|
|
1606
|
+
margin: 0 3px;
|
|
1607
|
+
background: rgba(255, 255, 255, 0.14);
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
/* The bar's primary ink rather than muted: the resting bar is already at
|
|
1611
|
+
reduced opacity, which dims whatever ink sits on it, and a hint is a message
|
|
1612
|
+
to be read. Its 10.5px size is what keeps it secondary. */
|
|
1613
|
+
.atx-bar-hint {
|
|
1614
|
+
display: none;
|
|
1615
|
+
padding-right: 4px;
|
|
1616
|
+
color: var(--atx-foreground);
|
|
1617
|
+
font: 500 10.5px/1 var(--atx-font-ui);
|
|
1618
|
+
white-space: nowrap;
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
.atx-bar-hint[data-on] {
|
|
1622
|
+
display: inline;
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
.atx-bar-btn {
|
|
1626
|
+
display: inline-flex;
|
|
1627
|
+
align-items: center;
|
|
1628
|
+
justify-content: center;
|
|
1629
|
+
gap: 6px;
|
|
1630
|
+
flex: 0 0 auto;
|
|
1631
|
+
/* 28px -- the small rung of the button ladder, shared with the launcher and
|
|
1632
|
+
the icon buttons so everything in the bar is one height, and the icon
|
|
1633
|
+
button (28px wide) is actually square. */
|
|
1634
|
+
height: 28px;
|
|
1635
|
+
width: auto;
|
|
1636
|
+
padding: 0 11px;
|
|
1637
|
+
border: none;
|
|
1638
|
+
border-radius: var(--atx-radius-md);
|
|
1639
|
+
background: ${BAR_CHIP};
|
|
1640
|
+
color: var(--atx-foreground);
|
|
1641
|
+
font: 500 13px/1 var(--atx-font-ui);
|
|
1642
|
+
white-space: nowrap;
|
|
1643
|
+
outline: none;
|
|
1644
|
+
cursor: pointer;
|
|
1645
|
+
transition: background 120ms, color 120ms;
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
.atx-bar-btn-icon {
|
|
1649
|
+
width: 28px;
|
|
1650
|
+
padding: 0;
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
.atx-bar-btn:focus-visible,
|
|
1654
|
+
.atx-menu-item:focus-visible {
|
|
1655
|
+
box-shadow: 0 0 0 2px ${hexToRgba(COLOR.ring, 0.7)};
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
.atx-bar-btn:hover:not(:disabled) {
|
|
1659
|
+
background: ${BAR_CHIP_HOVER};
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
.atx-menu-item {
|
|
1663
|
+
display: flex;
|
|
1664
|
+
align-items: center;
|
|
1665
|
+
gap: 8px;
|
|
1666
|
+
width: 100%;
|
|
1667
|
+
height: 32px;
|
|
1668
|
+
padding: 0 12px;
|
|
1669
|
+
border: none;
|
|
1670
|
+
border-radius: var(--atx-radius-md);
|
|
1671
|
+
background: transparent;
|
|
1672
|
+
color: var(--atx-foreground);
|
|
1673
|
+
font: 500 14px var(--atx-font-ui);
|
|
1674
|
+
text-align: left;
|
|
1675
|
+
outline: none;
|
|
1676
|
+
cursor: pointer;
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
/* Hover shifts the surface; it does not tint it. A hue on hover competes with
|
|
1680
|
+
the one colour that is supposed to mean something. */
|
|
1681
|
+
.atx-menu-item:hover:not(:disabled) {
|
|
1682
|
+
background: var(--atx-accent);
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1685
|
+
/* An item whose feature is currently on. Near-white with dark ink — the same
|
|
1686
|
+
emphasis the confirm button gets, and for the same reason. */
|
|
1687
|
+
.atx-bar-btn[data-active],
|
|
1688
|
+
.atx-menu-item[data-active] {
|
|
1689
|
+
background: var(--atx-primary);
|
|
1690
|
+
color: var(--atx-primary-fg);
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
.atx-bar-btn[data-active]:hover:not(:disabled),
|
|
1694
|
+
.atx-menu-item[data-active]:hover:not(:disabled) {
|
|
1695
|
+
background: ${hexToRgba(COLOR.primary, 0.9)};
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
.atx-bar-btn[data-off],
|
|
1699
|
+
.atx-menu-item[data-off] {
|
|
1700
|
+
cursor: default;
|
|
1701
|
+
}
|
|
1702
|
+
|
|
1703
|
+
.atx-bar-btn[data-hidden],
|
|
1704
|
+
.atx-menu-item[data-hidden] {
|
|
1705
|
+
display: none;
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1708
|
+
.atx-menu {
|
|
1709
|
+
position: fixed;
|
|
1710
|
+
z-index: ${Z + 4};
|
|
1711
|
+
display: none;
|
|
1712
|
+
flex-direction: column;
|
|
1713
|
+
min-width: 220px;
|
|
1714
|
+
padding: 6px;
|
|
1715
|
+
border: 1px solid var(--atx-border);
|
|
1716
|
+
border-radius: var(--atx-radius-lg);
|
|
1717
|
+
background: ${hexToRgba(COLOR.glassRaised, 0.97)};
|
|
1718
|
+
backdrop-filter: blur(14px);
|
|
1719
|
+
color: var(--atx-foreground);
|
|
1720
|
+
font: 500 13px var(--atx-font-ui);
|
|
1721
|
+
box-shadow: 0 8px 28px rgba(0, 0, 0, 0.42);
|
|
1722
|
+
cursor: auto;
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1725
|
+
.atx-menu[data-on] {
|
|
1726
|
+
display: flex;
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
.atx-menu-items {
|
|
1730
|
+
display: flex;
|
|
1731
|
+
flex-direction: column;
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
.atx-menu-foot {
|
|
1735
|
+
display: flex;
|
|
1736
|
+
align-items: center;
|
|
1737
|
+
gap: 7px;
|
|
1738
|
+
margin: 6px 4px 0;
|
|
1739
|
+
padding-top: 8px;
|
|
1740
|
+
border-top: 1px solid var(--atx-border);
|
|
1741
|
+
color: var(--atx-faint-fg);
|
|
1742
|
+
font: 500 10.5px var(--atx-font-mono);
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
.atx-menu-live {
|
|
1746
|
+
flex: 0 0 auto;
|
|
1747
|
+
width: 6px;
|
|
1748
|
+
height: 6px;
|
|
1749
|
+
border-radius: 50%;
|
|
1750
|
+
background: var(--atx-info);
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
/* == Entry drawer fields ===================================================
|
|
1754
|
+
editors/fields.ts and editors/entry.ts. One control per FieldType, all
|
|
1755
|
+
wrapped in the same label/error/help chrome. Two states are attributes
|
|
1756
|
+
rather than inline writes: [data-locked] on a field whose option the
|
|
1757
|
+
project's config owns, and [data-on] on the error line, which is the only
|
|
1758
|
+
part of the stack that is conditionally present. */
|
|
1759
|
+
|
|
1760
|
+
/* No margin of its own: distance between fields belongs to the group that
|
|
1761
|
+
holds them (.atx-field-group), so a field is the same object wherever it is
|
|
1762
|
+
mounted and a lone one does not push a gap below itself. */
|
|
1763
|
+
.atx-field {
|
|
1764
|
+
display: flex;
|
|
1765
|
+
flex-direction: column;
|
|
1766
|
+
}
|
|
1767
|
+
|
|
1768
|
+
/* A field the project's own config owns. The dim lands on the control, not on
|
|
1769
|
+
the whole stack: the label and the help text are what explain *why* it is
|
|
1770
|
+
locked, and dimming the explanation along with the thing it explains is the
|
|
1771
|
+
one part a reader still needs at full strength. */
|
|
1772
|
+
.atx-field[data-locked] [data-input],
|
|
1773
|
+
.atx-field[data-locked] .atx-field-check {
|
|
1774
|
+
opacity: 0.5;
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1777
|
+
/* Full opacity, not a dimmed foreground: a label is read, and dimming it was
|
|
1778
|
+
doing the job that a second ink tier does properly. */
|
|
1779
|
+
.atx-field-label {
|
|
1780
|
+
display: block;
|
|
1781
|
+
margin-bottom: 8px;
|
|
1782
|
+
color: var(--atx-foreground);
|
|
1783
|
+
font: 500 14px/1.4 var(--atx-font-ui);
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1786
|
+
/* 14px, not a size down. shadcn's field description is the same size as the
|
|
1787
|
+
label and separated by colour alone, which is what keeps a form of mostly
|
|
1788
|
+
help text readable rather than a wall of small print. */
|
|
1789
|
+
.atx-field-help {
|
|
1790
|
+
margin-top: 6px;
|
|
1791
|
+
color: var(--atx-muted-fg);
|
|
1792
|
+
font: 400 14px/1.45 var(--atx-font-ui);
|
|
1793
|
+
}
|
|
1794
|
+
|
|
1795
|
+
.atx-field-error {
|
|
1796
|
+
display: none;
|
|
1797
|
+
margin-top: 6px;
|
|
1798
|
+
color: var(--atx-destructive);
|
|
1799
|
+
font: 400 12px/1.45 var(--atx-font-ui);
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1802
|
+
.atx-field-error[data-on] {
|
|
1803
|
+
display: block;
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
.atx-field-check {
|
|
1807
|
+
display: flex;
|
|
1808
|
+
align-items: center;
|
|
1809
|
+
gap: 8px;
|
|
1810
|
+
min-height: 36px;
|
|
1811
|
+
color: var(--atx-foreground);
|
|
1812
|
+
font: 400 14px var(--atx-font-ui);
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1815
|
+
.atx-field-check-hint {
|
|
1816
|
+
opacity: 0.7;
|
|
1817
|
+
}
|
|
1818
|
+
|
|
1819
|
+
.atx-field-checkbox,
|
|
1820
|
+
.atx-field-select {
|
|
1821
|
+
cursor: pointer;
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1824
|
+
/* field-sizing grows the box with the prose in it, which is what a excerpt or
|
|
1825
|
+
a description wants; min-height is the floor for browsers without it, so the
|
|
1826
|
+
fallback is a fixed textarea rather than a collapsed one. */
|
|
1827
|
+
.atx-field-textarea {
|
|
1828
|
+
min-height: 64px;
|
|
1829
|
+
field-sizing: content;
|
|
1830
|
+
max-height: 40vh;
|
|
1831
|
+
}
|
|
1832
|
+
|
|
1833
|
+
/* A shape the panel cannot edit: read-only, monospaced, and dimmed so it
|
|
1834
|
+
reads as a report of the file rather than as an input. */
|
|
1835
|
+
.atx-field-json {
|
|
1836
|
+
min-height: 48px;
|
|
1837
|
+
font: 400 12px/1.5 var(--atx-font-mono);
|
|
1838
|
+
opacity: 0.6;
|
|
1839
|
+
resize: vertical;
|
|
1840
|
+
}
|
|
1841
|
+
|
|
1842
|
+
|
|
1843
|
+
|
|
1844
|
+
/* == Settings drawer =======================================================
|
|
1845
|
+
editors/settings-panel.ts. The option controls themselves come from
|
|
1846
|
+
fields.ts; what is here is the drawer's own prose, the Unsplash key section
|
|
1847
|
+
and the three status lines. Four states are attributes: [data-off] on the
|
|
1848
|
+
key section while the photo source is switched off, [data-on] on the error
|
|
1849
|
+
and gitignore-warning lines, [data-hidden] on the clear-key button, and
|
|
1850
|
+
[data-tone] / [data-mono] on a status word. */
|
|
1851
|
+
|
|
1852
|
+
/* A stack of cards under the tab strip, spaced like the entry drawer's. */
|
|
1853
|
+
.atx-settings-pane {
|
|
1854
|
+
display: flex;
|
|
1855
|
+
flex-direction: column;
|
|
1856
|
+
gap: 16px;
|
|
1857
|
+
padding-top: 16px;
|
|
1858
|
+
}
|
|
1859
|
+
|
|
1860
|
+
.atx-settings-status,
|
|
1861
|
+
.atx-settings-key-status {
|
|
1862
|
+
display: flex;
|
|
1863
|
+
align-items: center;
|
|
1864
|
+
gap: 6px;
|
|
1865
|
+
font: 13px var(--atx-font-ui);
|
|
1866
|
+
}
|
|
1867
|
+
|
|
1868
|
+
.atx-settings-status {
|
|
1869
|
+
margin: 0;
|
|
1870
|
+
}
|
|
1871
|
+
|
|
1872
|
+
.atx-settings-key-status {
|
|
1873
|
+
margin: 0 0 10px;
|
|
1874
|
+
color: var(--atx-foreground);
|
|
1875
|
+
}
|
|
1876
|
+
|
|
1877
|
+
.atx-settings-error {
|
|
1878
|
+
display: none;
|
|
1879
|
+
margin: 10px 0 0;
|
|
1880
|
+
color: var(--atx-warning);
|
|
1881
|
+
font: 13px/1.5 var(--atx-font-ui);
|
|
1882
|
+
}
|
|
1883
|
+
|
|
1884
|
+
.atx-settings-warning {
|
|
1885
|
+
display: none;
|
|
1886
|
+
margin: 12px 0 0;
|
|
1887
|
+
color: var(--atx-warning);
|
|
1888
|
+
font: 12px/1.5 var(--atx-font-ui);
|
|
1889
|
+
}
|
|
1890
|
+
|
|
1891
|
+
.atx-settings-error[data-on],
|
|
1892
|
+
.atx-settings-warning[data-on] {
|
|
1893
|
+
display: block;
|
|
1894
|
+
}
|
|
1895
|
+
|
|
1896
|
+
/* The access key is a secret with its own endpoint, not an option, so it is a
|
|
1897
|
+
card of its own rather than a heading inside the options card. It dims
|
|
1898
|
+
whole while the photo source it belongs to is off. */
|
|
1899
|
+
|
|
1900
|
+
.atx-settings-key-section[data-off] {
|
|
1901
|
+
opacity: 0.55;
|
|
1902
|
+
}
|
|
1903
|
+
|
|
1904
|
+
/* Body size, muted -- a card description that happens to carry a link, so it
|
|
1905
|
+
matches .atx-card-desc rather than being a size down from it. */
|
|
1906
|
+
.atx-settings-blurb {
|
|
1907
|
+
margin: 0 0 12px;
|
|
1908
|
+
color: var(--atx-muted-fg);
|
|
1909
|
+
font: 400 14px/1.45 var(--atx-font-ui);
|
|
1910
|
+
}
|
|
1911
|
+
|
|
1912
|
+
.atx-settings-link {
|
|
1913
|
+
color: var(--atx-brand-text);
|
|
1914
|
+
}
|
|
1915
|
+
|
|
1916
|
+
.atx-settings-hint {
|
|
1917
|
+
margin: 8px 0 0;
|
|
1918
|
+
color: var(--atx-muted-fg);
|
|
1919
|
+
font: 12px/1.5 var(--atx-font-ui);
|
|
1920
|
+
}
|
|
1921
|
+
|
|
1922
|
+
.atx-settings-row {
|
|
1923
|
+
display: flex;
|
|
1924
|
+
gap: 6px;
|
|
1925
|
+
}
|
|
1926
|
+
|
|
1927
|
+
.atx-settings-key {
|
|
1928
|
+
flex: 1 1 auto;
|
|
1929
|
+
}
|
|
1930
|
+
|
|
1931
|
+
/* The reveal toggle keeps its own width beside the growing key field. */
|
|
1932
|
+
.atx-settings-row > .atx-btn-outline {
|
|
1933
|
+
flex: 0 0 auto;
|
|
1934
|
+
}
|
|
1935
|
+
|
|
1936
|
+
.atx-settings-key-actions {
|
|
1937
|
+
display: flex;
|
|
1938
|
+
}
|
|
1939
|
+
|
|
1940
|
+
/* A destructive button pushes itself to the far left of a footer; here it is
|
|
1941
|
+
the only thing in its own row, so that margin has nothing to do. */
|
|
1942
|
+
.atx-settings-key-actions > .atx-btn-destructive {
|
|
1943
|
+
margin-right: 0;
|
|
1944
|
+
}
|
|
1945
|
+
|
|
1946
|
+
.atx-settings-key-actions > .atx-btn-destructive[data-hidden] {
|
|
1947
|
+
display: none;
|
|
1948
|
+
}
|
|
1949
|
+
|
|
1950
|
+
.atx-settings-text {
|
|
1951
|
+
font: 13px var(--atx-font-ui);
|
|
1952
|
+
}
|
|
1953
|
+
|
|
1954
|
+
.atx-settings-text[data-tone='muted'] {
|
|
1955
|
+
color: var(--atx-muted-fg);
|
|
1956
|
+
}
|
|
1957
|
+
|
|
1958
|
+
/* successText, not success: the plain token is a *background* — a dark green
|
|
1959
|
+
that reaches 2.7:1 as ink on a panel, which is the mistake the two-token
|
|
1960
|
+
split exists to prevent. */
|
|
1961
|
+
.atx-settings-text[data-tone='ok'] {
|
|
1962
|
+
color: var(--atx-success-text);
|
|
1963
|
+
}
|
|
1964
|
+
|
|
1965
|
+
.atx-settings-text[data-tone='warn'] {
|
|
1966
|
+
color: var(--atx-warning);
|
|
1967
|
+
}
|
|
1968
|
+
|
|
1969
|
+
.atx-settings-text[data-mono] {
|
|
1970
|
+
font: 12px var(--atx-font-mono);
|
|
1971
|
+
}
|
|
1972
|
+
|
|
1973
|
+
/* Muted, not amber: an option the project set in its own config is a normal
|
|
1974
|
+
state, and the one warning colour is spent on the uncommitted-secret line. */
|
|
1975
|
+
.atx-settings-lock {
|
|
1976
|
+
display: flex;
|
|
1977
|
+
align-items: center;
|
|
1978
|
+
gap: 6px;
|
|
1979
|
+
margin: 8px 0 0;
|
|
1980
|
+
color: var(--atx-muted-fg);
|
|
1981
|
+
font: 12px/1.45 var(--atx-font-ui);
|
|
1982
|
+
}
|
|
1983
|
+
|
|
1984
|
+
/* == Collections drawer ====================================================
|
|
1985
|
+
editors/collections-panel.ts. A field row spans two stores, and the drawer's
|
|
1986
|
+
job is to keep saying which is which, so most of what is here is the chrome
|
|
1987
|
+
that does the saying: the legend, the two captioned groups, the badges.
|
|
1988
|
+
States are attributes: [data-removed] on a card whose removal is queued,
|
|
1989
|
+
[data-off] on the schema half of a field the project keeps read-only,
|
|
1990
|
+
[data-hidden] on the Options row, which only a select field has,
|
|
1991
|
+
[data-on] on the error line, and [data-tone] on a badge or a note. */
|
|
1992
|
+
|
|
1993
|
+
.atx-collections {
|
|
1994
|
+
padding-top: 12px;
|
|
1995
|
+
}
|
|
1996
|
+
|
|
1997
|
+
/* The entry rows are whole-width buttons that read as rows in a list. The
|
|
1998
|
+
collection rows come from group.ts::item and only add the pointer -- these
|
|
1999
|
+
carry the same box as one, so both lists in the drawer are the same object. */
|
|
2000
|
+
.atx-collections-item {
|
|
2001
|
+
display: block;
|
|
2002
|
+
width: 100%;
|
|
2003
|
+
margin-bottom: 8px;
|
|
2004
|
+
padding: 10px 12px;
|
|
2005
|
+
border: 1px solid var(--atx-border);
|
|
2006
|
+
border-radius: var(--atx-radius-lg);
|
|
2007
|
+
background: var(--atx-card);
|
|
2008
|
+
color: var(--atx-foreground);
|
|
2009
|
+
text-align: left;
|
|
2010
|
+
cursor: pointer;
|
|
2011
|
+
}
|
|
2012
|
+
|
|
2013
|
+
.atx-collections-row {
|
|
2014
|
+
cursor: pointer;
|
|
2015
|
+
}
|
|
2016
|
+
|
|
2017
|
+
/* Hover moves the surface, the way a menu item and a tree row do. The chevron
|
|
2018
|
+
is the affordance at rest; the fill is the confirmation under the pointer. */
|
|
2019
|
+
.atx-collections-row:hover,
|
|
2020
|
+
.atx-collections-row:focus-visible {
|
|
2021
|
+
background: var(--atx-accent);
|
|
2022
|
+
outline: none;
|
|
2023
|
+
}
|
|
2024
|
+
|
|
2025
|
+
.atx-collections-row:focus-visible {
|
|
2026
|
+
box-shadow: 0 0 0 2px var(--atx-ring);
|
|
2027
|
+
}
|
|
2028
|
+
|
|
2029
|
+
.atx-collections-item-title {
|
|
2030
|
+
display: flex;
|
|
2031
|
+
align-items: center;
|
|
2032
|
+
gap: 8px;
|
|
2033
|
+
}
|
|
2034
|
+
|
|
2035
|
+
/* The row's title comes from group.ts::item, which already sets the type; it
|
|
2036
|
+
only needs room for the badges that sit beside the name. */
|
|
2037
|
+
.atx-collections-row .atx-item-title {
|
|
2038
|
+
display: flex;
|
|
2039
|
+
align-items: center;
|
|
2040
|
+
gap: 8px;
|
|
2041
|
+
overflow: visible;
|
|
2042
|
+
}
|
|
2043
|
+
|
|
2044
|
+
.atx-collections-item-title {
|
|
2045
|
+
font: 500 14px var(--atx-font-ui);
|
|
2046
|
+
}
|
|
2047
|
+
|
|
2048
|
+
.atx-collections-item-meta,
|
|
2049
|
+
.atx-collections-meta {
|
|
2050
|
+
color: var(--atx-muted-fg);
|
|
2051
|
+
font: 12px var(--atx-font-mono);
|
|
2052
|
+
}
|
|
2053
|
+
|
|
2054
|
+
/* Monospaced, because it is a path and two counts -- data about the row, not
|
|
2055
|
+
prose. Same size as any other item description.
|
|
2056
|
+
|
|
2057
|
+
It wraps rather than ellipsing, unlike every other item description: the
|
|
2058
|
+
switch and its label take real width out of this row, and a truncated
|
|
2059
|
+
directory tells you less than a wrapped one. The separators are spaces, so a
|
|
2060
|
+
wrap lands between two facts rather than inside a path. */
|
|
2061
|
+
.atx-collections-row .atx-item-desc {
|
|
2062
|
+
overflow: visible;
|
|
2063
|
+
white-space: normal;
|
|
2064
|
+
text-overflow: clip;
|
|
2065
|
+
font-family: var(--atx-font-mono);
|
|
2066
|
+
}
|
|
2067
|
+
|
|
2068
|
+
.atx-collections-item-meta {
|
|
2069
|
+
margin-top: 4px;
|
|
2070
|
+
}
|
|
2071
|
+
|
|
2072
|
+
/* The line under a collection's name: which directory, how many entries, what
|
|
2073
|
+
kind of schema. 12px is the item-description size, not a size below it -- it
|
|
2074
|
+
is read, and shrinking it was the reason the header felt crowded. */
|
|
2075
|
+
.atx-collections-meta {
|
|
2076
|
+
margin-top: 4px;
|
|
2077
|
+
margin-bottom: 16px;
|
|
2078
|
+
font-size: 12px;
|
|
2079
|
+
}
|
|
2080
|
+
|
|
2081
|
+
/* The one button on the collections list, under the rows. */
|
|
2082
|
+
.atx-collections-list > .atx-btn-outline {
|
|
2083
|
+
margin-top: 8px;
|
|
2084
|
+
}
|
|
2085
|
+
|
|
2086
|
+
.atx-collections-fieldspane,
|
|
2087
|
+
.atx-collections-items {
|
|
2088
|
+
padding-top: 16px;
|
|
2089
|
+
}
|
|
2090
|
+
|
|
2091
|
+
.atx-collections-head,
|
|
2092
|
+
.atx-collections-itembar {
|
|
2093
|
+
display: flex;
|
|
2094
|
+
align-items: center;
|
|
2095
|
+
gap: 8px;
|
|
2096
|
+
}
|
|
2097
|
+
|
|
2098
|
+
.atx-collections-head {
|
|
2099
|
+
margin-bottom: 8px;
|
|
2100
|
+
}
|
|
2101
|
+
|
|
2102
|
+
.atx-collections-head-create {
|
|
2103
|
+
margin-bottom: 16px;
|
|
2104
|
+
}
|
|
2105
|
+
|
|
2106
|
+
.atx-collections-itembar {
|
|
2107
|
+
margin-bottom: 16px;
|
|
2108
|
+
}
|
|
2109
|
+
|
|
2110
|
+
.atx-collections-title {
|
|
2111
|
+
color: var(--atx-foreground);
|
|
2112
|
+
font: 500 14px var(--atx-font-ui);
|
|
2113
|
+
}
|
|
2114
|
+
|
|
2115
|
+
.atx-collections-spacer {
|
|
2116
|
+
flex: 1 1 auto;
|
|
2117
|
+
}
|
|
2118
|
+
|
|
2119
|
+
.atx-collections-itemcount {
|
|
2120
|
+
flex: 1 1 auto;
|
|
2121
|
+
color: var(--atx-muted-fg);
|
|
2122
|
+
font: 14px var(--atx-font-ui);
|
|
2123
|
+
}
|
|
2124
|
+
|
|
2125
|
+
/* A column with a gap rather than a margin on each card: the gap belongs to
|
|
2126
|
+
the list, so a card is the same object wherever it is mounted and the last
|
|
2127
|
+
one does not push a space below itself. 12px is the item-group rung. */
|
|
2128
|
+
.atx-collections-fields,
|
|
2129
|
+
.atx-collections-newfields {
|
|
2130
|
+
display: flex;
|
|
2131
|
+
flex-direction: column;
|
|
2132
|
+
gap: 12px;
|
|
2133
|
+
margin-top: 16px;
|
|
2134
|
+
}
|
|
2135
|
+
|
|
2136
|
+
.atx-collections-error {
|
|
2137
|
+
display: none;
|
|
2138
|
+
margin: 16px 0 0;
|
|
2139
|
+
color: var(--atx-warning);
|
|
2140
|
+
font: 13px/1.5 var(--atx-font-ui);
|
|
2141
|
+
}
|
|
2142
|
+
|
|
2143
|
+
.atx-collections-error[data-on] {
|
|
2144
|
+
display: block;
|
|
2145
|
+
}
|
|
2146
|
+
|
|
2147
|
+
/* The footer band's action slot, filled by whichever view the pane is showing.
|
|
2148
|
+
display:contents so an empty slot takes no space in the band and a filled one
|
|
2149
|
+
lays its button out as if the slot were not there -- the list view leaves it
|
|
2150
|
+
empty, and a 0-width flex item would still collect the band's gap. */
|
|
2151
|
+
.atx-collections-primary {
|
|
2152
|
+
display: contents;
|
|
2153
|
+
}
|
|
2154
|
+
|
|
2155
|
+
/* One field: its name and remove control, then the two stores side by side.
|
|
2156
|
+
16px all round and on the item rung for radius -- this is a card in a list
|
|
2157
|
+
of cards, and the 10/12 it used to carry made a form of nine of them read as
|
|
2158
|
+
one dense block rather than nine things. */
|
|
2159
|
+
.atx-collections-field {
|
|
2160
|
+
padding: 16px;
|
|
2161
|
+
border: 1px solid var(--atx-border);
|
|
2162
|
+
border-radius: var(--atx-radius-lg);
|
|
2163
|
+
background: var(--atx-card);
|
|
2164
|
+
}
|
|
2165
|
+
|
|
2166
|
+
/* Queued for removal, not removed: the card dims and the button offers the
|
|
2167
|
+
undo, and nothing is written until Save. */
|
|
2168
|
+
.atx-collections-field[data-removed] {
|
|
2169
|
+
opacity: 0.45;
|
|
2170
|
+
}
|
|
2171
|
+
|
|
2172
|
+
.atx-collections-field-head {
|
|
2173
|
+
display: flex;
|
|
2174
|
+
align-items: center;
|
|
2175
|
+
gap: 8px;
|
|
2176
|
+
margin-bottom: 14px;
|
|
2177
|
+
}
|
|
2178
|
+
|
|
2179
|
+
.atx-collections-field-name {
|
|
2180
|
+
color: var(--atx-foreground);
|
|
2181
|
+
font: 600 12px var(--atx-font-mono);
|
|
2182
|
+
}
|
|
2183
|
+
|
|
2184
|
+
/* The zod expression this field currently compiles to, verbatim. Set off by
|
|
2185
|
+
the card's own rule, so it reads as a footnote about the field rather than
|
|
2186
|
+
as one more line of it. */
|
|
2187
|
+
.atx-collections-expr {
|
|
2188
|
+
display: block;
|
|
2189
|
+
margin-top: 16px;
|
|
2190
|
+
padding-top: 12px;
|
|
2191
|
+
border-top: 1px solid var(--atx-border);
|
|
2192
|
+
color: var(--atx-muted-fg);
|
|
2193
|
+
font: 12px var(--atx-font-mono);
|
|
2194
|
+
word-break: break-all;
|
|
2195
|
+
}
|
|
2196
|
+
|
|
2197
|
+
.atx-collections-addfield {
|
|
2198
|
+
margin-top: 16px;
|
|
2199
|
+
padding: 16px;
|
|
2200
|
+
border: 1px dashed var(--atx-border);
|
|
2201
|
+
border-radius: var(--atx-radius-lg);
|
|
2202
|
+
}
|
|
2203
|
+
|
|
2204
|
+
.atx-collections-addfield > .atx-btn-outline {
|
|
2205
|
+
margin-top: 12px;
|
|
2206
|
+
}
|
|
2207
|
+
|
|
2208
|
+
/* The two-store legend — the one piece of chrome that explains the drawer. */
|
|
2209
|
+
.atx-collections-legend {
|
|
2210
|
+
padding: 16px;
|
|
2211
|
+
border: 1px solid var(--atx-border);
|
|
2212
|
+
border-radius: var(--atx-radius-xl);
|
|
2213
|
+
background: var(--atx-background);
|
|
2214
|
+
color: var(--atx-muted-fg);
|
|
2215
|
+
font: 13px/1.6 var(--atx-font-ui);
|
|
2216
|
+
}
|
|
2217
|
+
|
|
2218
|
+
/* The colour is repeated from the wrapper rather than inherited: a host page's
|
|
2219
|
+
own paragraph colour rule outranks an inherited value, and silently
|
|
2220
|
+
repainted this legend in the page's body colour. */
|
|
2221
|
+
.atx-collections-legend-line {
|
|
2222
|
+
margin: 0;
|
|
2223
|
+
color: var(--atx-muted-fg);
|
|
2224
|
+
}
|
|
2225
|
+
|
|
2226
|
+
.atx-collections-legend-word {
|
|
2227
|
+
color: var(--atx-foreground);
|
|
2228
|
+
}
|
|
2229
|
+
|
|
2230
|
+
/* The field's two stores, side by side: they describe the *same* field, and
|
|
2231
|
+
the card's whole job is letting you read one against the other. auto-fit
|
|
2232
|
+
rather than a fixed pair -- a drawer narrow enough that a control would be
|
|
2233
|
+
squeezed off its label stacks them again instead. */
|
|
2234
|
+
.atx-collections-stores {
|
|
2235
|
+
display: grid;
|
|
2236
|
+
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
2237
|
+
align-items: start;
|
|
2238
|
+
gap: 16px 20px;
|
|
2239
|
+
}
|
|
2240
|
+
|
|
2241
|
+
.atx-collections-group {
|
|
2242
|
+
min-width: 0;
|
|
2243
|
+
padding-left: 14px;
|
|
2244
|
+
border-left: 2px solid var(--atx-border);
|
|
2245
|
+
}
|
|
2246
|
+
|
|
2247
|
+
.atx-collections-group[data-off] {
|
|
2248
|
+
opacity: 0.6;
|
|
2249
|
+
}
|
|
2250
|
+
|
|
2251
|
+
.atx-collections-caption {
|
|
2252
|
+
margin-bottom: 12px;
|
|
2253
|
+
color: var(--atx-muted-fg);
|
|
2254
|
+
font: 500 12px var(--atx-font-ui);
|
|
2255
|
+
letter-spacing: 0.06em;
|
|
2256
|
+
text-transform: uppercase;
|
|
2257
|
+
}
|
|
2258
|
+
|
|
2259
|
+
/* Muted sits on the label below, not here: colour set on the row is inherited
|
|
2260
|
+
by whatever the row holds, which quietly greys the value as well as the word
|
|
2261
|
+
naming it. */
|
|
2262
|
+
.atx-collections-control {
|
|
2263
|
+
display: flex;
|
|
2264
|
+
align-items: center;
|
|
2265
|
+
gap: 12px;
|
|
2266
|
+
margin: 0 0 12px;
|
|
2267
|
+
color: var(--atx-foreground);
|
|
2268
|
+
font: 14px var(--atx-font-ui);
|
|
2269
|
+
}
|
|
2270
|
+
|
|
2271
|
+
/* The last control in a store carries no gap of its own -- the card's padding
|
|
2272
|
+
is the space under it. */
|
|
2273
|
+
.atx-collections-control:last-child {
|
|
2274
|
+
margin-bottom: 0;
|
|
2275
|
+
}
|
|
2276
|
+
|
|
2277
|
+
/* Options belong to a select field and to nothing else. */
|
|
2278
|
+
.atx-collections-control[data-hidden] {
|
|
2279
|
+
display: none;
|
|
2280
|
+
}
|
|
2281
|
+
|
|
2282
|
+
.atx-collections-control-label {
|
|
2283
|
+
flex: 0 0 78px;
|
|
2284
|
+
color: var(--atx-muted-fg);
|
|
2285
|
+
}
|
|
2286
|
+
|
|
2287
|
+
/* min-width:0 so a control shrinks with its column: side by side, a store
|
|
2288
|
+
is half the card wide, and an input's intrinsic width would otherwise push
|
|
2289
|
+
the row past it. */
|
|
2290
|
+
.atx-collections-input,
|
|
2291
|
+
.atx-collections-select {
|
|
2292
|
+
min-width: 0;
|
|
2293
|
+
flex: 1 1 auto;
|
|
2294
|
+
}
|
|
2295
|
+
|
|
2296
|
+
.atx-collections-checkbox {
|
|
2297
|
+
display: flex;
|
|
2298
|
+
flex: 1 1 auto;
|
|
2299
|
+
align-items: center;
|
|
2300
|
+
gap: 6px;
|
|
2301
|
+
}
|
|
2302
|
+
|
|
2303
|
+
.atx-collections-check {
|
|
2304
|
+
margin: 0;
|
|
2305
|
+
accent-color: var(--atx-primary);
|
|
2306
|
+
}
|
|
2307
|
+
|
|
2308
|
+
.atx-collections-check-hint {
|
|
2309
|
+
color: var(--atx-muted-fg);
|
|
2310
|
+
font: 14px var(--atx-font-ui);
|
|
2311
|
+
}
|
|
2312
|
+
|
|
2313
|
+
/* The Content editor switch rides in a collection row's action slot, beside the
|
|
2314
|
+
chevron. Its label is a fixed two words, so nothing shifts as it flips. */
|
|
2315
|
+
.atx-collections-pageedit {
|
|
2316
|
+
flex: 0 0 auto;
|
|
2317
|
+
}
|
|
2318
|
+
|
|
2319
|
+
/* The detail route a collection was matched to. Its own line under the
|
|
2320
|
+
description, one step quieter, because it answers a different question: not
|
|
2321
|
+
what this collection is, but where the switch beside it takes effect. */
|
|
2322
|
+
.atx-collections-route {
|
|
2323
|
+
display: flex;
|
|
2324
|
+
align-items: center;
|
|
2325
|
+
gap: 4px;
|
|
2326
|
+
margin-top: 3px;
|
|
2327
|
+
color: var(--atx-faint-fg);
|
|
2328
|
+
font: 11px/1.4 var(--atx-font-mono);
|
|
2329
|
+
}
|
|
2330
|
+
|
|
2331
|
+
/* The meta-tag snippet a collection with no detected route offers. The note is
|
|
2332
|
+
a row of icon + text until it carries one; then the block and its copy button
|
|
2333
|
+
each take a line of their own. */
|
|
2334
|
+
.atx-collections-note-snippet {
|
|
2335
|
+
flex-wrap: wrap;
|
|
2336
|
+
}
|
|
2337
|
+
|
|
2338
|
+
.atx-collections-snippet {
|
|
2339
|
+
flex: 1 0 100%;
|
|
2340
|
+
margin: 8px 0 6px;
|
|
2341
|
+
padding: 8px 10px;
|
|
2342
|
+
overflow-x: auto;
|
|
2343
|
+
border-radius: var(--atx-radius-md);
|
|
2344
|
+
background: var(--atx-background);
|
|
2345
|
+
color: var(--atx-foreground);
|
|
2346
|
+
font: 11px/1.55 var(--atx-font-mono);
|
|
2347
|
+
white-space: pre;
|
|
2348
|
+
}
|
|
2349
|
+
|
|
2350
|
+
/* A field typed into the add form but not yet written. Outlined in the brand
|
|
2351
|
+
colour because it is the only thing on screen that is not yet in the file. */
|
|
2352
|
+
.atx-collections-new {
|
|
2353
|
+
display: flex;
|
|
2354
|
+
align-items: center;
|
|
2355
|
+
gap: 8px;
|
|
2356
|
+
padding: 10px 12px;
|
|
2357
|
+
border: 1px solid var(--atx-brand);
|
|
2358
|
+
border-radius: var(--atx-radius-lg);
|
|
2359
|
+
background: var(--atx-card);
|
|
2360
|
+
}
|
|
2361
|
+
|
|
2362
|
+
.atx-collections-new-name {
|
|
2363
|
+
color: var(--atx-foreground);
|
|
2364
|
+
font: 600 12px var(--atx-font-mono);
|
|
2365
|
+
}
|
|
2366
|
+
|
|
2367
|
+
.atx-collections-new-meta {
|
|
2368
|
+
flex: 1 1 auto;
|
|
2369
|
+
color: var(--atx-muted-fg);
|
|
2370
|
+
font: 12px var(--atx-font-ui);
|
|
2371
|
+
}
|
|
2372
|
+
|
|
2373
|
+
/* The way back out of a collection. It wears the corner-action shape from the
|
|
2374
|
+
button system and adds only the one thing that shape cannot know: the
|
|
2375
|
+
chevron points the other way, since it is the forward one turned around.
|
|
2376
|
+
|
|
2377
|
+
Neutral, not brand: brandText means *this is on your page and editable*,
|
|
2378
|
+
and a back button is the tool talking about its own navigation. */
|
|
2379
|
+
.atx-collections-back > .atx-ico:first-child {
|
|
2380
|
+
transform: rotate(180deg);
|
|
2381
|
+
}
|
|
2382
|
+
|
|
2383
|
+
.atx-collections-blurb {
|
|
2384
|
+
margin: 0 0 12px;
|
|
2385
|
+
color: var(--atx-muted-fg);
|
|
2386
|
+
font: 13px/1.5 var(--atx-font-ui);
|
|
2387
|
+
}
|
|
2388
|
+
|
|
2389
|
+
.atx-collections-note {
|
|
2390
|
+
display: flex;
|
|
2391
|
+
align-items: flex-start;
|
|
2392
|
+
gap: 5px;
|
|
2393
|
+
margin: 0 0 10px;
|
|
2394
|
+
font: 12px/1.5 var(--atx-font-ui);
|
|
2395
|
+
}
|
|
2396
|
+
|
|
2397
|
+
.atx-collections-note[data-tone='warn'] {
|
|
2398
|
+
color: var(--atx-warning);
|
|
2399
|
+
}
|
|
2400
|
+
|
|
2401
|
+
.atx-collections-note[data-tone='muted'] {
|
|
2402
|
+
color: var(--atx-muted-fg);
|
|
2403
|
+
}
|
|
2404
|
+
|
|
2405
|
+
/* == Image panel and asset picker ==========================================
|
|
2406
|
+
editors/image.ts (the in-page <img> panel) and editors/asset-picker.ts (the
|
|
2407
|
+
entry drawer's image field). Both show a preview over a checkerboard, so a
|
|
2408
|
+
transparent PNG reads as transparent rather than as a hole. An <img> that
|
|
2409
|
+
fails to load takes [data-hidden] rather than showing a broken-image glyph;
|
|
2410
|
+
a retry that finally succeeds removes it again — see ui.ts::setFreshSrc. */
|
|
2411
|
+
|
|
2412
|
+
.atx-image-preview {
|
|
2413
|
+
width: 100%;
|
|
2414
|
+
height: 180px;
|
|
2415
|
+
max-height: 180px;
|
|
2416
|
+
margin-bottom: 10px;
|
|
2417
|
+
overflow: hidden;
|
|
2418
|
+
border: 1px solid var(--atx-border);
|
|
2419
|
+
border-radius: var(--atx-radius-lg);
|
|
2420
|
+
background: ${CHECKER(14)};
|
|
2421
|
+
}
|
|
2422
|
+
|
|
2423
|
+
/* No display of its own, deliberately: it inherits the inline default, which
|
|
2424
|
+
the 180px overflow-hidden box above clips to the same pixels a block would
|
|
2425
|
+
occupy. Plan 2 can settle it; a conversion may not. */
|
|
2426
|
+
.atx-image-preview-img {
|
|
2427
|
+
width: 100%;
|
|
2428
|
+
height: 100%;
|
|
2429
|
+
object-fit: contain;
|
|
2430
|
+
}
|
|
2431
|
+
|
|
2432
|
+
.atx-image-meta {
|
|
2433
|
+
margin: 0 0 12px;
|
|
2434
|
+
overflow: hidden;
|
|
2435
|
+
color: var(--atx-muted-fg);
|
|
2436
|
+
font: 11px var(--atx-font-mono);
|
|
2437
|
+
white-space: nowrap;
|
|
2438
|
+
text-overflow: ellipsis;
|
|
2439
|
+
}
|
|
2440
|
+
|
|
2441
|
+
.atx-note {
|
|
2442
|
+
margin: 0 0 12px;
|
|
2443
|
+
color: var(--atx-warning);
|
|
2444
|
+
font: 400 13px/1.5 var(--atx-font-ui);
|
|
2445
|
+
}
|
|
2446
|
+
|
|
2447
|
+
.atx-alt-label {
|
|
2448
|
+
display: block;
|
|
2449
|
+
margin-bottom: 6px;
|
|
2450
|
+
color: var(--atx-foreground);
|
|
2451
|
+
font: 500 14px/1 var(--atx-font-ui);
|
|
2452
|
+
}
|
|
2453
|
+
|
|
2454
|
+
/* Hand-built rather than via inputEl, so it carries the control baseline
|
|
2455
|
+
itself. Keep it in step with [data-input]. */
|
|
2456
|
+
.atx-alt-input {
|
|
2457
|
+
width: 100%;
|
|
2458
|
+
min-height: 32px;
|
|
2459
|
+
box-sizing: border-box;
|
|
2460
|
+
margin-bottom: 16px;
|
|
2461
|
+
padding: 4px 10px;
|
|
2462
|
+
border: 1px solid transparent;
|
|
2463
|
+
border-radius: var(--atx-radius-lg);
|
|
2464
|
+
background: var(--atx-input-bg);
|
|
2465
|
+
color: var(--atx-foreground);
|
|
2466
|
+
font: 400 14px/1.45 var(--atx-font-ui);
|
|
2467
|
+
outline: none;
|
|
2468
|
+
transition: background 200ms, border-color 200ms, box-shadow 200ms;
|
|
2469
|
+
}
|
|
2470
|
+
|
|
2471
|
+
.atx-alt-input:focus-visible {
|
|
2472
|
+
border-color: var(--atx-ring);
|
|
2473
|
+
box-shadow: 0 0 0 3px ${hexToRgba(COLOR.ring, 0.3)};
|
|
2474
|
+
}
|
|
2475
|
+
|
|
2476
|
+
/* Alt text that comes from an expression: readable, but not yours to type in. */
|
|
2477
|
+
.atx-alt-input[data-off] {
|
|
2478
|
+
opacity: 0.5;
|
|
2479
|
+
}
|
|
2480
|
+
|
|
2481
|
+
/* The six most recent assets. Mirrors RECENTS in editors/image.ts. */
|
|
2482
|
+
.atx-image-recents {
|
|
2483
|
+
display: grid;
|
|
2484
|
+
grid-template-columns: repeat(6, 1fr);
|
|
2485
|
+
gap: 6px;
|
|
2486
|
+
}
|
|
2487
|
+
|
|
2488
|
+
.atx-image-recents-label {
|
|
2489
|
+
display: flex;
|
|
2490
|
+
align-items: baseline;
|
|
2491
|
+
gap: 8px;
|
|
2492
|
+
margin: 0 0 8px;
|
|
2493
|
+
color: var(--atx-foreground);
|
|
2494
|
+
font: 500 14px/1 var(--atx-font-ui);
|
|
2495
|
+
}
|
|
2496
|
+
|
|
2497
|
+
/* The strip is a convenience; if its listing fails it goes away quietly and
|
|
2498
|
+
the modal's Browse all still works. */
|
|
2499
|
+
.atx-image-recents-label[data-hidden] {
|
|
2500
|
+
display: none;
|
|
2501
|
+
}
|
|
2502
|
+
|
|
2503
|
+
/* Wears the corner-action shape; the only thing left here is where it sits. */
|
|
2504
|
+
.atx-image-browse-all {
|
|
2505
|
+
margin-left: auto;
|
|
2506
|
+
}
|
|
2507
|
+
|
|
2508
|
+
.atx-image-recent {
|
|
2509
|
+
width: 100%;
|
|
2510
|
+
padding: 0;
|
|
2511
|
+
aspect-ratio: 4 / 3;
|
|
2512
|
+
overflow: hidden;
|
|
2513
|
+
border: 1px solid var(--atx-border);
|
|
2514
|
+
border-radius: var(--atx-radius-lg);
|
|
2515
|
+
outline: none;
|
|
2516
|
+
background: ${CHECKER(10)};
|
|
2517
|
+
cursor: pointer;
|
|
2518
|
+
}
|
|
2519
|
+
|
|
2520
|
+
/* The one already on the page. */
|
|
2521
|
+
.atx-image-recent[data-current] {
|
|
2522
|
+
border-color: var(--atx-primary);
|
|
2523
|
+
outline: 1px solid var(--atx-primary);
|
|
2524
|
+
}
|
|
2525
|
+
|
|
2526
|
+
.atx-image-recent-thumb {
|
|
2527
|
+
display: block;
|
|
2528
|
+
width: 100%;
|
|
2529
|
+
height: 100%;
|
|
2530
|
+
object-fit: cover;
|
|
2531
|
+
}
|
|
2532
|
+
|
|
2533
|
+
.atx-image-preview-img[data-hidden],
|
|
2534
|
+
.atx-image-recent-thumb[data-hidden],
|
|
2535
|
+
.atx-image-field-thumb[data-hidden],
|
|
2536
|
+
.atx-image-field-empty[data-hidden],
|
|
2537
|
+
.atx-media-thumb[data-hidden],
|
|
2538
|
+
.atx-media-rail-img[data-hidden] {
|
|
2539
|
+
display: none;
|
|
2540
|
+
}
|
|
2541
|
+
|
|
2542
|
+
.atx-image-field {
|
|
2543
|
+
display: grid;
|
|
2544
|
+
gap: 8px;
|
|
2545
|
+
}
|
|
2546
|
+
|
|
2547
|
+
.atx-image-field-preview {
|
|
2548
|
+
display: flex;
|
|
2549
|
+
align-items: center;
|
|
2550
|
+
justify-content: center;
|
|
2551
|
+
width: 240px;
|
|
2552
|
+
height: 160px;
|
|
2553
|
+
padding: 0;
|
|
2554
|
+
overflow: hidden;
|
|
2555
|
+
border: 1px solid var(--atx-border);
|
|
2556
|
+
border-radius: var(--atx-radius-lg);
|
|
2557
|
+
background: ${CHECKER(16)};
|
|
2558
|
+
cursor: pointer;
|
|
2559
|
+
}
|
|
2560
|
+
|
|
2561
|
+
.atx-image-field-thumb {
|
|
2562
|
+
width: 100%;
|
|
2563
|
+
height: 100%;
|
|
2564
|
+
object-fit: cover;
|
|
2565
|
+
}
|
|
2566
|
+
|
|
2567
|
+
.atx-image-field-empty {
|
|
2568
|
+
color: var(--atx-muted-fg);
|
|
2569
|
+
font: 400 13px var(--atx-font-ui);
|
|
2570
|
+
pointer-events: none;
|
|
2571
|
+
}
|
|
2572
|
+
|
|
2573
|
+
.atx-image-field-row {
|
|
2574
|
+
display: flex;
|
|
2575
|
+
align-items: center;
|
|
2576
|
+
gap: 8px;
|
|
2577
|
+
}
|
|
2578
|
+
|
|
2579
|
+
.atx-image-field-path {
|
|
2580
|
+
flex: 1 1 auto;
|
|
2581
|
+
min-width: 0;
|
|
2582
|
+
font: 12px var(--atx-font-mono);
|
|
2583
|
+
}
|
|
2584
|
+
|
|
2585
|
+
/* Sits beside the path field in a flex row, so it takes the field's height
|
|
2586
|
+
and shape rather than a button's own. */
|
|
2587
|
+
/* Wears the shared outline button; the flex basis is the only thing left that
|
|
2588
|
+
belongs to this row rather than to the button system. */
|
|
2589
|
+
.atx-image-field-browse {
|
|
2590
|
+
flex: 0 0 auto;
|
|
2591
|
+
}
|
|
2592
|
+
|
|
2593
|
+
.atx-image-field-hint {
|
|
2594
|
+
color: var(--atx-muted-fg);
|
|
2595
|
+
font: 400 12px var(--atx-font-mono);
|
|
2596
|
+
}
|
|
2597
|
+
|
|
2598
|
+
/* == Media grid ============================================================
|
|
2599
|
+
editors/media-grid.ts. One tile shape for both panes; the caption sits
|
|
2600
|
+
outside the pick button so a credit link is clickable. [data-selected] is
|
|
2601
|
+
the ring and the tick together, and [aria-busy] — which the tile already
|
|
2602
|
+
carries for assistive tech — is also what dims it during an import. */
|
|
2603
|
+
|
|
2604
|
+
.atx-media-pane {
|
|
2605
|
+
flex: 1 1 auto;
|
|
2606
|
+
min-height: 0;
|
|
2607
|
+
padding: 2px;
|
|
2608
|
+
overflow-y: auto;
|
|
2609
|
+
}
|
|
2610
|
+
|
|
2611
|
+
/* 132px is the narrowest a tile can be and still read as a picture. */
|
|
2612
|
+
.atx-media-grid {
|
|
2613
|
+
display: grid;
|
|
2614
|
+
grid-template-columns: repeat(auto-fill, minmax(132px, 1fr));
|
|
2615
|
+
align-content: start;
|
|
2616
|
+
gap: 12px;
|
|
2617
|
+
}
|
|
2618
|
+
|
|
2619
|
+
.atx-media-more {
|
|
2620
|
+
display: flex;
|
|
2621
|
+
justify-content: center;
|
|
2622
|
+
padding: 14px 0 4px;
|
|
2623
|
+
}
|
|
2624
|
+
|
|
2625
|
+
.atx-media-tile {
|
|
2626
|
+
position: relative;
|
|
2627
|
+
min-width: 0;
|
|
2628
|
+
}
|
|
2629
|
+
|
|
2630
|
+
.atx-media-pick {
|
|
2631
|
+
position: relative;
|
|
2632
|
+
display: block;
|
|
2633
|
+
width: 100%;
|
|
2634
|
+
padding: 0;
|
|
2635
|
+
aspect-ratio: 4 / 3;
|
|
2636
|
+
overflow: hidden;
|
|
2637
|
+
border: 1px solid var(--atx-border);
|
|
2638
|
+
border-radius: var(--atx-radius-lg);
|
|
2639
|
+
background: ${CHECKER(12)};
|
|
2640
|
+
outline: 2px solid transparent;
|
|
2641
|
+
outline-offset: 2px;
|
|
2642
|
+
transition: outline-color 100ms;
|
|
2643
|
+
cursor: pointer;
|
|
2644
|
+
}
|
|
2645
|
+
|
|
2646
|
+
.atx-media-pick[data-selected] {
|
|
2647
|
+
outline-color: var(--atx-primary);
|
|
2648
|
+
}
|
|
2649
|
+
|
|
2650
|
+
.atx-media-pick[aria-busy='true'] {
|
|
2651
|
+
opacity: 0.45;
|
|
2652
|
+
pointer-events: none;
|
|
2653
|
+
cursor: progress;
|
|
2654
|
+
}
|
|
2655
|
+
|
|
2656
|
+
/* A tile that cannot be picked *here* -- an image() asset in a web-path picker
|
|
2657
|
+
or the reverse. It stays on screen and says why: the dimming is the shared
|
|
2658
|
+
:disabled rule above, and this is only the band that carries the reason.
|
|
2659
|
+
Text over the thumbnail, so it needs its own scrim rather than a token. */
|
|
2660
|
+
.atx-media-reason {
|
|
2661
|
+
position: absolute;
|
|
2662
|
+
right: 0;
|
|
2663
|
+
bottom: 0;
|
|
2664
|
+
left: 0;
|
|
2665
|
+
padding: 3px 6px;
|
|
2666
|
+
background: rgba(0, 0, 0, 0.72);
|
|
2667
|
+
color: var(--atx-foreground);
|
|
2668
|
+
font: 500 11px/1.4 var(--atx-font-ui);
|
|
2669
|
+
text-align: center;
|
|
2670
|
+
pointer-events: none;
|
|
2671
|
+
}
|
|
2672
|
+
|
|
2673
|
+
/* The caption belongs to the same tile, so it dims with it -- the :disabled
|
|
2674
|
+
rule above only reaches the button, and a full-strength filename under a
|
|
2675
|
+
greyed-out thumbnail reads as a rendering fault. */
|
|
2676
|
+
.atx-media-tile[data-off] .atx-media-cap {
|
|
2677
|
+
opacity: 0.5;
|
|
2678
|
+
}
|
|
2679
|
+
|
|
2680
|
+
.atx-media-thumb {
|
|
2681
|
+
display: block;
|
|
2682
|
+
width: 100%;
|
|
2683
|
+
height: 100%;
|
|
2684
|
+
object-fit: cover;
|
|
2685
|
+
}
|
|
2686
|
+
|
|
2687
|
+
/* Shown in place of an image that could not load — blocked by a CSP, offline,
|
|
2688
|
+
or deleted. The tile stays usable: its caption reads and it still picks. */
|
|
2689
|
+
.atx-media-fallback {
|
|
2690
|
+
display: none;
|
|
2691
|
+
align-items: center;
|
|
2692
|
+
justify-content: center;
|
|
2693
|
+
width: 100%;
|
|
2694
|
+
height: 100%;
|
|
2695
|
+
color: var(--atx-faint-fg);
|
|
2696
|
+
}
|
|
2697
|
+
|
|
2698
|
+
.atx-media-fallback[data-on] {
|
|
2699
|
+
display: flex;
|
|
2700
|
+
}
|
|
2701
|
+
|
|
2702
|
+
.atx-media-check {
|
|
2703
|
+
position: absolute;
|
|
2704
|
+
top: 6px;
|
|
2705
|
+
right: 6px;
|
|
2706
|
+
display: none;
|
|
2707
|
+
align-items: center;
|
|
2708
|
+
justify-content: center;
|
|
2709
|
+
width: 20px;
|
|
2710
|
+
height: 20px;
|
|
2711
|
+
border-radius: 50%;
|
|
2712
|
+
background: var(--atx-primary);
|
|
2713
|
+
color: var(--atx-primary-fg);
|
|
2714
|
+
pointer-events: none;
|
|
2715
|
+
}
|
|
2716
|
+
|
|
2717
|
+
.atx-media-pick[data-selected] .atx-media-check {
|
|
2718
|
+
display: flex;
|
|
2719
|
+
}
|
|
2720
|
+
|
|
2721
|
+
.atx-media-current {
|
|
2722
|
+
position: absolute;
|
|
2723
|
+
top: 6px;
|
|
2724
|
+
left: 6px;
|
|
2725
|
+
padding: 2px 6px;
|
|
2726
|
+
border-radius: var(--atx-radius-sm);
|
|
2727
|
+
background: rgba(0, 0, 0, 0.7);
|
|
2728
|
+
color: var(--atx-foreground);
|
|
2729
|
+
font: 500 11px var(--atx-font-ui);
|
|
2730
|
+
pointer-events: none;
|
|
2731
|
+
}
|
|
2732
|
+
|
|
2733
|
+
.atx-media-skeleton-thumb {
|
|
2734
|
+
width: 100%;
|
|
2735
|
+
aspect-ratio: 4 / 3;
|
|
2736
|
+
border: 1px solid var(--atx-border);
|
|
2737
|
+
border-radius: var(--atx-radius-lg);
|
|
2738
|
+
background: var(--atx-elevated);
|
|
2739
|
+
}
|
|
2740
|
+
|
|
2741
|
+
.atx-media-skeleton-cap {
|
|
2742
|
+
height: 10px;
|
|
2743
|
+
margin: 6px 0 0;
|
|
2744
|
+
border-radius: var(--atx-radius-sm);
|
|
2745
|
+
background: var(--atx-elevated);
|
|
2746
|
+
}
|
|
2747
|
+
|
|
2748
|
+
/* A message is a full-width row inside the grid, never laid out as a tile. */
|
|
2749
|
+
.atx-media-status {
|
|
2750
|
+
grid-column: 1 / -1;
|
|
2751
|
+
padding: 28px 12px;
|
|
2752
|
+
color: var(--atx-muted-fg);
|
|
2753
|
+
font: 14px/1.6 var(--atx-font-ui);
|
|
2754
|
+
text-align: center;
|
|
2755
|
+
}
|
|
2756
|
+
|
|
2757
|
+
/* Three buttons that were each a hand-copied outline variant -- same border,
|
|
2758
|
+
radius, height, ink, hover and focus ring, written out three times and free
|
|
2759
|
+
to drift. They wear .atx-btn-outline now; what is left here is only what is
|
|
2760
|
+
theirs: where each one sits. */
|
|
2761
|
+
.atx-btn-retry {
|
|
2762
|
+
margin: 12px auto 0;
|
|
2763
|
+
}
|
|
2764
|
+
|
|
2765
|
+
.atx-media-cap {
|
|
2766
|
+
margin: 6px 2px 0;
|
|
2767
|
+
overflow: hidden;
|
|
2768
|
+
color: var(--atx-muted-fg);
|
|
2769
|
+
font: 11px/1.4 var(--atx-font-mono);
|
|
2770
|
+
white-space: nowrap;
|
|
2771
|
+
text-overflow: ellipsis;
|
|
2772
|
+
}
|
|
2773
|
+
|
|
2774
|
+
/* A credit is prose, not a filename. It is permanently visible rather than
|
|
2775
|
+
revealed on hover: that is what the API guidelines ask for. */
|
|
2776
|
+
.atx-media-cap[data-credit] {
|
|
2777
|
+
font: 12px/1.4 var(--atx-font-ui);
|
|
2778
|
+
}
|
|
2779
|
+
|
|
2780
|
+
.atx-unsplash-credit {
|
|
2781
|
+
color: var(--atx-brand-text);
|
|
2782
|
+
}
|
|
2783
|
+
|
|
2784
|
+
/* == Media modal ===========================================================
|
|
2785
|
+
editors/media-modal.ts. The sized panel that holds both panes, the detail
|
|
2786
|
+
rail and the drop target. [data-on] opens the drag overlay and reveals the
|
|
2787
|
+
scope toggle, which only a scoped listing has anything to say with. */
|
|
2788
|
+
|
|
2789
|
+
/* Matched at the sized panel's own specificity: .atx-panel[data-sized]
|
|
2790
|
+
.atx-panel-body makes the body the scrolling region, and this body is the
|
|
2791
|
+
one that must not scroll — the grid inside it does. */
|
|
2792
|
+
.atx-media-body {
|
|
2793
|
+
display: flex;
|
|
2794
|
+
flex-direction: column;
|
|
2795
|
+
gap: 10px;
|
|
2796
|
+
}
|
|
2797
|
+
|
|
2798
|
+
.atx-panel[data-sized] .atx-media-body {
|
|
2799
|
+
overflow: hidden;
|
|
2800
|
+
}
|
|
2801
|
+
|
|
2802
|
+
.atx-media-tabhost {
|
|
2803
|
+
flex: 0 0 auto;
|
|
2804
|
+
}
|
|
2805
|
+
|
|
2806
|
+
.atx-media-upload {
|
|
2807
|
+
margin-left: auto;
|
|
2808
|
+
}
|
|
2809
|
+
|
|
2810
|
+
.atx-media-file {
|
|
2811
|
+
display: none;
|
|
2812
|
+
}
|
|
2813
|
+
|
|
2814
|
+
.atx-media-content {
|
|
2815
|
+
display: flex;
|
|
2816
|
+
flex: 1 1 auto;
|
|
2817
|
+
min-height: 0;
|
|
2818
|
+
}
|
|
2819
|
+
|
|
2820
|
+
.atx-media-panes {
|
|
2821
|
+
display: flex;
|
|
2822
|
+
flex: 1 1 auto;
|
|
2823
|
+
flex-direction: column;
|
|
2824
|
+
gap: 10px;
|
|
2825
|
+
min-width: 0;
|
|
2826
|
+
min-height: 0;
|
|
2827
|
+
}
|
|
2828
|
+
|
|
2829
|
+
.atx-media-rail {
|
|
2830
|
+
flex: 0 0 280px;
|
|
2831
|
+
width: 280px;
|
|
2832
|
+
margin-left: 14px;
|
|
2833
|
+
padding-left: 14px;
|
|
2834
|
+
border-left: 1px solid var(--atx-border);
|
|
2835
|
+
overflow-y: auto;
|
|
2836
|
+
}
|
|
2837
|
+
|
|
2838
|
+
.atx-media-drop {
|
|
2839
|
+
flex: 0 0 auto;
|
|
2840
|
+
color: var(--atx-muted-fg);
|
|
2841
|
+
font: 11px var(--atx-font-mono);
|
|
2842
|
+
text-align: center;
|
|
2843
|
+
}
|
|
2844
|
+
|
|
2845
|
+
.atx-media-foot-status {
|
|
2846
|
+
grid-column: auto;
|
|
2847
|
+
margin-right: auto;
|
|
2848
|
+
padding: 0;
|
|
2849
|
+
font: 13px var(--atx-font-ui);
|
|
2850
|
+
text-align: left;
|
|
2851
|
+
}
|
|
2852
|
+
|
|
2853
|
+
/* A dedicated dashed box would eat grid height, so the modal itself is the
|
|
2854
|
+
drop target and this tints it while a file is over it. */
|
|
2855
|
+
.atx-media-dropzone {
|
|
2856
|
+
position: absolute;
|
|
2857
|
+
inset: 0;
|
|
2858
|
+
z-index: 2;
|
|
2859
|
+
display: none;
|
|
2860
|
+
border: 2px dashed var(--atx-primary);
|
|
2861
|
+
border-radius: var(--atx-radius-md);
|
|
2862
|
+
background: ${hexToRgba(COLOR.primary, 0.18)};
|
|
2863
|
+
pointer-events: none;
|
|
2864
|
+
}
|
|
2865
|
+
|
|
2866
|
+
.atx-media-dropzone[data-on] {
|
|
2867
|
+
display: block;
|
|
2868
|
+
}
|
|
2869
|
+
|
|
2870
|
+
.atx-media-toolbar {
|
|
2871
|
+
display: flex;
|
|
2872
|
+
align-items: center;
|
|
2873
|
+
gap: 6px;
|
|
2874
|
+
}
|
|
2875
|
+
|
|
2876
|
+
.atx-asset-filter {
|
|
2877
|
+
flex: 1 1 auto;
|
|
2878
|
+
min-width: 0;
|
|
2879
|
+
font: 12px var(--atx-font-mono);
|
|
2880
|
+
}
|
|
2881
|
+
|
|
2882
|
+
/* Hidden until a listing has folders worth scoping to, so its display is a
|
|
2883
|
+
state rather than the shared inline-flex above. */
|
|
2884
|
+
.atx-asset-scope {
|
|
2885
|
+
display: none;
|
|
2886
|
+
flex: 0 0 auto;
|
|
2887
|
+
}
|
|
2888
|
+
|
|
2889
|
+
.atx-asset-scope[data-on] {
|
|
2890
|
+
display: inline-flex;
|
|
2891
|
+
}
|
|
2892
|
+
|
|
2893
|
+
.atx-media-sort,
|
|
2894
|
+
.atx-unsplash-orient,
|
|
2895
|
+
.atx-unsplash-width {
|
|
2896
|
+
flex: 0 0 auto;
|
|
2897
|
+
width: auto;
|
|
2898
|
+
font: 13px var(--atx-font-ui);
|
|
2899
|
+
}
|
|
2900
|
+
|
|
2901
|
+
/* --- the detail rail --- */
|
|
2902
|
+
|
|
2903
|
+
.atx-media-rail-empty {
|
|
2904
|
+
margin: 0;
|
|
2905
|
+
color: var(--atx-muted-fg);
|
|
2906
|
+
font: 13px/1.6 var(--atx-font-ui);
|
|
2907
|
+
}
|
|
2908
|
+
|
|
2909
|
+
.atx-media-rail-preview {
|
|
2910
|
+
width: 100%;
|
|
2911
|
+
aspect-ratio: 4 / 3;
|
|
2912
|
+
margin-bottom: 10px;
|
|
2913
|
+
overflow: hidden;
|
|
2914
|
+
border: 1px solid var(--atx-border);
|
|
2915
|
+
border-radius: var(--atx-radius-lg);
|
|
2916
|
+
background: ${CHECKER(12)};
|
|
2917
|
+
}
|
|
2918
|
+
|
|
2919
|
+
.atx-media-rail-img {
|
|
2920
|
+
display: block;
|
|
2921
|
+
width: 100%;
|
|
2922
|
+
height: 100%;
|
|
2923
|
+
object-fit: contain;
|
|
2924
|
+
}
|
|
2925
|
+
|
|
2926
|
+
.atx-media-rail-title {
|
|
2927
|
+
margin: 0 0 8px;
|
|
2928
|
+
overflow: hidden;
|
|
2929
|
+
color: var(--atx-foreground);
|
|
2930
|
+
font: 500 14px var(--atx-font-ui);
|
|
2931
|
+
text-overflow: ellipsis;
|
|
2932
|
+
}
|
|
2933
|
+
|
|
2934
|
+
.atx-media-rail-line {
|
|
2935
|
+
margin: 0 0 6px;
|
|
2936
|
+
}
|
|
2937
|
+
|
|
2938
|
+
.atx-media-rail-key {
|
|
2939
|
+
display: block;
|
|
2940
|
+
color: var(--atx-muted-fg);
|
|
2941
|
+
font: 500 11px var(--atx-font-ui);
|
|
2942
|
+
letter-spacing: 0.04em;
|
|
2943
|
+
text-transform: uppercase;
|
|
2944
|
+
}
|
|
2945
|
+
|
|
2946
|
+
.atx-media-rail-value {
|
|
2947
|
+
display: block;
|
|
2948
|
+
color: var(--atx-foreground);
|
|
2949
|
+
font: 11px/1.5 var(--atx-font-mono);
|
|
2950
|
+
word-break: break-all;
|
|
2951
|
+
}
|
|
2952
|
+
|
|
2953
|
+
.atx-media-rail-link {
|
|
2954
|
+
color: var(--atx-brand-text);
|
|
2955
|
+
font: 13px/1.5 var(--atx-font-ui);
|
|
2956
|
+
word-break: normal;
|
|
2957
|
+
}
|
|
2958
|
+
|
|
2959
|
+
/* == Unsplash pane =========================================================
|
|
2960
|
+
editors/unsplash-pane.ts. The search box is a div wearing the control
|
|
2961
|
+
baseline, because the magnifier and the field share one bordered box. */
|
|
2962
|
+
|
|
2963
|
+
.atx-unsplash-search {
|
|
2964
|
+
display: flex;
|
|
2965
|
+
flex: 1 1 auto;
|
|
2966
|
+
align-items: center;
|
|
2967
|
+
gap: 6px;
|
|
2968
|
+
min-width: 0;
|
|
2969
|
+
padding: 4px 8px;
|
|
2970
|
+
}
|
|
2971
|
+
|
|
2972
|
+
.atx-unsplash-search > .atx-ico {
|
|
2973
|
+
opacity: 0.6;
|
|
2974
|
+
}
|
|
2975
|
+
|
|
2976
|
+
.atx-unsplash-input {
|
|
2977
|
+
flex: 1 1 auto;
|
|
2978
|
+
min-width: 0;
|
|
2979
|
+
border: none;
|
|
2980
|
+
background: transparent;
|
|
2981
|
+
color: var(--atx-foreground);
|
|
2982
|
+
font: 14px var(--atx-font-ui);
|
|
2983
|
+
outline: none;
|
|
2984
|
+
}
|
|
2985
|
+
|
|
2986
|
+
.atx-media-error-more {
|
|
2987
|
+
margin-left: 10px;
|
|
2988
|
+
color: var(--atx-warning);
|
|
2989
|
+
font: 13px var(--atx-font-ui);
|
|
2990
|
+
}
|
|
2991
|
+
|
|
2992
|
+
.atx-media-error-title {
|
|
2993
|
+
margin: 0 0 6px;
|
|
2994
|
+
color: var(--atx-foreground);
|
|
2995
|
+
font: 500 14px var(--atx-font-ui);
|
|
2996
|
+
}
|
|
2997
|
+
|
|
2998
|
+
.atx-media-error-detail {
|
|
2999
|
+
margin: 0 0 12px;
|
|
3000
|
+
color: var(--atx-muted-fg);
|
|
3001
|
+
font: 13px/1.6 var(--atx-font-ui);
|
|
3002
|
+
}
|
|
3003
|
+
|
|
3004
|
+
/* The only button inside a full-width grid message, so it centres itself. */
|
|
3005
|
+
.atx-media-error-action {
|
|
3006
|
+
margin: 0 auto;
|
|
3007
|
+
}
|
|
3008
|
+
|
|
3009
|
+
.atx-unsplash-rate {
|
|
3010
|
+
margin: 14px 0 0;
|
|
3011
|
+
color: var(--atx-muted-fg);
|
|
3012
|
+
font: 11px var(--atx-font-mono);
|
|
3013
|
+
}
|
|
3014
|
+
|
|
3015
|
+
/* The last few requests of the hour are worth noticing. */
|
|
3016
|
+
.atx-unsplash-rate[data-tone='warn'] {
|
|
3017
|
+
color: var(--atx-warning);
|
|
3018
|
+
}
|
|
3019
|
+
|
|
3020
|
+
/* == Icons =================================================================
|
|
3021
|
+
icons.ts. One rule for every glyph in the overlay; the caller sets the size
|
|
3022
|
+
on the <svg> itself, so nothing about a size reaches this. */
|
|
3023
|
+
|
|
3024
|
+
.atx-ico {
|
|
3025
|
+
display: inline-flex;
|
|
3026
|
+
flex: 0 0 auto;
|
|
3027
|
+
align-items: center;
|
|
3028
|
+
justify-content: center;
|
|
3029
|
+
/* The spinner rotates the host, not the <svg> — an HTML element animates
|
|
3030
|
+
predictably where an SVG child would need transform-box juggling. */
|
|
3031
|
+
line-height: 0;
|
|
3032
|
+
}
|
|
3033
|
+
|
|
3034
|
+
/* == Small panels ==========================================================
|
|
3035
|
+
copy-panel.ts, notice.ts, source-popup.ts, markup.ts — four leaves that put
|
|
3036
|
+
one message or one control in a plain panel. [data-on] shows the popup's
|
|
3037
|
+
error line; [data-mono] picks the source popup's face, since both faces are
|
|
3038
|
+
fixed and only the choice between them is the caller's. */
|
|
3039
|
+
|
|
3040
|
+
.atx-copy-note {
|
|
3041
|
+
margin: 0 0 10px;
|
|
3042
|
+
color: var(--atx-warning);
|
|
3043
|
+
font: 14px/1.5 var(--atx-font-ui);
|
|
3044
|
+
}
|
|
3045
|
+
|
|
3046
|
+
.atx-copy-text {
|
|
3047
|
+
height: 300px;
|
|
3048
|
+
font: 12px/1.5 var(--atx-font-mono);
|
|
3049
|
+
white-space: pre;
|
|
3050
|
+
resize: vertical;
|
|
3051
|
+
}
|
|
3052
|
+
|
|
3053
|
+
/* Said before the reason, and about a different element than the reason is:
|
|
3054
|
+
muted, because it is context for what follows rather than the verdict. */
|
|
3055
|
+
.atx-notice-lead {
|
|
3056
|
+
margin: 0 0 8px;
|
|
3057
|
+
color: var(--atx-muted-fg);
|
|
3058
|
+
font: 14px/1.5 var(--atx-font-ui);
|
|
3059
|
+
}
|
|
3060
|
+
|
|
3061
|
+
.atx-notice-reason {
|
|
3062
|
+
margin: 0 0 6px;
|
|
3063
|
+
color: var(--atx-foreground);
|
|
3064
|
+
font: 14px/1.5 var(--atx-font-ui);
|
|
3065
|
+
}
|
|
3066
|
+
|
|
3067
|
+
/* The location line opens the source peek, so it reads as a link. */
|
|
3068
|
+
.atx-notice-loc {
|
|
3069
|
+
margin: 0 0 12px;
|
|
3070
|
+
color: var(--atx-muted-fg);
|
|
3071
|
+
font: 12px var(--atx-font-mono);
|
|
3072
|
+
cursor: pointer;
|
|
3073
|
+
}
|
|
3074
|
+
|
|
3075
|
+
.atx-notice-loc:hover {
|
|
3076
|
+
text-decoration: underline;
|
|
3077
|
+
}
|
|
3078
|
+
|
|
3079
|
+
.atx-notice-hint {
|
|
3080
|
+
margin: 0 0 4px;
|
|
3081
|
+
color: var(--atx-brand-text);
|
|
3082
|
+
font: 14px/1.5 var(--atx-font-ui);
|
|
3083
|
+
}
|
|
3084
|
+
|
|
3085
|
+
.atx-popup-label {
|
|
3086
|
+
display: block;
|
|
3087
|
+
margin-bottom: 10px;
|
|
3088
|
+
color: var(--atx-foreground);
|
|
3089
|
+
font: 500 13px var(--atx-font-ui);
|
|
3090
|
+
opacity: 0.8;
|
|
3091
|
+
}
|
|
3092
|
+
|
|
3093
|
+
.atx-popup-input {
|
|
3094
|
+
width: 100%;
|
|
3095
|
+
min-height: 120px;
|
|
3096
|
+
box-sizing: border-box;
|
|
3097
|
+
font: 14px/1.6 var(--atx-font-ui);
|
|
3098
|
+
white-space: pre-wrap;
|
|
3099
|
+
resize: vertical;
|
|
3100
|
+
}
|
|
3101
|
+
|
|
3102
|
+
.atx-popup-input[data-mono] {
|
|
3103
|
+
font: 12px/1.6 ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
3104
|
+
}
|
|
3105
|
+
|
|
3106
|
+
.atx-popup-error {
|
|
3107
|
+
display: none;
|
|
3108
|
+
margin: 10px 0 0;
|
|
3109
|
+
color: var(--atx-destructive);
|
|
3110
|
+
font: 13px/1.5 var(--atx-font-ui);
|
|
3111
|
+
}
|
|
3112
|
+
|
|
3113
|
+
.atx-popup-error[data-on] {
|
|
3114
|
+
display: block;
|
|
3115
|
+
}
|
|
3116
|
+
|
|
3117
|
+
.atx-markup-tags {
|
|
3118
|
+
display: flex;
|
|
3119
|
+
flex-wrap: wrap;
|
|
3120
|
+
align-items: center;
|
|
3121
|
+
gap: 4px;
|
|
3122
|
+
margin-top: 8px;
|
|
3123
|
+
}
|
|
3124
|
+
|
|
3125
|
+
.atx-markup-hint {
|
|
3126
|
+
margin-right: 2px;
|
|
3127
|
+
color: var(--atx-muted-fg);
|
|
3128
|
+
font: 12px/1.5 var(--atx-font-ui);
|
|
3129
|
+
}
|
|
3130
|
+
|
|
3131
|
+
.atx-markup-tag {
|
|
3132
|
+
padding: 2px 6px;
|
|
3133
|
+
border: 1px solid var(--atx-border);
|
|
3134
|
+
border-radius: var(--atx-radius-sm);
|
|
3135
|
+
background: var(--atx-background);
|
|
3136
|
+
color: var(--atx-foreground);
|
|
3137
|
+
font: 11px ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
3138
|
+
cursor: pointer;
|
|
3139
|
+
}
|
|
3140
|
+
|
|
3141
|
+
/* == Source peek ===========================================================
|
|
3142
|
+
editors/peek.ts. A read-only, syntax-tinted view of the file, scrolled to
|
|
3143
|
+
the element's line. Every row carries the focus border so the gutter stays
|
|
3144
|
+
aligned; only .atx-peek-focus's is visible. [data-tone] is how the loading
|
|
3145
|
+
line becomes a refusal or an error, and [data-token] is one run of code. */
|
|
3146
|
+
|
|
3147
|
+
.atx-peek-code {
|
|
3148
|
+
max-height: 65vh;
|
|
3149
|
+
padding: 8px 0;
|
|
3150
|
+
overflow: auto;
|
|
3151
|
+
background: var(--atx-background);
|
|
3152
|
+
font: 12.5px/1.65 var(--atx-font-mono);
|
|
3153
|
+
}
|
|
3154
|
+
|
|
3155
|
+
.atx-peek-more {
|
|
3156
|
+
padding: 4px 12px 4px 15px;
|
|
3157
|
+
color: var(--atx-faint-fg);
|
|
3158
|
+
font-style: italic;
|
|
3159
|
+
user-select: none;
|
|
3160
|
+
}
|
|
3161
|
+
|
|
3162
|
+
.atx-peek-line {
|
|
3163
|
+
display: flex;
|
|
3164
|
+
border-left: 3px solid transparent;
|
|
3165
|
+
background: transparent;
|
|
3166
|
+
}
|
|
3167
|
+
|
|
3168
|
+
.atx-peek-focus {
|
|
3169
|
+
border-left-color: var(--atx-brand);
|
|
3170
|
+
background: ${hexToRgba(COLOR.brand, 0.16)};
|
|
3171
|
+
}
|
|
3172
|
+
|
|
3173
|
+
.atx-peek-gutter {
|
|
3174
|
+
flex: 0 0 auto;
|
|
3175
|
+
padding: 0 12px 0 0;
|
|
3176
|
+
color: var(--atx-faint-fg);
|
|
3177
|
+
text-align: right;
|
|
3178
|
+
user-select: none;
|
|
3179
|
+
}
|
|
3180
|
+
|
|
3181
|
+
.atx-peek-focus .atx-peek-gutter {
|
|
3182
|
+
color: var(--atx-brand-text);
|
|
3183
|
+
}
|
|
3184
|
+
|
|
3185
|
+
.atx-peek-text {
|
|
3186
|
+
flex: 1 1 auto;
|
|
3187
|
+
padding-right: 16px;
|
|
3188
|
+
white-space: pre;
|
|
3189
|
+
tab-size: 2;
|
|
3190
|
+
}
|
|
3191
|
+
|
|
3192
|
+
.atx-peek-loading {
|
|
3193
|
+
padding: 24px 16px;
|
|
3194
|
+
background: var(--atx-background);
|
|
3195
|
+
color: var(--atx-muted-fg);
|
|
3196
|
+
font: 12.5px var(--atx-font-mono);
|
|
3197
|
+
}
|
|
3198
|
+
|
|
3199
|
+
/* Not source but a sentence, so it wants the line-height source does not. */
|
|
3200
|
+
.atx-peek-loading[data-tone='warn'] {
|
|
3201
|
+
color: var(--atx-warning);
|
|
3202
|
+
line-height: 1.6;
|
|
3203
|
+
}
|
|
3204
|
+
|
|
3205
|
+
.atx-peek-loading[data-tone='err'] {
|
|
3206
|
+
color: var(--atx-destructive);
|
|
3207
|
+
}
|
|
3208
|
+
|
|
3209
|
+
/* Code token colours on the panel's dark ground. Chosen for contrast, not to
|
|
3210
|
+
mimic any one editor theme. This is the only copy of the mapping — peek.ts
|
|
3211
|
+
emits the kind and nothing else. */
|
|
3212
|
+
.atx-peek-token[data-token='plain'] { color: var(--atx-foreground); }
|
|
3213
|
+
.atx-peek-token[data-token='comment'] { color: var(--atx-muted-fg); font-style: italic; }
|
|
3214
|
+
.atx-peek-token[data-token='string'] { color: var(--atx-success-text); }
|
|
3215
|
+
.atx-peek-token[data-token='tag'] { color: var(--atx-brand-text); }
|
|
3216
|
+
.atx-peek-token[data-token='attr'] { color: var(--atx-brand-text); }
|
|
3217
|
+
.atx-peek-token[data-token='keyword'] { color: var(--atx-destructive); }
|
|
3218
|
+
.atx-peek-token[data-token='number'] { color: var(--atx-warning); }
|
|
3219
|
+
.atx-peek-token[data-token='fence'] { color: var(--atx-muted-fg); }
|
|
3220
|
+
|
|
3221
|
+
/* == Rules card ============================================================
|
|
3222
|
+
css-inspect.ts. The applied CSS rules behind one class or ID chip on the
|
|
3223
|
+
hover pill. Its position is measured, so only that stays inline. The
|
|
3224
|
+
declaration palette is deliberately the peek palette's sibling — same job,
|
|
3225
|
+
different vocabulary. */
|
|
3226
|
+
|
|
3227
|
+
.atx-tooltip-rules {
|
|
3228
|
+
position: fixed;
|
|
3229
|
+
max-width: 360px;
|
|
3230
|
+
max-height: 50vh;
|
|
3231
|
+
padding: 8px 10px;
|
|
3232
|
+
overflow-y: auto;
|
|
3233
|
+
border: 1px solid var(--atx-border);
|
|
3234
|
+
border-radius: var(--atx-radius-md);
|
|
3235
|
+
background: var(--atx-card);
|
|
3236
|
+
color: var(--atx-foreground);
|
|
3237
|
+
font: 12px var(--atx-font-mono);
|
|
3238
|
+
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);
|
|
3239
|
+
cursor: default;
|
|
3240
|
+
}
|
|
3241
|
+
|
|
3242
|
+
.atx-tooltip-rules-empty {
|
|
3243
|
+
color: var(--atx-muted-fg);
|
|
3244
|
+
}
|
|
3245
|
+
|
|
3246
|
+
/* The first rule sits flush against the top of the card; every one after it
|
|
3247
|
+
is separated from the one before, which is what the hairline is for. */
|
|
3248
|
+
.atx-tooltip-rule {
|
|
3249
|
+
padding: 0 0 6px;
|
|
3250
|
+
}
|
|
3251
|
+
|
|
3252
|
+
.atx-tooltip-rule + .atx-tooltip-rule {
|
|
3253
|
+
padding: 6px 0;
|
|
3254
|
+
border-top: 1px solid var(--atx-border);
|
|
3255
|
+
}
|
|
3256
|
+
|
|
3257
|
+
.atx-tooltip-rule-sel {
|
|
3258
|
+
color: var(--atx-brand-text);
|
|
3259
|
+
word-break: break-all;
|
|
3260
|
+
}
|
|
3261
|
+
|
|
3262
|
+
/* 10px of breathing room above and below the properties list. */
|
|
3263
|
+
.atx-tooltip-rule-decl {
|
|
3264
|
+
margin: 10px 0;
|
|
3265
|
+
color: var(--atx-muted-fg);
|
|
3266
|
+
font: 12px var(--atx-font-mono);
|
|
3267
|
+
white-space: pre-wrap;
|
|
3268
|
+
word-break: break-word;
|
|
3269
|
+
}
|
|
3270
|
+
|
|
3271
|
+
.atx-tooltip-rule-foot {
|
|
3272
|
+
display: flex;
|
|
3273
|
+
align-items: center;
|
|
3274
|
+
justify-content: space-between;
|
|
3275
|
+
}
|
|
3276
|
+
|
|
3277
|
+
.atx-tooltip-rule-src {
|
|
3278
|
+
overflow: hidden;
|
|
3279
|
+
color: var(--atx-muted-fg);
|
|
3280
|
+
font: 10.5px var(--atx-font-mono);
|
|
3281
|
+
white-space: nowrap;
|
|
3282
|
+
text-overflow: ellipsis;
|
|
3283
|
+
}
|
|
3284
|
+
|
|
3285
|
+
.atx-css-token[data-css='prop'] { color: var(--atx-chart5); }
|
|
3286
|
+
.atx-css-token[data-css='value'] { color: var(--atx-muted-fg); }
|
|
3287
|
+
.atx-css-token[data-css='string'] { color: var(--atx-chart2); }
|
|
3288
|
+
.atx-css-token[data-css='number'] { color: var(--atx-chart3); }
|
|
3289
|
+
.atx-css-token[data-css='variable'] { color: var(--atx-chart1); }
|
|
3290
|
+
.atx-css-token[data-css='keyword'] { color: var(--atx-chart4); }
|
|
3291
|
+
.atx-css-token[data-css='punct'] { color: var(--atx-muted-fg); }
|
|
3292
|
+
|
|
3293
|
+
/* == Markdown body editor ==================================================
|
|
3294
|
+
editors/body-editor.ts. Everything here is the editor's *chrome* — the
|
|
3295
|
+
toolbar, the heading menu, the image panel. The writing surface itself
|
|
3296
|
+
(.atx-rte-content) is deliberately absent: it stays in the light DOM so
|
|
3297
|
+
Safari's selection and execCommand APIs can see it, and ::slotted() loses
|
|
3298
|
+
to the document, so its whole box lives in that module's CONTENT_CSS.
|
|
3299
|
+
|
|
3300
|
+
Three states: [data-on] on the heading menu and the image panel, which are
|
|
3301
|
+
both closed until asked for, and [data-hidden] on the format buttons, which
|
|
3302
|
+
are the half of the toolbar that means nothing in raw-markdown mode. */
|
|
3303
|
+
|
|
3304
|
+
.atx-rte-head {
|
|
3305
|
+
position: sticky;
|
|
3306
|
+
top: 0;
|
|
3307
|
+
z-index: 2;
|
|
3308
|
+
padding-bottom: 6px;
|
|
3309
|
+
background: var(--atx-card);
|
|
3310
|
+
}
|
|
3311
|
+
|
|
3312
|
+
.atx-rte-toolbar {
|
|
3313
|
+
display: flex;
|
|
3314
|
+
flex-wrap: wrap;
|
|
3315
|
+
align-items: center;
|
|
3316
|
+
gap: 2px;
|
|
3317
|
+
padding: 5px;
|
|
3318
|
+
border: 1px solid var(--atx-border);
|
|
3319
|
+
border-radius: var(--atx-radius-lg);
|
|
3320
|
+
background: var(--atx-card);
|
|
3321
|
+
}
|
|
3322
|
+
|
|
3323
|
+
.atx-rte-btn {
|
|
3324
|
+
display: inline-flex;
|
|
3325
|
+
align-items: center;
|
|
3326
|
+
justify-content: center;
|
|
3327
|
+
min-width: 30px;
|
|
3328
|
+
height: 28px;
|
|
3329
|
+
padding: 0 8px;
|
|
3330
|
+
border: none;
|
|
3331
|
+
border-radius: var(--atx-radius-md);
|
|
3332
|
+
background: transparent;
|
|
3333
|
+
color: var(--atx-foreground);
|
|
3334
|
+
font: 500 13px/1 var(--atx-font-ui);
|
|
3335
|
+
outline: none;
|
|
3336
|
+
transition: background 120ms, color 120ms;
|
|
3337
|
+
cursor: pointer;
|
|
3338
|
+
}
|
|
3339
|
+
|
|
3340
|
+
.atx-rte-btn:focus-visible {
|
|
3341
|
+
box-shadow: 0 0 0 2px ${hexToRgba(COLOR.ring, 0.7)};
|
|
3342
|
+
}
|
|
3343
|
+
|
|
3344
|
+
/* Each button wears what it does. */
|
|
3345
|
+
.atx-rte-btn-bold {
|
|
3346
|
+
font-weight: 800;
|
|
3347
|
+
}
|
|
3348
|
+
|
|
3349
|
+
.atx-rte-btn-italic {
|
|
3350
|
+
font-family: serif;
|
|
3351
|
+
font-style: italic;
|
|
3352
|
+
}
|
|
3353
|
+
|
|
3354
|
+
.atx-rte-btn-strike {
|
|
3355
|
+
text-decoration: line-through;
|
|
3356
|
+
}
|
|
3357
|
+
|
|
3358
|
+
.atx-rte-btn-pre {
|
|
3359
|
+
font: 700 10px var(--atx-font-mono);
|
|
3360
|
+
}
|
|
3361
|
+
|
|
3362
|
+
.atx-rte-btn-code {
|
|
3363
|
+
font: 700 13px var(--atx-font-mono);
|
|
3364
|
+
}
|
|
3365
|
+
|
|
3366
|
+
/* The MD/Rich toggle is the only thing on the right of the toolbar. It is the
|
|
3367
|
+
editor talking about itself, so it stays neutral. */
|
|
3368
|
+
.atx-rte-mode {
|
|
3369
|
+
margin-left: auto;
|
|
3370
|
+
font: 600 11px var(--atx-font-mono);
|
|
3371
|
+
letter-spacing: 0.04em;
|
|
3372
|
+
}
|
|
3373
|
+
|
|
3374
|
+
.atx-rte-divider {
|
|
3375
|
+
width: 1px;
|
|
3376
|
+
align-self: stretch;
|
|
3377
|
+
margin: 3px;
|
|
3378
|
+
background: var(--atx-border);
|
|
3379
|
+
}
|
|
3380
|
+
|
|
3381
|
+
.atx-rte-btn[data-hidden],
|
|
3382
|
+
.atx-rte-divider[data-hidden],
|
|
3383
|
+
.atx-rte-heading[data-hidden] {
|
|
3384
|
+
display: none;
|
|
3385
|
+
}
|
|
3386
|
+
|
|
3387
|
+
.atx-rte-heading {
|
|
3388
|
+
position: relative;
|
|
3389
|
+
display: inline-flex;
|
|
3390
|
+
}
|
|
3391
|
+
|
|
3392
|
+
.atx-rte-heading-menu {
|
|
3393
|
+
position: absolute;
|
|
3394
|
+
top: calc(100% + 4px);
|
|
3395
|
+
left: 0;
|
|
3396
|
+
z-index: 3;
|
|
3397
|
+
display: none;
|
|
3398
|
+
min-width: 150px;
|
|
3399
|
+
padding: 4px;
|
|
3400
|
+
border: 1px solid var(--atx-border);
|
|
3401
|
+
border-radius: var(--atx-radius-lg);
|
|
3402
|
+
background: var(--atx-card);
|
|
3403
|
+
box-shadow: 0 0 0 1px var(--atx-border), 0 8px 28px rgba(0, 0, 0, 0.4);
|
|
3404
|
+
}
|
|
3405
|
+
|
|
3406
|
+
.atx-rte-heading-menu[data-on] {
|
|
3407
|
+
display: block;
|
|
3408
|
+
}
|
|
3409
|
+
|
|
3410
|
+
.atx-rte-heading-item {
|
|
3411
|
+
display: flex;
|
|
3412
|
+
align-items: center;
|
|
3413
|
+
gap: 10px;
|
|
3414
|
+
width: 100%;
|
|
3415
|
+
padding: 6px 10px;
|
|
3416
|
+
border: none;
|
|
3417
|
+
border-radius: var(--atx-radius-md);
|
|
3418
|
+
background: transparent;
|
|
3419
|
+
color: var(--atx-foreground);
|
|
3420
|
+
font: 400 13px var(--atx-font-ui);
|
|
3421
|
+
text-align: left;
|
|
3422
|
+
cursor: pointer;
|
|
3423
|
+
}
|
|
3424
|
+
|
|
3425
|
+
.atx-rte-btn:hover,
|
|
3426
|
+
.atx-rte-heading-item:hover {
|
|
3427
|
+
background: var(--atx-accent);
|
|
3428
|
+
color: var(--atx-foreground);
|
|
3429
|
+
}
|
|
3430
|
+
|
|
3431
|
+
.atx-rte-heading-chip {
|
|
3432
|
+
min-width: 20px;
|
|
3433
|
+
font: 700 11px var(--atx-font-mono);
|
|
3434
|
+
opacity: 0.7;
|
|
3435
|
+
}
|
|
3436
|
+
|
|
3437
|
+
/* Each row previews its own level, so the size is the caller's. */
|
|
3438
|
+
.atx-rte-heading-name {
|
|
3439
|
+
font: 600 12px var(--atx-font-ui);
|
|
3440
|
+
}
|
|
3441
|
+
|
|
3442
|
+
/* Insert or replace an image, sharing the asset picker with the image field. */
|
|
3443
|
+
.atx-rte-image-panel {
|
|
3444
|
+
display: none;
|
|
3445
|
+
margin: 6px 0 0;
|
|
3446
|
+
padding: 12px;
|
|
3447
|
+
border: 1px solid var(--atx-border);
|
|
3448
|
+
border-radius: var(--atx-radius-lg);
|
|
3449
|
+
background: var(--atx-background);
|
|
3450
|
+
}
|
|
3451
|
+
|
|
3452
|
+
.atx-rte-image-panel[data-on] {
|
|
3453
|
+
display: block;
|
|
3454
|
+
}
|
|
3455
|
+
|
|
3456
|
+
.atx-rte-image-alt-label {
|
|
3457
|
+
display: block;
|
|
3458
|
+
margin: 10px 0 6px;
|
|
3459
|
+
color: var(--atx-foreground);
|
|
3460
|
+
font: 500 14px/1 var(--atx-font-ui);
|
|
3461
|
+
}
|
|
3462
|
+
|
|
3463
|
+
.atx-rte-image-actions {
|
|
3464
|
+
display: flex;
|
|
3465
|
+
justify-content: flex-end;
|
|
3466
|
+
gap: 8px;
|
|
3467
|
+
margin-top: 8px;
|
|
3468
|
+
}
|
|
3469
|
+
|
|
3470
|
+
/* Smaller than a footer button: these sit inside a panel inside a toolbar. */
|
|
3471
|
+
|
|
3472
|
+
/* The raw-markdown half of the MD/Rich toggle. */
|
|
3473
|
+
.atx-body-input {
|
|
3474
|
+
display: none;
|
|
3475
|
+
min-height: 40vh;
|
|
3476
|
+
padding: 12px;
|
|
3477
|
+
font: 400 13px/1.6 var(--atx-font-mono);
|
|
3478
|
+
resize: vertical;
|
|
3479
|
+
}
|
|
3480
|
+
|
|
3481
|
+
.atx-body-input[data-on] {
|
|
3482
|
+
display: block;
|
|
3483
|
+
}
|
|
3484
|
+
`;
|
|
3485
|
+
}
|