@rig-c/review 0.2.1 → 0.4.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.md +48 -7
- package/docs/DESIGN.md +29 -5
- package/docs/REVIEW_PACKAGE.md +2 -3
- package/package.json +1 -1
- package/src/animation-selection.ts +9 -5
- package/src/cli.ts +3 -2
- package/src/manifest.ts +5 -1
- package/src/open.ts +21 -4
- package/web/compare.css +26 -44
- package/web/index.html +6 -5
- package/web/main.ts +6 -1
- package/web/part-selection.ts +1 -1
- package/web/styles.css +74 -55
- package/web/view-controls.ts +33 -0
- package/web/workspace-layout.ts +31 -11
- package/web/workspace-preferences.ts +3 -0
- package/web/workspace-ui.ts +32 -24
package/README.md
CHANGED
|
@@ -205,6 +205,31 @@ printed: every current instance binds only `127.0.0.1`.
|
|
|
205
205
|
The printed feedback retrieval command uses POSIX shell quoting for literal path
|
|
206
206
|
arguments. It is not a PowerShell or cmd.exe command string.
|
|
207
207
|
|
|
208
|
+
### Launcher contract
|
|
209
|
+
|
|
210
|
+
A launcher may start `rig-review open` as a child process and rely on its stdout,
|
|
211
|
+
stderr and exit code alone:
|
|
212
|
+
|
|
213
|
+
1. On success stdout is exactly one line, the URL. `open` never writes anything
|
|
214
|
+
else to stdout.
|
|
215
|
+
2. On success stderr carries the human lines in this fixed order: the animation
|
|
216
|
+
diagnostic (when a clip is selected), the control notice (with `--control`),
|
|
217
|
+
then the copyable retrieval line `Review started|reused; feedback: rig-review feedback …`.
|
|
218
|
+
3. A refusal exits with code 1, writes nothing to stdout, and its first stderr line
|
|
219
|
+
starts with a name, followed by a colon and the explanation:
|
|
220
|
+
- `OPEN_INPUT_MISSING <path>`: the input or `--compare` path does not exist, or is neither a directory nor a regular file.
|
|
221
|
+
- `OPEN_INPUT_UNRECOGNIZED <path>`: the folder holds no recognized model, skeleton, motion, report, reference or pose sequence, or the file is not a JSON manifest.
|
|
222
|
+
- `OPEN_REFERENCE_REQUIRED`: `--compare` was given without an explicit `--reference`.
|
|
223
|
+
- `OPEN_ANIMATION_UNKNOWN <name>`: the explicit or report-named clip (JSON-quoted) is not in the compiled model.
|
|
224
|
+
- `OPEN_ANIMATION_AMBIGUOUS`: no flag or report selects one clip, or the A/B reports conflict.
|
|
225
|
+
- `OPEN_ANIMATION_UNVALIDATABLE`: a clip was requested but no compiled model can validate it.
|
|
226
|
+
- `OPEN_OPTION_INVALID <flag>`: an unknown, duplicate, empty or value-less option.
|
|
227
|
+
4. Exit codes: 0 applied, 1 refused. (`control` keeps its own exit 2 for an
|
|
228
|
+
unapplied command.)
|
|
229
|
+
|
|
230
|
+
Package validation and worker startup refusals keep their existing messages; the
|
|
231
|
+
named refusals above are decided before any state is written.
|
|
232
|
+
|
|
208
233
|
### Managed front and write capability
|
|
209
234
|
|
|
210
235
|
`open` is **managed by default**. It ensures one detached loopback front at
|
|
@@ -230,7 +255,10 @@ One origin cannot isolate scripts that run on it, so writes are bound to a
|
|
|
230
255
|
per-instance capability rather than to the page's origin or the key in its path.
|
|
231
256
|
Each worker mints a 256-bit capability kept only in its private 0600 open record.
|
|
232
257
|
`open` (managed or direct) and the foreground `review`/`review compare` commands
|
|
233
|
-
print it in the `#cap=` fragment, which browsers never send to a server.
|
|
258
|
+
print it in the `#cap=` fragment, which browsers never send to a server. That
|
|
259
|
+
printed link goes only to the terminal of the command that started the instance.
|
|
260
|
+
Output meant for a log does not carry it: the install smoke (`bun run smoke`, whose output CI publishes) prints the same
|
|
261
|
+
URL as `#cap=<redacted>` and fails if any capability value reaches its stdout. The page
|
|
234
262
|
moves it into memory and that tab's `sessionStorage`, then removes the fragment
|
|
235
263
|
from the address bar. Every mutation sends it as `X-Review-Capability`; the front
|
|
236
264
|
checks it before forwarding and the worker compares it again in constant time
|
|
@@ -331,15 +359,28 @@ Control capabilities are excluded from links and browser persistence.
|
|
|
331
359
|
|
|
332
360
|
## Workspace settings and panels
|
|
333
361
|
|
|
334
|
-
The
|
|
362
|
+
The review page has a left column (Parts explorer, with checkpoints and the
|
|
363
|
+
reference select folded above it), the stage, the Inspector below it with
|
|
364
|
+
**Setup**, **Animate** and **Feedback** sections, and a status footer. The
|
|
365
|
+
compare page keeps sources on the left and the Parts explorer on the right.
|
|
366
|
+
|
|
367
|
+
The view bar keeps the layer toggles in reach in every section: **Images**
|
|
368
|
+
(`I`), **Mesh edges** (`M`) and **Bone points** (`B`). The keys do nothing while
|
|
369
|
+
a text field, textarea, select or editable text has focus, with Ctrl/Alt/Cmd
|
|
370
|
+
held, or on key repeat; Space stays pan and Enter stays frame entry. A disabled
|
|
371
|
+
toggle shows why as a tooltip, and the footer shows the same layer status. The
|
|
372
|
+
footer also gathers the message, player, layout, renderer, idle, remote control
|
|
373
|
+
and draft state; **View region** opens the numeric view.
|
|
374
|
+
|
|
375
|
+
The side and bottom panels are resizable. Drag a separator, or focus it
|
|
335
376
|
and use the arrow keys (Shift for larger steps) and Home/End. Escape, a
|
|
336
377
|
cancelled pointer or window blur during a drag returns the previous size. The
|
|
337
|
-
stage keeps a usable minimum. When the window is too narrow, the
|
|
338
|
-
the
|
|
378
|
+
stage keeps a usable minimum. When the window is too narrow, the side columns
|
|
379
|
+
(left first on the compare page), and when it is too short, the Inspector, become explicit
|
|
339
380
|
drawers opened from buttons above the stage instead of shrinking to nothing. Responsive clamping
|
|
340
381
|
never overwrites the desired size, so a wider window restores it.
|
|
341
382
|
|
|
342
|
-
Settings,
|
|
383
|
+
Settings, at the bottom of the left column, holds browser defaults: Theme, Sync cameras by
|
|
343
384
|
default, Repeat idle by default, and Reset layout. Defaults seed a new
|
|
344
385
|
inspection only. A view link or source draft keeps its own appearance, camera
|
|
345
386
|
sync and idle choice, and restoring it neither changes the saved defaults nor
|
|
@@ -362,9 +403,9 @@ serving use per-port origins and keep them separate.
|
|
|
362
403
|
|
|
363
404
|
## Inspect a part
|
|
364
405
|
|
|
365
|
-
The
|
|
406
|
+
The Parts explorer (left column; right on the compare page) selects an actual model slot on a specific single/A/B side. All rows follow the public held snapshot's exact draw-order z, including hidden/null slots. Search matches the exact source slot names: a row is shown when its name contains the typed text, case-sensitively, with no case folding or normalization. Arrow keys/Home/End move between result buttons, Enter/Space selects, and Escape returns to search. Whole rig restores full drawing. Selecting a drawn row fits its conservative world bounds within the existing camera zoom range; Sync cameras applies the same normalized camera action to every visible pane, while independent mode changes only that selected pane. Hidden/empty geometry reports `PART_FIT_UNAVAILABLE` and keeps the camera.
|
|
366
407
|
|
|
367
|
-
Solo preserves the full held pose's resolved clips and filters only drawing. In context, the whole rig remains visible with an amber dashed conservative selection box; this box is geometry/clip bounds, not exact source-alpha coverage. Selection changes no track, skin, set, motion, source asset or review decision. The selected slot's side/mode survives source-bound links and local drafts. The
|
|
408
|
+
Solo preserves the full held pose's resolved clips and filters only drawing. In context, the whole rig remains visible with an amber dashed conservative selection box; this box is geometry/clip bounds, not exact source-alpha coverage. Selection changes no track, skin, set, motion, source asset or review decision. The selected slot's side/mode survives source-bound links and local drafts. The Setup section shows actual posed mesh counts or region corners, page/region, current skin/set context and declared attachment candidates. **Use slot for feedback** explicitly prefills the target; selecting a row alone submits nothing.
|
|
368
409
|
|
|
369
410
|
The pinned published `@rig-c/play`1.12.0 supplies `slotIdentity()` and frame generation. The explorer atomically retains the public identity snapshot with the same-generation held frame: exact attachment key, owning skin, active set/skin, full z order and explicit hidden/no-attachment/timeline-null/set-hide reasons. Hidden/null slots keep null keys with their reported reason; declarations and texture/UV names never replace that public result. An identity-known attachment need not have a visible primitive or positive fit bounds. The current UI follows its selected slot as animation changes, updating the displayed identity to the actual held frame.
|
|
370
411
|
|
package/docs/DESIGN.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
The delivery unit is an existing output folder or an optional curated review package handed from an agent to a person. This reusable app owns inspection, reference comparison, annotations and feedback export. Upstream compiler, splitter and runtime remain their respective tools. The existing producing agent needs no change to open its output here.
|
|
6
6
|
|
|
7
|
-
The person uses one inspection workspace with **View**, **Rig**, **Playback** and **Feedback**
|
|
7
|
+
The person uses one inspection workspace with **Setup**, **Animate** and **Feedback** sections (the compare page keeps **View**, **Rig**, **Playback** and **Feedback**). Setup covers setup geometry, source art, pivot/joint placement, meshes, weights, draw order and angle coverage; Animate covers frame intervals, reference video/poses, contact/deformation and audio inspection. Motion is strictly **view, annotate and export**: time/angle controls navigate inspection. This app does not author animation keys, retime motion or write `motion.json`. Future modeling touch-up remains in scope as a design goal; it is not implemented by this starter.
|
|
8
8
|
|
|
9
9
|
## Implemented foundation
|
|
10
10
|
|
|
@@ -34,6 +34,17 @@ Any future modeling edit needs provenance, undo/redo, explicit units and an expo
|
|
|
34
34
|
|
|
35
35
|
[design-prototype.html](design-prototype.html) preserves the interactive screen concept. It is **illustrative**, with sample controls/data, separate from the implemented app. Its older motion key/curve editing concept is superseded by the current view/annotate/export boundary. It does not demonstrate completed mesh, motion, angle or audio editing.
|
|
36
36
|
|
|
37
|
+
## Workspace layout
|
|
38
|
+
|
|
39
|
+
The single review page is one inspection workspace: a left column, the stage, the Inspector under the stage and one status footer. There is no right panel.
|
|
40
|
+
|
|
41
|
+
- **Left column.** The Parts explorer fills it. Checkpoints, the current checkpoint title and the reference select fold into a compact strip above the part list; the strip opens when the package supplies checkpoints or references and stays folded for a plain output folder. Browser Settings sit at the bottom.
|
|
42
|
+
- **View bar.** Pane choice, the persistent image / mesh edges / bone points layer toggles (keys `I`, `M`, `B`), camera sync and zoom, then appearance, repeat idle and Copy view link at the right end. The toggles stay native checkboxes, so their checked state is the pressed state, the draft input and the `layers` control value. A disabled toggle names the footer's layer reason as its tooltip. Turning images off leaves the mesh/bone overlay alone.
|
|
43
|
+
- **Inspector.** Three sections: **Setup** (selected part, part presets, modeling-review proposals, angle panel), **Animate** (transport, frame navigation, motion-review proposals, sound cues, report events) and **Feedback** (review status, reviewed source with the read-only source-file list, selected checkpoint, structured feedback, frame-strip export). Their element ids and saved section names (`rig`, `playback`, `feedback`) are unchanged; a saved `view` section from older links restores without selecting a section.
|
|
44
|
+
- **Footer.** Message (truncated, full text as tooltip) on the left; player, effective layout, renderer, layers, idle, remote control and draft state in the middle; the numeric view region (behind **View region**) and the source hash on the right.
|
|
45
|
+
|
|
46
|
+
The compare page keeps its own markup and stylesheet: sources left, Parts explorer right and its four Inspector sections.
|
|
47
|
+
|
|
37
48
|
## Inspection strips
|
|
38
49
|
|
|
39
50
|
An inclusive annotation interval can also become a derived frame strip. The single view selects one declared source video; compare retains its explicit shared video and chooses a producer clock basis for each interval. A batch preserves separate feedback intervals and side provenance. Source rows run down the sheet and requested producer frames run across it. A vertical batch repeats this grid rather than merging overlapping annotations.
|
|
@@ -45,7 +56,7 @@ The export uses dedicated real canvas players and leaves the live playhead/sessi
|
|
|
45
56
|
|
|
46
57
|
## Implemented slot inspection and public identity snapshots
|
|
47
58
|
|
|
48
|
-
The
|
|
59
|
+
The Parts explorer (the review page's left column; the compare page's right one) provides source-hash-bound slot selection, held-pose clipped Solo, Whole/context, conservative bounds fit, a diagnostic selection box, actual mesh/region counts and reproducible selection context. This is inspection rather than geometry editing. All rows use the public held identity snapshot's exact z order, including hidden/null slots. Current source-bound links/drafts and control observations retain side/slot/mode.
|
|
49
60
|
|
|
50
61
|
Published Play1.12.0 supplies a frozen `slotIdentity()` snapshot coherent with `posedFrame().generation`. Review retains copied frame/identity together only after equal-generation and source-slot/order checks, uses the player's default public filter, and never seeks/reposes/changes physics merely to read identity. Exact owning skin/key, active set/skin and null reasons come from that snapshot, not a unique-declaration or texture match. A resolved identity can remain known without visible geometry or fit bounds. Clip declarations do not invent keys when the API reports no drawable attachment.
|
|
51
62
|
|
|
@@ -55,16 +66,29 @@ A future optimization operation sends explicit source-bound selection/bounds to
|
|
|
55
66
|
|
|
56
67
|
## Workspace geometry and browser preferences
|
|
57
68
|
|
|
58
|
-
|
|
69
|
+
Side and bottom panels have pointer and keyboard separators with separator semantics: orientation, the controlled panel, an accessible name and the actual reachable minimum/maximum/current pixels. Arrow keys act only when the separator itself has focus, so text-entry arrows are never intercepted. A drag holds pointer capture; cancel, lost capture, blur, Escape and disposal restore the previous desired size and release it.
|
|
59
70
|
|
|
60
|
-
The allocator measures the layout rectangle minus stage chrome and keeps a 240×180 CSS-pixel stage minimum. It shrinks side panels proportionally toward their own minima; when the width still cannot hold them it turns the left, then the right
|
|
71
|
+
The allocator measures the layout rectangle minus stage chrome and keeps a 240×180 CSS-pixel stage minimum. It shrinks side panels proportionally toward their own minima; when the width still cannot hold them it turns the left, then the right column, and when the height cannot hold it the Inspector, into a native modal drawer opened from a trigger outside it. Effective sizes are computed from desired sizes and never written back; a separator inverts the allocator so a clamped pixel value is not saved as the desktop desire.
|
|
61
72
|
|
|
62
|
-
Browser defaults live in one closed, versioned record: theme, default camera sync, default idle repeat and the three desired panel sizes. Each field carries a logical stamp, so stale whole-record writes from other tabs converge per field without wall-clock ordering; an equal-stamp disagreement quarantines only that field until an explicit edit repairs it. Denied, unavailable or corrupt storage falls back to in-page defaults. The legacy same-origin appearance key migrates once, only when no new record exists. Reset layout changes geometry only.
|
|
73
|
+
Browser defaults live in one closed, versioned record: theme, default camera sync, default idle repeat and the three desired panel sizes. The saved `rightWidth` is the Parts explorer's width wherever the page shows it, so the review page's left parts column keeps the width chosen before the move; `leftWidth` is the compare page's sources column. Each field carries a logical stamp, so stale whole-record writes from other tabs converge per field without wall-clock ordering; an equal-stamp disagreement quarantines only that field until an explicit edit repairs it. Denied, unavailable or corrupt storage falls back to in-page defaults. The legacy same-origin appearance key migrates once, only when no new record exists. Reset layout changes geometry only.
|
|
63
74
|
|
|
64
75
|
Content state stays source-bound. Each Inspector section's x/y scroll offset is captured before its panel hides and restored after it shows, after two frames of layout, and again when its content extent changes. Restoring clamps to the current extent without replacing the remembered offset, and intentional user scrolling supersedes a pending restore. Offsets are kept in the source draft's UI state, never in the global record or a copied view link.
|
|
65
76
|
|
|
66
77
|
Browser storage is origin-scoped. Managed opens (the default) share one loopback front origin that routes `/instances/<key>/…` to each worker, so Settings, sizes and drafts carry across reopens; `--direct` keeps per-port origins. Because one origin cannot isolate its scripts, writes require a per-instance capability handed off only in the printed `#cap=` fragment and checked by both the front and the worker; a fragment-less copied link is read-only. Draft scopes carry the source hash, so revisions of one package never clobber each other's drafts.
|
|
67
78
|
|
|
79
|
+
## Theme
|
|
80
|
+
|
|
81
|
+
The palette comes from the rig-c.org site stylesheet. Its dark `:root` values (`--bg`, `--fg`, `--fg-strong`, `--fg-soft`, `--fg-muted`, `--border`, `--border-strong`, `--hover-bg`, `--active-bg`, `--link`, `--link-hover`, `--btn-fg`, `--code-*`, `--footer-bg`, `--pass`, `--warn`, `--fail`, the font stacks and `--radius`) are copied into the `:root` block of `web/styles.css` and `web/compare.css`; nothing is fetched at runtime, so the viewer stays offline. The site loads Inter and JetBrains Mono from a font CDN; the review keeps only the family stacks and falls back to local fonts. The site has no light scheme, so the light values and every review-only surface token (`--rv-*`: bars, panels, controls, fields, pressed state, focus ring, backdrop, stage) are review-local derivations of the same palette.
|
|
82
|
+
|
|
83
|
+
Rules:
|
|
84
|
+
|
|
85
|
+
- No literal colour (`#hex`, `rgb()`, `hsl()`) outside the `:root` token blocks of either stylesheet. Rules use `var(--…)` only.
|
|
86
|
+
- Every colour token is defined for both schemes: `:root` holds dark (the default before any script runs) and `:root[data-theme=light]` redefines the same set. The appearance control resolves `system` through `prefers-color-scheme` and always writes `data-theme=dark|light` on the root element, so the stylesheets carry no per-component light overrides.
|
|
87
|
+
- State colours: pressed and current selections use the `--pass` family, primary actions and links `--link`, dirty and draft notices `--warn`, errors `--fail`.
|
|
88
|
+
- The stage (pane gradient, grid dots, pane labels, empty-state text, video letterbox) stays dark in both schemes so source pixels and overlays read the same; its tokens carry equal values in both blocks.
|
|
89
|
+
|
|
90
|
+
`tests/theme.test.ts` enforces the first two rules.
|
|
91
|
+
|
|
68
92
|
## Comparison eligibility
|
|
69
93
|
|
|
70
94
|
A layout is offered only for inputs that actually loaded and rendered. One pure decision (`src/comparison-eligibility.ts`) takes load outcomes, never declared paths: the rig counts after its first successful draw, an image after `decode()`, a pose after it parses, and a video after `seeked` at the held time with a decoded frame. A new video stays pending until then; a held time outside its duration is refused as `REFERENCE_OUT_OF_DOMAIN`, never clamped.
|
package/docs/REVIEW_PACKAGE.md
CHANGED
|
@@ -239,9 +239,8 @@ A strip plan with one clock basis reports `plan.basis:"single"|"a"|"b"`. A compa
|
|
|
239
239
|
|
|
240
240
|
`rig-review open <input> [--reference <local-video>] [--compare <input>]
|
|
241
241
|
[--animation <name>]` hashes and validates the input, starts/reuses a fixed-binding
|
|
242
|
-
background instance and
|
|
243
|
-
|
|
244
|
-
command is printed on stderr. Existing `feedback compare ...` remains supported.
|
|
242
|
+
background instance and follows the [launcher contract](../README.md#launcher-contract)
|
|
243
|
+
for stdout, stderr and exit codes. Existing `feedback compare ...` remains supported.
|
|
245
244
|
Open animation selection uses `--animation` first, a normalized report `animation`
|
|
246
245
|
second, and a unique actual model clip (or common A/B clip) last. Report metadata
|
|
247
246
|
comes from the exact already-hashed recognized report bytes, never a separately
|
package/package.json
CHANGED
|
@@ -2,21 +2,25 @@
|
|
|
2
2
|
import type {FeedbackCatalog} from './feedback-validation.ts';
|
|
3
3
|
import type {ReportSeeds} from './report.ts';
|
|
4
4
|
export interface AnimationSelection {name:string;source:'cli'|'report'|'unique';reportPaths?:string[]}
|
|
5
|
+
/** The message stays the shared check/feedback text; `kind` lets the open launcher name the refusal. */
|
|
6
|
+
export class AnimationSelectionError extends Error {
|
|
7
|
+
constructor(message:string,readonly kind:'empty'|'unknown'|'ambiguous'|'unvalidatable',readonly label:string|null=null,readonly animation?:string){super(message);}
|
|
8
|
+
}
|
|
5
9
|
interface Source {label:string;catalog:Pick<FeedbackCatalog,'animations'>;report:Pick<ReportSeeds,'animation'|'path'>|null}
|
|
6
10
|
export function selectAnimation(sources:Source[],explicit?:string,allowStatic=false):AnimationSelection|null {
|
|
7
11
|
if(!sources.length||sources.length>2)throw new Error('open.animation: expected one or two actual catalogs');
|
|
8
|
-
if(explicit!==undefined&&(!explicit.trim()))throw new
|
|
9
|
-
const requireName=(name:string)=>{for(const source of sources){if(source.catalog.animations===null)throw new
|
|
12
|
+
if(explicit!==undefined&&(!explicit.trim()))throw new AnimationSelectionError('open.animation: expected nonempty animation name','empty');
|
|
13
|
+
const requireName=(name:string)=>{for(const source of sources){if(source.catalog.animations===null)throw new AnimationSelectionError(`open.${source.label}.animation: validation unavailable without compiled model`,'unvalidatable',source.label);if(!source.catalog.animations.some(clip=>clip.name===name))throw new AnimationSelectionError(`open.${source.label}.animation: model has no animation ${JSON.stringify(name)}`,'unknown',source.label,name);}};
|
|
10
14
|
if(explicit!==undefined){requireName(explicit);return {name:explicit,source:'cli'};}
|
|
11
15
|
if(allowStatic&&sources.length===1&&sources[0]!.catalog.animations===null)return null;
|
|
12
16
|
const reports=sources.filter(source=>source.report?.animation!==undefined);
|
|
13
17
|
const names=[...new Set(reports.map(source=>source.report!.animation!))];
|
|
14
|
-
if(names.length>1)throw new
|
|
18
|
+
if(names.length>1)throw new AnimationSelectionError(`open.animation: conflicting report animations: ${reports.map(source=>`${source.label}=${JSON.stringify(source.report!.animation)}`).join(', ')}; select --animation explicitly`,'ambiguous');
|
|
15
19
|
if(names.length===1){requireName(names[0]!);return {name:names[0]!,source:'report',reportPaths:[...new Set(reports.map(source=>source.report!.path))]};}
|
|
16
|
-
if(sources.some(source=>source.catalog.animations===null))throw new
|
|
20
|
+
if(sources.some(source=>source.catalog.animations===null))throw new AnimationSelectionError('open.animation: validation unavailable without compiled models','unvalidatable');
|
|
17
21
|
const common=sources[0]!.catalog.animations!.filter(clip=>sources.every(source=>source.catalog.animations!.some(other=>other.name===clip.name))).map(clip=>clip.name);
|
|
18
22
|
if(allowStatic&&sources.length===1&&sources[0]!.catalog.animations!.length===0)return null;
|
|
19
|
-
if(common.length!==1)throw new
|
|
23
|
+
if(common.length!==1)throw new AnimationSelectionError(`open.animation: expected --animation; ${sources.length>1?'common animations':'model animations'}: ${common.join(', ')||'(none)'}`,'ambiguous');
|
|
20
24
|
return {name:common[0]!,source:'unique'};
|
|
21
25
|
}
|
|
22
26
|
export function animationDiagnostic(selection:AnimationSelection):string {
|
package/src/cli.ts
CHANGED
|
@@ -35,7 +35,7 @@ async function stripCommand(args:string[],options:{stateDir?:string}){
|
|
|
35
35
|
}
|
|
36
36
|
export function parseOpenInputs(args:string[]):OpenInputs {
|
|
37
37
|
const input=args.shift();if(!input||input.startsWith('--'))throw new Error(usage);const result:OpenInputs={input},seen=new Set<string>();
|
|
38
|
-
while(args.length){const flag=args.shift()!;if(seen.has(flag))throw new Error(`Duplicate open option ${flag}`);seen.add(flag);if(flag==='--control'){result.control=true;continue;}if(flag==='--direct'){result.direct=true;continue;}const value=args.shift();if(value===undefined||value==='')throw new Error(`Missing value for ${flag}`);if(flag==='--reference')result.reference=value;else if(flag==='--compare')result.compare=value;else if(flag==='--animation')result.animation=value;else throw new Error(`Unknown open option ${flag}`);}return result;
|
|
38
|
+
while(args.length){const flag=args.shift()!;if(seen.has(flag))throw new Error(`OPEN_OPTION_INVALID ${flag}: Duplicate open option ${flag}`);seen.add(flag);if(flag==='--control'){result.control=true;continue;}if(flag==='--direct'){result.direct=true;continue;}const value=args.shift();if(value===undefined||value==='')throw new Error(`OPEN_OPTION_INVALID ${flag}: Missing value for ${flag}`);if(flag==='--reference')result.reference=value;else if(flag==='--compare')result.compare=value;else if(flag==='--animation')result.animation=value;else throw new Error(`OPEN_OPTION_INVALID ${flag}: Unknown open option ${flag}`);}return result;
|
|
39
39
|
}
|
|
40
40
|
async function controlCommand(args:string[],stateDir?:string){
|
|
41
41
|
const input=args.shift();if(!input||input==='--help'||input==='-h'){console.log(usage);return;}
|
|
@@ -83,7 +83,8 @@ function commandHelpRequested(args:readonly string[],command:'open'|'review'|'ch
|
|
|
83
83
|
export async function main(args:string[],options:{stateDir?:string}={}){
|
|
84
84
|
options={stateDir:options.stateDir??defaultStateDir()};if(args[0]==='--')args.shift();
|
|
85
85
|
if(args[0]==='control')return controlCommand(args.slice(1),options.stateDir);
|
|
86
|
-
if(args[0]==='open'){const openArgs=args.slice(1);if(commandHelpRequested(openArgs,'open')){console.log(usage);return;}const inputs=parseOpenInputs(openArgs);const result=await openReview(inputs,options)
|
|
86
|
+
if(args[0]==='open'){const openArgs=args.slice(1);if(commandHelpRequested(openArgs,'open')){console.log(usage);return;}const inputs=parseOpenInputs(openArgs);const result=await openReview(inputs,options);// Launcher contract (README): stdout is the URL line alone; stderr is diagnostic, control notice, retrieval, in that order.
|
|
87
|
+
console.log(result.url);if(result.selection)console.error(animationDiagnostic(result.selection));if(inputs.control)console.error('Inspection control enabled; query the current connected page with rig-review control and the same inputs.');console.error(`Review ${result.reused?'reused':'started'}; feedback: rig-review feedback ${[result.inputs.input,...(result.inputs.compare?['--compare',result.inputs.compare]:[]),...(result.inputs.reference?['--reference',result.inputs.reference]:[]),...(result.inputs.animation?['--animation',result.inputs.animation]:[])].map(shellWord).join(' ')}`);return;}
|
|
87
88
|
if(args[0]==='strip'){args.shift();if(args.includes('--help')){console.log(usage+'\nStrip: strip <package> --reference <declared-video-id> --animation <name> (--frames FIRST:LAST | --all-feedback) [--json] [--out path]\n strip compare <a> <b> --reference <video> --animation <name> --basis a|b (--frames FIRST:LAST | --all-feedback) [--json] [--out path]');return;}return stripCommand(args,options);}let command:'review'|'check'|'feedback'='review';if(args[0]==='check'||args[0]==='feedback'){command=args.shift() as 'check'|'feedback';}
|
|
88
89
|
if(args[0]==='--help'||args[0]==='-h'){console.log(usage);return;}
|
|
89
90
|
let format:'json'|'issue'='json';if(command==='feedback'){const parsed=feedbackFormat(args);args=parsed.args;format=parsed.format;}
|
package/src/manifest.ts
CHANGED
|
@@ -103,6 +103,10 @@ export async function loadReview(input: string): Promise<ReviewPackage> {
|
|
|
103
103
|
return {root,manifestPath,manifest,sourceHash:hash.digest('hex'),reviewedSource,files,report,feedbackCatalog};
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
+
/** Same text for every command; `root` lets the open launcher name the refused input. */
|
|
107
|
+
export class UnrecognizedOutputError extends Error {
|
|
108
|
+
constructor(readonly root:string){super('Output folder: no recognized skeleton.*, build/skeleton.model.json, motion.json, report.json, source video, pose sequence or contact strip');}
|
|
109
|
+
}
|
|
106
110
|
/** An adapter over an existing output directory; never writes a manifest there. */
|
|
107
111
|
async function folderManifest(root:string):Promise<ReviewManifest>{
|
|
108
112
|
const entries=(await readdir(root,{withFileTypes:true})).filter(v=>v.isFile()||v.isSymbolicLink()).map(v=>v.name).sort();
|
|
@@ -121,7 +125,7 @@ async function folderManifest(root:string):Promise<ReviewManifest>{
|
|
|
121
125
|
}
|
|
122
126
|
const name=basename(root);const packageId=name.replace(/[^a-zA-Z0-9._-]+/g,'-').replace(/^[^a-zA-Z0-9]+/,'').slice(0,128)||'output-bundle';
|
|
123
127
|
const authoring=entries.filter(n=>n==='motion.json'||n==='report.json'||(/^skeleton\./i.test(n)&&n!=='skeleton.model.json')).map(path=>({label:path+' · read only',path}));
|
|
124
|
-
if(!references.length&&!authoring.length&&!model)throw new
|
|
128
|
+
if(!references.length&&!authoring.length&&!model)throw new UnrecognizedOutputError(root);
|
|
125
129
|
return parseManifest({schema:'rig-review/1',id:packageId,title:name,revision:'folder-adapter/1',model,motion:has('motion.json')?'motion.json':undefined,references,authoring,checkpoints:references.map(r=>({id:r.id,title:'Inspect '+r.label,reference:r.id,workspace:'motion'}))});
|
|
126
130
|
}
|
|
127
131
|
|
package/src/open.ts
CHANGED
|
@@ -3,16 +3,16 @@ import {parseControlCommand,type ControlCommand,type ControlReceipt} from './con
|
|
|
3
3
|
import {createHash,randomBytes,randomUUID} from 'node:crypto';
|
|
4
4
|
import {spawn} from 'node:child_process';
|
|
5
5
|
import {homedir} from 'node:os';
|
|
6
|
-
import {mkdir,readFile,readdir,writeFile,rename,unlink,rm,rmdir,realpath,lstat,open as openFile} from 'node:fs/promises';
|
|
6
|
+
import {mkdir,readFile,readdir,writeFile,rename,unlink,rm,rmdir,realpath,lstat,stat,open as openFile} from 'node:fs/promises';
|
|
7
7
|
import {constants} from 'node:fs';
|
|
8
8
|
import {basename,dirname,join,relative,resolve,isAbsolute} from 'node:path';
|
|
9
9
|
import {fileURLToPath} from 'node:url';
|
|
10
10
|
import {loadReferencedReview,selectedReference,assertReviewUnchanged} from './reference.ts';
|
|
11
|
-
import {loadReview,type ReviewPackage} from './manifest.ts';
|
|
11
|
+
import {loadReview,UnrecognizedOutputError,type ReviewPackage} from './manifest.ts';
|
|
12
12
|
import {loadCompare,assertCompareUnchanged,type ComparePackage} from './compare.ts';
|
|
13
13
|
import {startServer} from './server.ts';
|
|
14
14
|
import {startCompareServer} from './compare-server.ts';
|
|
15
|
-
import {selectAnimation,type AnimationSelection} from './animation-selection.ts';
|
|
15
|
+
import {selectAnimation,AnimationSelectionError,type AnimationSelection} from './animation-selection.ts';
|
|
16
16
|
import {capabilityHeader,capabilityPattern,type OpenIdentity} from './open-protocol.ts';
|
|
17
17
|
import {ensureFront,frontPort} from './front.ts';
|
|
18
18
|
import {playPackagePath} from './versions.ts';
|
|
@@ -74,11 +74,28 @@ export async function acquireClaim<T>(control:string,name:string,nonce:string,re
|
|
|
74
74
|
catch(error){if((error as NodeJS.ErrnoException).code!=='ENOENT')throw error;}if(performance.now()>deadline)throw new Error(busy);await sleep(30);}}}
|
|
75
75
|
catch(error){await rm(candidate,{recursive:true,force:true});throw error;}
|
|
76
76
|
}
|
|
77
|
+
/** Launcher contract (README "Launcher contract"): an `open` refusal's first line starts with a stable OPEN_* name.
|
|
78
|
+
* The shared loaders keep their check/feedback text; only this path renames them. */
|
|
79
|
+
async function assertOpenInput(path:string):Promise<void>{
|
|
80
|
+
let info;try{info=await stat(path);}catch(e){if(['ENOENT','ENOTDIR','ELOOP','ENAMETOOLONG'].includes((e as NodeJS.ErrnoException).code??''))throw new Error(`OPEN_INPUT_MISSING ${path}: no such path; expected an existing output directory or review.json`);throw e;}
|
|
81
|
+
if(!info.isDirectory()&&!info.isFile())throw new Error(`OPEN_INPUT_MISSING ${path}: found a non-regular file; expected an output directory or a review.json manifest`);
|
|
82
|
+
if(info.isFile()){try{JSON.parse(await readFile(path,'utf8'));}catch(e){throw new Error(`OPEN_INPUT_UNRECOGNIZED ${path}: not a JSON manifest; expected an output directory or a review.json manifest`,{cause:e});}}
|
|
83
|
+
}
|
|
84
|
+
async function openRefusal(error:unknown,inputs:OpenInputs):Promise<unknown>{
|
|
85
|
+
if(error instanceof UnrecognizedOutputError){const first=await realpath(resolve(inputs.input)).catch(()=>null),path=inputs.compare!==undefined&&error.root!==first?inputs.compare:inputs.input;return new Error(`OPEN_INPUT_UNRECOGNIZED ${path}: ${error.message}`,{cause:error});}
|
|
86
|
+
if(!(error instanceof AnimationSelectionError))return error;
|
|
87
|
+
const detail=(error.label==='a'||error.label==='b'?`side ${error.label}: `:'')+error.message.replace(/^open\.(?:[a-z]+\.)?animation: /,'');
|
|
88
|
+
const name=error.kind==='empty'?'OPEN_OPTION_INVALID --animation':error.kind==='unknown'?`OPEN_ANIMATION_UNKNOWN ${JSON.stringify(error.animation)}`:error.kind==='ambiguous'?'OPEN_ANIMATION_AMBIGUOUS':'OPEN_ANIMATION_UNVALIDATABLE';
|
|
89
|
+
return new Error(`${name}: ${detail}`,{cause:error});
|
|
90
|
+
}
|
|
77
91
|
/** Managed is the default: the printed URL goes through the one loopback front (127.0.0.1:4318, or
|
|
78
92
|
* RIG_REVIEW_FRONT_PORT) so browser storage keeps one origin across reopens. `direct` keeps the worker's own port. */
|
|
79
93
|
export async function openReview(inputs:OpenInputs,options:{stateDir?:string;idleMs?:number;direct?:boolean;frontPort?:number;frontIdleMs?:number}={}):Promise<{url:string;reused:boolean;nonce:string;inputs:OpenInputs;selection:AnimationSelection|null;direct:boolean}>{
|
|
80
94
|
const direct=options.direct??inputs.direct??false,port=direct?0:frontPort(options.frontPort);
|
|
81
|
-
const
|
|
95
|
+
for(const path of [inputs.input,...(inputs.compare!==undefined?[inputs.compare]:[])])await assertOpenInput(path);
|
|
96
|
+
if(inputs.compare!==undefined&&!inputs.reference)throw new Error('OPEN_REFERENCE_REQUIRED: compare: explicit --reference required');
|
|
97
|
+
let p:OpenPackage;try{p=await loadOpenPackage(inputs);}catch(error){throw await openRefusal(error,inputs);}
|
|
98
|
+
const fingerprint=await openFingerprint(),key=openKey(p,fingerprint);
|
|
82
99
|
const stateDir=await canonicalDestination(resolve(options.stateDir??defaultStateDir())),roots=p.mode==='single'?[p.p.root]:[p.p.a.root,p.p.b.root];
|
|
83
100
|
for(const root of roots){const rel=relative(root,stateDir);if(!rel||rel!=='..'&&!rel.startsWith('..'+(process.platform==='win32'?'\\':'/'))&&!isAbsolute(rel))throw new Error('open.state: must be outside input packages');}
|
|
84
101
|
const control=join(stateDir,'open');await mkdir(control,{recursive:true,mode:0o700});const canonicalControl=await realpath(control);if((await lstat(control)).isSymbolicLink())throw new Error('open.state: control directory cannot be a symlink');
|
package/web/compare.css
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
|
|
1
|
+
/* Theme tokens: site values copied from rig-c.org styles.css (dark); light and --rv-* are review-local derivations. No literal colour outside these blocks. */
|
|
2
|
+
:root{--font-sans:Inter,system-ui,sans-serif;--font-display:Inter,system-ui,sans-serif;--font-mono:"JetBrains Mono",ui-monospace,monospace;--radius:10px;--bg:#070b18;--fg:#c9d4e8;--fg-strong:#f3f6fc;--fg-soft:#aebbd4;--fg-muted:#7f8ba6;--border:#18213a;--border-strong:#2a3657;--hover-bg:#0f1730;--active-bg:#142042;--link:#7cc8ff;--link-hover:#b3e0ff;--btn-fg:#08101f;--code-bg:#050814;--code-fg:#d6e2f5;--code-border:#18213a;--code-inline-bg:#121a30;--footer-bg:#050813;--pass:#30d158;--warn:#ffcc00;--fail:#ff453a;--rv-bar:#0b1124;--rv-panel:#0d152b;--rv-raised:var(--code-inline-bg);--rv-control:var(--code-inline-bg);--rv-control-hover:var(--active-bg);--rv-field:var(--code-bg);--rv-segmented:var(--code-bg);--rv-pressed-bg:#133733;--rv-pressed-border:#207c44;--rv-pressed-fg:var(--fg-strong);--rv-focus:var(--link-hover);--rv-backdrop:rgba(7,11,24,.82);--rv-pane-hi:#142042;--rv-pane-lo:#070b18;--rv-pane-label-bg:rgba(7,11,24,.85);--rv-stage-fg:#aebbd4;--rv-grid-dot:rgba(127,139,166,.2);--rv-media-bg:#0b1020}
|
|
3
|
+
:root[data-theme=light]{--bg:#f5f7fb;--fg:#26324a;--fg-strong:#0c1324;--fg-soft:#3b4864;--fg-muted:#5d6982;--border:#d5dcea;--border-strong:#b3bed3;--hover-bg:#e8edf6;--active-bg:#dbe3f3;--link:#1a6fc0;--link-hover:#0f528f;--btn-fg:#ffffff;--code-bg:#eef1f7;--code-fg:#1d2940;--code-border:#d5dcea;--code-inline-bg:#e3e8f2;--footer-bg:#eceff6;--pass:#1e8a3c;--warn:#8a6400;--fail:#b3261e;--rv-bar:#edf1f7;--rv-panel:#e9eef6;--rv-raised:var(--code-inline-bg);--rv-control:var(--code-inline-bg);--rv-control-hover:#d5ddec;--rv-field:#ffffff;--rv-segmented:var(--code-inline-bg);--rv-pressed-bg:#c9ded8;--rv-pressed-border:#6fb286;--rv-pressed-fg:var(--fg-strong);--rv-focus:var(--link);--rv-backdrop:rgba(12,19,36,.45);--rv-pane-hi:#142042;--rv-pane-lo:#070b18;--rv-pane-label-bg:rgba(7,11,24,.85);--rv-stage-fg:#aebbd4;--rv-grid-dot:rgba(127,139,166,.2);--rv-media-bg:#0b1020}
|
|
4
|
+
:root{font:13px/1.5 var(--font-sans);color:var(--fg);background:var(--bg);color-scheme:dark}*{box-sizing:border-box}body{margin:0}button,input,select{font:inherit}button{padding:7px 10px;border:1px solid var(--border-strong);border-radius:6px;color:inherit;background:var(--rv-control);cursor:pointer}button:hover{background:var(--rv-control-hover)}button:disabled{opacity:.4;cursor:default}.primary{background:var(--link);color:var(--btn-fg);border-color:var(--link)}.primary:hover{background:var(--link-hover)}input,select{border:1px solid var(--border-strong);background:var(--rv-field);border-radius:6px;color:inherit;padding:8px;max-width:100%;width:100%}input[type=range],input[type=checkbox]{accent-color:var(--link);padding:0}input[type=checkbox]{width:auto}input[type=range]{min-width:60px}button:focus-visible,input:focus-visible,select:focus-visible,summary:focus-visible{outline:2px solid var(--rv-focus);outline-offset:3px}header{padding:15px 21px;display:flex;align-items:center;gap:24px;border-bottom:1px solid var(--border);background:var(--rv-bar);flex-wrap:wrap}.brand{font-size:19px;letter-spacing:-.5px;white-space:nowrap}.brand span{font-size:10px;letter-spacing:.08em;display:block;color:var(--fg-muted)}h1{font-size:14px;font-weight:500;margin:0}h2{font-size:13px;font-weight:500;margin:0 0 13px}.hint{font-size:11px;color:var(--fg-muted)}.actions{display:flex;align-items:center;gap:7px;margin-left:auto;flex-wrap:wrap}.actions button{font-size:11px}.layout{display:grid;grid-template-columns:minmax(0,1fr) 300px;min-height:calc(100vh - 118px)}main{min-width:0;display:flex;flex-direction:column}.toolbar{display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap;padding:12px 16px;border-bottom:1px solid var(--border);font-size:11px;color:var(--fg-muted)}.row{display:flex;gap:5px;align-items:center;flex-wrap:wrap}.row button{font-size:11px;padding:4px 8px}.row output{font:11px var(--font-mono);padding:0 4px}.stages{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px;padding:12px;flex:1;min-height:500px}.pane{display:flex;flex-direction:column;min-width:0;border:1px solid var(--border-strong);border-radius:8px;overflow:hidden;background:var(--rv-panel)}.pane-head{display:flex;align-items:center;gap:8px;padding:10px 12px;border-bottom:1px solid var(--border);font-size:11px;color:var(--fg-soft);min-height:45px;flex-wrap:wrap}.pane-head strong{font-weight:500}.side-label{display:inline-grid;place-items:center;width:23px;height:23px;border:1px solid var(--rv-pressed-border);border-radius:5px;color:var(--rv-pressed-fg);flex:none}.rig-viewport,.reference-viewport{position:relative;min-height:420px;overflow:hidden;flex:1;background:radial-gradient(ellipse at 50% 40%,var(--rv-pane-hi),var(--rv-pane-lo) 75%)}.rig-viewport{background-image:radial-gradient(var(--rv-grid-dot) .7px,transparent .7px);background-size:22px 22px}.rig-root{position:absolute;transform-origin:0 0}video{width:100%;height:100%;position:absolute;inset:0;object-fit:contain;background:var(--rv-media-bg)}.empty{position:absolute;inset:50px 16px 30px;display:grid;place-items:center;text-align:center;font-size:12px;color:var(--rv-stage-fg);line-height:1.7;overflow-wrap:anywhere}.pane-foot{display:flex;justify-content:space-between;align-items:center;gap:8px;flex-wrap:wrap;padding:10px 11px;border-top:1px solid var(--border);color:var(--fg-muted);font:11px var(--font-mono);min-height:42px}.pane-foot label{display:flex;align-items:center;gap:4px;font:11px -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}.transport{display:flex;gap:11px;align-items:center;flex-wrap:wrap;padding:13px 17px;border-top:1px solid var(--border);background:var(--rv-panel)}.transport input[type=range]{flex:1}.transport input[type=number]{width:100px}.transport output{font:11px var(--font-mono);white-space:nowrap;color:var(--fg-muted)}.compare-info{display:flex;justify-content:space-between;gap:9px;flex-wrap:wrap;padding:9px 17px;color:var(--fg-muted);font-size:11px}.trace{font-size:11px;color:var(--fg-muted);padding:4px 17px}.trace summary,.provenance summary{cursor:pointer}.trace pre{font:11px/1.6 var(--font-mono);white-space:pre-wrap;overflow-wrap:anywhere}.boundary{font-size:11px;color:var(--fg-muted);padding:7px 17px;margin:0}aside{background:var(--rv-panel);padding:21px 17px;border-left:1px solid var(--border)}.eyebrow{font-size:11px;letter-spacing:.08em;color:var(--fg-muted);margin-bottom:15px}aside label{display:block;font-size:11px;color:var(--fg-muted);margin:12px 0 6px}.provenance{padding:13px 11px;margin-top:22px;background:var(--rv-raised);border:1px solid var(--border-strong);border-radius:7px;font-size:11px}.provenance p{margin:7px 0;color:var(--fg-muted);overflow-wrap:anywhere}.provenance details{margin-top:12px}.provenance pre{font:11px/1.6 var(--font-mono);overflow-wrap:anywhere;white-space:pre-wrap}.feedback-form{border-top:1px solid var(--border);margin-top:24px;padding-top:21px}.frame-range{display:grid;grid-template-columns:1fr 1fr;gap:9px}.block{width:100%;margin-top:14px}.feedback-item{font-size:11px;overflow-wrap:anywhere;padding:12px 0;border-top:1px solid var(--border);margin-top:13px}.feedback-item strong{display:block;font-weight:500;color:var(--fg-strong)}.feedback-item button{padding:3px 7px;font-size:11px;margin-top:7px}.dirty{color:var(--warn)!important}.error{color:var(--fail)!important}footer{display:flex;justify-content:space-between;gap:13px;flex-wrap:wrap;padding:11px 18px;background:var(--footer-bg);border-top:1px solid var(--border);font-size:11px;color:var(--fg-soft)}[hidden]{display:none!important}.sr-only{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%)}
|
|
2
5
|
@media(min-width:1600px){.rig-viewport,.reference-viewport{min-height:620px}.layout{grid-template-columns:minmax(0,1fr) 320px}}
|
|
3
6
|
@media(max-width:1100px){.layout{grid-template-columns:minmax(0,1fr) 260px}aside{padding:17px 12px}.stages{gap:7px;padding:9px;min-height:430px}.rig-viewport,.reference-viewport{min-height:380px}.pane-head{padding:9px}.pane-foot{padding:9px 7px}}
|
|
4
|
-
@media(max-width:850px){.layout{grid-template-columns:1fr}aside{border-left:0;border-top:1px solid
|
|
5
|
-
@media(max-width:550px){header{gap:12px;padding:13px}.stages{grid-template-columns:repeat(2,minmax(0,1fr));gap:7px}.pane:first-child{grid-column:1/-1}.reference-viewport{min-height:240px}.rig-viewport{min-height:365px}.pane-head{padding:7px}.pane-foot{font-size:11px}aside{display:block}.feedback-form{margin-top:23px;padding-top:20px;border-top:1px solid
|
|
7
|
+
@media(max-width:850px){.layout{grid-template-columns:1fr}aside{border-left:0;border-top:1px solid var(--border);display:grid;grid-template-columns:1fr 1fr;gap:12px 24px}aside>.eyebrow,aside>label,aside>select{grid-column:1/-1}.feedback-form,.provenance{margin-top:0}.feedback-form{padding-top:0;border-top:0}.actions{margin-left:0}.rig-viewport,.reference-viewport{min-height:420px}}
|
|
8
|
+
@media(max-width:550px){header{gap:12px;padding:13px}.stages{grid-template-columns:repeat(2,minmax(0,1fr));gap:7px}.pane:first-child{grid-column:1/-1}.reference-viewport{min-height:240px}.rig-viewport{min-height:365px}.pane-head{padding:7px}.pane-foot{font-size:11px}aside{display:block}.feedback-form{margin-top:23px;padding-top:20px;border-top:1px solid var(--border)}.toolbar{padding:11px 13px}.transport{padding:12px;gap:8px}.transport input[type=range]{min-width:130px}.transport input[type=number]{width:84px}.provenance{margin-top:18px}}
|
|
6
9
|
@media(pointer:coarse){button{min-height:42px}input,select{font-size:16px}.row button{min-width:38px}}
|
|
7
10
|
|
|
8
11
|
/* Inspection layout is bounded independently of pane content and review forms. */
|
|
@@ -13,37 +16,19 @@
|
|
|
13
16
|
#stage-layout [data-panning=true]{cursor:grabbing!important}
|
|
14
17
|
#stage-layout img{-webkit-user-drag:none;user-select:none}
|
|
15
18
|
.rig-overlay{position:absolute;inset:0;pointer-events:none;width:100%;height:100%;z-index:1}
|
|
16
|
-
.inspection-controls{display:flex;align-items:center;gap:8px 14px;flex-wrap:wrap;padding:9px 14px;border-bottom:1px solid
|
|
19
|
+
.inspection-controls{display:flex;align-items:center;gap:8px 14px;flex-wrap:wrap;padding:9px 14px;border-bottom:1px solid var(--border);font-size:11px;background:var(--rv-panel)}
|
|
17
20
|
.inspection-controls label{display:inline-flex;align-items:center;gap:5px;margin:0;white-space:nowrap}
|
|
18
21
|
.inspection-controls select{width:auto;padding:4px;font-size:11px}
|
|
19
22
|
.inspection-controls input[type=checkbox]{width:auto;margin:0}
|
|
20
23
|
.inspection-controls fieldset{display:flex;gap:10px;align-items:center;border:0;padding:0;margin:0;min-width:0;flex-wrap:wrap}
|
|
21
24
|
.inspection-controls legend{float:left;margin-right:9px;font-size:11px}
|
|
22
25
|
#control-state:empty{display:none}
|
|
23
|
-
:root[data-theme=light]{color-scheme:light
|
|
26
|
+
:root[data-theme=light]{color-scheme:light}
|
|
24
27
|
:root[data-theme=dark]{color-scheme:dark}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
[data-theme=light] button:hover{background:#d0e2e8}
|
|
30
|
-
[data-theme=light] button.primary,[data-theme=light] .primary{background:#24694d;border-color:#24694d;color:#fff}
|
|
31
|
-
[data-theme=light] input,[data-theme=light] select,[data-theme=light] textarea{background:#fff;border-color:#8faab7;color:#243e4a;accent-color:#276c50}
|
|
32
|
-
[data-theme=light] input::placeholder,[data-theme=light] textarea::placeholder{color:#607986}
|
|
33
|
-
[data-theme=light] .hint,[data-theme=light] .subtle,[data-theme=light] .label,[data-theme=light] .eyebrow,[data-theme=light] aside label,[data-theme=light] .tool small,[data-theme=light] .stage-footer,[data-theme=light] .compare-info,[data-theme=light] .boundary,[data-theme=light] .trace,[data-theme=light] .transport output,[data-theme=light] .zoom-controls output,[data-theme=light] .frame-range label,[data-theme=light] .frame-controls label{color:#496774}
|
|
34
|
-
[data-theme=light] .workspaces button,[data-theme=light] .segmented button{color:#3e5c69}
|
|
35
|
-
[data-theme=light] .workspaces button[aria-pressed=true]{color:#185738;border-bottom-color:#276c50}
|
|
36
|
-
[data-theme=light] .workspaces button span{color:#58727e}
|
|
37
|
-
[data-theme=light] .segmented,[data-theme=light] .provenance,[data-theme=light] .report-selection pre{background:#e1ebef;border-color:#9cb5c0;color:#294552}
|
|
38
|
-
[data-theme=light] .segmented button[aria-pressed=true],[data-theme=light] .checkpoint[aria-current=true],[data-theme=light] .report-event-list button[aria-current=true]{background:#cde4d8;border-color:#56866e;color:#1f4937}
|
|
39
|
-
[data-theme=light] .checkpoint small{color:#4b6c59}
|
|
40
|
-
[data-theme=light] .provenance p,[data-theme=light] .provenance-row span,[data-theme=light] .provenance details,[data-theme=light] .report-selection details,[data-theme=light] .report-event-list small{color:#486470}
|
|
41
|
-
[data-theme=light] .report-event-list button,[data-theme=light] .report-empty-categories span{background:#e5eef2;border-color:#acbfca;color:#294552}
|
|
42
|
-
[data-theme=light] .feedback-item strong{color:#1e4e3b}
|
|
43
|
-
[data-theme=light] a{color:#175f48}
|
|
44
|
-
[data-theme=light] .error{color:#a03225!important}
|
|
45
|
-
[data-theme=light] .dirty,[data-theme=light] .report-event-list .no-time{color:#875800!important}
|
|
46
|
-
[data-theme=light] :focus-visible{outline-color:#24694d}
|
|
28
|
+
|
|
29
|
+
input::placeholder,textarea::placeholder{color:var(--fg-muted)}
|
|
30
|
+
|
|
31
|
+
:focus-visible{outline-color:var(--rv-focus)}
|
|
47
32
|
/* Source pixels retain their original color; the neutral stage stays dark in both themes. */
|
|
48
33
|
|
|
49
34
|
#stage-layout{grid-template-columns:repeat(3,minmax(0,1fr))}#stage-layout[data-orientation=vertical]{grid-template-columns:minmax(0,1fr);grid-template-rows:repeat(3,minmax(0,1fr))}
|
|
@@ -55,12 +40,12 @@ html,body{height:100%;overflow:hidden}body{height:100dvh}#app{height:100dvh;disp
|
|
|
55
40
|
main{min-height:0;overflow:hidden;display:flex;flex-direction:column}
|
|
56
41
|
.left-panel{min-height:0;overflow:auto;padding:13px 10px}
|
|
57
42
|
#stage-layout{height:auto;min-height:0;flex:1;overflow:hidden;padding:8px;gap:7px}
|
|
58
|
-
.inspector{height:clamp(205px,32dvh,340px);min-height:180px;flex:none;display:flex;flex-direction:column;border-top:1px solid
|
|
59
|
-
.inspector-heading{display:flex;align-items:center;gap:16px;flex-wrap:wrap;padding:7px 12px;border-bottom:1px solid
|
|
43
|
+
.inspector{height:clamp(205px,32dvh,340px);min-height:180px;flex:none;display:flex;flex-direction:column;border-top:1px solid var(--border);background:var(--rv-panel);overflow:hidden}
|
|
44
|
+
.inspector-heading{display:flex;align-items:center;gap:16px;flex-wrap:wrap;padding:7px 12px;border-bottom:1px solid var(--border);flex:none;font-size:11px}
|
|
60
45
|
.inspector-heading strong{font-size:10px;letter-spacing:.08em;font-weight:500}
|
|
61
46
|
.inspector-heading [role=tablist]{display:flex;gap:4px}
|
|
62
47
|
.inspector-heading button{padding:4px 9px;font-size:11px}
|
|
63
|
-
.inspector-heading button[aria-selected=true]{background
|
|
48
|
+
.inspector-heading button[aria-selected=true]{background:var(--rv-pressed-bg);color:var(--rv-pressed-fg);border-color:var(--rv-pressed-border)}
|
|
64
49
|
.inspector-body{flex:1;min-height:0;overflow:hidden}
|
|
65
50
|
.inspector-body>[role=tabpanel]{height:100%;overflow:auto;padding:12px 15px;min-height:0}
|
|
66
51
|
.inspector .inspection-controls{padding:0;border:0;background:transparent}
|
|
@@ -77,9 +62,7 @@ main{min-height:0;overflow:hidden;display:flex;flex-direction:column}
|
|
|
77
62
|
#inspector-rig #part-set{max-width:190px}
|
|
78
63
|
#inspector-playback #motion-tools{display:flex;gap:8px;flex-wrap:wrap}
|
|
79
64
|
#inspector-playback .tool{width:auto}
|
|
80
|
-
|
|
81
|
-
[data-theme=light] .inspector-heading{border-color:#b3c6d0}
|
|
82
|
-
[data-theme=light] .inspector-heading button[aria-selected=true]{background:#cde4d8;color:#1e4d36;border-color:#59836b}
|
|
65
|
+
|
|
83
66
|
@media(max-width:780px){.layout{grid-template-columns:130px minmax(0,1fr)!important}.inspector{height:36dvh}.stage-meta{padding:6px 10px 0}.stage-footer{padding:5px 10px}.inspector-heading>.hint{display:none}}
|
|
84
67
|
@media(max-width:540px){.layout{grid-template-columns:minmax(0,1fr)!important}.left-panel{display:block;padding:8px;font-size:11px}.layout{grid-template-columns:105px minmax(0,1fr)!important}.topbar,header{padding:7px 10px;gap:8px}.topbar .brand,header .brand{font-size:14px}.top-actions,.actions{gap:4px}.top-actions button,.actions button{padding:4px 6px;font-size:10px}.workspaces button{padding:6px}.inspector{height:40dvh;min-height:150px}.inspector-heading{gap:8px;padding:5px 8px}.inspector-heading button{padding:4px 7px}.stage-footer{display:none}}
|
|
85
68
|
|
|
@@ -91,12 +74,12 @@ body{display:grid;grid-template-rows:auto minmax(0,1fr) auto}.layout{grid-templa
|
|
|
91
74
|
|
|
92
75
|
.rig-layer-controls{display:flex;gap:14px;align-items:center;flex-wrap:wrap;margin-bottom:12px;font-size:11px}.rig-layer-controls fieldset{display:flex;gap:12px;align-items:center;flex-wrap:wrap;border:0;margin:0;padding:0}.rig-layer-controls legend{float:left;margin-right:10px}.rig-layer-controls label{display:inline-flex;gap:5px;align-items:center}.rig-layer-controls input[type=checkbox]{width:auto}
|
|
93
76
|
|
|
94
|
-
.numeric-view{font:11px/1.5
|
|
77
|
+
.numeric-view{font:11px/1.5 var(--font-mono);white-space:pre-wrap;overflow-wrap:anywhere;max-width:100%;padding:8px;background:var(--code-bg);color:var(--code-fg);border-radius:5px}.view-link-controls{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin-bottom:8px}.draft-notice{flex:none;font-size:11px;padding:0 12px;color:var(--warn)}.draft-notice:has(#draft-state:empty){display:none}.draft-notice #draft-recovery{display:inline-flex;gap:8px;margin-left:8px;flex-wrap:wrap}.draft-notice button{font-size:11px;padding:2px 6px}
|
|
95
78
|
|
|
96
79
|
.camera-sync{display:inline-flex;align-items:center;gap:5px;font-size:11px;white-space:nowrap}.camera-sync input[type=checkbox]{width:auto;flex:none;margin:0}
|
|
97
80
|
|
|
98
81
|
/* Slot explorer shares the stage row; bottom Inspector remains in the main column. */
|
|
99
|
-
.layout{grid-template-columns:150px minmax(0,1fr) 235px!important}.parts-explorer{display:block;grid-template-columns:none;gap:normal;min-width:0;min-height:0;overflow:auto;border-left:1px solid
|
|
82
|
+
.layout{grid-template-columns:150px minmax(0,1fr) 235px!important}.parts-explorer{display:block;grid-template-columns:none;gap:normal;min-width:0;min-height:0;overflow:auto;border-left:1px solid var(--border);background:var(--rv-panel);padding:12px;overscroll-behavior:contain}.parts-explorer h2{font-size:12px;margin:0 0 10px}.parts-explorer>label{display:block;font-size:11px;margin-top:12px}.parts-explorer input[type=search]{width:100%;min-width:0}.parts-explorer .part-solo{display:flex;gap:6px;align-items:center}.parts-explorer ul{list-style:none;margin:8px 0;padding:0;display:grid;gap:4px}.parts-explorer li{min-width:0}.parts-explorer li button{display:block;width:100%;text-align:left;overflow-wrap:anywhere;font-size:11px}.parts-explorer button[aria-pressed=true]{background:var(--rv-pressed-bg);border-color:var(--rv-pressed-border)}.parts-explorer h2,#selected-part h3{font-weight:600}#selected-part{margin-bottom:12px}#selected-part h3{margin:0 0 8px;overflow-wrap:anywhere}#selected-part p{overflow-wrap:anywhere}
|
|
100
83
|
@media(max-width:1050px){.layout{grid-template-columns:110px minmax(0,1fr) 185px!important}.parts-explorer{padding:8px}}
|
|
101
84
|
@media(max-width:700px){.layout{grid-template-columns:minmax(0,1fr) 165px!important}.left-panel{display:none!important}}
|
|
102
85
|
@media(max-width:460px){.layout{grid-template-columns:minmax(0,1fr) 132px!important}.parts-explorer{padding:6px}.parts-explorer li button{padding:6px}.parts-explorer>.hint{font-size:10px}}
|
|
@@ -108,9 +91,9 @@ body{display:grid;grid-template-rows:auto minmax(0,1fr) auto}.layout{grid-templa
|
|
|
108
91
|
.workspace-layout>main{display:grid;min-width:0;min-height:0;overflow:hidden;grid-column:3;grid-row:1}
|
|
109
92
|
.workspace-stage-stack{grid-row:1;display:flex;flex-direction:column;min-width:0;min-height:0;overflow:hidden}
|
|
110
93
|
.workspace-stage-stack>#stage-layout{flex:1;min-height:0;height:auto;overflow:hidden}
|
|
111
|
-
.workspace-layout>.left-panel{grid-column:1;grid-row:1;display:flex!important;flex-direction:column;overflow:hidden;min-width:0;padding:0;border-right:1px solid
|
|
94
|
+
.workspace-layout>.left-panel{grid-column:1;grid-row:1;display:flex!important;flex-direction:column;overflow:hidden;min-width:0;padding:0;border-right:1px solid var(--border)}
|
|
112
95
|
.workspace-sources{flex:1;min-height:0;overflow:auto;padding:10px;overscroll-behavior:contain}
|
|
113
|
-
.workspace-settings{flex:none;max-height:65%;overflow:auto;padding:10px;border-top:1px solid
|
|
96
|
+
.workspace-settings{flex:none;max-height:65%;overflow:auto;padding:10px;border-top:1px solid var(--border)}
|
|
114
97
|
.workspace-settings h2{font-size:12px;margin:0 0 8px}
|
|
115
98
|
.workspace-settings label{display:flex;align-items:center;gap:6px;font-size:11px;margin:7px 0}
|
|
116
99
|
.workspace-settings select{width:auto;min-width:0;flex:1}
|
|
@@ -118,23 +101,22 @@ body{display:grid;grid-template-rows:auto minmax(0,1fr) auto}.layout{grid-templa
|
|
|
118
101
|
.workspace-settings input[type=checkbox]{width:auto;flex:none}
|
|
119
102
|
.workspace-layout>.parts-explorer{display:block;grid-template-columns:none;grid-column:5;grid-row:1;width:auto;min-width:0;min-height:0;height:auto;overflow:auto}
|
|
120
103
|
.workspace-layout .inspector{grid-row:3;height:auto!important;min-height:0!important;max-height:none;overflow:hidden}
|
|
121
|
-
.workspace-separator{position:relative;z-index:3;background
|
|
104
|
+
.workspace-separator{position:relative;z-index:3;background:var(--border-strong);touch-action:none;cursor:col-resize;min-width:0;min-height:0}
|
|
122
105
|
.workspace-separator::after{content:'';position:absolute;inset:0 -4px}
|
|
123
|
-
.workspace-separator:focus-visible{outline:2px solid
|
|
124
|
-
.workspace-separator[data-resizing=true]{background
|
|
106
|
+
.workspace-separator:focus-visible{outline:2px solid var(--rv-focus);outline-offset:-2px;background:var(--link)}
|
|
107
|
+
.workspace-separator[data-resizing=true]{background:var(--link-hover)}
|
|
125
108
|
.workspace-leftWidth{grid-column:2;grid-row:1}.workspace-rightWidth{grid-column:4;grid-row:1}
|
|
126
109
|
.workspace-inspectorHeight{grid-row:2;cursor:row-resize}.workspace-inspectorHeight::after{inset:-4px 0}
|
|
127
110
|
.workspace-drawer-triggers{display:flex;gap:6px;flex-wrap:wrap;flex:none;padding:4px 8px;min-height:0}
|
|
128
111
|
.workspace-drawer-triggers:not(:has(button:not([hidden]))){display:none}
|
|
129
112
|
.workspace-drawer-triggers button{padding:4px 7px;font-size:11px;min-height:28px}
|
|
130
|
-
.workspace-drawer{position:fixed;inset:0;width:min(94vw,950px);height:min(92dvh,850px);max-width:94vw;max-height:92dvh;margin:auto;padding:8px;border:1px solid
|
|
113
|
+
.workspace-drawer{position:fixed;inset:0;width:min(94vw,950px);height:min(92dvh,850px);max-width:94vw;max-height:92dvh;margin:auto;padding:8px;border:1px solid var(--border-strong);border-radius:8px;background:var(--rv-panel);color:inherit;overflow:hidden;z-index:20}
|
|
131
114
|
.workspace-drawer:not([open]){display:none}.workspace-drawer[open]{display:flex;flex-direction:column;gap:8px}
|
|
132
|
-
.workspace-drawer::backdrop{background
|
|
115
|
+
.workspace-drawer::backdrop{background:var(--rv-backdrop)}
|
|
133
116
|
.workspace-drawer-close{align-self:flex-end;flex:none}
|
|
134
117
|
.workspace-drawer>.left-panel{display:flex!important;flex-direction:column;flex:1;overflow:hidden;min-height:0;padding:0}
|
|
135
118
|
.workspace-drawer>.parts-explorer{display:block;grid-template-columns:none;flex:1;width:auto;min-width:0;min-height:0;height:auto;overflow:auto}
|
|
136
119
|
.workspace-drawer>.inspector{flex:1;height:auto!important;min-height:0!important;max-height:none;overflow:hidden}
|
|
137
|
-
|
|
138
|
-
[data-theme=light] .workspace-separator{background:#b2c7cf}[data-theme=light] .workspace-separator:focus-visible{outline-color:#24694d;background:#70a58c}
|
|
120
|
+
|
|
139
121
|
@media(pointer:coarse){.workspace-separator::after{inset:0 -7px}.workspace-inspectorHeight::after{inset:-7px 0}.workspace-drawer-triggers button{min-height:38px}}
|
|
140
122
|
#stage-layout[data-panes="1"]{grid-template-columns:minmax(0,1fr);grid-template-rows:minmax(0,1fr)}
|