vite-plugin-nora 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/NOTICE +26 -0
- package/README.md +113 -0
- package/licenses/geist-OFL.txt +93 -0
- package/licenses/lucide-ISC.txt +17 -0
- package/package.json +90 -0
- package/src/cli.js +101 -0
- package/src/client/App.jsx +216 -0
- package/src/client/canvas/Canvas.jsx +95 -0
- package/src/client/canvas/ErrorBoundary.jsx +56 -0
- package/src/client/canvas/Preview.jsx +70 -0
- package/src/client/canvas/Viewport.jsx +211 -0
- package/src/client/chords.js +38 -0
- package/src/client/css.d.ts +5 -0
- package/src/client/fonts.css +48 -0
- package/src/client/frame.css +107 -0
- package/src/client/frame.html +11 -0
- package/src/client/frame.jsx +73 -0
- package/src/client/index.html +12 -0
- package/src/client/main.jsx +10 -0
- package/src/client/open-folder.js +84 -0
- package/src/client/selection.js +22 -0
- package/src/client/styles.css +1264 -0
- package/src/client/sweep/SweepPanel.jsx +206 -0
- package/src/client/sweep/measure.js +230 -0
- package/src/client/sweep/report.js +54 -0
- package/src/client/sweep/run-sweep.js +185 -0
- package/src/client/toolbar/Picker.jsx +343 -0
- package/src/client/toolbar/Toolbar.jsx +231 -0
- package/src/client/toolbar/ViewportMenu.jsx +83 -0
- package/src/client/toolbar/bar-shape.js +402 -0
- package/src/client/toolbar/icons.jsx +129 -0
- package/src/client/toolbar/use-draggable-bar.js +204 -0
- package/src/client/viewports.js +43 -0
- package/src/client/virtual.d.ts +28 -0
- package/src/index.js +4 -0
- package/src/server/create-server.js +147 -0
- package/src/server/plugin.js +207 -0
- package/src/server/safe-path.js +42 -0
- package/src/server/scan.js +350 -0
- package/types/index.d.ts +3 -0
- package/types/server/create-server.d.ts +22 -0
- package/types/server/plugin.d.ts +20 -0
- package/types/server/safe-path.d.ts +25 -0
- package/types/server/scan.d.ts +106 -0
|
@@ -0,0 +1,1264 @@
|
|
|
1
|
+
/* =========================================================================
|
|
2
|
+
nora — client chrome
|
|
3
|
+
|
|
4
|
+
The canvas honours a light/dark toggle; the bar is always dark, the way a
|
|
5
|
+
piece of hardware sitting on top of the work would be.
|
|
6
|
+
========================================================================= */
|
|
7
|
+
|
|
8
|
+
@import "./fonts.css";
|
|
9
|
+
|
|
10
|
+
.nora-app {
|
|
11
|
+
--nora-canvas: #f1f2f4;
|
|
12
|
+
--nora-canvas-ink: #14171c;
|
|
13
|
+
--nora-canvas-muted: #5c6672;
|
|
14
|
+
--nora-canvas-line: #d8dce1;
|
|
15
|
+
--nora-accent: #1b7da3;
|
|
16
|
+
|
|
17
|
+
/* The three colours the sweep speaks in: one for "this is wrong", one for
|
|
18
|
+
"this is a fact about your layout", one for "there was nothing to find".
|
|
19
|
+
Defined here because the panel is on the bar, which is always dark, so
|
|
20
|
+
they don't change with the canvas. The green is pitched to sit with the
|
|
21
|
+
other two rather than to announce itself — a clean sweep is the expected
|
|
22
|
+
outcome, not an achievement. */
|
|
23
|
+
--nora-flag: #f0a0a0;
|
|
24
|
+
--nora-flag-solid: #d76a6a;
|
|
25
|
+
--nora-note: #7cc4e2;
|
|
26
|
+
--nora-ok: #8ed3a9;
|
|
27
|
+
|
|
28
|
+
--nora-pill: #16181c;
|
|
29
|
+
--nora-pill-hi: #22262d;
|
|
30
|
+
--nora-pill-ink: #ffffff;
|
|
31
|
+
--nora-pill-soft: rgba(255, 255, 255, 0.1);
|
|
32
|
+
--nora-pill-soft-hi: rgba(255, 255, 255, 0.17);
|
|
33
|
+
--nora-pill-dim: rgba(255, 255, 255, 0.45);
|
|
34
|
+
--nora-pill-line: rgba(255, 255, 255, 0.08);
|
|
35
|
+
|
|
36
|
+
--nora-ease: cubic-bezier(0.19, 1, 0.22, 1);
|
|
37
|
+
--nora-shadow: 0 2px 8px rgba(0, 0, 0, 0.14), 0 12px 32px rgba(0, 0, 0, 0.16);
|
|
38
|
+
|
|
39
|
+
/* Geist and Geist Mono are bundled as inlined subsets — see fonts.css. The
|
|
40
|
+
fallbacks matter anyway: they cover the handful of glyphs the subset does
|
|
41
|
+
not carry, the macOS ⌘ and ⌥ among them. */
|
|
42
|
+
--nora-sans:
|
|
43
|
+
"Nora Geist", ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
|
44
|
+
--nora-mono: "Nora Geist Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.nora-app[data-theme="dark"] {
|
|
48
|
+
--nora-canvas: #101317;
|
|
49
|
+
--nora-canvas-ink: #e6e9ed;
|
|
50
|
+
--nora-canvas-muted: #8b95a2;
|
|
51
|
+
--nora-canvas-line: #262d36;
|
|
52
|
+
--nora-accent: #57add0;
|
|
53
|
+
|
|
54
|
+
/* The bar is always dark, but on a dark canvas "always dark" stops being a
|
|
55
|
+
silhouette and starts being camouflage — #16181c on #101317 is separated
|
|
56
|
+
by a drop shadow and nothing else. Lift the fill and add a hairline rim so
|
|
57
|
+
the chrome still reads as sitting *on top of* the work. */
|
|
58
|
+
--nora-pill: #1e2127;
|
|
59
|
+
--nora-pill-hi: #2a2e36;
|
|
60
|
+
--nora-pill-rim: rgba(255, 255, 255, 0.11);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
html,
|
|
64
|
+
body,
|
|
65
|
+
#nora-root {
|
|
66
|
+
height: 100%;
|
|
67
|
+
margin: 0;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/* Scoped to our own chrome on purpose. A global `* { box-sizing: border-box }`
|
|
71
|
+
would silently change how a previewed component lays out compared to the
|
|
72
|
+
real app, which is the one thing this tool must never do. */
|
|
73
|
+
.nora-bar-area,
|
|
74
|
+
.nora-bar-area *,
|
|
75
|
+
.nora-bar-area *::before,
|
|
76
|
+
.nora-bar-area *::after,
|
|
77
|
+
.nora-empty,
|
|
78
|
+
.nora-empty * {
|
|
79
|
+
box-sizing: border-box;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.nora-app {
|
|
83
|
+
height: 100%;
|
|
84
|
+
display: flex;
|
|
85
|
+
flex-direction: column;
|
|
86
|
+
background: var(--nora-canvas);
|
|
87
|
+
color: var(--nora-canvas-ink);
|
|
88
|
+
font-family: var(--nora-sans);
|
|
89
|
+
transition: background 0.25s ease;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/* ---------- canvas ---------- */
|
|
93
|
+
|
|
94
|
+
.nora-canvas {
|
|
95
|
+
flex: 1;
|
|
96
|
+
min-height: 0;
|
|
97
|
+
display: flex;
|
|
98
|
+
align-items: center;
|
|
99
|
+
justify-content: center;
|
|
100
|
+
overflow: hidden;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/* ---------- viewport frame ---------- */
|
|
104
|
+
|
|
105
|
+
.nora-viewport {
|
|
106
|
+
position: relative;
|
|
107
|
+
flex: 1;
|
|
108
|
+
min-width: 0;
|
|
109
|
+
min-height: 0;
|
|
110
|
+
height: 100%;
|
|
111
|
+
display: flex;
|
|
112
|
+
flex-direction: column;
|
|
113
|
+
align-items: center;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/* At Fit the iframe fills the canvas edge to edge: no padding, no border, no
|
|
117
|
+
shadow. The frame document does its own centring, so a second layer of
|
|
118
|
+
padding out here would only push the component off-centre. */
|
|
119
|
+
.nora-viewport[data-framed="true"] {
|
|
120
|
+
padding: 1.5rem 1.25rem;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/* Not inset: the frame is the canvas, so it gets the whole thing. */
|
|
124
|
+
.nora-viewport[data-framed="false"] {
|
|
125
|
+
padding: 0;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.nora-frame-holder {
|
|
129
|
+
flex: 1;
|
|
130
|
+
min-height: 0;
|
|
131
|
+
overflow: hidden;
|
|
132
|
+
transition: width 0.28s var(--nora-ease);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.nora-viewport[data-framed="true"] .nora-frame-holder {
|
|
136
|
+
border-radius: 10px;
|
|
137
|
+
border: 1px solid var(--nora-canvas-line);
|
|
138
|
+
box-shadow:
|
|
139
|
+
0 1px 2px rgba(0, 0, 0, 0.05),
|
|
140
|
+
0 10px 30px rgba(0, 0, 0, 0.07);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.nora-frame {
|
|
144
|
+
display: block;
|
|
145
|
+
border: 0;
|
|
146
|
+
background: transparent;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/* Covers the frame for the few seconds a sweep runs. The frame underneath
|
|
150
|
+
keeps laying out and painting — that is what the measurements read — but the
|
|
151
|
+
hundred width changes never reach the eye. */
|
|
152
|
+
.nora-sweep-cover {
|
|
153
|
+
position: absolute;
|
|
154
|
+
inset: 0;
|
|
155
|
+
z-index: 2;
|
|
156
|
+
background: var(--nora-canvas);
|
|
157
|
+
display: flex;
|
|
158
|
+
flex-direction: column;
|
|
159
|
+
align-items: center;
|
|
160
|
+
justify-content: center;
|
|
161
|
+
gap: 0.7rem;
|
|
162
|
+
animation: nora-cover-in 0.18s ease both;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
.nora-sweep-cover-label {
|
|
166
|
+
font-family: var(--nora-mono);
|
|
167
|
+
font-size: 0.74rem;
|
|
168
|
+
letter-spacing: 0.02em;
|
|
169
|
+
color: var(--nora-canvas-muted);
|
|
170
|
+
animation: nora-breathe 1.8s ease-in-out infinite;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/* The progress bar lives here, under the label, rather than inside the button
|
|
174
|
+
that started the sweep: once the canvas is covered, the bar is the one place
|
|
175
|
+
nobody is looking. */
|
|
176
|
+
.nora-sweep-cover-track {
|
|
177
|
+
/* A shade wider than the label above it, so the two read as one block. */
|
|
178
|
+
width: 15rem;
|
|
179
|
+
height: 2px;
|
|
180
|
+
border-radius: 1px;
|
|
181
|
+
background: var(--nora-canvas-line, rgb(128 128 128 / 0.25));
|
|
182
|
+
overflow: hidden;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
.nora-sweep-cover-fill {
|
|
186
|
+
display: block;
|
|
187
|
+
width: 100%;
|
|
188
|
+
height: 100%;
|
|
189
|
+
background: var(--nora-canvas-muted);
|
|
190
|
+
transform-origin: left center;
|
|
191
|
+
transform: scaleX(0);
|
|
192
|
+
transition: transform 0.12s linear;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
@keyframes nora-cover-in {
|
|
196
|
+
from {
|
|
197
|
+
opacity: 0;
|
|
198
|
+
}
|
|
199
|
+
to {
|
|
200
|
+
opacity: 1;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
@keyframes nora-breathe {
|
|
205
|
+
0%,
|
|
206
|
+
100% {
|
|
207
|
+
opacity: 0.55;
|
|
208
|
+
}
|
|
209
|
+
50% {
|
|
210
|
+
opacity: 1;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/* Width transitions would lag a hundred steps behind the sweep. */
|
|
215
|
+
.nora-viewport[data-sweeping="true"] .nora-frame-holder {
|
|
216
|
+
transition: none;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
@media (prefers-reduced-motion: reduce) {
|
|
220
|
+
.nora-sweep-cover,
|
|
221
|
+
.nora-sweep-cover-label {
|
|
222
|
+
animation: none;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
.nora-sweep-cover-fill {
|
|
226
|
+
transition: none;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/* Nothing renders an error in *this* document — a component that throws does so
|
|
231
|
+
inside the frame, where the boundary lives, so the error surface is
|
|
232
|
+
`.noraf-error` in frame.css. */
|
|
233
|
+
.nora-empty {
|
|
234
|
+
font-family: var(--nora-sans);
|
|
235
|
+
font-size: 0.84rem;
|
|
236
|
+
color: var(--nora-canvas-muted);
|
|
237
|
+
text-align: center;
|
|
238
|
+
line-height: 1.75;
|
|
239
|
+
display: flex;
|
|
240
|
+
flex-direction: column;
|
|
241
|
+
gap: 0.15rem;
|
|
242
|
+
max-width: 46ch;
|
|
243
|
+
padding: 2.5rem;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
.nora-empty-title {
|
|
247
|
+
color: var(--nora-canvas-ink);
|
|
248
|
+
font-weight: 500;
|
|
249
|
+
letter-spacing: -0.01em;
|
|
250
|
+
margin-bottom: 0.35rem;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
.nora-empty-line {
|
|
254
|
+
font-size: 0.79rem;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/* =========================================================================
|
|
258
|
+
The bar
|
|
259
|
+
|
|
260
|
+
One silhouette, three clusters, buttons on top.
|
|
261
|
+
|
|
262
|
+
The dark shape is an SVG path drawn by bar-shape.js, not a background on any
|
|
263
|
+
of these elements. That is the load-bearing decision here: as separate
|
|
264
|
+
rounded boxes with painted bridges between them, the junctions showed a
|
|
265
|
+
hairline ridge at every zoom level, because two antialiased edges meeting on
|
|
266
|
+
one pixel never sum back to full opacity. A single filled path has no
|
|
267
|
+
interior edges to seam.
|
|
268
|
+
|
|
269
|
+
Everything below therefore describes only the *contents*. The clusters are
|
|
270
|
+
plain flex rows with padding; the shape wraps whatever boxes they end up
|
|
271
|
+
occupying, which is why changing the buttons here needs no change there.
|
|
272
|
+
========================================================================= */
|
|
273
|
+
|
|
274
|
+
.nora-bar-area {
|
|
275
|
+
position: fixed;
|
|
276
|
+
bottom: 1.25rem;
|
|
277
|
+
right: 1.25rem;
|
|
278
|
+
z-index: 2147483000;
|
|
279
|
+
display: flex;
|
|
280
|
+
flex-direction: column;
|
|
281
|
+
align-items: flex-end;
|
|
282
|
+
gap: 0.5rem;
|
|
283
|
+
font-family: var(--nora-sans);
|
|
284
|
+
-webkit-font-smoothing: antialiased;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/* Anchored to whichever corner the bar was dragged nearest, so its panels
|
|
288
|
+
always open away from the edge rather than off the screen. */
|
|
289
|
+
.nora-bar-area[data-vside="top"] {
|
|
290
|
+
flex-direction: column-reverse;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
.nora-bar-area[data-hside="left"] {
|
|
294
|
+
align-items: flex-start;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
.nora-bar-area[data-dragging="true"] {
|
|
298
|
+
cursor: grabbing;
|
|
299
|
+
user-select: none;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/* The frame must not swallow the pointer mid-drag. */
|
|
303
|
+
.nora-bar-area[data-dragging="true"] ~ * iframe,
|
|
304
|
+
.nora-app:has(.nora-bar-area[data-dragging="true"]) iframe {
|
|
305
|
+
pointer-events: none;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
.nora-pill {
|
|
309
|
+
/* The expanded width. One number, one place. */
|
|
310
|
+
--nora-bar-w: 462px;
|
|
311
|
+
|
|
312
|
+
position: relative;
|
|
313
|
+
display: flex;
|
|
314
|
+
align-items: center;
|
|
315
|
+
height: 44px;
|
|
316
|
+
color: var(--nora-pill-ink);
|
|
317
|
+
cursor: grab;
|
|
318
|
+
transition: transform 0.2s var(--nora-ease);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/* No `transition: width` anywhere on the pill, deliberately. The silhouette is
|
|
322
|
+
an SVG path, and a path cannot be interpolated by the compositor — so a CSS
|
|
323
|
+
width transition here would animate the box while the shape jumped, and the
|
|
324
|
+
buttons would sit outside their own outline for the length of it. Opening and
|
|
325
|
+
shutting is driven from bar-shape.js instead, which writes this width and the
|
|
326
|
+
path together on the same frame. These two rules are the resting values it
|
|
327
|
+
hands back to. */
|
|
328
|
+
.nora-pill[data-state="collapsed"] {
|
|
329
|
+
width: 44px;
|
|
330
|
+
justify-content: center;
|
|
331
|
+
cursor: pointer;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
.nora-pill[data-state="expanded"] {
|
|
335
|
+
width: var(--nora-bar-w);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/* ---------- the silhouette ---------- */
|
|
339
|
+
|
|
340
|
+
.nora-shape {
|
|
341
|
+
position: absolute;
|
|
342
|
+
left: 0;
|
|
343
|
+
top: 0;
|
|
344
|
+
overflow: visible;
|
|
345
|
+
pointer-events: none;
|
|
346
|
+
filter: drop-shadow(0 2px 8px rgba(0, 0, 0, 0.16)) drop-shadow(0 12px 32px rgba(0, 0, 0, 0.18));
|
|
347
|
+
transition: filter 0.2s ease;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/* The rim rides the same path as the fill, so it is continuous by construction
|
|
351
|
+
— a border drawn round separate cluster elements is the seam problem all
|
|
352
|
+
over again. */
|
|
353
|
+
.nora-shape path {
|
|
354
|
+
fill: var(--nora-pill);
|
|
355
|
+
stroke: var(--nora-pill-rim, transparent);
|
|
356
|
+
stroke-width: 1;
|
|
357
|
+
transition: fill 0.15s ease;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
.nora-pill[data-state="collapsed"]:hover .nora-shape path {
|
|
361
|
+
fill: var(--nora-pill-hi);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
.nora-pill[data-state="collapsed"]:active {
|
|
365
|
+
transform: scale(0.95);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
.nora-bar-area[data-dragging="true"] .nora-shape {
|
|
369
|
+
filter: drop-shadow(0 4px 12px rgba(0, 0, 0, 0.22)) drop-shadow(0 18px 44px rgba(0, 0, 0, 0.24));
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/* ---------- the two faces ---------- */
|
|
373
|
+
|
|
374
|
+
/* Pinned to the held edge at exactly the shut bar's width, rather than
|
|
375
|
+
stretched across the pill: while the bar opens, the pill's width is changing
|
|
376
|
+
every frame, and an `inset: 0` face would drift left with it as it faded. */
|
|
377
|
+
.nora-pill-face {
|
|
378
|
+
position: absolute;
|
|
379
|
+
right: 0;
|
|
380
|
+
top: 0;
|
|
381
|
+
bottom: 0;
|
|
382
|
+
width: 44px;
|
|
383
|
+
display: flex;
|
|
384
|
+
align-items: center;
|
|
385
|
+
justify-content: center;
|
|
386
|
+
color: var(--nora-pill-dim);
|
|
387
|
+
transition:
|
|
388
|
+
opacity 0.14s var(--nora-ease),
|
|
389
|
+
color 0.15s ease;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
.nora-pill[data-state="collapsed"]:hover .nora-pill-face {
|
|
393
|
+
color: var(--nora-pill-ink);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
.nora-pill-face.is-out {
|
|
397
|
+
opacity: 0;
|
|
398
|
+
pointer-events: none;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/* Absolutely positioned at the full expanded width and pinned to the edge the
|
|
402
|
+
bar opens and shuts against, so it is the same box in every state. That is
|
|
403
|
+
what lets the silhouette animate at all: the boxes feeding the path are
|
|
404
|
+
measured from these clusters, and if the container reflowed as the pill grew,
|
|
405
|
+
the flexible subject cluster would re-solve on every frame of the morph and
|
|
406
|
+
the path would be chasing a layout that was still settling.
|
|
407
|
+
Opacity is the only thing that changes here — composited, no layout, no
|
|
408
|
+
repaint of the subtree. The `filter: blur()` that used to sit on this element
|
|
409
|
+
animated a filter over live text on every frame of the expansion, next to a
|
|
410
|
+
path being rebuilt on those same frames, and was a large part of why opening
|
|
411
|
+
the bar stuttered. */
|
|
412
|
+
.nora-pill-controls {
|
|
413
|
+
position: absolute;
|
|
414
|
+
right: 0;
|
|
415
|
+
top: 0;
|
|
416
|
+
bottom: 0;
|
|
417
|
+
width: var(--nora-bar-w);
|
|
418
|
+
display: flex;
|
|
419
|
+
align-items: center;
|
|
420
|
+
transition: opacity 0.13s var(--nora-ease);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/* While the shape is morphing, opacity is written per frame from
|
|
424
|
+
bar-shape.js — on a squared curve, so the text is still nearly invisible
|
|
425
|
+
while the outline is narrow enough to crop it. A transition here would lag
|
|
426
|
+
those writes, and a live button hanging outside a half-open outline would
|
|
427
|
+
be clickable. */
|
|
428
|
+
.nora-pill[data-morphing] .nora-pill-controls,
|
|
429
|
+
.nora-pill[data-morphing] .nora-pill-face {
|
|
430
|
+
transition: none;
|
|
431
|
+
pointer-events: none;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
.nora-pill-controls.is-out {
|
|
435
|
+
opacity: 0;
|
|
436
|
+
pointer-events: none;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
.nora-pill-controls.is-in {
|
|
440
|
+
opacity: 1;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/* ---------- clusters ---------- */
|
|
444
|
+
|
|
445
|
+
.nora-cluster {
|
|
446
|
+
display: flex;
|
|
447
|
+
align-items: center;
|
|
448
|
+
gap: 2px;
|
|
449
|
+
padding: 5px;
|
|
450
|
+
flex: none;
|
|
451
|
+
min-width: 0;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/* The one that stretches: the component name has no natural width, and the
|
|
455
|
+
bar has a fixed one. */
|
|
456
|
+
.nora-cluster-subject {
|
|
457
|
+
flex: 1;
|
|
458
|
+
min-width: 0;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/* The gap a bridge spans. Empty on purpose — the bridge is drawn by the path;
|
|
462
|
+
this only reserves the space it curves through. */
|
|
463
|
+
.nora-bridge {
|
|
464
|
+
flex: none;
|
|
465
|
+
width: 6px;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/* ---------- buttons ---------- */
|
|
469
|
+
|
|
470
|
+
.nora-icon-btn,
|
|
471
|
+
.nora-name-btn,
|
|
472
|
+
.nora-vp-btn {
|
|
473
|
+
display: inline-flex;
|
|
474
|
+
align-items: center;
|
|
475
|
+
height: 34px;
|
|
476
|
+
border: none;
|
|
477
|
+
background: transparent;
|
|
478
|
+
color: var(--nora-pill-ink);
|
|
479
|
+
font-family: var(--nora-sans);
|
|
480
|
+
cursor: pointer;
|
|
481
|
+
border-radius: 999px;
|
|
482
|
+
padding: 0;
|
|
483
|
+
transition:
|
|
484
|
+
background 0.15s ease,
|
|
485
|
+
opacity 0.15s ease;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
.nora-icon-btn {
|
|
489
|
+
width: 34px;
|
|
490
|
+
justify-content: center;
|
|
491
|
+
flex: none;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
.nora-icon-btn:hover:not(:disabled),
|
|
495
|
+
.nora-name-btn:hover:not(:disabled),
|
|
496
|
+
.nora-vp-btn:hover:not(:disabled) {
|
|
497
|
+
background: var(--nora-pill-soft);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/* `:hover:not(:disabled)` is three simple selectors and would otherwise
|
|
501
|
+
out-specify a bare `.is-active`, so a button whose panel is open loses its
|
|
502
|
+
accent the moment the pointer is over it — which is exactly when you are
|
|
503
|
+
looking at it. Matching the hover state here is what keeps that from
|
|
504
|
+
happening; the brightness bump below is the hover feedback instead. */
|
|
505
|
+
.nora-icon-btn.is-active,
|
|
506
|
+
.nora-icon-btn.is-active:hover,
|
|
507
|
+
.nora-vp-btn.is-active,
|
|
508
|
+
.nora-vp-btn.is-active:hover {
|
|
509
|
+
background: var(--nora-accent);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
.nora-icon-btn.is-active:hover,
|
|
513
|
+
.nora-vp-btn.is-active:hover {
|
|
514
|
+
filter: brightness(1.14);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/* The name field is the widest thing on the bar, so accent-filling it while its
|
|
518
|
+
palette is open turns a third of the bar into a slab of blue and buries the
|
|
519
|
+
name it is meant to be showing. A held-down look is enough. */
|
|
520
|
+
.nora-name-btn.is-active,
|
|
521
|
+
.nora-name-btn.is-active:hover {
|
|
522
|
+
background: var(--nora-pill-soft-hi);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
.nora-icon-btn:disabled,
|
|
526
|
+
.nora-name-btn:disabled,
|
|
527
|
+
.nora-vp-btn:disabled {
|
|
528
|
+
opacity: 0.4;
|
|
529
|
+
cursor: default;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
.nora-name-btn {
|
|
533
|
+
flex: 1;
|
|
534
|
+
min-width: 0;
|
|
535
|
+
gap: 0.45rem;
|
|
536
|
+
padding: 0 0.7rem;
|
|
537
|
+
font-size: 0.78rem;
|
|
538
|
+
font-weight: 500;
|
|
539
|
+
letter-spacing: -0.005em;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
.nora-name-search {
|
|
543
|
+
flex: none;
|
|
544
|
+
opacity: 0.5;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
.nora-name-label {
|
|
548
|
+
flex: 1;
|
|
549
|
+
min-width: 0;
|
|
550
|
+
overflow: hidden;
|
|
551
|
+
text-overflow: ellipsis;
|
|
552
|
+
white-space: nowrap;
|
|
553
|
+
text-align: left;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/* Numbers are the one place the mono face earns its keep: tabular figures mean
|
|
557
|
+
the width doesn't jitter as it counts through a sweep. */
|
|
558
|
+
.nora-vp-btn {
|
|
559
|
+
flex: none;
|
|
560
|
+
justify-content: center;
|
|
561
|
+
gap: 0.3rem;
|
|
562
|
+
min-width: 44px;
|
|
563
|
+
padding: 0 0.65rem;
|
|
564
|
+
font-family: var(--nora-mono);
|
|
565
|
+
font-size: 0.72rem;
|
|
566
|
+
font-weight: 500;
|
|
567
|
+
font-variant-numeric: tabular-nums;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/* Shown only when a preset is wider than the window and the frame is scaled
|
|
571
|
+
down — the one piece of viewport state the width alone can't tell you. */
|
|
572
|
+
.nora-vp-scale {
|
|
573
|
+
font-size: 0.62rem;
|
|
574
|
+
color: var(--nora-pill-dim);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
.nora-vp-btn.is-active .nora-vp-scale {
|
|
578
|
+
color: rgba(255, 255, 255, 0.75);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
.nora-viewport-menu {
|
|
582
|
+
width: 220px;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
.nora-row.is-current {
|
|
586
|
+
color: var(--nora-accent);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
/* Where the keyboard cursor is, as distinct from which width is in use. The
|
|
590
|
+
panel takes focus itself so the arrows have somewhere to land, and this
|
|
591
|
+
highlight is the affordance, so the panel's own ring would only be noise. */
|
|
592
|
+
.nora-row.is-at {
|
|
593
|
+
background: var(--nora-pill-soft);
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
.nora-viewport-menu:focus {
|
|
597
|
+
outline: none;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
.nora-icon-btn:focus-visible,
|
|
601
|
+
.nora-name-btn:focus-visible,
|
|
602
|
+
.nora-vp-btn:focus-visible,
|
|
603
|
+
.nora-pill:focus-visible,
|
|
604
|
+
.nora-row:focus-visible,
|
|
605
|
+
.nora-row-pick:focus-visible,
|
|
606
|
+
.nora-palette-row:focus-visible,
|
|
607
|
+
.nora-panel-close:focus-visible,
|
|
608
|
+
.nora-panel-up:focus-visible {
|
|
609
|
+
outline: 2px solid var(--nora-accent);
|
|
610
|
+
outline-offset: 2px;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/* Drawn inside: the palette input spans the full popover, so an outset ring
|
|
614
|
+
would be clipped by the viewport edge the bar is anchored to. */
|
|
615
|
+
.nora-palette-input:focus-visible {
|
|
616
|
+
outline: 2px solid var(--nora-accent);
|
|
617
|
+
outline-offset: -2px;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/* ---------- shared popover surface ---------- */
|
|
621
|
+
|
|
622
|
+
.nora-panel,
|
|
623
|
+
.nora-palette {
|
|
624
|
+
width: 384px;
|
|
625
|
+
max-width: calc(100vw - 2.5rem);
|
|
626
|
+
background: var(--nora-pill);
|
|
627
|
+
color: var(--nora-pill-ink);
|
|
628
|
+
border-radius: 18px;
|
|
629
|
+
box-shadow: var(--nora-shadow);
|
|
630
|
+
padding: 0.4rem;
|
|
631
|
+
display: flex;
|
|
632
|
+
flex-direction: column;
|
|
633
|
+
gap: 0.2rem;
|
|
634
|
+
animation: nora-rise 0.24s var(--nora-ease);
|
|
635
|
+
transform-origin: bottom right;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
/* Same reason as the bar's rim: on a dark canvas a dark popover needs an edge. */
|
|
639
|
+
.nora-app[data-theme="dark"] .nora-panel,
|
|
640
|
+
.nora-app[data-theme="dark"] .nora-palette {
|
|
641
|
+
box-shadow:
|
|
642
|
+
var(--nora-shadow),
|
|
643
|
+
inset 0 0 0 1px var(--nora-pill-rim);
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
.nora-bar-area[data-vside="top"] .nora-panel,
|
|
647
|
+
.nora-bar-area[data-vside="top"] .nora-palette {
|
|
648
|
+
transform-origin: top right;
|
|
649
|
+
animation-name: nora-drop;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
.nora-bar-area[data-hside="left"] .nora-panel,
|
|
653
|
+
.nora-bar-area[data-hside="left"] .nora-palette {
|
|
654
|
+
transform-origin: bottom left;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
.nora-bar-area[data-vside="top"][data-hside="left"] .nora-panel,
|
|
658
|
+
.nora-bar-area[data-vside="top"][data-hside="left"] .nora-palette {
|
|
659
|
+
transform-origin: top left;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
@keyframes nora-drop {
|
|
663
|
+
from {
|
|
664
|
+
opacity: 0;
|
|
665
|
+
transform: scale(0.92) translateY(-8px);
|
|
666
|
+
}
|
|
667
|
+
to {
|
|
668
|
+
opacity: 1;
|
|
669
|
+
transform: scale(1) translateY(0);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
@keyframes nora-rise {
|
|
674
|
+
from {
|
|
675
|
+
opacity: 0;
|
|
676
|
+
transform: scale(0.92) translateY(8px);
|
|
677
|
+
}
|
|
678
|
+
to {
|
|
679
|
+
opacity: 1;
|
|
680
|
+
transform: scale(1) translateY(0);
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
.nora-panel-head {
|
|
685
|
+
display: flex;
|
|
686
|
+
align-items: center;
|
|
687
|
+
justify-content: space-between;
|
|
688
|
+
gap: 0.5rem;
|
|
689
|
+
padding: 0.45rem 0.6rem 0.35rem;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
.nora-panel-title {
|
|
693
|
+
font-family: var(--nora-mono);
|
|
694
|
+
font-size: 0.63rem;
|
|
695
|
+
letter-spacing: 0.1em;
|
|
696
|
+
text-transform: uppercase;
|
|
697
|
+
color: var(--nora-pill-dim);
|
|
698
|
+
overflow: hidden;
|
|
699
|
+
text-overflow: ellipsis;
|
|
700
|
+
white-space: nowrap;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
.nora-panel-up {
|
|
704
|
+
display: inline-flex;
|
|
705
|
+
align-items: center;
|
|
706
|
+
gap: 0.25rem;
|
|
707
|
+
border: none;
|
|
708
|
+
background: var(--nora-pill-soft);
|
|
709
|
+
color: var(--nora-pill-ink);
|
|
710
|
+
font-family: var(--nora-sans);
|
|
711
|
+
font-size: 0.68rem;
|
|
712
|
+
padding: 0.2rem 0.45rem;
|
|
713
|
+
border-radius: 7px;
|
|
714
|
+
cursor: pointer;
|
|
715
|
+
flex: none;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
.nora-panel-up:hover {
|
|
719
|
+
background: var(--nora-pill-soft-hi);
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
.nora-panel-body,
|
|
723
|
+
.nora-palette-list {
|
|
724
|
+
max-height: min(46vh, 340px);
|
|
725
|
+
overflow-y: auto;
|
|
726
|
+
display: flex;
|
|
727
|
+
flex-direction: column;
|
|
728
|
+
gap: 0.1rem;
|
|
729
|
+
scrollbar-width: thin;
|
|
730
|
+
scrollbar-color: rgba(255, 255, 255, 0.2) transparent;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
.nora-row-wrap {
|
|
734
|
+
display: flex;
|
|
735
|
+
align-items: center;
|
|
736
|
+
gap: 0.2rem;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
.nora-row {
|
|
740
|
+
flex: 1;
|
|
741
|
+
min-width: 0;
|
|
742
|
+
display: flex;
|
|
743
|
+
align-items: center;
|
|
744
|
+
gap: 0.5rem;
|
|
745
|
+
padding: 0.45rem 0.6rem;
|
|
746
|
+
border: none;
|
|
747
|
+
border-radius: 9px;
|
|
748
|
+
background: none;
|
|
749
|
+
color: var(--nora-pill-ink);
|
|
750
|
+
font-family: var(--nora-sans);
|
|
751
|
+
font-size: 0.8rem;
|
|
752
|
+
letter-spacing: -0.005em;
|
|
753
|
+
text-align: left;
|
|
754
|
+
cursor: pointer;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
.nora-row:hover:not(:disabled) {
|
|
758
|
+
background: var(--nora-pill-soft);
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
.nora-row:disabled {
|
|
762
|
+
opacity: 0.5;
|
|
763
|
+
cursor: default;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
.nora-row svg {
|
|
767
|
+
flex: none;
|
|
768
|
+
opacity: 0.55;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
.nora-row-name {
|
|
772
|
+
flex: 1;
|
|
773
|
+
min-width: 0;
|
|
774
|
+
overflow: hidden;
|
|
775
|
+
text-overflow: ellipsis;
|
|
776
|
+
white-space: nowrap;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
.nora-row-count {
|
|
780
|
+
flex: none;
|
|
781
|
+
font-family: var(--nora-mono);
|
|
782
|
+
font-size: 0.66rem;
|
|
783
|
+
font-variant-numeric: tabular-nums;
|
|
784
|
+
color: var(--nora-pill-dim);
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
.nora-row-primary {
|
|
788
|
+
color: var(--nora-accent);
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
.nora-row-pick {
|
|
792
|
+
flex: none;
|
|
793
|
+
border: none;
|
|
794
|
+
background: var(--nora-pill-soft);
|
|
795
|
+
color: var(--nora-pill-ink);
|
|
796
|
+
font-family: var(--nora-sans);
|
|
797
|
+
font-size: 0.66rem;
|
|
798
|
+
padding: 0.28rem 0.5rem;
|
|
799
|
+
border-radius: 7px;
|
|
800
|
+
cursor: pointer;
|
|
801
|
+
opacity: 0;
|
|
802
|
+
transition: opacity 0.12s ease;
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
.nora-row-wrap:hover .nora-row-pick,
|
|
806
|
+
.nora-row-pick:focus-visible {
|
|
807
|
+
opacity: 1;
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
.nora-row-pick:hover {
|
|
811
|
+
background: var(--nora-accent);
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
.nora-panel-empty,
|
|
815
|
+
.nora-panel-error {
|
|
816
|
+
padding: 0.8rem 0.6rem;
|
|
817
|
+
font-size: 0.76rem;
|
|
818
|
+
color: var(--nora-pill-dim);
|
|
819
|
+
text-align: center;
|
|
820
|
+
line-height: 1.5;
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
.nora-panel-error {
|
|
824
|
+
color: var(--nora-flag);
|
|
825
|
+
text-align: left;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
.nora-panel-close {
|
|
829
|
+
border: none;
|
|
830
|
+
background: none;
|
|
831
|
+
color: var(--nora-pill-dim);
|
|
832
|
+
font-family: var(--nora-sans);
|
|
833
|
+
font-size: 0.68rem;
|
|
834
|
+
padding: 0.4rem;
|
|
835
|
+
cursor: pointer;
|
|
836
|
+
border-top: 1px solid var(--nora-pill-line);
|
|
837
|
+
/* Same balance as .nora-sweep-actions: the button's own 0.4rem sits below the
|
|
838
|
+
rule, so the space above it has to match that, not the old 0.15rem. */
|
|
839
|
+
margin-top: 0.4rem;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
.nora-panel-close:hover {
|
|
843
|
+
color: var(--nora-pill-ink);
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
/* ---------- palette ---------- */
|
|
847
|
+
|
|
848
|
+
.nora-palette-input {
|
|
849
|
+
width: 100%;
|
|
850
|
+
border: none;
|
|
851
|
+
outline: none;
|
|
852
|
+
background: var(--nora-pill-soft);
|
|
853
|
+
color: var(--nora-pill-ink);
|
|
854
|
+
font-family: var(--nora-sans);
|
|
855
|
+
font-size: 0.82rem;
|
|
856
|
+
letter-spacing: -0.005em;
|
|
857
|
+
padding: 0.55rem 0.7rem;
|
|
858
|
+
border-radius: 11px;
|
|
859
|
+
margin-bottom: 0.25rem;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
.nora-palette-input::placeholder {
|
|
863
|
+
color: var(--nora-pill-dim);
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
.nora-palette-row {
|
|
867
|
+
display: flex;
|
|
868
|
+
align-items: baseline;
|
|
869
|
+
gap: 0.55rem;
|
|
870
|
+
width: 100%;
|
|
871
|
+
padding: 0.45rem 0.6rem;
|
|
872
|
+
border: none;
|
|
873
|
+
border-radius: 9px;
|
|
874
|
+
background: none;
|
|
875
|
+
color: var(--nora-pill-ink);
|
|
876
|
+
font-family: var(--nora-sans);
|
|
877
|
+
font-size: 0.8rem;
|
|
878
|
+
letter-spacing: -0.005em;
|
|
879
|
+
text-align: left;
|
|
880
|
+
cursor: pointer;
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
.nora-palette-row.is-active {
|
|
884
|
+
background: var(--nora-pill-soft);
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
.nora-palette-row.is-selected .nora-palette-name {
|
|
888
|
+
color: var(--nora-accent);
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
.nora-palette-row.is-unsupported {
|
|
892
|
+
opacity: 0.55;
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
.nora-palette-name {
|
|
896
|
+
flex: none;
|
|
897
|
+
font-weight: 500;
|
|
898
|
+
max-width: 55%;
|
|
899
|
+
overflow: hidden;
|
|
900
|
+
text-overflow: ellipsis;
|
|
901
|
+
white-space: nowrap;
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
/* A file path is a path: the mono face is doing real work here, not styling. */
|
|
905
|
+
.nora-palette-path {
|
|
906
|
+
flex: 1;
|
|
907
|
+
min-width: 0;
|
|
908
|
+
font-family: var(--nora-mono);
|
|
909
|
+
font-size: 0.65rem;
|
|
910
|
+
color: var(--nora-pill-dim);
|
|
911
|
+
overflow: hidden;
|
|
912
|
+
text-overflow: ellipsis;
|
|
913
|
+
white-space: nowrap;
|
|
914
|
+
text-align: right;
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
/* ---------- picker: folders sitting alongside components ---------- */
|
|
918
|
+
|
|
919
|
+
/* Where you are. The list holds two kinds of thing and the folder rows are all
|
|
920
|
+
relative to this path, so it has to be on screen — but it is context rather
|
|
921
|
+
than a result, so it sits between the input and the list instead of in it. */
|
|
922
|
+
.nora-picker-where {
|
|
923
|
+
display: flex;
|
|
924
|
+
align-items: baseline;
|
|
925
|
+
justify-content: space-between;
|
|
926
|
+
gap: 0.5rem;
|
|
927
|
+
padding: 0.35rem 0.6rem 0.2rem;
|
|
928
|
+
font-family: var(--nora-mono);
|
|
929
|
+
font-size: 0.6rem;
|
|
930
|
+
letter-spacing: 0.04em;
|
|
931
|
+
color: var(--nora-pill-dim);
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
.nora-picker-path {
|
|
935
|
+
min-width: 0;
|
|
936
|
+
overflow: hidden;
|
|
937
|
+
text-overflow: ellipsis;
|
|
938
|
+
white-space: nowrap;
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
.nora-picker-pending {
|
|
942
|
+
flex: none;
|
|
943
|
+
color: var(--nora-accent);
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
/* The rule between what is *in* this folder and what is *next to* it. At this
|
|
947
|
+
row density a plain gap reads as a rendering accident rather than a divide. */
|
|
948
|
+
.nora-picker-break {
|
|
949
|
+
margin-top: 0.3rem;
|
|
950
|
+
padding-top: 0.3rem;
|
|
951
|
+
border-top: 1px solid var(--nora-pill-line);
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
/* Folder rows carry an icon, so they centre rather than sitting on the baseline
|
|
955
|
+
that the component rows share with their file names. */
|
|
956
|
+
.nora-picker-dir {
|
|
957
|
+
align-items: center;
|
|
958
|
+
gap: 0.5rem;
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
.nora-picker-dir svg {
|
|
962
|
+
flex: none;
|
|
963
|
+
color: var(--nora-pill-dim);
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
/* A folder name has no file path beside it to yield room to, so unlike a
|
|
967
|
+
component name it takes the whole row and pushes the count to the edge. */
|
|
968
|
+
.nora-picker-dir .nora-palette-name {
|
|
969
|
+
flex: 1;
|
|
970
|
+
max-width: none;
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
.nora-picker-dir:hover:not(:disabled) {
|
|
974
|
+
background: var(--nora-pill-soft);
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
.nora-picker-dir:disabled {
|
|
978
|
+
opacity: 0.5;
|
|
979
|
+
cursor: default;
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
/* The one row that lands you somewhere when you have browsed away from the
|
|
983
|
+
scanned folder, so it carries the accent the way a primary action should. */
|
|
984
|
+
.nora-picker-open .nora-palette-name,
|
|
985
|
+
.nora-picker-open svg {
|
|
986
|
+
color: var(--nora-accent);
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
.nora-picker-count {
|
|
990
|
+
flex: none;
|
|
991
|
+
margin-left: auto;
|
|
992
|
+
font-family: var(--nora-mono);
|
|
993
|
+
font-size: 0.62rem;
|
|
994
|
+
color: var(--nora-pill-dim);
|
|
995
|
+
font-variant-numeric: tabular-nums;
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
.nora-palette-foot {
|
|
999
|
+
display: flex;
|
|
1000
|
+
flex-wrap: wrap;
|
|
1001
|
+
row-gap: 0.3rem;
|
|
1002
|
+
gap: 0.9rem;
|
|
1003
|
+
padding: 0.5rem 0.6rem 0.25rem;
|
|
1004
|
+
border-top: 1px solid var(--nora-pill-line);
|
|
1005
|
+
margin-top: 0.2rem;
|
|
1006
|
+
font-size: 0.64rem;
|
|
1007
|
+
color: var(--nora-pill-dim);
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
.nora-palette-foot kbd {
|
|
1011
|
+
font-family: var(--nora-mono);
|
|
1012
|
+
background: var(--nora-pill-soft);
|
|
1013
|
+
border-radius: 4px;
|
|
1014
|
+
padding: 0.05rem 0.28rem;
|
|
1015
|
+
margin-right: 0.15rem;
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1019
|
+
.nora-pill,
|
|
1020
|
+
.nora-pill-face,
|
|
1021
|
+
.nora-pill-controls,
|
|
1022
|
+
.nora-panel,
|
|
1023
|
+
.nora-palette,
|
|
1024
|
+
.nora-app {
|
|
1025
|
+
transition-duration: 0.01ms !important;
|
|
1026
|
+
animation-duration: 0.01ms !important;
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
/* ==========================================================================
|
|
1031
|
+
Sweep — a popover on the bar, sharing the .nora-panel surface
|
|
1032
|
+
========================================================================== */
|
|
1033
|
+
|
|
1034
|
+
.nora-sweep-btn {
|
|
1035
|
+
position: relative;
|
|
1036
|
+
overflow: hidden;
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
.nora-sweep-btn.is-running {
|
|
1040
|
+
background: var(--nora-accent);
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
.nora-sweep-panel {
|
|
1044
|
+
gap: 0.1rem;
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
.nora-sweep-summary {
|
|
1048
|
+
display: flex;
|
|
1049
|
+
gap: 0.6rem;
|
|
1050
|
+
font-family: var(--nora-mono);
|
|
1051
|
+
font-size: 0.65rem;
|
|
1052
|
+
margin-left: auto;
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
.nora-sweep-bad {
|
|
1056
|
+
color: var(--nora-flag);
|
|
1057
|
+
font-weight: 600;
|
|
1058
|
+
}
|
|
1059
|
+
.nora-sweep-neutral {
|
|
1060
|
+
color: var(--nora-note);
|
|
1061
|
+
}
|
|
1062
|
+
/* The one line carrying the verdict, previously the same --nora-pill-dim as
|
|
1063
|
+
every other muted label here — so the result read as chrome. */
|
|
1064
|
+
.nora-sweep-ok {
|
|
1065
|
+
display: inline-flex;
|
|
1066
|
+
align-items: center;
|
|
1067
|
+
gap: 0.28rem;
|
|
1068
|
+
color: var(--nora-ok);
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
/* Every width passed, so the whole track says so. This is the same grammar as
|
|
1072
|
+
an overflow band two notches quieter: the sweep already speaks in bands, and
|
|
1073
|
+
a clean run is simply the band that covers the entire range. It also stops
|
|
1074
|
+
the axis reading as an empty widget on the one result that has no findings
|
|
1075
|
+
to plot. */
|
|
1076
|
+
.nora-sweep-axis[data-clean="true"] .nora-sweep-track {
|
|
1077
|
+
background: linear-gradient(
|
|
1078
|
+
90deg,
|
|
1079
|
+
rgba(142, 211, 169, 0.16),
|
|
1080
|
+
rgba(142, 211, 169, 0.34) 50%,
|
|
1081
|
+
rgba(142, 211, 169, 0.16)
|
|
1082
|
+
);
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
/* The axis is the point: a finding lives at a width, so it belongs on a ruler. */
|
|
1086
|
+
.nora-sweep-axis {
|
|
1087
|
+
position: relative;
|
|
1088
|
+
height: 30px;
|
|
1089
|
+
margin: 0.2rem 0.6rem 0;
|
|
1090
|
+
cursor: crosshair;
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
/* Of those 30px, 21 sit above the track and exist only to hold tick labels. A
|
|
1094
|
+
sweep that found no breakpoints has no labels to put there, so the space
|
|
1095
|
+
became a hole directly beneath the heading. Keep just enough for the track. */
|
|
1096
|
+
.nora-sweep-axis[data-ticks="false"] {
|
|
1097
|
+
height: 21px;
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
.nora-sweep-track {
|
|
1101
|
+
position: absolute;
|
|
1102
|
+
left: 0;
|
|
1103
|
+
right: 0;
|
|
1104
|
+
bottom: 6px;
|
|
1105
|
+
height: 3px;
|
|
1106
|
+
border-radius: 2px;
|
|
1107
|
+
background: rgba(255, 255, 255, 0.12);
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
.nora-sweep-band {
|
|
1111
|
+
position: absolute;
|
|
1112
|
+
bottom: 4px;
|
|
1113
|
+
height: 7px;
|
|
1114
|
+
border-radius: 2px;
|
|
1115
|
+
background: var(--nora-flag-solid);
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
.nora-sweep-tick {
|
|
1119
|
+
position: absolute;
|
|
1120
|
+
bottom: 3px;
|
|
1121
|
+
width: 1px;
|
|
1122
|
+
height: 12px;
|
|
1123
|
+
background: var(--nora-note);
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
.nora-sweep-tick-label {
|
|
1127
|
+
position: absolute;
|
|
1128
|
+
bottom: 13px;
|
|
1129
|
+
left: 50%;
|
|
1130
|
+
transform: translateX(-50%);
|
|
1131
|
+
font-family: var(--nora-mono);
|
|
1132
|
+
font-size: 0.6rem;
|
|
1133
|
+
color: var(--nora-note);
|
|
1134
|
+
font-variant-numeric: tabular-nums;
|
|
1135
|
+
white-space: nowrap;
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
.nora-sweep-cursor {
|
|
1139
|
+
position: absolute;
|
|
1140
|
+
bottom: 1px;
|
|
1141
|
+
width: 2px;
|
|
1142
|
+
height: 16px;
|
|
1143
|
+
background: var(--nora-pill-ink);
|
|
1144
|
+
border-radius: 1px;
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
/* The axis and its end labels are one object, so they sit tight together; the
|
|
1148
|
+
findings are a separate readout and need to look like one. At the panel's
|
|
1149
|
+
0.1rem gap the first finding read as a third scale label. */
|
|
1150
|
+
.nora-sweep-panel .nora-panel-body {
|
|
1151
|
+
margin-top: 0.45rem;
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
.nora-sweep-scale {
|
|
1155
|
+
display: flex;
|
|
1156
|
+
justify-content: space-between;
|
|
1157
|
+
padding: 0 0.6rem;
|
|
1158
|
+
font-family: var(--nora-mono);
|
|
1159
|
+
font-size: 0.58rem;
|
|
1160
|
+
color: var(--nora-pill-dim);
|
|
1161
|
+
font-variant-numeric: tabular-nums;
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
.nora-finding {
|
|
1165
|
+
display: flex;
|
|
1166
|
+
align-items: baseline;
|
|
1167
|
+
gap: 0.6rem;
|
|
1168
|
+
width: 100%;
|
|
1169
|
+
border: none;
|
|
1170
|
+
background: none;
|
|
1171
|
+
border-radius: 9px;
|
|
1172
|
+
padding: 0.4rem 0.6rem;
|
|
1173
|
+
cursor: pointer;
|
|
1174
|
+
text-align: left;
|
|
1175
|
+
font-family: var(--nora-sans);
|
|
1176
|
+
font-size: 0.76rem;
|
|
1177
|
+
color: var(--nora-pill-ink);
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
.nora-finding:hover {
|
|
1181
|
+
background: var(--nora-pill-soft);
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
.nora-finding-range {
|
|
1185
|
+
flex: none;
|
|
1186
|
+
min-width: 4.8rem;
|
|
1187
|
+
font-family: var(--nora-mono);
|
|
1188
|
+
color: var(--nora-note);
|
|
1189
|
+
font-variant-numeric: tabular-nums;
|
|
1190
|
+
font-size: 0.69rem;
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
.nora-finding.is-bad .nora-finding-range {
|
|
1194
|
+
color: var(--nora-flag);
|
|
1195
|
+
font-weight: 600;
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
.nora-finding-text {
|
|
1199
|
+
flex: 1;
|
|
1200
|
+
min-width: 0;
|
|
1201
|
+
font-size: 0.72rem;
|
|
1202
|
+
color: var(--nora-pill-dim);
|
|
1203
|
+
overflow: hidden;
|
|
1204
|
+
text-overflow: ellipsis;
|
|
1205
|
+
white-space: nowrap;
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
/* Deliberately not .nora-panel-empty. An empty findings list is the *good*
|
|
1209
|
+
outcome here rather than a nothing-to-show state, and the summary in the head
|
|
1210
|
+
now does the announcing, so this line is elaboration and is weighted as such.
|
|
1211
|
+
Shorter, too: the range is already stated twice above it, and at the old
|
|
1212
|
+
length it wrapped and orphaned "1600." onto a line of its own. */
|
|
1213
|
+
.nora-sweep-clear {
|
|
1214
|
+
padding: 0.55rem 0.6rem 0.7rem;
|
|
1215
|
+
font-size: 0.72rem;
|
|
1216
|
+
color: var(--nora-pill-dim);
|
|
1217
|
+
text-align: center;
|
|
1218
|
+
line-height: 1.5;
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
/* Equal air on both sides of the button text, measured to the two edges the
|
|
1222
|
+
eye actually reads: the rule above, the panel's own rounded edge below.
|
|
1223
|
+
Each side is this row's 0.4rem plus the button's own 0.4rem of padding —
|
|
1224
|
+
6.4 + 6.4 above the glyphs, and 6.4 plus the panel's 0.4rem below them.
|
|
1225
|
+
Add nothing here without taking it off the panel. */
|
|
1226
|
+
.nora-sweep-actions {
|
|
1227
|
+
display: flex;
|
|
1228
|
+
align-items: center;
|
|
1229
|
+
gap: 0.3rem;
|
|
1230
|
+
border-top: 1px solid var(--nora-pill-line);
|
|
1231
|
+
margin-top: 0.5rem;
|
|
1232
|
+
padding-top: 0.4rem;
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
.nora-sweep-rerun,
|
|
1236
|
+
.nora-sweep-copy,
|
|
1237
|
+
.nora-sweep-dismiss {
|
|
1238
|
+
flex: 1;
|
|
1239
|
+
border: none;
|
|
1240
|
+
background: none;
|
|
1241
|
+
color: var(--nora-pill-dim);
|
|
1242
|
+
font-family: var(--nora-sans);
|
|
1243
|
+
font-size: 0.68rem;
|
|
1244
|
+
padding: 0.4rem;
|
|
1245
|
+
cursor: pointer;
|
|
1246
|
+
border-radius: 7px;
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
.nora-sweep-rerun:hover:not(:disabled),
|
|
1250
|
+
.nora-sweep-copy:hover,
|
|
1251
|
+
.nora-sweep-dismiss:hover {
|
|
1252
|
+
color: var(--nora-pill-ink);
|
|
1253
|
+
background: var(--nora-pill-soft);
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
.nora-sweep-rerun:disabled {
|
|
1257
|
+
opacity: 0.5;
|
|
1258
|
+
cursor: default;
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
.nora-sweep-dismiss {
|
|
1262
|
+
border-top: none;
|
|
1263
|
+
margin-top: 0;
|
|
1264
|
+
}
|