justskill-design 0.0.1 → 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +235 -0
- package/README.md +184 -11
- package/SKILL.md +435 -0
- package/canvas/app.mjs +2870 -0
- package/canvas/groups.mjs +204 -0
- package/canvas/index.html +141 -0
- package/canvas/styles.css +1032 -0
- package/canvas/tree.mjs +249 -0
- package/cordis.patch.yml +8 -0
- package/design.mjs +172 -0
- package/docs/dsh-workbench.png +0 -0
- package/dsh/client.js +864 -0
- package/dsh/index.js +354 -0
- package/dsh/scan.js +139 -0
- package/dsh/skill.js +41 -0
- package/lib/base-css.mjs +92 -0
- package/lib/detect.mjs +123 -0
- package/lib/diagnostics.mjs +382 -0
- package/lib/export-html.mjs +309 -0
- package/lib/export-svg.mjs +111 -0
- package/lib/fragment.mjs +702 -0
- package/lib/geometry.mjs +827 -0
- package/lib/icons.mjs +106 -0
- package/lib/levenshtein.mjs +133 -0
- package/lib/node/cli.mjs +387 -0
- package/lib/node/export-svg.mjs +75 -0
- package/lib/node/fs.mjs +309 -0
- package/lib/node/op-json.mjs +75 -0
- package/lib/node/raster-run.mjs +115 -0
- package/lib/node/server.mjs +1264 -0
- package/lib/node/static.mjs +206 -0
- package/lib/node/verbs/add.mjs +220 -0
- package/lib/node/verbs/export.mjs +161 -0
- package/lib/node/verbs/format.mjs +106 -0
- package/lib/node/verbs/import.mjs +126 -0
- package/lib/node/verbs/ls.mjs +98 -0
- package/lib/node/verbs/migrate.mjs +236 -0
- package/lib/node/verbs/move.mjs +76 -0
- package/lib/node/verbs/new.mjs +284 -0
- package/lib/node/verbs/remove.mjs +41 -0
- package/lib/node/verbs/rename.mjs +87 -0
- package/lib/node/verbs/serve.mjs +164 -0
- package/lib/node/verbs/set.mjs +150 -0
- package/lib/node/verbs/show.mjs +122 -0
- package/lib/node/verbs/tokens.mjs +167 -0
- package/lib/node/verbs/validate.mjs +130 -0
- package/lib/node/write-verbs.mjs +215 -0
- package/lib/normalize.mjs +288 -0
- package/lib/ops.mjs +2063 -0
- package/lib/parse.mjs +997 -0
- package/lib/raster.mjs +186 -0
- package/lib/readers/pen.mjs +1684 -0
- package/lib/receipt.mjs +203 -0
- package/lib/render.mjs +503 -0
- package/lib/serialize.mjs +441 -0
- package/lib/spans.mjs +177 -0
- package/lib/svg-path.mjs +483 -0
- package/lib/table.mjs +1047 -0
- package/lib/tokens.mjs +295 -0
- package/lib/validate.mjs +929 -0
- package/package.json +68 -6
- package/references/cookbook.md +156 -0
- package/references/images.md +171 -0
- package/references/product-flow.md +408 -0
- package/references/schema.md +90 -0
- package/references/styling.md +181 -0
- package/scripts/install.mjs +95 -0
|
@@ -0,0 +1,1032 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The canvas chrome only.
|
|
3
|
+
*
|
|
4
|
+
* Nothing here reaches a design file. Each page is mounted inside its own shadow
|
|
5
|
+
* root, so a page's own styles cannot restyle this chrome and this chrome cannot
|
|
6
|
+
* restyle a design. That separation is what keeps "what you see" equal to "what
|
|
7
|
+
* you export".
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
:root {
|
|
11
|
+
/*
|
|
12
|
+
* Paper. The chrome sits on the same warm field as the pages, so the page
|
|
13
|
+
* cards read as paper on a table instead of white shapes on a dark stage.
|
|
14
|
+
* Every value here is a measured one: the field is a shade darker than the
|
|
15
|
+
* panels so a panel edge is visible without a border doing all the work, and
|
|
16
|
+
* `--chrome-dim` clears 4.5:1 on `--chrome-panel` (11px labels included).
|
|
17
|
+
*/
|
|
18
|
+
--chrome-bg: #f1efec;
|
|
19
|
+
--chrome-field: #f1efec;
|
|
20
|
+
--chrome-dot: #d9d5cc;
|
|
21
|
+
--chrome-panel: #fbfaf8;
|
|
22
|
+
--chrome-pill: #fcfbf9;
|
|
23
|
+
--chrome-input: #ffffff;
|
|
24
|
+
--chrome-track: #f1eee8;
|
|
25
|
+
--chrome-hover: #f1eee8;
|
|
26
|
+
--chrome-selected: #efece5;
|
|
27
|
+
--chrome-line: rgba(20, 19, 15, 0.10);
|
|
28
|
+
--chrome-ink: #14130f;
|
|
29
|
+
--chrome-dim: #6f6a63;
|
|
30
|
+
--chrome-accent: #2f5fd0;
|
|
31
|
+
--chrome-danger: #b4453f;
|
|
32
|
+
--chrome-warn: #9a6b12;
|
|
33
|
+
--chrome-shadow-panel: 0 1px 2px rgba(20, 19, 15, 0.05), 0 16px 36px -18px rgba(20, 19, 15, 0.18);
|
|
34
|
+
--chrome-shadow-pill: 0 1px 2px rgba(20, 19, 15, 0.05), 0 10px 24px -12px rgba(20, 19, 15, 0.16);
|
|
35
|
+
--chrome-shadow-page: 0 1px 2px rgba(20, 19, 15, 0.06), 0 24px 52px -22px rgba(20, 19, 15, 0.28);
|
|
36
|
+
--chrome-shadow-drawer: 0 -1px 2px rgba(20, 19, 15, 0.04), 0 -18px 40px -24px rgba(20, 19, 15, 0.20);
|
|
37
|
+
|
|
38
|
+
/*
|
|
39
|
+
* The three columns, in one place.
|
|
40
|
+
*
|
|
41
|
+
* `#stage` reads these for its tracks, `.drawer` reads them for its left and
|
|
42
|
+
* right edge, and the panel code writes them. A hard-coded `240px` in two
|
|
43
|
+
* rules is how a drawer ends up 240px wide over a panel that is no longer
|
|
44
|
+
* there — which is exactly what the first version of this layout did.
|
|
45
|
+
*
|
|
46
|
+
* `--gutter-*` is the width of the drag handle, and it goes to 0 with the
|
|
47
|
+
* panel: a hidden panel must leave neither a column nor a gap behind.
|
|
48
|
+
*/
|
|
49
|
+
--panel-left: 240px;
|
|
50
|
+
--panel-right: 260px;
|
|
51
|
+
--gutter-left: 6px;
|
|
52
|
+
--gutter-right: 6px;
|
|
53
|
+
|
|
54
|
+
color-scheme: light;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
* { box-sizing: border-box; }
|
|
58
|
+
|
|
59
|
+
html, body {
|
|
60
|
+
margin: 0;
|
|
61
|
+
height: 100%;
|
|
62
|
+
background: var(--chrome-field);
|
|
63
|
+
color: var(--chrome-ink);
|
|
64
|
+
font: 13px/1.5 -apple-system, "SF Pro Text", "PingFang SC", "Microsoft YaHei", system-ui, sans-serif;
|
|
65
|
+
overflow: hidden;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
body { display: block; }
|
|
69
|
+
|
|
70
|
+
/* -- top bar ---------------------------------------------------------------- */
|
|
71
|
+
|
|
72
|
+
/*
|
|
73
|
+
* The chrome floats over the field instead of taking a band off the top.
|
|
74
|
+
*
|
|
75
|
+
* `left`/`right` are the panel tracks plus their gutters, so the two pills stay
|
|
76
|
+
* over the canvas when a panel is folded away: a bar pinned to the window would
|
|
77
|
+
* hang over the layers panel at 1600px and over nothing at 900px.
|
|
78
|
+
*/
|
|
79
|
+
.bar {
|
|
80
|
+
position: fixed;
|
|
81
|
+
top: 12px;
|
|
82
|
+
left: calc(var(--panel-left) + var(--gutter-left) + 12px + var(--edge-left, 0px));
|
|
83
|
+
right: calc(var(--panel-right) + var(--gutter-right) + 12px + var(--edge-right, 0px));
|
|
84
|
+
display: flex;
|
|
85
|
+
align-items: center;
|
|
86
|
+
justify-content: space-between;
|
|
87
|
+
gap: 12px;
|
|
88
|
+
height: 36px;
|
|
89
|
+
padding: 0;
|
|
90
|
+
border: 0;
|
|
91
|
+
background: none;
|
|
92
|
+
z-index: 30;
|
|
93
|
+
pointer-events: none;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.bar > * { pointer-events: auto; }
|
|
97
|
+
|
|
98
|
+
.bar-left, .bar-right {
|
|
99
|
+
display: flex;
|
|
100
|
+
flex: none;
|
|
101
|
+
align-items: center;
|
|
102
|
+
gap: 4px;
|
|
103
|
+
height: 36px;
|
|
104
|
+
padding: 0 6px;
|
|
105
|
+
background: var(--chrome-pill);
|
|
106
|
+
border: 1px solid var(--chrome-line);
|
|
107
|
+
border-radius: 999px;
|
|
108
|
+
box-shadow: var(--chrome-shadow-pill);
|
|
109
|
+
white-space: nowrap;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/*
|
|
113
|
+
* The bar has five widths, and `applyBarFit()` in app.mjs picks the widest one
|
|
114
|
+
* that fits the canvas. Waiting for a media query would be wrong here: the room
|
|
115
|
+
* the bar has is the window minus both panels, so opening a panel can squeeze it
|
|
116
|
+
* at 1600px while a closed pair is roomy at 900px.
|
|
117
|
+
*
|
|
118
|
+
* The order of what goes is the order of what is least missed: the file name and
|
|
119
|
+
* revision are identity, not action; `Export` duplicates `Export all`; the
|
|
120
|
+
* Problems label keeps its count; and at the narrowest step only the actions
|
|
121
|
+
* themselves survive.
|
|
122
|
+
*/
|
|
123
|
+
.bar:is([data-fit="tight"], [data-fit="narrow"], [data-fit="mini"], [data-fit="micro"]) :is(.file, #revision) { display: none; }
|
|
124
|
+
|
|
125
|
+
.bar:is([data-fit="narrow"], [data-fit="mini"], [data-fit="micro"]) :is(.export-sep, #export-page) { display: none; }
|
|
126
|
+
|
|
127
|
+
.bar:is([data-fit="mini"], [data-fit="micro"]) :is(#undo, #redo, .sep, #fit) { display: none; }
|
|
128
|
+
|
|
129
|
+
/* The last step gives up the export action, not the diagnostics: at this width a
|
|
130
|
+
panel has to be closed to get the file out, while a warning count is the one
|
|
131
|
+
thing a canvas this cramped must still show. */
|
|
132
|
+
.bar[data-fit="micro"] #export-all { display: none; }
|
|
133
|
+
|
|
134
|
+
.bar button { height: 26px; padding: 0 9px; border-radius: 999px; }
|
|
135
|
+
|
|
136
|
+
.brand { font-weight: 600; letter-spacing: 0.02em; padding-left: 6px; }
|
|
137
|
+
.file { color: var(--chrome-dim); }
|
|
138
|
+
|
|
139
|
+
.pill {
|
|
140
|
+
padding: 1px 7px;
|
|
141
|
+
border: 1px solid var(--chrome-line);
|
|
142
|
+
border-radius: 999px;
|
|
143
|
+
background: var(--chrome-track);
|
|
144
|
+
color: var(--chrome-dim);
|
|
145
|
+
font-size: 11px;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.pill.warn { color: var(--chrome-warn); border-color: var(--chrome-warn); }
|
|
149
|
+
|
|
150
|
+
.zoom { color: var(--chrome-dim); min-width: 44px; text-align: right; }
|
|
151
|
+
|
|
152
|
+
button {
|
|
153
|
+
font: inherit;
|
|
154
|
+
color: var(--chrome-dim);
|
|
155
|
+
background: transparent;
|
|
156
|
+
border: 1px solid transparent;
|
|
157
|
+
border-radius: 7px;
|
|
158
|
+
padding: 3px 9px;
|
|
159
|
+
cursor: pointer;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
button:hover { background: var(--chrome-hover); color: var(--chrome-ink); }
|
|
163
|
+
|
|
164
|
+
/*
|
|
165
|
+
* One focus ring, everywhere.
|
|
166
|
+
*
|
|
167
|
+
* The ring is ink, two pixels, and it sits outside the control with a two-pixel
|
|
168
|
+
* gap — the same weight as the selection ring, so "this is the thing the
|
|
169
|
+
* keyboard is on" reads the same as "this is the thing you picked". It is only
|
|
170
|
+
* drawn for `:focus-visible`, so a mouse click on a button leaves no ring
|
|
171
|
+
* behind, while a text field still shows it (a field is always "keyboard
|
|
172
|
+
* input", which is what the pseudo-class means).
|
|
173
|
+
*
|
|
174
|
+
* Contexts that cannot afford an outside ring tighten the offset below: a row
|
|
175
|
+
* inside a scrolling tree, a segment inside its track, a row inside the
|
|
176
|
+
* problems list. They keep the two pixels; only the gap changes.
|
|
177
|
+
*/
|
|
178
|
+
button:focus-visible,
|
|
179
|
+
input:focus-visible,
|
|
180
|
+
select:focus-visible,
|
|
181
|
+
textarea:focus-visible,
|
|
182
|
+
[tabindex]:focus-visible {
|
|
183
|
+
outline: 2px solid var(--chrome-ink);
|
|
184
|
+
outline-offset: 2px;
|
|
185
|
+
}
|
|
186
|
+
button:disabled { opacity: 0.4; cursor: default; }
|
|
187
|
+
|
|
188
|
+
button.active { border-color: var(--chrome-accent); color: var(--chrome-accent); }
|
|
189
|
+
|
|
190
|
+
.count {
|
|
191
|
+
display: inline-block;
|
|
192
|
+
min-width: 16px;
|
|
193
|
+
padding: 0 4px;
|
|
194
|
+
margin-left: 4px;
|
|
195
|
+
border-radius: 8px;
|
|
196
|
+
background: var(--chrome-track);
|
|
197
|
+
font-size: 11px;
|
|
198
|
+
text-align: center;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
.count.has-errors { background: var(--chrome-danger); color: #ffffff; }
|
|
202
|
+
|
|
203
|
+
.sep { width: 1px; height: 18px; background: var(--chrome-line); }
|
|
204
|
+
|
|
205
|
+
/* -- layout ----------------------------------------------------------------- */
|
|
206
|
+
|
|
207
|
+
#stage {
|
|
208
|
+
height: 100vh;
|
|
209
|
+
display: grid;
|
|
210
|
+
grid-template-columns:
|
|
211
|
+
var(--panel-left) var(--gutter-left)
|
|
212
|
+
minmax(0, 1fr)
|
|
213
|
+
var(--gutter-right) var(--panel-right);
|
|
214
|
+
min-height: 0;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/*
|
|
218
|
+
* Every column is named, not placed.
|
|
219
|
+
*
|
|
220
|
+
* The five children are laid into the five tracks in order — until one of them
|
|
221
|
+
* is `hidden` and drops out of the grid, at which point the rest shuffle one
|
|
222
|
+
* track to the left: the canvas lands in the zero-width panel track and
|
|
223
|
+
* disappears, and the inspector takes the `1fr`. Naming the columns is what
|
|
224
|
+
* makes hiding a panel a change of width instead of a change of layout.
|
|
225
|
+
*/
|
|
226
|
+
#panel-left { grid-column: 1; }
|
|
227
|
+
#divider-left { grid-column: 2; }
|
|
228
|
+
.viewport { grid-column: 3; }
|
|
229
|
+
#divider-right { grid-column: 4; }
|
|
230
|
+
#panel-right { grid-column: 5; }
|
|
231
|
+
|
|
232
|
+
/*
|
|
233
|
+
* Both panels are cards on the field.
|
|
234
|
+
*
|
|
235
|
+
* A 12px margin leaves the field visible around each one, so the paper language
|
|
236
|
+
* reaches the chrome without either panel taking canvas away: the columns still
|
|
237
|
+
* size the grid, and the card is what the margin draws inside them.
|
|
238
|
+
*/
|
|
239
|
+
.panel {
|
|
240
|
+
background: var(--chrome-panel);
|
|
241
|
+
display: flex;
|
|
242
|
+
flex-direction: column;
|
|
243
|
+
min-height: 0;
|
|
244
|
+
overflow: hidden;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
.panel.left {
|
|
248
|
+
margin: 12px;
|
|
249
|
+
border: 1px solid var(--chrome-line);
|
|
250
|
+
border-radius: 14px;
|
|
251
|
+
box-shadow: var(--chrome-shadow-panel);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
.panel.right {
|
|
255
|
+
margin: 12px;
|
|
256
|
+
border: 1px solid var(--chrome-line);
|
|
257
|
+
border-radius: 14px;
|
|
258
|
+
box-shadow: var(--chrome-shadow-panel);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/*
|
|
262
|
+
* A `hidden` panel is taken off screen by the one `[hidden]` rule at the foot of
|
|
263
|
+
* this sheet.
|
|
264
|
+
*
|
|
265
|
+
* It was spelled out here first, per class — `.panel[hidden]`, `.divider[hidden]`,
|
|
266
|
+
* `.viewport-toggle[hidden]` — and that list is how the Problems drawer stayed on
|
|
267
|
+
* screen: the drawer is given a `display` too, and it was written before the list
|
|
268
|
+
* was. One rule at the end cannot go stale that way.
|
|
269
|
+
*/
|
|
270
|
+
|
|
271
|
+
.panel-head {
|
|
272
|
+
display: flex;
|
|
273
|
+
align-items: center;
|
|
274
|
+
justify-content: space-between;
|
|
275
|
+
gap: 8px;
|
|
276
|
+
padding: 6px 8px 6px 10px;
|
|
277
|
+
border-bottom: 1px solid var(--chrome-line);
|
|
278
|
+
color: var(--chrome-dim);
|
|
279
|
+
font-size: 11px;
|
|
280
|
+
text-transform: uppercase;
|
|
281
|
+
letter-spacing: 0.06em;
|
|
282
|
+
flex: none;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/*
|
|
286
|
+
* The two buttons in the Layers header: expand all, collapse all.
|
|
287
|
+
*
|
|
288
|
+
* Icons rather than words, because the header is 240px wide and
|
|
289
|
+
* `text-transform: uppercase` turns any label into a shout that does not fit
|
|
290
|
+
* beside "LAYERS". The tooltip and the accessible name carry the meaning.
|
|
291
|
+
*/
|
|
292
|
+
.head-actions { display: flex; align-items: center; gap: 2px; }
|
|
293
|
+
|
|
294
|
+
/* The fold control and the panel's name share the window edge. */
|
|
295
|
+
.head-title { display: flex; align-items: center; gap: 6px; min-width: 0; }
|
|
296
|
+
.head-title .label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
297
|
+
|
|
298
|
+
/* The name in the right header is the only child that may shrink; the toggle
|
|
299
|
+
beside it must keep its 20px. */
|
|
300
|
+
#selection-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
301
|
+
|
|
302
|
+
.head-button {
|
|
303
|
+
display: flex;
|
|
304
|
+
align-items: center;
|
|
305
|
+
justify-content: center;
|
|
306
|
+
width: 20px;
|
|
307
|
+
height: 20px;
|
|
308
|
+
padding: 0;
|
|
309
|
+
background: none;
|
|
310
|
+
border: 0;
|
|
311
|
+
border-radius: 4px;
|
|
312
|
+
color: var(--chrome-dim);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
.head-button:hover { color: var(--chrome-ink); background: var(--chrome-hover); }
|
|
316
|
+
.head-button:focus-visible,
|
|
317
|
+
.edge-toggle:focus-visible,
|
|
318
|
+
.bar button:focus-visible { outline-offset: 1px; }
|
|
319
|
+
.head-button svg { display: block; }
|
|
320
|
+
|
|
321
|
+
.panel-scroll { flex: 1; min-height: 0; overflow: auto; }
|
|
322
|
+
|
|
323
|
+
.panel-body { padding: 10px; }
|
|
324
|
+
|
|
325
|
+
/* -- panel divider ---------------------------------------------------------- */
|
|
326
|
+
|
|
327
|
+
/*
|
|
328
|
+
* A 6px handle with a 1px hairline down its middle.
|
|
329
|
+
*
|
|
330
|
+
* The hairline is a background layer, not a border: a border would take a pixel
|
|
331
|
+
* off the grab area and the line would jump a pixel sideways when the pointer
|
|
332
|
+
* landed on it. `touch-action: none` is what lets a finger drag the edge
|
|
333
|
+
* instead of scrolling the panel under it.
|
|
334
|
+
*/
|
|
335
|
+
.divider {
|
|
336
|
+
position: relative;
|
|
337
|
+
width: 100%;
|
|
338
|
+
cursor: col-resize;
|
|
339
|
+
background: transparent;
|
|
340
|
+
touch-action: none;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
.divider:hover, .divider.dragging { background: var(--chrome-line) center / 2px 100% no-repeat; }
|
|
344
|
+
|
|
345
|
+
.divider:focus-visible { outline: 2px solid var(--chrome-ink); outline-offset: -2px; }
|
|
346
|
+
|
|
347
|
+
/* -- panel toggles ---------------------------------------------------------- */
|
|
348
|
+
|
|
349
|
+
/*
|
|
350
|
+
* The tab a folded-away panel leaves behind.
|
|
351
|
+
*
|
|
352
|
+
* It exists only while its panel is closed (`hidden` is toggled with the panel),
|
|
353
|
+
* so the canvas never carries a control that duplicates one in a header.
|
|
354
|
+
*/
|
|
355
|
+
/*
|
|
356
|
+
* The tabs sit on the bar's row, not under it: a folded panel's control is
|
|
357
|
+
* chrome, and chrome lives at the top. `--edge-left`/`--edge-right` (written by
|
|
358
|
+
* `applyPanels`) hold the space open so the pills never sit on top of a tab.
|
|
359
|
+
*/
|
|
360
|
+
.edge-toggle {
|
|
361
|
+
position: absolute;
|
|
362
|
+
top: 16px;
|
|
363
|
+
z-index: 6;
|
|
364
|
+
display: flex;
|
|
365
|
+
align-items: center;
|
|
366
|
+
justify-content: center;
|
|
367
|
+
width: 28px;
|
|
368
|
+
height: 28px;
|
|
369
|
+
padding: 0;
|
|
370
|
+
border-radius: 8px;
|
|
371
|
+
background: var(--chrome-pill);
|
|
372
|
+
border: 1px solid var(--chrome-line);
|
|
373
|
+
box-shadow: var(--chrome-shadow-pill);
|
|
374
|
+
color: var(--chrome-dim);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
.edge-toggle.left { left: 12px; }
|
|
378
|
+
.edge-toggle.right { right: 12px; }
|
|
379
|
+
|
|
380
|
+
.edge-toggle:hover { color: var(--chrome-ink); background: var(--chrome-panel); }
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
.edge-toggle svg { display: block; }
|
|
384
|
+
|
|
385
|
+
/* -- the layer tree --------------------------------------------------------- */
|
|
386
|
+
|
|
387
|
+
/*
|
|
388
|
+
* One tree, pages at its root. The nesting is real: a `<ul>` inside the `<li>`
|
|
389
|
+
* is the child list, so the indent is the browser's, not `depth * 12px` on a
|
|
390
|
+
* flat list. A flat list can be indented to look nested; it cannot be collapsed,
|
|
391
|
+
* and collapse is the whole reason this panel exists.
|
|
392
|
+
*/
|
|
393
|
+
.tree, .tree-children {
|
|
394
|
+
margin: 0;
|
|
395
|
+
padding: 0;
|
|
396
|
+
list-style: none;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
.tree-children { padding-left: 13px; }
|
|
400
|
+
|
|
401
|
+
.tree-item { min-width: 0; }
|
|
402
|
+
|
|
403
|
+
.tree-row {
|
|
404
|
+
display: flex;
|
|
405
|
+
align-items: center;
|
|
406
|
+
gap: 4px;
|
|
407
|
+
padding: 3px 8px 3px 4px;
|
|
408
|
+
cursor: default;
|
|
409
|
+
white-space: nowrap;
|
|
410
|
+
overflow: hidden;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
.tree-row { border-radius: 6px; margin: 1px 6px; padding-left: 4px; }
|
|
414
|
+
|
|
415
|
+
.tree-row:hover { background: var(--chrome-hover); }
|
|
416
|
+
|
|
417
|
+
.tree-item[aria-selected="true"] > .tree-row {
|
|
418
|
+
background: var(--chrome-selected);
|
|
419
|
+
box-shadow: inset 2px 0 0 var(--chrome-ink);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
.tree-item:focus-visible { outline: none; }
|
|
423
|
+
.tree-item:focus-visible > .tree-row { outline: 2px solid var(--chrome-ink); outline-offset: -2px; }
|
|
424
|
+
|
|
425
|
+
/*
|
|
426
|
+
* The chevron is drawn, not typed. A glyph would inherit whatever the system
|
|
427
|
+
* font does with `▸`, and it would have to be rewritten on every expand; a
|
|
428
|
+
* rotated triangle is one rule keyed off `aria-expanded`, the attribute that
|
|
429
|
+
* already has to be right for the keyboard and the screen reader.
|
|
430
|
+
*/
|
|
431
|
+
.chevron {
|
|
432
|
+
flex: none;
|
|
433
|
+
width: 14px;
|
|
434
|
+
height: 14px;
|
|
435
|
+
display: flex;
|
|
436
|
+
align-items: center;
|
|
437
|
+
justify-content: center;
|
|
438
|
+
color: var(--chrome-dim);
|
|
439
|
+
border-radius: 3px;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
.chevron::before {
|
|
443
|
+
content: "";
|
|
444
|
+
width: 0;
|
|
445
|
+
height: 0;
|
|
446
|
+
border-left: 4px solid currentColor;
|
|
447
|
+
border-top: 3.5px solid transparent;
|
|
448
|
+
border-bottom: 3.5px solid transparent;
|
|
449
|
+
transform: rotate(0deg);
|
|
450
|
+
transition: transform 90ms linear;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
.tree-item[aria-expanded="true"] > .tree-row > .chevron::before { transform: rotate(90deg); }
|
|
454
|
+
|
|
455
|
+
.chevron.leaf::before { display: none; }
|
|
456
|
+
.chevron:hover { color: var(--chrome-ink); background: var(--chrome-hover); }
|
|
457
|
+
|
|
458
|
+
.tree-label { overflow: hidden; text-overflow: ellipsis; }
|
|
459
|
+
|
|
460
|
+
.tree-type {
|
|
461
|
+
flex: none;
|
|
462
|
+
color: var(--chrome-dim);
|
|
463
|
+
font-size: 11px;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/* A hidden element keeps its row, greyed: it is still in the file, and a layer
|
|
467
|
+
list that drops it would make the next edit a guess. */
|
|
468
|
+
.tree-item[data-hidden="true"] > .tree-row > .tree-label { color: var(--chrome-dim); text-decoration: line-through; }
|
|
469
|
+
|
|
470
|
+
.tree-item .badge {
|
|
471
|
+
flex: none;
|
|
472
|
+
margin-left: auto;
|
|
473
|
+
padding-left: 6px;
|
|
474
|
+
font-size: 10px;
|
|
475
|
+
color: var(--chrome-dim);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
.tree-empty { padding: 8px 10px; color: var(--chrome-dim); }
|
|
479
|
+
|
|
480
|
+
/* -- viewport --------------------------------------------------------------- */
|
|
481
|
+
|
|
482
|
+
/*
|
|
483
|
+
* The field: warm paper with one dot, repeated.
|
|
484
|
+
*
|
|
485
|
+
* The pitch and the offset are written by `applyCamera` in screen pixels: the
|
|
486
|
+
* pitch is clamped to 14–44px so the grid stays a texture at every zoom instead
|
|
487
|
+
* of becoming noise at 10% or an empty page at 400%, and the offset follows the
|
|
488
|
+
* camera so panning moves the paper. `background-size` here is only the first
|
|
489
|
+
* paint, before the first camera write. Below 30% the dots are taken away and
|
|
490
|
+
* the field keeps its colour.
|
|
491
|
+
*/
|
|
492
|
+
.viewport {
|
|
493
|
+
position: relative;
|
|
494
|
+
overflow: hidden;
|
|
495
|
+
background-color: var(--chrome-field);
|
|
496
|
+
background-image: radial-gradient(circle, var(--chrome-dot) 1px, transparent 1.7px);
|
|
497
|
+
background-size: 26px 26px;
|
|
498
|
+
cursor: default;
|
|
499
|
+
min-width: 0;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
.viewport[data-zoom="far"] { background-image: none; }
|
|
503
|
+
|
|
504
|
+
/*
|
|
505
|
+
* While the canvas is being dragged, nothing on it is text to select.
|
|
506
|
+
*
|
|
507
|
+
* `user-select: none` on the viewport is inherited into the pages' shadow roots,
|
|
508
|
+
* so it covers the design's own text as well as the chrome — which is what stops
|
|
509
|
+
* a pan from leaving a blue smear over every string in the file.
|
|
510
|
+
*/
|
|
511
|
+
.viewport.panning { cursor: grabbing; user-select: none; }
|
|
512
|
+
|
|
513
|
+
/*
|
|
514
|
+
* A geometry drag is not a text gesture.
|
|
515
|
+
*
|
|
516
|
+
* `user-select: none` on the viewport reaches the chrome, and the page hosts
|
|
517
|
+
* resolve their own `auto` against it, so the design's text stops being
|
|
518
|
+
* selectable while an element is being dragged. It is scoped to the drag: with
|
|
519
|
+
* the class off, selecting a string inside a page works as before.
|
|
520
|
+
*/
|
|
521
|
+
.viewport.moving, .viewport.moving .page-host { user-select: none; }
|
|
522
|
+
.viewport.move-mode { cursor: move; }
|
|
523
|
+
|
|
524
|
+
.canvas {
|
|
525
|
+
position: absolute;
|
|
526
|
+
inset: 0;
|
|
527
|
+
transform-origin: 0 0;
|
|
528
|
+
/*
|
|
529
|
+
* No `will-change: transform` here, on purpose.
|
|
530
|
+
*
|
|
531
|
+
* This element holds every page in the file and it is the thing the camera
|
|
532
|
+
* scales. `will-change` asks the engine to keep it as one composited layer
|
|
533
|
+
* with its own texture, and a texture has a size limit: past it the layer is
|
|
534
|
+
* rasterized once at a low scale and the compositor stretches the result. The
|
|
535
|
+
* text goes soft and nothing in the document says why.
|
|
536
|
+
*
|
|
537
|
+
* The transform changes when the user zooms or pans, not every frame, so there
|
|
538
|
+
* is nothing to keep the layer warm for.
|
|
539
|
+
*/
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
.page-box {
|
|
543
|
+
position: absolute;
|
|
544
|
+
border-radius: 14px;
|
|
545
|
+
background: #ffffff;
|
|
546
|
+
border: 1px solid var(--chrome-line);
|
|
547
|
+
box-shadow: var(--chrome-shadow-page);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
.page-box.selected { outline: 2px solid var(--chrome-ink); outline-offset: 2px; }
|
|
551
|
+
|
|
552
|
+
/*
|
|
553
|
+
* A page's name is chrome, so it does not zoom with the drawing.
|
|
554
|
+
*
|
|
555
|
+
* The label sits inside the page box, and the page box is inside the element the
|
|
556
|
+
* camera scales — so the name used to be a 1.9px smudge at 10% and a headline at
|
|
557
|
+
* 400%, and a name is only a name when it can be read. `--chrome-unzoom` is the
|
|
558
|
+
* reciprocal of the zoom, written by `applyCamera`; scaling by it gives the pill
|
|
559
|
+
* its own size in screen pixels at every step of the ladder.
|
|
560
|
+
*
|
|
561
|
+
* The bottom edge is the anchor, because the lift has to be a screen distance
|
|
562
|
+
* too. Pinned by its top — the `top: -22px` this used to be — the pill's 3.5px
|
|
563
|
+
* of air scaled with everything else: 14px over the page it names at 400%, and
|
|
564
|
+
* under a pixel at 25%, where the label sits as good as flat on the design.
|
|
565
|
+
* `bottom: 100%` moves the anchor onto the page's top edge, and translating by
|
|
566
|
+
* the same reciprocal leaves the same 4px of air at every zoom. At 100% that is
|
|
567
|
+
* where the old offset landed, to within the half pixel the pill's own height
|
|
568
|
+
* rounds by.
|
|
569
|
+
*
|
|
570
|
+
* Undone by a transform, not by a second element or a per-zoom class: there is
|
|
571
|
+
* one label per page, in one place, and its hit box is the pill you can see — as
|
|
572
|
+
* draggable at 10% as at 100%.
|
|
573
|
+
*/
|
|
574
|
+
.page-label {
|
|
575
|
+
position: absolute;
|
|
576
|
+
left: 0;
|
|
577
|
+
bottom: 100%;
|
|
578
|
+
padding: 1px 7px;
|
|
579
|
+
border-radius: 999px;
|
|
580
|
+
background: var(--chrome-ink);
|
|
581
|
+
color: #fbfaf8;
|
|
582
|
+
font-size: 10.5px;
|
|
583
|
+
letter-spacing: 0.02em;
|
|
584
|
+
white-space: nowrap;
|
|
585
|
+
cursor: move;
|
|
586
|
+
user-select: none;
|
|
587
|
+
--label-gap: 4px;
|
|
588
|
+
transform-origin: left bottom;
|
|
589
|
+
transform:
|
|
590
|
+
translateY(calc(var(--label-gap) * var(--chrome-unzoom, 1) * -1))
|
|
591
|
+
scale(var(--chrome-unzoom, 1));
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
.page-box.selected .page-label { color: #ffffff; background: #2a2823; }
|
|
595
|
+
|
|
596
|
+
.page-box.dragging { opacity: 0.85; }
|
|
597
|
+
|
|
598
|
+
/* -- selection overlay ------------------------------------------------------ */
|
|
599
|
+
|
|
600
|
+
.overlay { position: absolute; inset: 0; pointer-events: none; }
|
|
601
|
+
|
|
602
|
+
/*
|
|
603
|
+
* A box is a line *around* the element, and nothing else.
|
|
604
|
+
*
|
|
605
|
+
* Three decisions, all of them about not covering the thing being looked at:
|
|
606
|
+
*
|
|
607
|
+
* `outline`, not `border`. An outline is painted outside the border box, so
|
|
608
|
+
* the weight of the line costs the design nothing — a border eats the first
|
|
609
|
+
* pixels of every edge, and on a 3px-wide element at 11% zoom that is the whole
|
|
610
|
+
* element. Because the line is free, its weight can be chosen for legibility
|
|
611
|
+
* alone: two pixels, drawn in viewport pixels rather than the design's, so it
|
|
612
|
+
* is the same at 11% and at 400%. One pixel was a hairline against a dark page
|
|
613
|
+
* or a busy photograph.
|
|
614
|
+
* No fill. The selection used to be washed with 10% blue, which on a full-page
|
|
615
|
+
* frame is a veil over the entire artboard: you cannot judge a colour, a
|
|
616
|
+
* gradient or a photograph through it.
|
|
617
|
+
* Weight by kind, colour by commitment. Two pixels means "a level", one means
|
|
618
|
+
* "a child of one". *Which* level — the committed one or the one the pointer is
|
|
619
|
+
* standing on — is said by the colour and by the four handles, which only the
|
|
620
|
+
* selection has. Encoding that in weight instead left the hover box thinner
|
|
621
|
+
* than the children it was framing, which is backwards: the container is the
|
|
622
|
+
* frame of the decision, not a cursor.
|
|
623
|
+
*/
|
|
624
|
+
/*
|
|
625
|
+
* The selection is ink, with a white halo underneath it.
|
|
626
|
+
*
|
|
627
|
+
* The field is light, the pages are white and many designs put a photograph
|
|
628
|
+
* under the box — a dark line alone disappears on a dark image, a light line
|
|
629
|
+
* alone disappears on paper. The halo is one pixel of white so the ring reads on
|
|
630
|
+
* any of the three.
|
|
631
|
+
*/
|
|
632
|
+
.outline {
|
|
633
|
+
position: absolute;
|
|
634
|
+
outline: 2px solid var(--chrome-ink);
|
|
635
|
+
outline-offset: 1px;
|
|
636
|
+
background: none;
|
|
637
|
+
pointer-events: none;
|
|
638
|
+
box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.95);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
/*
|
|
642
|
+
* The level the pointer is standing on: the same weight as the selection, a
|
|
643
|
+
* lighter blue, and no handles.
|
|
644
|
+
*/
|
|
645
|
+
.outline.level { outline-color: rgba(20, 19, 15, 0.55); box-shadow: none; }
|
|
646
|
+
|
|
647
|
+
/*
|
|
648
|
+
* Its direct children: what a double-click steps into, one rung down.
|
|
649
|
+
*
|
|
650
|
+
* One pixel, and spelled out rather than inherited from `.outline`, which is a
|
|
651
|
+
* rule the class shares — the width was inherited once, and thickening the
|
|
652
|
+
* selection thickened every dashed box with it.
|
|
653
|
+
*
|
|
654
|
+
* One, not two, because a stack of rows with no gaps between them is the case
|
|
655
|
+
* this has to survive: thirteen rows in a card, each with a two pixel dashed box,
|
|
656
|
+
* is 26 pixels of dash across a 148 pixel card at 29% zoom — a mesh rather than
|
|
657
|
+
* thirteen boxes. Fading the colour instead only makes the mesh paler, which is
|
|
658
|
+
* why the width is what moved.
|
|
659
|
+
*/
|
|
660
|
+
.outline.child {
|
|
661
|
+
outline-style: dashed;
|
|
662
|
+
outline-width: 1px;
|
|
663
|
+
outline-color: rgba(20, 19, 15, 0.45);
|
|
664
|
+
box-shadow: none;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
/*
|
|
668
|
+
* The one the pointer is actually over — the box a click would take.
|
|
669
|
+
*
|
|
670
|
+
* It cannot be told apart by a wash any more, so it is told apart by the line:
|
|
671
|
+
* two pixels against its siblings' one, and brighter. It still reads as a dashed
|
|
672
|
+
* box, which is what it is — the child you are about to step into.
|
|
673
|
+
*/
|
|
674
|
+
.outline.child.under {
|
|
675
|
+
outline-width: 2px;
|
|
676
|
+
outline-color: rgba(20, 19, 15, 0.75);
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
/* A box too small to have usable corners: the ring stays, the handles go. */
|
|
680
|
+
.outline.small .handle { display: none; }
|
|
681
|
+
|
|
682
|
+
.handle {
|
|
683
|
+
position: absolute;
|
|
684
|
+
width: 9px;
|
|
685
|
+
height: 9px;
|
|
686
|
+
margin: -4.5px 0 0 -4.5px;
|
|
687
|
+
background: #ffffff;
|
|
688
|
+
border: 1.5px solid var(--chrome-ink);
|
|
689
|
+
border-radius: 50%;
|
|
690
|
+
pointer-events: auto;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
/* -- inspector -------------------------------------------------------------- */
|
|
694
|
+
|
|
695
|
+
.field { margin-bottom: 8px; min-width: 0; }
|
|
696
|
+
|
|
697
|
+
.field label {
|
|
698
|
+
display: block;
|
|
699
|
+
color: var(--chrome-dim);
|
|
700
|
+
font-size: 11px;
|
|
701
|
+
margin-bottom: 2px;
|
|
702
|
+
overflow: hidden;
|
|
703
|
+
text-overflow: ellipsis;
|
|
704
|
+
white-space: nowrap;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
.field input[type="text"], .field input[type="number"], .field select, .field textarea {
|
|
708
|
+
width: 100%;
|
|
709
|
+
font: inherit;
|
|
710
|
+
font-variant-numeric: tabular-nums;
|
|
711
|
+
color: var(--chrome-ink);
|
|
712
|
+
background: var(--chrome-input);
|
|
713
|
+
border: 1px solid var(--chrome-line);
|
|
714
|
+
border-radius: 5px;
|
|
715
|
+
padding: 4px 6px;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
.field input[type="color"] {
|
|
719
|
+
width: 100%;
|
|
720
|
+
height: 26px;
|
|
721
|
+
padding: 0;
|
|
722
|
+
background: var(--chrome-input);
|
|
723
|
+
border: 1px solid var(--chrome-line);
|
|
724
|
+
border-radius: 5px;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
/*
|
|
728
|
+
* These sit after the base field rules on purpose: `border` above is a shorthand,
|
|
729
|
+
* and a `border-color` written before it loses to it however specific the
|
|
730
|
+
* selector is. The ring still shows without this, but the field should also
|
|
731
|
+
* change its edge — a screenshot of two rings at 11% is not enough to tell which
|
|
732
|
+
* one has the keyboard.
|
|
733
|
+
*/
|
|
734
|
+
.field input:hover:not(:disabled), .field select:hover:not(:disabled), .field textarea:hover:not(:disabled) {
|
|
735
|
+
border-color: rgba(20, 19, 15, 0.22);
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
.field input:focus-visible, .field select:focus-visible, .field textarea:focus-visible,
|
|
739
|
+
.drawer-fix textarea:focus-visible {
|
|
740
|
+
outline-offset: 1px;
|
|
741
|
+
border-color: var(--chrome-ink);
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
/*
|
|
745
|
+
* One row, two columns — used when a section has a pair of properties.
|
|
746
|
+
*
|
|
747
|
+
* `1fr 1fr` and a single child is a child in the first column, which is what an
|
|
748
|
+
* unpaired property wants: half a row would be a misleading amount of space for
|
|
749
|
+
* a value that has no partner.
|
|
750
|
+
*/
|
|
751
|
+
.field-grid {
|
|
752
|
+
display: grid;
|
|
753
|
+
grid-template-columns: 1fr 1fr;
|
|
754
|
+
gap: 0 6px;
|
|
755
|
+
min-width: 0;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
/*
|
|
759
|
+
* A row with one property, not a pair.
|
|
760
|
+
*
|
|
761
|
+
* The two-column grid is for width/height, left/top and the other pairs that are
|
|
762
|
+
* read together. A property without a partner took half of that grid and left
|
|
763
|
+
* the other half empty, which is how `padding:26px 26px 20px 26px` came to be
|
|
764
|
+
* cut off at "20p" in a panel with room for it. One property, one full row.
|
|
765
|
+
*/
|
|
766
|
+
.field-grid.single { grid-template-columns: 1fr; }
|
|
767
|
+
|
|
768
|
+
/* -- inspector sections ----------------------------------------------------- */
|
|
769
|
+
|
|
770
|
+
.section { margin-bottom: 4px; border-top: 1px solid var(--chrome-line); }
|
|
771
|
+
|
|
772
|
+
.section-head {
|
|
773
|
+
display: flex;
|
|
774
|
+
align-items: center;
|
|
775
|
+
width: 100%;
|
|
776
|
+
padding: 7px 2px;
|
|
777
|
+
border: 0;
|
|
778
|
+
border-radius: 0;
|
|
779
|
+
background: none;
|
|
780
|
+
color: var(--chrome-dim);
|
|
781
|
+
font-size: 11px;
|
|
782
|
+
text-transform: uppercase;
|
|
783
|
+
letter-spacing: 0.06em;
|
|
784
|
+
text-align: left;
|
|
785
|
+
cursor: pointer;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
.section-head:hover { color: var(--chrome-ink); background: none; }
|
|
789
|
+
.section-head:focus-visible { outline-offset: -2px; }
|
|
790
|
+
|
|
791
|
+
/* A triangle that turns: the fold is visible before it is used. */
|
|
792
|
+
.section-head::before {
|
|
793
|
+
content: "";
|
|
794
|
+
width: 0;
|
|
795
|
+
height: 0;
|
|
796
|
+
margin-right: 6px;
|
|
797
|
+
border-left: 4px solid currentColor;
|
|
798
|
+
border-top: 3.5px solid transparent;
|
|
799
|
+
border-bottom: 3.5px solid transparent;
|
|
800
|
+
transform: rotate(90deg);
|
|
801
|
+
transition: transform 90ms linear;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
.section.folded .section-head::before { transform: rotate(0deg); }
|
|
805
|
+
|
|
806
|
+
.section-body { padding-bottom: 6px; }
|
|
807
|
+
.section.folded .section-body { display: none; }
|
|
808
|
+
|
|
809
|
+
/* -- segmented control ------------------------------------------------------ */
|
|
810
|
+
|
|
811
|
+
/*
|
|
812
|
+
* Two or three short alternatives as buttons. `justify-content` has five values
|
|
813
|
+
* and gets a dropdown instead; the rule lives in `canvas/groups.mjs`.
|
|
814
|
+
*/
|
|
815
|
+
.seg {
|
|
816
|
+
display: flex;
|
|
817
|
+
gap: 2px;
|
|
818
|
+
padding: 2px;
|
|
819
|
+
background: var(--chrome-track);
|
|
820
|
+
border: 1px solid var(--chrome-line);
|
|
821
|
+
border-radius: 6px;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
.seg-option {
|
|
825
|
+
flex: 1;
|
|
826
|
+
min-width: 0;
|
|
827
|
+
padding: 3px 2px;
|
|
828
|
+
border: 0;
|
|
829
|
+
border-radius: 4px;
|
|
830
|
+
background: none;
|
|
831
|
+
color: var(--chrome-dim);
|
|
832
|
+
font-size: 11px;
|
|
833
|
+
overflow: hidden;
|
|
834
|
+
text-overflow: ellipsis;
|
|
835
|
+
white-space: nowrap;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
.seg-option:hover { background: var(--chrome-hover); }
|
|
839
|
+
.seg-option:focus-visible { outline-offset: -2px; }
|
|
840
|
+
.seg-option.active {
|
|
841
|
+
background: var(--chrome-input);
|
|
842
|
+
color: var(--chrome-ink);
|
|
843
|
+
box-shadow: 0 1px 2px rgba(20, 19, 15, 0.10);
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
.scrub { cursor: ew-resize; }
|
|
847
|
+
|
|
848
|
+
.value-line {
|
|
849
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
850
|
+
font-size: 11px;
|
|
851
|
+
color: var(--chrome-dim);
|
|
852
|
+
word-break: break-all;
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
.empty { color: var(--chrome-dim); }
|
|
856
|
+
.muted { color: var(--chrome-dim); }
|
|
857
|
+
|
|
858
|
+
dl.attrs { margin: 0 0 6px; }
|
|
859
|
+
dl.attrs dt { color: var(--chrome-dim); font-size: 11px; }
|
|
860
|
+
dl.attrs dd { margin: 0 0 6px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 11px; word-break: break-all; }
|
|
861
|
+
|
|
862
|
+
/* -- problems drawer -------------------------------------------------------- */
|
|
863
|
+
|
|
864
|
+
/*
|
|
865
|
+
* The problems sheet.
|
|
866
|
+
*
|
|
867
|
+
* A card on the same field as the panels, 12px in from the canvas edges: closing
|
|
868
|
+
* a panel widens it instead of leaving it over a track that is no longer there.
|
|
869
|
+
* Inside it is the same paper language — an eyebrow with a count, monospaced
|
|
870
|
+
* codes, a severity chip, and the hint under the message rather than beside it,
|
|
871
|
+
* because a hint is a sentence and a sentence reads down, not across.
|
|
872
|
+
*/
|
|
873
|
+
.drawer {
|
|
874
|
+
position: absolute;
|
|
875
|
+
left: calc(var(--panel-left) + var(--gutter-left) + 12px);
|
|
876
|
+
right: calc(var(--panel-right) + var(--gutter-right) + 12px);
|
|
877
|
+
bottom: 12px;
|
|
878
|
+
max-height: 42%;
|
|
879
|
+
display: flex;
|
|
880
|
+
flex-direction: column;
|
|
881
|
+
overflow: hidden;
|
|
882
|
+
background: var(--chrome-panel);
|
|
883
|
+
border: 1px solid var(--chrome-line);
|
|
884
|
+
border-radius: 14px;
|
|
885
|
+
box-shadow: var(--chrome-shadow-panel);
|
|
886
|
+
z-index: 20;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
.drawer-head {
|
|
890
|
+
display: flex;
|
|
891
|
+
align-items: center;
|
|
892
|
+
justify-content: space-between;
|
|
893
|
+
gap: 12px;
|
|
894
|
+
flex: none;
|
|
895
|
+
padding: 10px 14px;
|
|
896
|
+
border-bottom: 1px solid var(--chrome-line);
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
.drawer-title { display: flex; align-items: baseline; gap: 10px; min-width: 0; }
|
|
900
|
+
|
|
901
|
+
.drawer-summary {
|
|
902
|
+
font-size: 11px;
|
|
903
|
+
color: var(--chrome-dim);
|
|
904
|
+
font-variant-numeric: tabular-nums;
|
|
905
|
+
white-space: nowrap;
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
.drawer-actions { display: flex; align-items: center; gap: 4px; }
|
|
909
|
+
.drawer-actions button { height: 26px; padding: 0 10px; border-radius: 999px; }
|
|
910
|
+
.drawer-close { background: var(--chrome-track); }
|
|
911
|
+
|
|
912
|
+
.drawer-body { overflow: auto; padding: 4px 6px 6px; min-height: 0; }
|
|
913
|
+
|
|
914
|
+
.drawer-fix {
|
|
915
|
+
display: flex;
|
|
916
|
+
flex-direction: column;
|
|
917
|
+
gap: 8px;
|
|
918
|
+
padding: 12px;
|
|
919
|
+
border-top: 1px solid var(--chrome-line);
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
.drawer-hint { margin: 0; font-size: 11px; color: var(--chrome-dim); }
|
|
923
|
+
|
|
924
|
+
/* The whole file, monospaced, because that is what it is. `wrap="off"` on the element
|
|
925
|
+
keeps a long line from folding, which would make an offset impossible to find. */
|
|
926
|
+
.drawer-fix textarea {
|
|
927
|
+
width: 100%;
|
|
928
|
+
height: 32vh;
|
|
929
|
+
resize: vertical;
|
|
930
|
+
font: 12px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
931
|
+
background: var(--chrome-input);
|
|
932
|
+
color: var(--chrome-ink);
|
|
933
|
+
border: 1px solid var(--chrome-line);
|
|
934
|
+
border-radius: 6px;
|
|
935
|
+
padding: 6px 8px;
|
|
936
|
+
white-space: pre;
|
|
937
|
+
overflow: auto;
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
.issue {
|
|
941
|
+
display: grid;
|
|
942
|
+
grid-template-columns: 74px minmax(0, 1fr);
|
|
943
|
+
gap: 10px;
|
|
944
|
+
align-items: start;
|
|
945
|
+
padding: 10px 12px;
|
|
946
|
+
border-radius: 10px;
|
|
947
|
+
border-bottom: 1px solid var(--chrome-line);
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
.issue:last-child { border-bottom: 0; }
|
|
951
|
+
.issue[tabindex] { cursor: pointer; }
|
|
952
|
+
.issue[tabindex]:hover { background: var(--chrome-hover); }
|
|
953
|
+
.issue:focus-visible { outline-offset: -2px; }
|
|
954
|
+
|
|
955
|
+
.issue .sev {
|
|
956
|
+
justify-self: start;
|
|
957
|
+
padding: 2px 7px;
|
|
958
|
+
border: 1px solid currentColor;
|
|
959
|
+
border-radius: 999px;
|
|
960
|
+
font-size: 9.5px;
|
|
961
|
+
letter-spacing: 0.08em;
|
|
962
|
+
text-transform: uppercase;
|
|
963
|
+
}
|
|
964
|
+
.issue .sev.error { color: var(--chrome-danger); }
|
|
965
|
+
.issue .sev.warning { color: var(--chrome-warn); }
|
|
966
|
+
|
|
967
|
+
.issue-body { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
|
|
968
|
+
.issue .code {
|
|
969
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
970
|
+
font-size: 10.5px;
|
|
971
|
+
color: var(--chrome-dim);
|
|
972
|
+
}
|
|
973
|
+
.issue .msg {
|
|
974
|
+
margin: 0;
|
|
975
|
+
font-size: 12.5px;
|
|
976
|
+
line-height: 1.5;
|
|
977
|
+
color: var(--chrome-ink);
|
|
978
|
+
overflow-wrap: anywhere;
|
|
979
|
+
}
|
|
980
|
+
.issue .hint { margin: 0; font-size: 11px; line-height: 1.5; color: var(--chrome-dim); }
|
|
981
|
+
|
|
982
|
+
/* The empty state is a row-shaped sentence, not a row of a table. */
|
|
983
|
+
.issue.empty { display: block; padding: 18px 14px; color: var(--chrome-dim); border-bottom: 0; }
|
|
984
|
+
|
|
985
|
+
/* -- toast ------------------------------------------------------------------ */
|
|
986
|
+
|
|
987
|
+
.toast {
|
|
988
|
+
position: fixed;
|
|
989
|
+
left: 50%;
|
|
990
|
+
bottom: 22px;
|
|
991
|
+
transform: translateX(-50%);
|
|
992
|
+
max-width: 70vw;
|
|
993
|
+
padding: 8px 14px;
|
|
994
|
+
border-radius: 999px;
|
|
995
|
+
background: var(--chrome-ink);
|
|
996
|
+
border: 1px solid var(--chrome-ink);
|
|
997
|
+
color: #fbfaf8;
|
|
998
|
+
box-shadow: var(--chrome-shadow-pill);
|
|
999
|
+
z-index: 40;
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
.toast.refused { border-color: var(--chrome-danger); }
|
|
1003
|
+
|
|
1004
|
+
/* -- hidden ----------------------------------------------------------------- */
|
|
1005
|
+
|
|
1006
|
+
/*
|
|
1007
|
+
* `hidden` means hidden, whatever else this sheet says about the element.
|
|
1008
|
+
*
|
|
1009
|
+
* The browser's own `[hidden] { display: none }` lives in the user-agent sheet,
|
|
1010
|
+
* and every author rule beats it however low its specificity — so `.drawer {
|
|
1011
|
+
* display: flex }` left the Problems drawer on screen with the attribute set. It
|
|
1012
|
+
* covered the bottom 42% of the canvas the moment the file loaded, its Close
|
|
1013
|
+
* button closed nothing, and the empty fix textarea sat under every report.
|
|
1014
|
+
*
|
|
1015
|
+
* Written once, at the end, so no rule above it can beat it and no element given
|
|
1016
|
+
* a `display` later can be missed — which is what a per-class list of `[hidden]`
|
|
1017
|
+
* selectors did.
|
|
1018
|
+
*/
|
|
1019
|
+
[hidden] { display: none !important; }
|
|
1020
|
+
|
|
1021
|
+
/* -- embedded mode ---------------------------------------------------------
|
|
1022
|
+
*
|
|
1023
|
+
* The DSH plugin iframes the canvas in a right-sidebar tab. The layer tree and
|
|
1024
|
+
* inspector are folded by `enterEmbed()` in app.mjs; the bar itself keeps only
|
|
1025
|
+
* what a narrow panel can use: the brand, zoom and Fit. Hiding by attribute
|
|
1026
|
+
* rather than waiting for `applyBarFit()` also makes the first paint correct,
|
|
1027
|
+
* before the bar has measured a single pill.
|
|
1028
|
+
*/
|
|
1029
|
+
body[data-embed] :is(.bar-left .file, .bar-left .pill, .bar-left .warn,
|
|
1030
|
+
#toggle-problems, .export-sep, #export-page, #export-all) {
|
|
1031
|
+
display: none !important;
|
|
1032
|
+
}
|