@odori/cli 0.0.5 → 0.0.6
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/bin/odori.mjs +33 -11
- package/dist/{chunk-RHG23EWW.js → chunk-6CE7U5KB.js} +48 -6
- package/dist/cli.js +1 -1
- package/dist/index.js +1 -1
- package/dist/{registry-snapshot-JEVXYGS2.js → registry-snapshot-ADKSTGDR.js} +3 -3
- package/package.json +3 -3
- package/src/commands/init.ts +34 -1
- package/src/commands/test.ts +24 -2
- package/src/registry-snapshot.json +3 -3
- package/studio/index.html +26 -0
- package/studio/src/components/CanvasStage.tsx +40 -3
- package/studio/src/components/Inspector.tsx +36 -30
- package/studio/src/components/Navigator.tsx +59 -1
- package/studio/src/components/Transport.tsx +41 -6
- package/studio/src/components/ui.tsx +14 -10
- package/studio/src/studio.css +173 -19
|
@@ -95,12 +95,18 @@ export const Inspector = ({
|
|
|
95
95
|
event.preventDefault();
|
|
96
96
|
document.body.style.userSelect = "none";
|
|
97
97
|
document.body.style.cursor = "col-resize";
|
|
98
|
+
/* The column eases when a pane is put away, and a drag writes that
|
|
99
|
+
same column many times a second. Easing it then means the track
|
|
100
|
+
is always behind the pointer while the pane's own width is not,
|
|
101
|
+
and the difference between them is a strip of empty page. */
|
|
102
|
+
document.documentElement.dataset.resizing = "";
|
|
98
103
|
const onMove = (move: PointerEvent) => applyWidth(window.innerWidth - move.clientX);
|
|
99
104
|
const onUp = () => {
|
|
100
105
|
window.removeEventListener("pointermove", onMove);
|
|
101
106
|
window.removeEventListener("pointerup", onUp);
|
|
102
107
|
document.body.style.userSelect = "";
|
|
103
108
|
document.body.style.cursor = "";
|
|
109
|
+
delete document.documentElement.dataset.resizing;
|
|
104
110
|
const width = getComputedStyle(document.documentElement).getPropertyValue(WIDTH_VAR).trim();
|
|
105
111
|
if (width) window.localStorage.setItem(STORAGE_KEY, String(Number.parseInt(width, 10)));
|
|
106
112
|
};
|
|
@@ -113,37 +119,37 @@ export const Inspector = ({
|
|
|
113
119
|
window.localStorage.removeItem(STORAGE_KEY);
|
|
114
120
|
}}
|
|
115
121
|
/>
|
|
122
|
+
<div className="inspector-head">
|
|
123
|
+
<span className="inspector-title" title={hint ?? title}>
|
|
124
|
+
{title}
|
|
125
|
+
</span>
|
|
126
|
+
{files.length > 0 ? (
|
|
127
|
+
<div className="inspector-views" role="tablist">
|
|
128
|
+
{(["inspect", "code"] as const).map((view) => (
|
|
129
|
+
<button
|
|
130
|
+
key={view}
|
|
131
|
+
type="button"
|
|
132
|
+
role="tab"
|
|
133
|
+
aria-selected={showing === view}
|
|
134
|
+
data-active={showing === view ? "true" : undefined}
|
|
135
|
+
onClick={() => setShowing(view)}
|
|
136
|
+
>
|
|
137
|
+
{view === "inspect" ? "Inspect" : "Code"}
|
|
138
|
+
</button>
|
|
139
|
+
))}
|
|
140
|
+
</div>
|
|
141
|
+
) : null}
|
|
142
|
+
<button
|
|
143
|
+
type="button"
|
|
144
|
+
className="inspector-toggle"
|
|
145
|
+
aria-label="Hide inspector"
|
|
146
|
+
title="Hide inspector"
|
|
147
|
+
onClick={() => setCollapsed(true)}
|
|
148
|
+
>
|
|
149
|
+
<Icon name="sidebar" />
|
|
150
|
+
</button>
|
|
151
|
+
</div>
|
|
116
152
|
<div className="inspector-scroll">
|
|
117
|
-
<div className="inspector-head">
|
|
118
|
-
<span className="inspector-title" title={hint ?? title}>
|
|
119
|
-
{title}
|
|
120
|
-
</span>
|
|
121
|
-
{files.length > 0 ? (
|
|
122
|
-
<div className="inspector-views" role="tablist">
|
|
123
|
-
{(["inspect", "code"] as const).map((view) => (
|
|
124
|
-
<button
|
|
125
|
-
key={view}
|
|
126
|
-
type="button"
|
|
127
|
-
role="tab"
|
|
128
|
-
aria-selected={showing === view}
|
|
129
|
-
data-active={showing === view ? "true" : undefined}
|
|
130
|
-
onClick={() => setShowing(view)}
|
|
131
|
-
>
|
|
132
|
-
{view === "inspect" ? "Inspect" : "Code"}
|
|
133
|
-
</button>
|
|
134
|
-
))}
|
|
135
|
-
</div>
|
|
136
|
-
) : null}
|
|
137
|
-
<button
|
|
138
|
-
type="button"
|
|
139
|
-
className="inspector-toggle"
|
|
140
|
-
aria-label="Hide inspector"
|
|
141
|
-
title="Hide inspector"
|
|
142
|
-
onClick={() => setCollapsed(true)}
|
|
143
|
-
>
|
|
144
|
-
<Icon name="sidebar" />
|
|
145
|
-
</button>
|
|
146
|
-
</div>
|
|
147
153
|
{files.length > 0 && showing === "code" ? (
|
|
148
154
|
<>
|
|
149
155
|
{/* One file is the file; several want naming, so the pane says
|
|
@@ -15,6 +15,19 @@ import {Icon} from "./ui";
|
|
|
15
15
|
* is the part a gallery cannot do at all.
|
|
16
16
|
*/
|
|
17
17
|
const COLLAPSED_KEY = "odori-navigator-collapsed";
|
|
18
|
+
const WIDTH_VAR = "--navigator-column";
|
|
19
|
+
const WIDTH_KEY = "odori-navigator-width";
|
|
20
|
+
/* Narrow enough for a filter box and a title, wide enough for a nested
|
|
21
|
+
category path without truncating every row. */
|
|
22
|
+
const MIN = 180;
|
|
23
|
+
const MAX = 420;
|
|
24
|
+
|
|
25
|
+
const clampWidth = (value: number): number =>
|
|
26
|
+
Math.round(Math.min(Math.max(value, MIN), Math.min(MAX, window.innerWidth * 0.4)));
|
|
27
|
+
|
|
28
|
+
const applyWidth = (value: number) => {
|
|
29
|
+
document.documentElement.style.setProperty(WIDTH_VAR, `${clampWidth(value)}px`);
|
|
30
|
+
};
|
|
18
31
|
|
|
19
32
|
export type NavigatorItem = {
|
|
20
33
|
id: string;
|
|
@@ -37,7 +50,15 @@ export const Navigator = ({
|
|
|
37
50
|
selected: string | null;
|
|
38
51
|
onSelect: (id: string) => void;
|
|
39
52
|
}) => {
|
|
40
|
-
|
|
53
|
+
/* Closed until asked for. Studio is most often docked beside something else,
|
|
54
|
+
where the stage wants every pixel, and the list is one click and a
|
|
55
|
+
remembered choice away. Anyone who opens it keeps it open. */
|
|
56
|
+
const [collapsed, setCollapsed] = useState(() => window.localStorage.getItem(COLLAPSED_KEY) !== "false");
|
|
57
|
+
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
const stored = Number(window.localStorage.getItem(WIDTH_KEY));
|
|
60
|
+
if (Number.isFinite(stored) && stored > 0) applyWidth(stored);
|
|
61
|
+
}, []);
|
|
41
62
|
const [query, setQuery] = useState("");
|
|
42
63
|
const field = useRef<HTMLInputElement>(null);
|
|
43
64
|
|
|
@@ -86,6 +107,43 @@ export const Navigator = ({
|
|
|
86
107
|
|
|
87
108
|
return (
|
|
88
109
|
<aside className="navigator" aria-label={label}>
|
|
110
|
+
{/* The mirror of the inspector's handle, on the inside edge for the same
|
|
111
|
+
reason: the pane clips its overflow, so a divider hung across the
|
|
112
|
+
border loses the clipped half of its hit area. */}
|
|
113
|
+
<div
|
|
114
|
+
className="navigator-resize"
|
|
115
|
+
role="separator"
|
|
116
|
+
aria-orientation="vertical"
|
|
117
|
+
aria-label="Resize list"
|
|
118
|
+
onPointerDown={(event) => {
|
|
119
|
+
// Eight pixels wide, and a fast drag leaves it between events, so
|
|
120
|
+
// the window owns the move for the duration rather than the handle.
|
|
121
|
+
event.preventDefault();
|
|
122
|
+
document.body.style.userSelect = "none";
|
|
123
|
+
document.body.style.cursor = "col-resize";
|
|
124
|
+
/* The column eases when a pane is put away, and a drag writes that
|
|
125
|
+
same column many times a second. Easing it then means the track
|
|
126
|
+
is always behind the pointer while the pane's own width is not,
|
|
127
|
+
and the difference between them is a strip of empty page. */
|
|
128
|
+
document.documentElement.dataset.resizing = "";
|
|
129
|
+
const onMove = (move: PointerEvent) => applyWidth(move.clientX);
|
|
130
|
+
const onUp = () => {
|
|
131
|
+
window.removeEventListener("pointermove", onMove);
|
|
132
|
+
window.removeEventListener("pointerup", onUp);
|
|
133
|
+
document.body.style.userSelect = "";
|
|
134
|
+
document.body.style.cursor = "";
|
|
135
|
+
delete document.documentElement.dataset.resizing;
|
|
136
|
+
const width = getComputedStyle(document.documentElement).getPropertyValue(WIDTH_VAR).trim();
|
|
137
|
+
if (width) window.localStorage.setItem(WIDTH_KEY, String(Number.parseInt(width, 10)));
|
|
138
|
+
};
|
|
139
|
+
window.addEventListener("pointermove", onMove);
|
|
140
|
+
window.addEventListener("pointerup", onUp);
|
|
141
|
+
}}
|
|
142
|
+
onDoubleClick={() => {
|
|
143
|
+
document.documentElement.style.removeProperty(WIDTH_VAR);
|
|
144
|
+
window.localStorage.removeItem(WIDTH_KEY);
|
|
145
|
+
}}
|
|
146
|
+
/>
|
|
89
147
|
<div className="navigator-head">
|
|
90
148
|
<button
|
|
91
149
|
type="button"
|
|
@@ -109,6 +109,16 @@ export const Transport = ({
|
|
|
109
109
|
const scenes = timeline?.scenes ?? [];
|
|
110
110
|
const active = scenes.find((scene) => shown >= scene.start && shown < scene.start + scene.durationInFrames);
|
|
111
111
|
|
|
112
|
+
/* The widest each readout will get, in characters. The transport is set in
|
|
113
|
+
the mono face, so a `ch` is exactly one column and this is a measurement
|
|
114
|
+
rather than an estimate. */
|
|
115
|
+
const digits = String(Math.max(0, durationInFrames - 1)).length;
|
|
116
|
+
const frameChars = "frame of ".length + digits * 2;
|
|
117
|
+
const sceneChars =
|
|
118
|
+
scenes.length > 0
|
|
119
|
+
? "scene ".length + Math.max(...scenes.map((scene) => (scene.name ?? scene.id).length))
|
|
120
|
+
: 0;
|
|
121
|
+
|
|
112
122
|
/*
|
|
113
123
|
* The ruler is drawn to the width it actually has. A fixed twelve marks is
|
|
114
124
|
* fine in a wide window and unreadable in a narrow one, where "10.0s" next
|
|
@@ -147,9 +157,10 @@ export const Transport = ({
|
|
|
147
157
|
onClick={(event) => {
|
|
148
158
|
const next = frameAt(event.clientX);
|
|
149
159
|
if (next === null) return;
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
160
|
+
/* Picking a frame is not a request to stop. A cut that is running
|
|
161
|
+
keeps running from where you pointed, which is what every player
|
|
162
|
+
does and what the audio guide already promised; a paused one
|
|
163
|
+
stays where you put it. */
|
|
153
164
|
playback.seek(next);
|
|
154
165
|
showPreview(null);
|
|
155
166
|
}}
|
|
@@ -182,6 +193,11 @@ export const Transport = ({
|
|
|
182
193
|
<div className="playhead" style={{left: `${(frame / Math.max(1, durationInFrames - 1)) * 100}%`}} />
|
|
183
194
|
</div>
|
|
184
195
|
|
|
196
|
+
{/* Always present, the way the scene track always is.
|
|
197
|
+
It used to appear only once the runtime reported cues, which is
|
|
198
|
+
after the first paint, so the transport grew 26px and took them off
|
|
199
|
+
the stage on every load. A lane that is there and empty also says
|
|
200
|
+
something a missing one cannot: this video is silent. */}
|
|
185
201
|
{track && track.cues.length > 0 ? (
|
|
186
202
|
<div className="audio-track">
|
|
187
203
|
{track.cues.map((cue) => {
|
|
@@ -211,7 +227,11 @@ export const Transport = ({
|
|
|
211
227
|
);
|
|
212
228
|
})}
|
|
213
229
|
</div>
|
|
214
|
-
) :
|
|
230
|
+
) : (
|
|
231
|
+
<div className="audio-track" data-empty="true">
|
|
232
|
+
<span>{track ? "no audio" : "\u00a0"}</span>
|
|
233
|
+
</div>
|
|
234
|
+
)}
|
|
215
235
|
</div>
|
|
216
236
|
|
|
217
237
|
<div className="transport-row">
|
|
@@ -270,10 +290,25 @@ export const Transport = ({
|
|
|
270
290
|
<span className="timecode" style={{marginLeft: 8}} data-preview={preview !== null ? "true" : undefined}>
|
|
271
291
|
<b>{formatTimecode(shown, fps)}</b> / {formatTimecode(durationInFrames - 1, fps)}
|
|
272
292
|
</span>
|
|
273
|
-
|
|
293
|
+
{/* Both of these change width as the cursor sweeps: the frame number
|
|
294
|
+
grows a digit at a time, and a scene called Resolution is half
|
|
295
|
+
again as wide as one called Proof. Tabular figures fix the digits
|
|
296
|
+
and do nothing for the count or the word, so each reserves the
|
|
297
|
+
widest it will ever need. Without that the row reflows under the
|
|
298
|
+
cursor and, in a pane narrow enough for it to wrap, takes a line
|
|
299
|
+
back from the stage while you are looking at it. */}
|
|
300
|
+
<span
|
|
301
|
+
className="timecode"
|
|
302
|
+
style={{minWidth: `${frameChars}ch`}}
|
|
303
|
+
data-preview={preview !== null ? "true" : undefined}
|
|
304
|
+
>
|
|
274
305
|
frame <b>{shown}</b> of {durationInFrames - 1}
|
|
275
306
|
</span>
|
|
276
|
-
{
|
|
307
|
+
{/* Rendered even with no scene under the cursor, so a gap in the
|
|
308
|
+
timeline does not collapse the box and shuffle the row. */}
|
|
309
|
+
<span className="timecode" style={{minWidth: `${sceneChars}ch`}}>
|
|
310
|
+
{active ? <>scene <b>{active.name ?? active.id}</b></> : null}
|
|
311
|
+
</span>
|
|
277
312
|
</div>
|
|
278
313
|
</div>
|
|
279
314
|
);
|
|
@@ -76,17 +76,21 @@ export const Icon = ({
|
|
|
76
76
|
}) => {
|
|
77
77
|
const paths: Record<string, ReactNode> = {
|
|
78
78
|
play: <path d="M4.5 2.8v8.4l7-4.2z" fill="currentColor" />,
|
|
79
|
+
/* The ordinary gear, on Lucide's 24 grid scaled to this 14 one. Drawing a
|
|
80
|
+
new one by eye is how you get a gear that reads as almost right; the
|
|
81
|
+
stroke is doubled so it comes out at the 1.2 everything else uses. */
|
|
79
82
|
settings: (
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
/>
|
|
89
|
-
|
|
83
|
+
<g
|
|
84
|
+
transform="scale(0.5833)"
|
|
85
|
+
fill="none"
|
|
86
|
+
stroke="currentColor"
|
|
87
|
+
strokeWidth="2"
|
|
88
|
+
strokeLinecap="round"
|
|
89
|
+
strokeLinejoin="round"
|
|
90
|
+
>
|
|
91
|
+
<path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z" />
|
|
92
|
+
<circle cx="12" cy="12" r="3" />
|
|
93
|
+
</g>
|
|
90
94
|
),
|
|
91
95
|
sun: (
|
|
92
96
|
<>
|
package/studio/src/studio.css
CHANGED
|
@@ -201,9 +201,21 @@ a.button {
|
|
|
201
201
|
border-color: var(--border);
|
|
202
202
|
color: var(--foreground);
|
|
203
203
|
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
204
|
+
/*
|
|
205
|
+
* Hover says a control is live. Selected says what the setting is. They have
|
|
206
|
+
* to differ in kind, not in degree.
|
|
207
|
+
*
|
|
208
|
+
* Hovering an unselected option used to darken its border and fill it, which
|
|
209
|
+
* is what being chosen looks like, so passing the cursor over 2x read as
|
|
210
|
+
* having clicked 2x. And this rule outranked the selected one - a pseudo-class
|
|
211
|
+
* beats an attribute - so hovering the option that *was* chosen replaced its
|
|
212
|
+
* fill with this one and the choice appeared to move under the cursor.
|
|
213
|
+
*
|
|
214
|
+
* Now hover only lifts the surface a step, the border never moves, and
|
|
215
|
+
* selected always wins.
|
|
216
|
+
*/
|
|
217
|
+
.button[data-variant="outline"]:hover:not([data-active="true"]) {
|
|
218
|
+
background: var(--hover);
|
|
207
219
|
}
|
|
208
220
|
.button[data-variant="primary"] {
|
|
209
221
|
background: var(--foreground);
|
|
@@ -217,6 +229,25 @@ a.button {
|
|
|
217
229
|
background: var(--active);
|
|
218
230
|
color: var(--foreground);
|
|
219
231
|
}
|
|
232
|
+
/*
|
|
233
|
+
* A chosen option in a group: filled and outlined, which is how the source
|
|
234
|
+
* tabs and the catalog filter already say the same thing.
|
|
235
|
+
*
|
|
236
|
+
* Fill alone was not enough of a difference. Hover is #f2f2f2 and chosen is
|
|
237
|
+
* #ebebeb, seven levels apart on a #fafafa panel, so the cursor moving across
|
|
238
|
+
* 0.5x / 1x / 2x looked like the choice following it. The border is the part
|
|
239
|
+
* hover never touches, so it is the part that carries the state.
|
|
240
|
+
*
|
|
241
|
+
* Both listed, and last, so the chosen option keeps looking chosen under the
|
|
242
|
+
* cursor: a pseudo-class outranks an attribute, and the plain hover rule was
|
|
243
|
+
* winning and taking the fill away.
|
|
244
|
+
*/
|
|
245
|
+
.button[data-variant="outline"][data-active="true"],
|
|
246
|
+
.button[data-variant="outline"][data-active="true"]:hover {
|
|
247
|
+
background: var(--active);
|
|
248
|
+
border-color: var(--border-strong);
|
|
249
|
+
color: var(--foreground);
|
|
250
|
+
}
|
|
220
251
|
.button:disabled {
|
|
221
252
|
cursor: not-allowed;
|
|
222
253
|
opacity: 0.45;
|
|
@@ -563,12 +594,77 @@ select.input {
|
|
|
563
594
|
without a rule per combination of what is open. The inspector's width is
|
|
564
595
|
a root variable so its drag handle can resize it without React re-laying
|
|
565
596
|
anything out. */
|
|
566
|
-
|
|
597
|
+
/*
|
|
598
|
+
* The side columns are stated rather than measured from their contents.
|
|
599
|
+
*
|
|
600
|
+
* A track that is `auto` collapses because the pane inside it left the DOM,
|
|
601
|
+
* and that is a used-value change, which no transition fires on: the stage
|
|
602
|
+
* jumped to its new width a frame after the click. Stating both widths, and
|
|
603
|
+
* letting the attributes the panes already set drive them, makes the track
|
|
604
|
+
* list itself change, which is a computed value and animatable. The
|
|
605
|
+
* inspector's is the same variable its drag handle writes.
|
|
606
|
+
*/
|
|
607
|
+
grid-template-columns:
|
|
608
|
+
var(--navigator-column, 232px)
|
|
609
|
+
minmax(0, 1fr)
|
|
610
|
+
clamp(240px, var(--inspector-width, 320px), 50vw);
|
|
567
611
|
overflow: hidden;
|
|
568
612
|
}
|
|
613
|
+
html[data-navigator="collapsed"] .main {
|
|
614
|
+
--navigator-column: 0px;
|
|
615
|
+
}
|
|
616
|
+
/* Zero, not `auto`: the pane is gone and the column has to say so in a length
|
|
617
|
+
the track list can interpolate towards. */
|
|
618
|
+
html[data-inspector="collapsed"] .main {
|
|
619
|
+
--inspector-width: 0px;
|
|
620
|
+
}
|
|
621
|
+
html[data-inspector="collapsed"] .main:not([data-single="true"]) {
|
|
622
|
+
grid-template-columns: var(--navigator-column, 232px) minmax(0, 1fr) 0px;
|
|
623
|
+
}
|
|
569
624
|
.main[data-single="true"] {
|
|
570
625
|
grid-template-columns: 1fr;
|
|
571
626
|
}
|
|
627
|
+
/*
|
|
628
|
+
* Each pane owns its column by number rather than by arriving in order.
|
|
629
|
+
*
|
|
630
|
+
* A pane that is put away renders nothing but its reopen button, and that
|
|
631
|
+
* button is fixed, so it is not a grid item. Auto-placement then slid every
|
|
632
|
+
* remaining pane one column to the left: closing the list handed the stage the
|
|
633
|
+
* sidebar's auto column, which sizes to content rather than to the space, and
|
|
634
|
+
* pushed the inspector into the middle column and its 273px out through the
|
|
635
|
+
* right edge of the grid. Naming the column makes where a pane sits
|
|
636
|
+
* independent of how many panes happen to exist, so any of them can be put
|
|
637
|
+
* away without moving the others.
|
|
638
|
+
*/
|
|
639
|
+
.main:not([data-single="true"]) > .navigator {
|
|
640
|
+
grid-column: 1;
|
|
641
|
+
}
|
|
642
|
+
.main:not([data-single="true"]) > .stage {
|
|
643
|
+
grid-column: 2;
|
|
644
|
+
}
|
|
645
|
+
.main:not([data-single="true"]) > .inspector {
|
|
646
|
+
grid-column: 3;
|
|
647
|
+
}
|
|
648
|
+
/*
|
|
649
|
+
* Putting a pane away is a move, so it should read as one.
|
|
650
|
+
*
|
|
651
|
+
* The columns are what actually change, so they are what animates: the stage
|
|
652
|
+
* grows into the space as the list leaves rather than snapping to a new width
|
|
653
|
+
* a frame later. Only the track list is transitioned, not `all`, because the
|
|
654
|
+
* inspector's drag handle writes its width continuously and easing that would
|
|
655
|
+
* make the pane lag the cursor.
|
|
656
|
+
*/
|
|
657
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
658
|
+
.main:not([data-single="true"]) {
|
|
659
|
+
transition: grid-template-columns 220ms cubic-bezier(0.32, 0.72, 0, 1);
|
|
660
|
+
}
|
|
661
|
+
/* Except while a divider is being dragged, when the column has to track the
|
|
662
|
+
pointer exactly. A pane follows its column instantly and the column would
|
|
663
|
+
not, so the two would part company and show the page between them. */
|
|
664
|
+
html[data-resizing] .main {
|
|
665
|
+
transition: none;
|
|
666
|
+
}
|
|
667
|
+
}
|
|
572
668
|
|
|
573
669
|
/* ---------- navigator ---------- */
|
|
574
670
|
|
|
@@ -578,14 +674,39 @@ select.input {
|
|
|
578
674
|
display: flex;
|
|
579
675
|
flex-direction: column;
|
|
580
676
|
overflow: hidden;
|
|
581
|
-
width
|
|
677
|
+
/* The column carries the width now, so the pane fills whatever it is given
|
|
678
|
+
and the drag has one number to move rather than two to keep in step. */
|
|
679
|
+
min-width: 0;
|
|
680
|
+
position: relative;
|
|
681
|
+
width: 100%;
|
|
582
682
|
}
|
|
583
|
-
.navigator-
|
|
683
|
+
.navigator-resize {
|
|
684
|
+
cursor: col-resize;
|
|
685
|
+
/* Fully inside the pane, like the inspector's: the pane clips overflow, and
|
|
686
|
+
a handle hung across the border loses the clipped half of its hit area. */
|
|
687
|
+
inset: 0 0 0 auto;
|
|
688
|
+
position: absolute;
|
|
689
|
+
touch-action: none;
|
|
690
|
+
width: 8px;
|
|
691
|
+
z-index: 2;
|
|
692
|
+
}
|
|
693
|
+
/*
|
|
694
|
+
* Both pane headers, from one rule, because they are the same thing on either
|
|
695
|
+
* side of the workspace and describing them separately is how they drifted:
|
|
696
|
+
* one sat at a 10px gutter and the other at 16px, so the two sides of the top
|
|
697
|
+
* of the window did not line up with each other.
|
|
698
|
+
*
|
|
699
|
+
* 49px is 28px of control with ten above and below and the rule underneath.
|
|
700
|
+
*/
|
|
701
|
+
.navigator-head,
|
|
702
|
+
.inspector-head {
|
|
584
703
|
align-items: center;
|
|
585
704
|
border-bottom: 1px solid var(--border);
|
|
586
705
|
display: flex;
|
|
587
|
-
|
|
588
|
-
|
|
706
|
+
flex: none;
|
|
707
|
+
gap: 8px;
|
|
708
|
+
min-height: 49px;
|
|
709
|
+
padding: 10px 16px;
|
|
589
710
|
}
|
|
590
711
|
.navigator-search {
|
|
591
712
|
height: 28px;
|
|
@@ -594,7 +715,9 @@ select.input {
|
|
|
594
715
|
}
|
|
595
716
|
.navigator-scroll {
|
|
596
717
|
overflow-y: auto;
|
|
597
|
-
|
|
718
|
+
/* The header's gutter, so a row lines up with the title above it and with
|
|
719
|
+
the inspector's content across the window. */
|
|
720
|
+
padding: 16px;
|
|
598
721
|
scrollbar-gutter: stable;
|
|
599
722
|
}
|
|
600
723
|
.navigator-group {
|
|
@@ -630,11 +753,12 @@ select.input {
|
|
|
630
753
|
background: var(--hover);
|
|
631
754
|
color: var(--foreground);
|
|
632
755
|
}
|
|
633
|
-
/* Away, the way back
|
|
756
|
+
/* Away, the way back sits exactly where the way out was: the header's own
|
|
757
|
+
16px gutter, and its top edge plus 10px of padding. */
|
|
634
758
|
.navigator-reopen {
|
|
635
759
|
background: var(--panel);
|
|
636
760
|
border-color: var(--border);
|
|
637
|
-
left:
|
|
761
|
+
left: 16px;
|
|
638
762
|
position: fixed;
|
|
639
763
|
top: 58px;
|
|
640
764
|
z-index: 5;
|
|
@@ -646,6 +770,8 @@ select.input {
|
|
|
646
770
|
width: clamp(240px, var(--inspector-width, 320px), 50vw);
|
|
647
771
|
container-type: inline-size;
|
|
648
772
|
display: flex;
|
|
773
|
+
/* A header above a scroller, which is what the navigator is. */
|
|
774
|
+
flex-direction: column;
|
|
649
775
|
overflow: hidden;
|
|
650
776
|
position: relative;
|
|
651
777
|
}
|
|
@@ -661,12 +787,19 @@ select.input {
|
|
|
661
787
|
}
|
|
662
788
|
|
|
663
789
|
|
|
790
|
+
/*
|
|
791
|
+
* The pane's header, matched to the navigator's across the workspace.
|
|
792
|
+
*
|
|
793
|
+
* It used to be the first thing inside the scroller, so the title and the tabs
|
|
794
|
+
* slid away as you read and there was no line to say where the pane stopped
|
|
795
|
+
* and its contents started. Out here it stays put and carries the same rule
|
|
796
|
+
* the list's header does. Ten pixels top and bottom is what makes the two the
|
|
797
|
+
* same height, since both hold a 28px control; sixteen at the sides is what
|
|
798
|
+
* puts the title over the content underneath it rather than six pixels to its
|
|
799
|
+
* left.
|
|
800
|
+
*/
|
|
664
801
|
.inspector-head {
|
|
665
|
-
align-items: center;
|
|
666
|
-
display: flex;
|
|
667
|
-
gap: 8px;
|
|
668
802
|
justify-content: space-between;
|
|
669
|
-
margin-bottom: 14px;
|
|
670
803
|
}
|
|
671
804
|
.inspector-title {
|
|
672
805
|
color: var(--foreground);
|
|
@@ -798,6 +931,8 @@ select.input {
|
|
|
798
931
|
white-space: pre;
|
|
799
932
|
}
|
|
800
933
|
|
|
934
|
+
/* 28px, the same as the list's pair: two controls doing the same job either
|
|
935
|
+
side of the workspace should not be different sizes. */
|
|
801
936
|
.inspector-toggle,
|
|
802
937
|
.inspector-reopen {
|
|
803
938
|
align-items: center;
|
|
@@ -808,21 +943,28 @@ select.input {
|
|
|
808
943
|
cursor: pointer;
|
|
809
944
|
display: inline-flex;
|
|
810
945
|
flex: none;
|
|
811
|
-
height:
|
|
946
|
+
height: 28px;
|
|
812
947
|
justify-content: center;
|
|
813
|
-
width:
|
|
948
|
+
width: 28px;
|
|
814
949
|
}
|
|
815
950
|
.inspector-toggle:hover,
|
|
816
951
|
.inspector-reopen:hover {
|
|
817
952
|
background: var(--hover);
|
|
818
953
|
color: var(--foreground);
|
|
819
954
|
}
|
|
820
|
-
/*
|
|
955
|
+
/*
|
|
956
|
+
* Away, the way back sits exactly where the way out was.
|
|
957
|
+
*
|
|
958
|
+
* These are the same control in two states, so it must not jump between them.
|
|
959
|
+
* 16px is the header's own side padding, which is where the toggle sits while
|
|
960
|
+
* the pane is open; it was 10px, and the button hopped six pixels right on
|
|
961
|
+
* every close. 58px is the header's top edge plus its 10px of padding.
|
|
962
|
+
*/
|
|
821
963
|
.inspector-reopen {
|
|
822
964
|
background: var(--panel);
|
|
823
965
|
border-color: var(--border);
|
|
824
966
|
position: fixed;
|
|
825
|
-
right:
|
|
967
|
+
right: 16px;
|
|
826
968
|
top: 58px;
|
|
827
969
|
z-index: 5;
|
|
828
970
|
}
|
|
@@ -1149,6 +1291,18 @@ select.input {
|
|
|
1149
1291
|
height: 20px;
|
|
1150
1292
|
position: relative;
|
|
1151
1293
|
}
|
|
1294
|
+
/* Empty until the runtime has reported, then labelled. Same height either
|
|
1295
|
+
way, so nothing above it moves. */
|
|
1296
|
+
.audio-track[data-empty="true"] {
|
|
1297
|
+
align-items: center;
|
|
1298
|
+
border: 1px dashed var(--border);
|
|
1299
|
+
border-radius: var(--radius-sm);
|
|
1300
|
+
box-sizing: border-box;
|
|
1301
|
+
color: var(--subtle);
|
|
1302
|
+
display: flex;
|
|
1303
|
+
font-size: 11px;
|
|
1304
|
+
padding: 0 8px;
|
|
1305
|
+
}
|
|
1152
1306
|
.audio-track .cue {
|
|
1153
1307
|
align-items: center;
|
|
1154
1308
|
background: var(--hover);
|