@rig-c/review 0.3.0 → 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 +19 -6
- package/docs/DESIGN.md +29 -5
- package/package.json +1 -1
- 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
|
@@ -359,15 +359,28 @@ Control capabilities are excluded from links and browser persistence.
|
|
|
359
359
|
|
|
360
360
|
## Workspace settings and panels
|
|
361
361
|
|
|
362
|
-
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
|
|
363
376
|
and use the arrow keys (Shift for larger steps) and Home/End. Escape, a
|
|
364
377
|
cancelled pointer or window blur during a drag returns the previous size. The
|
|
365
|
-
stage keeps a usable minimum. When the window is too narrow, the
|
|
366
|
-
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
|
|
367
380
|
drawers opened from buttons above the stage instead of shrinking to nothing. Responsive clamping
|
|
368
381
|
never overwrites the desired size, so a wider window restores it.
|
|
369
382
|
|
|
370
|
-
Settings,
|
|
383
|
+
Settings, at the bottom of the left column, holds browser defaults: Theme, Sync cameras by
|
|
371
384
|
default, Repeat idle by default, and Reset layout. Defaults seed a new
|
|
372
385
|
inspection only. A view link or source draft keeps its own appearance, camera
|
|
373
386
|
sync and idle choice, and restoring it neither changes the saved defaults nor
|
|
@@ -390,9 +403,9 @@ serving use per-port origins and keep them separate.
|
|
|
390
403
|
|
|
391
404
|
## Inspect a part
|
|
392
405
|
|
|
393
|
-
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.
|
|
394
407
|
|
|
395
|
-
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.
|
|
396
409
|
|
|
397
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.
|
|
398
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/package.json
CHANGED
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)}
|
package/web/index.html
CHANGED
|
@@ -5,9 +5,10 @@
|
|
|
5
5
|
<div id="app">
|
|
6
6
|
<header class="topbar"><div class="brand"><span class="brand-mark">r</span>rig review</div><div><strong id="package-title">Review package</strong><span id="revision" class="subtle"></span></div><div class="top-actions"><span id="dirty" class="subtle">Loading…</span><button id="export">Feedback JSON</button><button id="export-markdown">Issue Markdown</button><button id="save" class="primary" disabled>Submit review</button></div></header>
|
|
7
7
|
<div class="layout">
|
|
8
|
-
|
|
9
|
-
<main><div class="viewbar"><div class="segmented" aria-label="View"><button data-view="compare" aria-pressed="true">Side by side</button><button data-view="reference" aria-pressed="false">Reference</button><button data-view="rig" aria-pressed="false">Rig</button></div><div class="zoom-controls"><label class="camera-sync"><input id="camera-sync" type="checkbox" checked>Sync cameras</label><button id="zoom-out" aria-label="Zoom out">−</button><output id="zoom-value">100%</output><button id="zoom-in" aria-label="Zoom in">+</button><button id="fit">Fit</button></div></div><div class="stage-meta"><span id="checkpoint-title">Select a checkpoint</span><span id="player-state" class="subtle">No rig supplied</span><span id="layout-state" class="subtle" role="status" aria-label="Effective layout">Loading inputs…</span><span id="renderer-state" class="subtle" aria-label="Last draw backend" title="DOM images for rigid parts; WebGL meshes when available, Canvas2D fallback.">Not rendered</span></div><div id="stage-layout" class="stages"><section id="reference-pane" class="stage-pane" aria-label="Reference"><span id="reference-label" class="pane-label">Reference</span><div id="reference-viewport" class="viewport"><div id="reference-content" class="reference-content"></div><div id="reference-empty" class="empty">No reference supplied.</div></div></section><section id="rig-pane" class="stage-pane" aria-label="Rig"><span class="pane-label">Generated rig · read only</span><div id="rig-viewport" class="viewport"><div id="rig-root"></div><canvas id="rig-overlay" class="rig-overlay" aria-label="Actual rig mesh and bone overlay"></canvas><div id="rig-empty" class="empty">No compiled model supplied.</div></div></section></div><div class="stage-footer"><span id="coordinates">Drag to pan · scroll to zoom · cameras synchronized</span><span>Source files remain unchanged.</span></div><section class="inspector" aria-label="Inspection controls"><div class="inspector-heading"><strong>INSPECTOR</strong><div role="tablist" aria-label="Inspector sections"><button role="tab" id="tab-view" data-inspector="view" aria-controls="inspector-view" aria-selected="true" tabindex="0">View</button><button role="tab" id="tab-rig" data-inspector="rig" aria-controls="inspector-rig" aria-selected="false" tabindex="-1">Rig</button><button role="tab" id="tab-playback" data-inspector="playback" aria-controls="inspector-playback" aria-selected="false" tabindex="-1">Playback</button><button role="tab" id="tab-feedback" data-inspector="feedback" aria-controls="inspector-feedback" aria-selected="false" tabindex="-1">Feedback</button></div><span class="hint">Local view controls · source files stay unchanged</span></div><div class="draft-notice"><span id="draft-state" role="status" aria-live="polite"></span><div id="draft-recovery"></div></div><div class="inspector-body"><section id="inspector-view" role="tabpanel" aria-labelledby="tab-view" data-inspector-panel="view"><div class="view-link-controls"><button id="copy-view" type="button">Copy view link</button><span class="hint">Numeric source region · opening this link restores a paused inspection</span></div><pre id="view-coordinates" class="numeric-view" aria-label="Current numeric inspection region"></pre><div class="inspection-controls" aria-label="Inspection display controls"><label>Appearance <select id="appearance"><option value="system">System</option><option value="dark">Dark</option><option value="light">Light</option></select></label><label><input id="idle-repeat" type="checkbox" checked>Repeat idle</label><span id="idle-state" class="hint" role="status">Idle preview unavailable</span><span id="control-state" class="hint" role="status"></span></div><p class="hint">Sync cameras links fit-relative zoom and viewport-relative pan across visible panes. Turn it off to inspect each pane independently. Toolbar zoom and Fit use the last active pane. Hold Space while dragging a video to pan without taking over its native controls.</p></section><section id="inspector-rig" role="tabpanel" aria-labelledby="tab-rig" data-inspector-panel="rig" hidden><section id="selected-part" aria-label="Selected part" aria-live="polite"></section><div class="rig-layer-controls"><fieldset id="layer-controls"><legend>Rig layers</legend><label><input id="layer-images" type="checkbox" checked disabled>Images</label><label><input id="layer-mesh" type="checkbox" disabled>Mesh edges</label><label><input id="layer-bones" type="checkbox" disabled>Bone points</label></fieldset><span id="layer-state" class="hint">No rig supplied</span></div><section id="modeling-tools"><div class="eyebrow">MODELING REVIEW</div><button data-proposal="mesh" class="tool">Mesh density <small>Add feedback</small></button><button data-proposal="contour" class="tool">Contour curvature <small>Add feedback</small></button><button data-proposal="chest" class="tool">Chest position <small>Add feedback</small></button><button data-proposal="angle" class="tool">Angle inspection <small id="axis-state">No inspection axes supplied</small></button><label for="part-set" class="label space-top">Part presets</label><select id="part-set" disabled><option>No sets supplied</option></select><p class="hint">Preview only · does not create motion keys.</p></section><section id="angle-panel" class="angle-panel" hidden><label for="axis-select">Rig inspection axis</label><select id="axis-select"></select><label id="axis-label" for="axis-value"></label><input id="axis-value" type="range" disabled><output id="axis-value-label"></output><p class="hint">Maps one inspection axis to its animation time. This does not rotate the camera.</p></section></section><section id="inspector-playback" role="tabpanel" aria-labelledby="tab-playback" data-inspector-panel="playback" hidden><section id="transport" class="transport"><button id="play" aria-label="Play" disabled>▶</button><label for="animation-select" class="sr-only">Animation</label><select id="animation-select" disabled><option>No animations supplied</option></select><input id="scrub" type="range" min="0" max="1" step="0.001" value="0" aria-label="Playback time"><output id="time-label">0.0000 / 0.0000 s</output><output id="frame-label">rate unknown</output></section><section id="frame-controls" class="frame-controls" hidden aria-label="Frame navigation using the explicit clock"><label for="frame-number">Frame</label><input id="frame-number" type="number" min="0" step="1" inputmode="numeric" aria-label="Frame number to inspect"><button id="frame-jump" type="button">Go</button><input id="frame-scrub" type="range" min="0" max="1" step="1" value="0" aria-label="Scrub by frame"><span id="frame-clock-label" class="hint"></span><p id="frame-outside" class="hint" hidden>The current time is outside the explicit frame range. Enter a frame number to go to its exact time.</p></section><section id="motion-tools"><div class="eyebrow">MOTION · READ ONLY</div><button data-proposal="pose" class="tool">Inspect pose <small>Add feedback</small></button><button data-proposal="timing" class="tool">Inspect interval <small>Add feedback</small></button><button data-proposal="audio" class="tool">Audio links <small>Add feedback</small></button></section><section id="sound-panel" class="sound-panel"><div class="row"><span class="eyebrow">SOUND CUES</span><label><input id="sync-audio" type="checkbox">Play cue audio in sync</label></div><div id="cues"></div><div class="row"><label for="audio-volume">Audio volume</label><input id="audio-volume" type="range" min="0" max="1" step="0.05" value="0.8"><output id="audio-volume-label">80%</output></div><div id="event-log" class="hint" aria-live="polite">Runtime events: none</div></section><section id="report-panel" class="report-panel" aria-labelledby="report-heading"><div class="report-header"><h2 id="report-heading">Report events</h2><span id="report-summary" class="subtle"></span><a id="report-source-link" target="_blank" rel="noopener" hidden>Source report</a></div><p id="report-unavailable" class="hint" hidden>No rig-pose-report/1 events supplied.</p><div id="report-browser"><div class="report-controls"><label for="report-category" class="sr-only">Report event category</label><select id="report-category"><option value="all">All events</option></select><button id="report-previous" type="button" aria-label="Previous event page">Previous</button><output id="report-page"></output><button id="report-next" type="button" aria-label="Next event page">Next</button></div><div id="report-empty-categories" class="report-empty-categories"></div><p id="report-missing" class="hint" hidden></p><ol id="report-events" class="report-event-list" aria-label="Read-only report events"></ol><section id="report-selection" class="report-selection" hidden aria-labelledby="report-selection-heading"><h3 id="report-selection-heading"></h3><p id="report-timing" class="hint"></p><div class="report-controls"><label id="report-time-label" for="report-time" hidden>Select explicit time</label><select id="report-time" hidden></select><button id="report-seek" type="button" disabled>Go to explicit time</button></div><div id="report-target-control" hidden><label for="report-target" class="label space-top">Select a target named in the report</label><select id="report-target"></select></div><p id="report-prefill-note" class="hint"></p><p id="report-provenance" class="hint"></p><details><summary>Source event details</summary><pre id="report-details"></pre></details></section></div></section></section><section id="inspector-feedback" role="tabpanel" aria-labelledby="tab-feedback" data-inspector-panel="feedback" hidden><div class="review-summary"><div class="eyebrow">REVIEW</div><label for="review-status" class="label">Review status</label><select id="review-status"><option value="pending">Pending</option><option value="approved">Approved</option><option value="changes-requested">Changes requested</option></select><section class="provenance" aria-label="Reviewed source"><div class="eyebrow">REVIEWED SOURCE</div><div class="provenance-row"><span>Package revision</span><strong id="provenance-version">—</strong></div><div class="provenance-row"><span>rig SHA-256</span><code id="provenance-rig">—</code></div><div class="provenance-row"><span>motion SHA-256</span><code id="provenance-motion">—</code></div><details><summary>Full provenance</summary><pre id="provenance-full"></pre></details><p class="hint">Submits server-verified file hashes and tool versions. Your selected review status is preserved.</p></section><div class="divider"></div></div><div id="checkpoint-detail"><div class="eyebrow">SELECTED CHECKPOINT</div><h1 id="detail-title">Checkpoint</h1><p id="detail-note" class="hint"></p><fieldset class="outcome"><legend>Checkpoint outcome</legend><label><input type="radio" name="outcome" value="pending" checked>Pending</label><label><input type="radio" name="outcome" value="pass">Pass</label><label><input type="radio" name="outcome" value="fail">Needs changes</label></fieldset><label for="checkpoint-note" class="label space-top">Review notes</label><textarea id="checkpoint-note" rows="5" placeholder="Describe the part, conditions and what looks wrong."></textarea></div><div class="divider"></div><section id="proposal-panel"><div class="eyebrow">STRUCTURED FEEDBACK</div><div class="label">Frame range · inclusive</div><div class="frame-range"><label>Start<input id="feedback-start" type="number" min="0" step="1" inputmode="numeric" placeholder="e.g. 18"></label><label>End<input id="feedback-end" type="number" min="0" step="1" inputmode="numeric" placeholder="e.g. 32"></label></div><p id="feedback-range-hint" class="hint">Checking frame bounds…</p><label for="feedback-target-kind" class="label space-top">Target type</label><select id="feedback-target-kind"><option value="">Select target</option><option value="rig">Entire rig</option><option value="bone">Bone</option><option value="slot">Slot</option></select><label for="feedback-target-name" class="label space-top">Target name</label><input id="feedback-target-name" type="text" list="feedback-target-names" autocomplete="off" placeholder="Actual bone or slot name"><datalist id="feedback-target-names"></datalist><p id="feedback-target-hint" class="hint"></p><label for="feedback-category" class="label space-top">Category</label><select id="feedback-category"><option value="">Select category</option><option value="defect">Defect</option><option value="quality">Quality</option></select><label for="feedback-summary" class="label space-top">One-line summary</label><input id="feedback-summary" type="text" maxlength="1000" placeholder="e.g. The left wrist moves outside the sleeve"><button id="add-proposal" class="outline block">Add feedback</button><p id="proposal-boundary" class="hint">Records the current checkpoint, time, reference, camera and inspection axis. Motion is read only: inspect, mark ranges and export feedback. No keys or source files are edited.</p><p id="feedback-warnings" class="hint" role="status" hidden></p><div id="proposal-list"></div></section><details id="strip-panel" class="report-panel"><summary>Export frame strip</summary><p class="hint">Uses the feedback form start and end frames, inclusive. Inspection time and source files remain unchanged.</p><label for="strip-scope" class="label">Export range</label><select id="strip-scope"><option value="interval">Entered start–end frames</option><option value="feedback">All feedback ranges</option></select><label for="strip-reference" class="label space-top">Reference video</label><select id="strip-reference"><option value="">Select reference video</option></select><label for="strip-format" class="label space-top">Format</label><select id="strip-format"><option value="png">PNG strip</option><option value="json">JSON frames and provenance</option></select><p class="hint">PNG requires frameTimes in the selected report. Without it, JSON preserves requested frames with time: null. Batch export requires every feedback item to match the selected animation. Unsubmitted feedback is included; exporting does not submit the review.</p><button id="strip-export" type="button" disabled>Download strip</button><p id="strip-status" class="hint" role="status" aria-live="polite">Loading package…</p></details></section></div></section></main><aside id="parts-explorer" class="parts-explorer" aria-label="Parts explorer"><h2>PARTS</h2><button type="button" data-part-whole aria-pressed="true">Whole rig</button><label for="part-search">Find slot</label><input id="part-search" data-part-search type="search" autocomplete="off" placeholder="Exact slot name or text"><label class="part-solo"><input id="part-solo" data-part-solo type="checkbox" disabled>Solo selected slot</label><p data-part-status class="hint" role="status">Loading actual slots…</p><ul data-part-list aria-label="Actual rig slots"></ul><p class="hint">Select in context or enable Solo. Type icons and main preview represent actual slots; atlas pages are not part thumbnails.</p></aside>
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
</div><script type="module" src="main.js"></script>
|
|
8
|
+
<aside class="left-panel"><details id="package-context" class="package-context"><summary><span class="eyebrow">CHECKPOINTS</span><span id="checkpoint-title">Select a checkpoint</span></summary><nav id="checkpoints" aria-label="Checkpoints"></nav><section><label for="reference-select" class="label">Reference</label><select id="reference-select"></select><div id="reference-info" class="hint"></div><div id="person-control" hidden><label for="person-select" class="label space-top">Person in input pose</label><select id="person-select"></select></div></section></details><aside id="parts-explorer" class="parts-explorer" aria-label="Parts explorer"><h2>PARTS</h2><button type="button" data-part-whole aria-pressed="true">Whole rig</button><label for="part-search">Find slot</label><input id="part-search" data-part-search type="search" autocomplete="off" placeholder="Exact slot name or text"><label class="part-solo"><input id="part-solo" data-part-solo type="checkbox" disabled>Solo selected slot</label><p data-part-status class="hint" role="status">Loading actual slots…</p><ul data-part-list aria-label="Actual rig slots"></ul><p class="hint">Select in context or enable Solo. Type icons and main preview represent actual slots; atlas pages are not part thumbnails.</p></aside></aside>
|
|
9
|
+
<main><div class="viewbar"><div class="segmented" aria-label="View"><button data-view="compare" aria-pressed="true">Side by side</button><button data-view="reference" aria-pressed="false">Reference</button><button data-view="rig" aria-pressed="false">Rig</button></div><fieldset id="layer-controls" class="layer-toggles"><legend class="sr-only">Rig layers</legend><label class="layer-toggle" title="No rig supplied"><input aria-keyshortcuts="I" id="layer-images" type="checkbox" checked disabled><span>Images</span><kbd aria-hidden="true">I</kbd></label><label class="layer-toggle" title="No rig supplied"><input aria-keyshortcuts="M" id="layer-mesh" type="checkbox" disabled><span>Mesh edges</span><kbd aria-hidden="true">M</kbd></label><label class="layer-toggle" title="No rig supplied"><input aria-keyshortcuts="B" id="layer-bones" type="checkbox" disabled><span>Bone points</span><kbd aria-hidden="true">B</kbd></label></fieldset><div class="zoom-controls"><label class="camera-sync"><input id="camera-sync" type="checkbox" checked>Sync cameras</label><button id="zoom-out" aria-label="Zoom out">−</button><output id="zoom-value">100%</output><button id="zoom-in" aria-label="Zoom in">+</button><button id="fit">Fit</button></div><div class="view-options" aria-label="Inspection display controls"><label>Appearance <select id="appearance"><option value="system">System</option><option value="dark">Dark</option><option value="light">Light</option></select></label><label><input id="idle-repeat" type="checkbox" checked>Repeat idle</label><button id="copy-view" type="button">Copy view link</button></div></div><div id="stage-layout" class="stages"><section id="reference-pane" class="stage-pane" aria-label="Reference"><span id="reference-label" class="pane-label">Reference</span><div id="reference-viewport" class="viewport"><div id="reference-content" class="reference-content"></div><div id="reference-empty" class="empty">No reference supplied.</div></div></section><section id="rig-pane" class="stage-pane" aria-label="Rig"><span class="pane-label">Generated rig · read only</span><div id="rig-viewport" class="viewport"><div id="rig-root"></div><canvas id="rig-overlay" class="rig-overlay" aria-label="Actual rig mesh and bone overlay"></canvas><div id="rig-empty" class="empty">No compiled model supplied.</div></div></section></div><div class="stage-footer"><span id="coordinates">Drag to pan · scroll to zoom · cameras synchronized</span><span>Source files remain unchanged.</span></div><section class="inspector" aria-label="Inspection controls"><div class="inspector-heading"><strong>INSPECTOR</strong><div role="tablist" aria-label="Inspector sections">
|
|
10
|
+
<button role="tab" id="tab-rig" data-inspector="rig" aria-controls="inspector-rig" aria-selected="true" tabindex="0">Setup</button>
|
|
11
|
+
<button role="tab" id="tab-playback" data-inspector="playback" aria-controls="inspector-playback" aria-selected="false" tabindex="-1">Animate</button>
|
|
12
|
+
<button role="tab" id="tab-feedback" data-inspector="feedback" aria-controls="inspector-feedback" aria-selected="false" tabindex="-1">Feedback</button></div><span class="hint">Local view controls · source files stay unchanged</span></div><div class="inspector-body"><section id="inspector-rig" role="tabpanel" aria-labelledby="tab-rig" data-inspector-panel="rig"><section id="selected-part" aria-label="Selected part" aria-live="polite"></section><section id="modeling-tools"><div class="eyebrow">MODELING REVIEW</div><button data-proposal="mesh" class="tool">Mesh density <small>Add feedback</small></button><button data-proposal="contour" class="tool">Contour curvature <small>Add feedback</small></button><button data-proposal="chest" class="tool">Chest position <small>Add feedback</small></button><button data-proposal="angle" class="tool">Angle inspection <small id="axis-state">No inspection axes supplied</small></button><label for="part-set" class="label space-top">Part presets</label><select id="part-set" disabled><option>No sets supplied</option></select><p class="hint">Preview only · does not create motion keys.</p></section><section id="angle-panel" class="angle-panel" hidden><label for="axis-select">Rig inspection axis</label><select id="axis-select"></select><label id="axis-label" for="axis-value"></label><input id="axis-value" type="range" disabled><output id="axis-value-label"></output><p class="hint">Maps one inspection axis to its animation time. This does not rotate the camera.</p></section></section><section id="inspector-playback" role="tabpanel" aria-labelledby="tab-playback" data-inspector-panel="playback" hidden><section id="transport" class="transport"><button id="play" aria-label="Play" disabled>▶</button><label for="animation-select" class="sr-only">Animation</label><select id="animation-select" disabled><option>No animations supplied</option></select><input id="scrub" type="range" min="0" max="1" step="0.001" value="0" aria-label="Playback time"><output id="time-label">0.0000 / 0.0000 s</output><output id="frame-label">rate unknown</output></section><section id="frame-controls" class="frame-controls" hidden aria-label="Frame navigation using the explicit clock"><label for="frame-number">Frame</label><input id="frame-number" type="number" min="0" step="1" inputmode="numeric" aria-label="Frame number to inspect"><button id="frame-jump" type="button">Go</button><input id="frame-scrub" type="range" min="0" max="1" step="1" value="0" aria-label="Scrub by frame"><span id="frame-clock-label" class="hint"></span><p id="frame-outside" class="hint" hidden>The current time is outside the explicit frame range. Enter a frame number to go to its exact time.</p></section><section id="motion-tools"><div class="eyebrow">MOTION · READ ONLY</div><button data-proposal="pose" class="tool">Inspect pose <small>Add feedback</small></button><button data-proposal="timing" class="tool">Inspect interval <small>Add feedback</small></button><button data-proposal="audio" class="tool">Audio links <small>Add feedback</small></button></section><section id="sound-panel" class="sound-panel"><div class="row"><span class="eyebrow">SOUND CUES</span><label><input id="sync-audio" type="checkbox">Play cue audio in sync</label></div><div id="cues"></div><div class="row"><label for="audio-volume">Audio volume</label><input id="audio-volume" type="range" min="0" max="1" step="0.05" value="0.8"><output id="audio-volume-label">80%</output></div><div id="event-log" class="hint" aria-live="polite">Runtime events: none</div></section><section id="report-panel" class="report-panel" aria-labelledby="report-heading"><div class="report-header"><h2 id="report-heading">Report events</h2><span id="report-summary" class="subtle"></span><a id="report-source-link" target="_blank" rel="noopener" hidden>Source report</a></div><p id="report-unavailable" class="hint" hidden>No rig-pose-report/1 events supplied.</p><div id="report-browser"><div class="report-controls"><label for="report-category" class="sr-only">Report event category</label><select id="report-category"><option value="all">All events</option></select><button id="report-previous" type="button" aria-label="Previous event page">Previous</button><output id="report-page"></output><button id="report-next" type="button" aria-label="Next event page">Next</button></div><div id="report-empty-categories" class="report-empty-categories"></div><p id="report-missing" class="hint" hidden></p><ol id="report-events" class="report-event-list" aria-label="Read-only report events"></ol><section id="report-selection" class="report-selection" hidden aria-labelledby="report-selection-heading"><h3 id="report-selection-heading"></h3><p id="report-timing" class="hint"></p><div class="report-controls"><label id="report-time-label" for="report-time" hidden>Select explicit time</label><select id="report-time" hidden></select><button id="report-seek" type="button" disabled>Go to explicit time</button></div><div id="report-target-control" hidden><label for="report-target" class="label space-top">Select a target named in the report</label><select id="report-target"></select></div><p id="report-prefill-note" class="hint"></p><p id="report-provenance" class="hint"></p><details><summary>Source event details</summary><pre id="report-details"></pre></details></section></div></section></section><section id="inspector-feedback" role="tabpanel" aria-labelledby="tab-feedback" data-inspector-panel="feedback" hidden><div class="review-summary"><div class="eyebrow">REVIEW</div><label for="review-status" class="label">Review status</label><select id="review-status"><option value="pending">Pending</option><option value="approved">Approved</option><option value="changes-requested">Changes requested</option></select><section class="provenance" aria-label="Reviewed source"><div class="eyebrow">REVIEWED SOURCE</div><div class="provenance-row"><span>Package revision</span><strong id="provenance-version">—</strong></div><div class="provenance-row"><span>rig SHA-256</span><code id="provenance-rig">—</code></div><div class="provenance-row"><span>motion SHA-256</span><code id="provenance-motion">—</code></div><details><summary>Full provenance</summary><pre id="provenance-full"></pre></details><div class="source-files"><div class="eyebrow">SOURCE FILES · READ ONLY</div><div id="source-files"></div></div><p class="hint">Submits server-verified file hashes and tool versions. Your selected review status is preserved.</p></section><div class="divider"></div></div><div id="checkpoint-detail"><div class="eyebrow">SELECTED CHECKPOINT</div><h1 id="detail-title">Checkpoint</h1><p id="detail-note" class="hint"></p><fieldset class="outcome"><legend>Checkpoint outcome</legend><label><input type="radio" name="outcome" value="pending" checked>Pending</label><label><input type="radio" name="outcome" value="pass">Pass</label><label><input type="radio" name="outcome" value="fail">Needs changes</label></fieldset><label for="checkpoint-note" class="label space-top">Review notes</label><textarea id="checkpoint-note" rows="5" placeholder="Describe the part, conditions and what looks wrong."></textarea></div><div class="divider"></div><section id="proposal-panel"><div class="eyebrow">STRUCTURED FEEDBACK</div><div class="label">Frame range · inclusive</div><div class="frame-range"><label>Start<input id="feedback-start" type="number" min="0" step="1" inputmode="numeric" placeholder="e.g. 18"></label><label>End<input id="feedback-end" type="number" min="0" step="1" inputmode="numeric" placeholder="e.g. 32"></label></div><p id="feedback-range-hint" class="hint">Checking frame bounds…</p><label for="feedback-target-kind" class="label space-top">Target type</label><select id="feedback-target-kind"><option value="">Select target</option><option value="rig">Entire rig</option><option value="bone">Bone</option><option value="slot">Slot</option></select><label for="feedback-target-name" class="label space-top">Target name</label><input id="feedback-target-name" type="text" list="feedback-target-names" autocomplete="off" placeholder="Actual bone or slot name"><datalist id="feedback-target-names"></datalist><p id="feedback-target-hint" class="hint"></p><label for="feedback-category" class="label space-top">Category</label><select id="feedback-category"><option value="">Select category</option><option value="defect">Defect</option><option value="quality">Quality</option></select><label for="feedback-summary" class="label space-top">One-line summary</label><input id="feedback-summary" type="text" maxlength="1000" placeholder="e.g. The left wrist moves outside the sleeve"><button id="add-proposal" class="outline block">Add feedback</button><p id="proposal-boundary" class="hint">Records the current checkpoint, time, reference, camera and inspection axis. Motion is read only: inspect, mark ranges and export feedback. No keys or source files are edited.</p><p id="feedback-warnings" class="hint" role="status" hidden></p><div id="proposal-list"></div></section><details id="strip-panel" class="report-panel"><summary>Export frame strip</summary><p class="hint">Uses the feedback form start and end frames, inclusive. Inspection time and source files remain unchanged.</p><label for="strip-scope" class="label">Export range</label><select id="strip-scope"><option value="interval">Entered start–end frames</option><option value="feedback">All feedback ranges</option></select><label for="strip-reference" class="label space-top">Reference video</label><select id="strip-reference"><option value="">Select reference video</option></select><label for="strip-format" class="label space-top">Format</label><select id="strip-format"><option value="png">PNG strip</option><option value="json">JSON frames and provenance</option></select><p class="hint">PNG requires frameTimes in the selected report. Without it, JSON preserves requested frames with time: null. Batch export requires every feedback item to match the selected animation. Unsubmitted feedback is included; exporting does not submit the review.</p><button id="strip-export" type="button" disabled>Download strip</button><p id="strip-status" class="hint" role="status" aria-live="polite">Loading package…</p></details></section></div></section></main></div><footer class="status-bar"><span id="message" class="status-message" role="status" aria-live="polite">Loading local review package…</span><div class="status-readouts" aria-label="Status"><span id="player-state" class="subtle">No rig supplied</span><span id="layout-state" class="subtle" role="status" aria-label="Effective layout">Loading inputs…</span><span id="renderer-state" class="subtle" aria-label="Last draw backend" title="DOM images for rigid parts; WebGL meshes when available, Canvas2D fallback.">Not rendered</span><span id="layer-state" class="hint">No rig supplied</span><span id="idle-state" class="hint" role="status">Idle preview unavailable</span><span id="control-state" class="hint" role="status"></span><div class="draft-notice"><span id="draft-state" role="status" aria-live="polite"></span><div id="draft-recovery"></div></div></div><div class="status-source"><details class="view-region"><summary>View region</summary><div class="view-region-body"><span class="hint">Numeric source region · opening this link restores a paused inspection</span><pre id="view-coordinates" class="numeric-view" aria-label="Current numeric inspection region"></pre><p class="hint">Sync cameras links fit-relative zoom and viewport-relative pan across visible panes. Turn it off to inspect each pane independently. Toolbar zoom and Fit use the last active pane. Hold Space while dragging a video to pan without taking over its native controls.</p></div></details><span id="source-hash" class="subtle"></span></div></footer></div>
|
|
13
|
+
<script type="module" src="main.js"></script>
|
|
13
14
|
</body></html>
|
package/web/main.ts
CHANGED
|
@@ -2,7 +2,7 @@ import {PartExplorer,loadPartCatalog} from './part-selection.ts';
|
|
|
2
2
|
import { loadModel, RigPlayer, type ModelAssets, type PoseBounds } from '@rig-c/play';
|
|
3
3
|
import {cameraFrame,cameraSignature,cameraFromSignature,assertCameraView,boundsFitSignature,dragSignature,zoomSignature,type CameraFrame,type CameraSignature} from './camera-view.ts';
|
|
4
4
|
import {rendererStatus} from './renderer-status.ts';
|
|
5
|
-
import {bindAppearance,bindPan,bindStageLayout,syncStageLayout,bindInspectorTabs,idleAvailable,initialIdleAnimation,automaticIdlePreview,type ViewPoint} from './view-controls.ts';
|
|
5
|
+
import {bindAppearance,bindPan,bindStageLayout,syncStageLayout,bindInspectorTabs,idleAvailable,initialIdleAnimation,automaticIdlePreview,bindLayerShortcuts,describeLayerToggles,type ViewPoint} from './view-controls.ts';
|
|
6
6
|
import {RigOverlays} from './rig-overlays.ts';
|
|
7
7
|
import {bindWorkspaceUI,type WorkspaceUI} from './workspace-ui.ts';
|
|
8
8
|
import {connectControl} from './control.ts';
|
|
@@ -92,6 +92,7 @@ let selectedReportTarget: number | undefined;
|
|
|
92
92
|
|
|
93
93
|
function message(value: string, error = false): void {
|
|
94
94
|
text('message', value);
|
|
95
|
+
el('message').title = value; // the footer truncates long messages
|
|
95
96
|
el('message').classList.toggle('error', error);
|
|
96
97
|
}
|
|
97
98
|
|
|
@@ -553,6 +554,7 @@ async function initPlayer(initialCheckpoint?:Checkpoint): Promise<void> {
|
|
|
553
554
|
select('axis-select').replaceChildren(); for (const axis of validAxes) select('axis-select').append(option(axis.id, axis.label));
|
|
554
555
|
input('axis-value').disabled = !validAxes.length;
|
|
555
556
|
for(const id of ['layer-images','layer-mesh','layer-bones'])input(id).disabled=false;
|
|
557
|
+
describeLayerToggles(document,el('layer-state').textContent??'');
|
|
556
558
|
fitCamera();
|
|
557
559
|
} catch (error) { text('renderer-state',rendererStatus(null,'failed').label);player?.dispose(); player = null; assets?.dispose(); assets = null; text('player-state', 'Rig load failed'); text('rig-empty', `Could not open rig: ${errorText(error)}`); message(errorText(error), true); }
|
|
558
560
|
}
|
|
@@ -783,6 +785,7 @@ function bind(): void {
|
|
|
783
785
|
viewDisposers.push(bindStageLayout(el('stage-layout'),fitCamera));
|
|
784
786
|
for(const target of ['rig','reference'] as const)viewDisposers.push(bindPan(el(target+'-viewport'),(dx,dy)=>cameraAction(()=>panCamera(target,dx,dy))));
|
|
785
787
|
for(const [id,key] of [['layer-images','images'],['layer-mesh','mesh'],['layer-bones','bones']] as const)input(id).onchange=()=>{layers[key]=input(id).checked;redrawOverlay(false);};
|
|
788
|
+
viewDisposers.push(bindLayerShortcuts(document));describeLayerToggles(document,el('layer-state').textContent??'');
|
|
786
789
|
input('idle-repeat').onchange=()=>{renderIdle();if(canRepeatIdle())void playPreview();};
|
|
787
790
|
for(const id of ['proposal-panel','checkpoint-detail'])el(id).addEventListener('focusin',pause);
|
|
788
791
|
bindInspector();
|
|
@@ -802,6 +805,8 @@ async function start(): Promise<void> {
|
|
|
802
805
|
select('reference-select').replaceChildren(); for (const reference of data.manifest.references) select('reference-select').append(option(reference.id, `${reference.label} · ${reference.type}`));
|
|
803
806
|
for (const reference of data.manifest.references.filter(item => item.type === 'video')) select('strip-reference').append(option(reference.id, reference.label));
|
|
804
807
|
if (!data.manifest.references.length) { select('reference-select').append(option('', 'No references supplied')); select('reference-select').disabled = true; }
|
|
808
|
+
// Checkpoints and reference fold away when the package supplies neither.
|
|
809
|
+
(el('package-context') as HTMLDetailsElement).open = data.manifest.checkpoints.length > 0 || data.manifest.references.length > 0;
|
|
805
810
|
const sourceFiles = el('source-files'); for (const file of data.manifest.authoring) { const a = document.createElement('a'); a.href = assetUrl(file.path); a.textContent = file.label; a.className = 'source-link'; a.target = '_blank'; a.rel = 'noopener'; sourceFiles.append(a); } if (!data.manifest.authoring.length) sourceFiles.textContent = 'No source files supplied';
|
|
806
811
|
bind(); parts=new PartExplorer(el('parts-explorer'),el('selected-part'),previous=>{redrawOverlay(true);const part=parts?.value();if(part){activeCamera='rig';if(view==='reference')setView('rig');showInspector('rig');renderCameraControls();}if(!cameraRestoring&&(previous?.side!==part?.side||previous?.slot!==part?.slot))cameraAction(()=>fitActiveCamera(part?'rig':availableCamera()));persistence?.save();},part=>{select('feedback-target-kind').value='slot';renderFeedbackTargets(true);input('feedback-target-name').value=part.slot;showInspector('feedback');}); updateStripControls(); stripStatus('Choose the entered start–end frames or all feedback ranges to download. Unsubmitted feedback is included; exporting does not submit the review.'); renderFeedbackTargets(true); renderFeedbackRange(); renderCheckpoints(); renderProposals(); renderProvenance(); updateSavedState(); renderCues(); initReport();
|
|
807
812
|
const requestedId = new URLSearchParams(location.search).get('checkpoint'),initial=initialReviewSelection(data.manifest,requestedId,data.explicitReference?.id,data.initialAnimation??undefined),first=initial.checkpoint;
|
package/web/part-selection.ts
CHANGED
|
@@ -127,7 +127,7 @@ export class PartExplorer extends PartInspection {
|
|
|
127
127
|
return {title,info,declarations,bounds,unavailable,feedback,status};
|
|
128
128
|
})();
|
|
129
129
|
nodes.title.textContent=observation?`${observation.side==='single'?'':observation.side.toUpperCase()+' · '}${observation.slot}`:'Whole rig';
|
|
130
|
-
nodes.info.textContent=observation?`${observation.mode==='solo'?'Solo':'In context'} · ${observation.drawn?observation.kind==='mesh'?`${observation.vertices} posed vertices · ${observation.triangles} triangles`:'Region · four posed corners':'Not drawn at current pose'} · Skin: ${observation.skin??'not set'} · Set: ${observation.set??'not set'}`:refusal?`Parts unavailable · ${refusal.message}. The whole rig stays drawn; no slot is selected, soloed or targeted from stale data.`:'Select an actual slot in the
|
|
130
|
+
nodes.info.textContent=observation?`${observation.mode==='solo'?'Solo':'In context'} · ${observation.drawn?observation.kind==='mesh'?`${observation.vertices} posed vertices · ${observation.triangles} triangles`:'Region · four posed corners':'Not drawn at current pose'} · Skin: ${observation.skin??'not set'} · Set: ${observation.set??'not set'}`:refusal?`Parts unavailable · ${refusal.message}. The whole rig stays drawn; no slot is selected, soloed or targeted from stale data.`:'Select an actual slot in the parts explorer. Selection preserves animation, time and physics history.';
|
|
131
131
|
for(const node of [nodes.declarations,nodes.bounds,nodes.unavailable,nodes.feedback])node.hidden=!observation;nodes.feedback.disabled=!observation;
|
|
132
132
|
nodes.declarations.textContent=observation?`${observation.activeAttachment?'Resolved public attachment: '+observation.activeAttachment.skin+' / '+observation.activeAttachment.attachment+'.':(observation.identityUnavailable??'No active drawable attachment: '+observation.reason)+'.'} Declared: ${observation.declarations.map(d=>`${d.skin} / ${d.attachment} (${d.kind})`).join('; ')||'none'}. ${observation.page?'Drawn page: '+observation.page:''}${observation.region?' · region '+observation.region:''}${observation.sequence!==null?' · sequence '+observation.sequence:''}`:'';
|
|
133
133
|
nodes.bounds.textContent=observation?observation.bounds?`Selection bounds · world units / Y up: x ${observation.bounds.x}, y ${observation.bounds.y}, width ${observation.bounds.width}, height ${observation.bounds.height}. Conservative geometry/clip bounds; fit uses the existing 0.25–8× camera range.`:'PART_FIT_UNAVAILABLE: no positive drawn bounds at the held pose.':'';
|
package/web/styles.css
CHANGED
|
@@ -1,17 +1,20 @@
|
|
|
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-family:var(--font-sans);color:var(--fg);background:var(--bg);font-size:13px;color-scheme:dark;font-synthesis:none}*{box-sizing:border-box}body{margin:0}button,input,select,textarea{font:inherit}button{color:inherit;background:var(--rv-control);border:1px solid var(--border-strong);border-radius:6px;padding:7px 10px;cursor:pointer}button:hover{background:var(--rv-control-hover)}button:disabled{opacity:.45;cursor:default}button[aria-pressed=true]{background:var(--rv-pressed-bg);border-color:var(--rv-pressed-border);color:var(--rv-pressed-fg)}button.primary{background:var(--link);border-color:var(--link);color:var(--btn-fg);font-weight:600}button.outline{background:transparent}button.block{width:100%;margin-top:10px}a{color:var(--link)}input,select,textarea{color:var(--fg);background:var(--rv-field);border:1px solid var(--border-strong);border-radius:6px;padding:8px;max-width:100%}select,input[type=text],textarea{width:100%}textarea{resize:vertical;line-height:1.6}input[type=range]{accent-color:var(--link);padding:0;min-width:0}input[type=checkbox],input[type=radio]{accent-color:var(--link)}button:focus-visible,a:focus-visible,input:focus-visible,select:focus-visible,textarea:focus-visible{outline:2px solid var(--rv-focus);outline-offset:3px}[hidden]{display:none!important}.topbar{min-height:72px;padding:15px 22px;display:flex;align-items:center;gap:25px;border-bottom:1px solid var(--border);flex-wrap:wrap;background:var(--rv-bar)}.brand{font-size:19px;letter-spacing:-.6px;white-space:nowrap}.brand-mark{display:inline-grid;place-items:center;width:24px;height:24px;border:1px solid var(--link);color:var(--link);border-radius:50%;margin-right:9px;font-size:14px}.topbar strong{font-weight:500;font-size:13px}.topbar #revision{margin-left:9px}.top-actions{display:flex;align-items:center;gap:8px;margin-left:auto;flex-wrap:wrap}.subtle,.hint{color:var(--fg-muted);font-size:11px}.hint{line-height:1.6}.workspaces{display:flex;gap:10px;padding:0 18px;border-bottom:1px solid var(--border);background:var(--rv-bar)}.workspaces button{border:0;border-bottom:2px solid transparent;border-radius:0;background:transparent;padding:15px 13px;font-size:12px;letter-spacing:.06em;color:var(--fg-soft)}.workspaces button[aria-pressed=true]{border-bottom-color:var(--link);color:var(--fg-strong);background:transparent}.workspaces button span{font-size:11px;margin-left:8px;letter-spacing:0;color:var(--fg-muted)}.layout{display:grid;grid-template-columns:190px minmax(250px,1fr) 286px;min-height:calc(100vh - 160px)}aside{background:var(--rv-panel);padding:20px 15px}.left-panel{border-right:1px solid var(--border)}.right-panel{border-left:1px solid var(--border)}.eyebrow{font-size:11px;letter-spacing:.08em;color:var(--fg-muted);margin:0 0 11px}.label{display:block;color:var(--fg-muted);font-size:11px;margin:0 0 7px}.space-top{margin-top:15px}.left-panel section{margin-top:26px}.tool{width:100%;display:flex;justify-content:space-between;align-items:center;text-align:left;gap:5px;border-color:transparent;background:transparent;padding:9px 6px;margin:3px 0}.tool small{font-size:11px;color:var(--fg-muted)}.checkpoint{display:block;width:100%;background:transparent;border:1px solid transparent;text-align:left;margin:4px 0;padding:10px 9px;white-space:normal;line-height:1.5}.checkpoint[aria-current=true]{background:var(--rv-pressed-bg);border-color:var(--rv-pressed-border)}.checkpoint small{display:block;font-size:11px;color:var(--fg-muted);margin-top:3px}.source-link{display:block;font-size:11px;overflow-wrap:anywhere;margin:8px 0}main{min-width:0;display:flex;flex-direction:column;background:var(--bg)}.viewbar{display:flex;align-items:center;justify-content:space-between;gap:10px;flex-wrap:wrap;padding:12px;border-bottom:1px solid var(--border)}.segmented{display:flex;gap:3px;padding:3px;border:1px solid var(--border-strong);border-radius:8px;background:var(--rv-segmented)}.segmented button{border:0;background:transparent;font-size:11px;padding:5px 9px}.segmented button[aria-pressed=true]{background:var(--rv-pressed-bg);color:var(--fg)}.zoom-controls{display:flex;gap:4px;align-items:center}.zoom-controls button{padding:4px 8px;font-size:12px}.zoom-controls output{font:11px var(--font-mono);min-width:39px;text-align:center;color:var(--fg-muted)}.stages{display:flex;gap:10px;padding:12px 12px 0;flex:1;min-height:460px}.stage-pane{position:relative;min-width:0;flex:1;border:1px solid var(--border-strong);border-radius:8px;overflow:hidden;background:radial-gradient(ellipse at 50% 40%,var(--rv-pane-hi),var(--rv-pane-lo) 74%)}.pane-label{position:absolute;left:10px;top:10px;z-index:2;background:var(--rv-pane-label-bg);border-radius:4px;padding:4px 7px;color:var(--rv-stage-fg);font-size:11px;pointer-events:none}.viewport{position:relative;overflow:hidden;width:100%;height:100%;min-height:450px;touch-action:none;background-image:radial-gradient(var(--rv-grid-dot) .7px,transparent .7px);background-size:22px 22px}.empty{position:absolute;inset:65px 22px 30px;display:grid;place-items:center;text-align:center;color:var(--rv-stage-fg);font-size:12px;line-height:1.8}.reference-content,#rig-root{position:absolute;transform-origin:0 0}.reference-content img,.reference-content video{display:block;max-width:none}.reference-content svg{display:block;max-width:none}.reference-content video{object-fit:contain}.stage-footer{display:flex;align-items:center;justify-content:space-between;gap:10px;flex-wrap:wrap;padding:11px 16px;color:var(--fg-muted);font-size:11px}.transport{display:flex;align-items:center;gap:11px;padding:13px;border-top:1px solid var(--border);background:var(--rv-panel)}.transport select{width:auto;max-width:150px;font-size:11px}.transport input{flex:1}.transport output{font:11px var(--font-mono);white-space:nowrap;color:var(--fg-muted)}.sound-panel,.angle-panel{padding:15px;border-top:1px solid var(--border);background:var(--rv-panel)}.row{display:flex;align-items:center;gap:11px;flex-wrap:wrap}.row .eyebrow{margin:0}.sound-panel .row{font-size:11px;margin-bottom:9px}.sound-panel input[type=range]{flex:1;max-width:160px}.cue{display:flex;gap:9px;align-items:center;justify-content:space-between;border-top:1px solid var(--border);padding:9px 0;font-size:11px;flex-wrap:wrap}.cue button{padding:4px 7px;font-size:11px}.angle-panel{display:flex;gap:10px;align-items:center;flex-wrap:wrap;font-size:11px}.angle-panel select{width:auto;max-width:150px}.angle-panel .hint{width:100%;margin:0}.angle-panel input{flex:1}.divider{height:1px;background:var(--border-strong);margin:22px 0}h1{font-size:15px;font-weight:500;line-height:1.6;margin:0 0 7px}.outcome{border:0;padding:0;margin:18px 0 0;display:flex;gap:11px;flex-wrap:wrap}.outcome legend{font-size:11px;color:var(--fg-muted);margin-bottom:8px;padding:0}.outcome label{display:flex;align-items:center;gap:3px;font-size:11px}.proposal-item{border-top:1px solid var(--border);padding:11px 0;margin-top:12px;font-size:11px;overflow-wrap:anywhere}.proposal-item strong{display:block;font-weight:500;margin-bottom:5px}.proposal-item button{font-size:11px;padding:3px 6px;margin-top:7px}footer{display:flex;justify-content:space-between;align-items:center;gap:6px 15px;flex-wrap:wrap;padding:8px 18px;border-top:1px solid var(--border);background:var(--footer-bg);font-size:11px;color:var(--fg-soft)}.error{color:var(--fail)!important}.dirty{color:var(--warn)!important}.sr-only{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%)}
|
|
2
5
|
@media(min-width:1500px){.layout{grid-template-columns:218px minmax(250px,1fr) 310px}.stages{min-height:600px}}
|
|
3
6
|
@media(max-width:1000px){.layout{grid-template-columns:158px minmax(250px,1fr) 250px}aside{padding:17px 12px}.topbar{gap:14px}.topbar #revision{display:block;margin-left:0}.stages{min-height:400px}.viewport{min-height:400px}.workspaces span{display:none}}
|
|
4
|
-
@media(max-width:780px){.layout{grid-template-columns:150px minmax(0,1fr)}.right-panel{grid-column:1/-1;border-left:0;border-top:1px solid
|
|
5
|
-
@media(max-width:540px){.layout{grid-template-columns:1fr}.left-panel{border-right:0;border-bottom:1px solid
|
|
7
|
+
@media(max-width:780px){.layout{grid-template-columns:150px minmax(0,1fr)}.right-panel{grid-column:1/-1;border-left:0;border-top:1px solid var(--border);display:grid;grid-template-columns:1fr 1fr;gap:12px 24px}.right-panel>.eyebrow,.right-panel>.label,.right-panel>#review-status{grid-column:1/-1}.right-panel>.divider{display:none}#checkpoint-detail,#proposal-panel{min-width:0}.top-actions{margin-left:0}.stages{min-height:420px}.viewport{min-height:420px}.transport{flex-wrap:wrap}.transport input{min-width:130px}.topbar{padding:14px 16px}}
|
|
8
|
+
@media(max-width:540px){.layout{grid-template-columns:1fr}.left-panel{border-right:0;border-bottom:1px solid var(--border);display:grid;grid-template-columns:1fr 1fr;gap:12px}.left-panel>.eyebrow,.left-panel>#checkpoints{grid-column:1/-1}.left-panel section{margin-top:8px}#checkpoints{display:flex;gap:5px;flex-wrap:wrap}.checkpoint{width:auto;flex:1;min-width:120px}.right-panel{display:block}#proposal-panel{margin-top:25px}.stages{gap:6px;padding:8px}.stage-pane{border-radius:5px}.viewport{min-height:365px}.stages{min-height:365px}.pane-label{left:5px;top:6px;font-size:11px;padding:3px}.viewbar{gap:8px}.workspaces{padding:0 8px}.workspaces button{padding:13px 9px}.stage-footer{padding:9px 12px}.source-link{font-size:11px}footer{padding:11px 12px}}
|
|
6
9
|
@media(pointer:coarse){button{min-height:42px}input,select,textarea{font-size:16px}.outcome label{min-height:40px}.zoom-controls button{min-width:38px}}
|
|
7
10
|
|
|
8
|
-
.frame-range{display:grid;grid-template-columns:1fr 1fr;gap:9px}.frame-range label{font-size:11px;color
|
|
11
|
+
.frame-range{display:grid;grid-template-columns:1fr 1fr;gap:9px}.frame-range label{font-size:11px;color:var(--fg-muted)}.frame-range input{width:100%;margin-top:6px}
|
|
9
12
|
|
|
10
|
-
.provenance{margin-top:20px;padding:12px;background
|
|
13
|
+
.provenance{margin-top:20px;padding:12px;background:var(--rv-raised);border:1px solid var(--border-strong);border-radius:7px}.provenance-row{display:flex;gap:9px;justify-content:space-between;font-size:11px;margin:6px 0}.provenance-row span{color:var(--fg-muted)}.provenance-row strong{font-weight:500;overflow-wrap:anywhere}.provenance code{font:11px var(--font-mono)}.provenance details{margin-top:10px;font-size:11px;color:var(--fg-muted)}.provenance summary{cursor:pointer}.provenance pre{white-space:pre-wrap;overflow-wrap:anywhere;font:11px/1.6 var(--font-mono)}.right-panel>.provenance{grid-column:1/-1}
|
|
11
14
|
|
|
12
|
-
.report-panel{padding:15px;border-top:1px solid
|
|
15
|
+
.report-panel{padding:15px;border-top:1px solid var(--border);background:var(--rv-panel)}.report-header{display:flex;align-items:center;gap:12px;flex-wrap:wrap}.report-header h2{font-size:13px;font-weight:500;margin:0}.report-header a{font-size:11px;margin-left:auto}.report-controls{display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-top:12px}.report-controls select{width:auto;max-width:250px;flex:1;font-size:11px}.report-controls button{font-size:11px;padding:5px 9px}.report-controls output{font:11px var(--font-mono);color:var(--fg-muted)}.report-empty-categories{display:flex;gap:6px;flex-wrap:wrap;margin-top:11px}.report-empty-categories span{font-size:11px;color:var(--fg-muted);background:var(--rv-raised);padding:2px 6px;border-radius:4px}.report-event-list{list-style:none;padding:0;margin:10px 0 0;display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:5px}.report-event-list li{min-width:0}.report-event-list button{display:flex;flex-direction:column;gap:2px;text-align:left;width:100%;height:100%;padding:8px 9px;border-color:var(--border);background:var(--rv-raised);white-space:normal;overflow-wrap:anywhere;line-height:1.5}.report-event-list button[aria-current=true]{border-color:var(--rv-pressed-border);background:var(--rv-pressed-bg)}.report-event-list strong{font-size:11px;font-weight:500}.report-event-list small{font-size:11px;color:var(--fg-muted)}.report-event-list .no-time{color:var(--warn)}.report-selection{border-top:1px solid var(--border);margin-top:12px;padding-top:12px}.report-selection h3{font-size:12px;font-weight:500;margin:0}.report-selection details{font-size:11px;color:var(--fg-muted);margin-top:10px}.report-selection summary{cursor:pointer}.report-selection pre{white-space:pre-wrap;overflow-wrap:anywhere;font:11px/1.6 var(--font-mono);background:var(--code-bg);padding:10px;border-radius:5px}.report-selection #report-time{max-width:100%}@media(max-width:1000px){.report-event-list{grid-template-columns:1fr}}@media(max-width:540px){.report-controls select{min-width:140px}.report-header a{margin-left:0}}
|
|
13
16
|
|
|
14
|
-
.transport #frame-label{color
|
|
17
|
+
.transport #frame-label{color:var(--fg-strong);font-size:11px}.frame-controls{display:flex;align-items:center;gap:9px;flex-wrap:wrap;padding:10px 14px;background:var(--rv-panel);border-top:1px solid var(--border);font-size:11px}.frame-controls label{color:var(--fg-muted)}.frame-controls input[type=number]{width:86px;font-variant-numeric:tabular-nums}.frame-controls input[type=range]{flex:1;min-width:100px}.frame-controls button{font-size:11px;padding:5px 9px}.frame-controls .hint{margin:0}.frame-controls #frame-outside{width:100%}
|
|
15
18
|
|
|
16
19
|
/* Inspection layout is bounded independently of pane content and review forms. */
|
|
17
20
|
#stage-layout{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));grid-template-rows:minmax(0,1fr);height:clamp(480px,calc(100dvh - 300px),900px);min-height:0;flex:none;align-items:stretch}
|
|
@@ -21,37 +24,19 @@
|
|
|
21
24
|
#stage-layout [data-panning=true]{cursor:grabbing!important}
|
|
22
25
|
#stage-layout img{-webkit-user-drag:none;user-select:none}
|
|
23
26
|
.rig-overlay{position:absolute;inset:0;pointer-events:none;width:100%;height:100%;z-index:1}
|
|
24
|
-
.inspection-controls{display:flex;align-items:center;gap:8px 14px;flex-wrap:wrap;padding:9px 14px;border-bottom:1px solid
|
|
27
|
+
.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)}
|
|
25
28
|
.inspection-controls label{display:inline-flex;align-items:center;gap:5px;margin:0;white-space:nowrap}
|
|
26
29
|
.inspection-controls select{width:auto;padding:4px;font-size:11px}
|
|
27
30
|
.inspection-controls input[type=checkbox]{width:auto;margin:0}
|
|
28
31
|
.inspection-controls fieldset{display:flex;gap:10px;align-items:center;border:0;padding:0;margin:0;min-width:0;flex-wrap:wrap}
|
|
29
32
|
.inspection-controls legend{float:left;margin-right:9px;font-size:11px}
|
|
30
33
|
#control-state:empty{display:none}
|
|
31
|
-
:root[data-theme=light]{color-scheme:light
|
|
34
|
+
:root[data-theme=light]{color-scheme:light}
|
|
32
35
|
:root[data-theme=dark]{color-scheme:dark}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
[data-theme=light] button:hover{background:#d0e2e8}
|
|
38
|
-
[data-theme=light] button.primary,[data-theme=light] .primary{background:#24694d;border-color:#24694d;color:#fff}
|
|
39
|
-
[data-theme=light] input,[data-theme=light] select,[data-theme=light] textarea{background:#fff;border-color:#8faab7;color:#243e4a;accent-color:#276c50}
|
|
40
|
-
[data-theme=light] input::placeholder,[data-theme=light] textarea::placeholder{color:#607986}
|
|
41
|
-
[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}
|
|
42
|
-
[data-theme=light] .workspaces button,[data-theme=light] .segmented button{color:#3e5c69}
|
|
43
|
-
[data-theme=light] .workspaces button[aria-pressed=true]{color:#185738;border-bottom-color:#276c50}
|
|
44
|
-
[data-theme=light] .workspaces button span{color:#58727e}
|
|
45
|
-
[data-theme=light] .segmented,[data-theme=light] .provenance,[data-theme=light] .report-selection pre{background:#e1ebef;border-color:#9cb5c0;color:#294552}
|
|
46
|
-
[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}
|
|
47
|
-
[data-theme=light] .checkpoint small{color:#4b6c59}
|
|
48
|
-
[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}
|
|
49
|
-
[data-theme=light] .report-event-list button,[data-theme=light] .report-empty-categories span{background:#e5eef2;border-color:#acbfca;color:#294552}
|
|
50
|
-
[data-theme=light] .feedback-item strong{color:#1e4e3b}
|
|
51
|
-
[data-theme=light] a{color:#175f48}
|
|
52
|
-
[data-theme=light] .error{color:#a03225!important}
|
|
53
|
-
[data-theme=light] .dirty,[data-theme=light] .report-event-list .no-time{color:#875800!important}
|
|
54
|
-
[data-theme=light] :focus-visible{outline-color:#24694d}
|
|
36
|
+
|
|
37
|
+
input::placeholder,textarea::placeholder{color:var(--fg-muted)}
|
|
38
|
+
|
|
39
|
+
:focus-visible{outline-color:var(--rv-focus)}
|
|
55
40
|
/* Source pixels retain their original color; the neutral stage stays dark in both themes. */
|
|
56
41
|
|
|
57
42
|
/* The application shell never scrolls; each Inspector tab and the source list owns overflow. */
|
|
@@ -60,12 +45,12 @@ html,body{height:100%;overflow:hidden}body{height:100dvh}#app{height:100dvh;disp
|
|
|
60
45
|
main{min-height:0;overflow:hidden;display:flex;flex-direction:column}
|
|
61
46
|
.left-panel{min-height:0;overflow:auto;padding:13px 10px}
|
|
62
47
|
#stage-layout{height:auto;min-height:0;flex:1;overflow:hidden;padding:8px;gap:7px}
|
|
63
|
-
.inspector{height:clamp(205px,32dvh,340px);min-height:180px;flex:none;display:flex;flex-direction:column;border-top:1px solid
|
|
64
|
-
.inspector-heading{display:flex;align-items:center;gap:16px;flex-wrap:wrap;padding:7px 12px;border-bottom:1px solid
|
|
48
|
+
.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}
|
|
49
|
+
.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}
|
|
65
50
|
.inspector-heading strong{font-size:10px;letter-spacing:.08em;font-weight:500}
|
|
66
51
|
.inspector-heading [role=tablist]{display:flex;gap:4px}
|
|
67
52
|
.inspector-heading button{padding:4px 9px;font-size:11px}
|
|
68
|
-
.inspector-heading button[aria-selected=true]{background
|
|
53
|
+
.inspector-heading button[aria-selected=true]{background:var(--rv-pressed-bg);color:var(--rv-pressed-fg);border-color:var(--rv-pressed-border)}
|
|
69
54
|
.inspector-body{flex:1;min-height:0;overflow:hidden}
|
|
70
55
|
.inspector-body>[role=tabpanel]{height:100%;overflow:auto;padding:12px 15px;min-height:0}
|
|
71
56
|
.inspector .inspection-controls{padding:0;border:0;background:transparent}
|
|
@@ -82,35 +67,31 @@ main{min-height:0;overflow:hidden;display:flex;flex-direction:column}
|
|
|
82
67
|
#inspector-rig #part-set{max-width:190px}
|
|
83
68
|
#inspector-playback #motion-tools{display:flex;gap:8px;flex-wrap:wrap}
|
|
84
69
|
#inspector-playback .tool{width:auto}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
[data-theme=light] .inspector-heading button[aria-selected=true]{background:#cde4d8;color:#1e4d36;border-color:#59836b}
|
|
88
|
-
@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}}
|
|
70
|
+
|
|
71
|
+
@media(max-width:780px){.layout{grid-template-columns:130px minmax(0,1fr)!important}.inspector{height:36dvh}.stage-footer{padding:5px 10px}.inspector-heading>.hint{display:none}}
|
|
89
72
|
@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}}
|
|
90
73
|
|
|
91
74
|
.inspector .feedback-form>label,.inspector .review-summary>label{display:block;font-size:11px;margin:10px 0 5px}.inspector .frame-range input{width:100%}.inspector [role=tabpanel]{overscroll-behavior:contain}.left-panel{overscroll-behavior:contain}#stage-layout .pane-label{max-width:calc(100% - 15px);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
92
75
|
|
|
93
|
-
.
|
|
94
|
-
|
|
95
|
-
.numeric-view{font:11px/1.5 ui-monospace,SFMono-Regular,monospace;white-space:pre-wrap;overflow-wrap:anywhere;max-width:100%;padding:8px;background:#14232c;color:#c9dfe7;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:#e0c58e}.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}[data-theme=light] .numeric-view{background:#e1eaee;color:#264653}[data-theme=light] .draft-notice{color:#80521a}[data-theme=light] .outcome legend,[data-theme=light] .brand span,[data-theme=light] .toolbar{color:#496774}
|
|
76
|
+
.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}
|
|
96
77
|
|
|
97
78
|
.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}
|
|
98
79
|
#stage-layout:has(>.stage-pane[hidden]){grid-template-columns:minmax(0,1fr);grid-template-rows:minmax(0,1fr)}
|
|
99
80
|
|
|
100
81
|
/* Slot explorer shares the stage row; bottom Inspector remains in the main column. */
|
|
101
|
-
.layout{grid-template-columns:
|
|
102
|
-
@media(max-width:1050px){.layout{grid-template-columns:
|
|
103
|
-
@media(max-width:700px){.layout{grid-template-columns:minmax(0,1fr)
|
|
104
|
-
@media(max-width:460px){.layout{grid-template-columns:minmax(0,1fr)
|
|
82
|
+
.layout{grid-template-columns:260px minmax(0,1fr)!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}
|
|
83
|
+
@media(max-width:1050px){.layout{grid-template-columns:200px minmax(0,1fr)!important}.parts-explorer{padding:8px}}
|
|
84
|
+
@media(max-width:700px){.layout{grid-template-columns:minmax(0,1fr)!important}.left-panel{display:none!important}}
|
|
85
|
+
@media(max-width:460px){.layout{grid-template-columns:minmax(0,1fr)!important}.parts-explorer{padding:6px}.parts-explorer li button{padding:6px}.parts-explorer>.hint{font-size:10px}}
|
|
105
86
|
|
|
106
87
|
/* Resizable workspace: one measured shell; workspace-ui clamps the desired browser sizes. */
|
|
107
88
|
.layout.workspace-layout{display:grid;grid-template-columns:var(--workspace-columns)!important;min-height:0!important;overflow:hidden}
|
|
108
89
|
.workspace-layout>main{display:grid;min-width:0;min-height:0;overflow:hidden;grid-column:3;grid-row:1}
|
|
109
90
|
.workspace-stage-stack{grid-row:1;display:flex;flex-direction:column;min-width:0;min-height:0;overflow:hidden}
|
|
110
91
|
.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
|
|
92
|
+
.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
93
|
.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
|
|
94
|
+
.workspace-settings{flex:none;max-height:65%;overflow:auto;padding:10px;border-top:1px solid var(--border)}
|
|
114
95
|
.workspace-settings h2{font-size:12px;margin:0 0 8px}
|
|
115
96
|
.workspace-settings label{display:flex;align-items:center;gap:6px;font-size:11px;margin:7px 0}
|
|
116
97
|
.workspace-settings select{width:auto;min-width:0;flex:1}
|
|
@@ -118,22 +99,60 @@ main{min-height:0;overflow:hidden;display:flex;flex-direction:column}
|
|
|
118
99
|
.workspace-settings input[type=checkbox]{width:auto;flex:none}
|
|
119
100
|
.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
101
|
.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
|
|
102
|
+
.workspace-separator{position:relative;z-index:3;background:var(--border-strong);touch-action:none;cursor:col-resize;min-width:0;min-height:0}
|
|
122
103
|
.workspace-separator::after{content:'';position:absolute;inset:0 -4px}
|
|
123
|
-
.workspace-separator:focus-visible{outline:2px solid
|
|
124
|
-
.workspace-separator[data-resizing=true]{background
|
|
125
|
-
.workspace-
|
|
104
|
+
.workspace-separator:focus-visible{outline:2px solid var(--rv-focus);outline-offset:-2px;background:var(--link)}
|
|
105
|
+
.workspace-separator[data-resizing=true]{background:var(--link-hover)}
|
|
106
|
+
.workspace-side-left{grid-column:2;grid-row:1}.workspace-side-right{grid-column:4;grid-row:1}
|
|
126
107
|
.workspace-inspectorHeight{grid-row:2;cursor:row-resize}.workspace-inspectorHeight::after{inset:-4px 0}
|
|
127
108
|
.workspace-drawer-triggers{display:flex;gap:6px;flex-wrap:wrap;flex:none;padding:4px 8px;min-height:0}
|
|
128
109
|
.workspace-drawer-triggers:not(:has(button:not([hidden]))){display:none}
|
|
129
110
|
.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
|
|
111
|
+
.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
112
|
.workspace-drawer:not([open]){display:none}.workspace-drawer[open]{display:flex;flex-direction:column;gap:8px}
|
|
132
|
-
.workspace-drawer::backdrop{background
|
|
113
|
+
.workspace-drawer::backdrop{background:var(--rv-backdrop)}
|
|
133
114
|
.workspace-drawer-close{align-self:flex-end;flex:none}
|
|
134
115
|
.workspace-drawer>.left-panel{display:flex!important;flex-direction:column;flex:1;overflow:hidden;min-height:0;padding:0}
|
|
135
116
|
.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
117
|
.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}
|
|
118
|
+
|
|
139
119
|
@media(pointer:coarse){.workspace-separator::after{inset:0 -7px}.workspace-inspectorHeight::after{inset:-7px 0}.workspace-drawer-triggers button{min-height:38px}}
|
|
120
|
+
|
|
121
|
+
/* Workspace rework: parts explorer is the left column; checkpoints and reference fold above it. */
|
|
122
|
+
.package-context{border-bottom:1px solid var(--border);padding-bottom:8px;margin-bottom:10px}
|
|
123
|
+
.package-context>summary{cursor:pointer;display:flex;flex-wrap:wrap;align-items:baseline;gap:4px 8px;font-size:11px}
|
|
124
|
+
.package-context>summary .eyebrow{margin:0}
|
|
125
|
+
.package-context #checkpoint-title{color:var(--fg-soft);overflow-wrap:anywhere}
|
|
126
|
+
.package-context #checkpoints{max-height:30dvh;overflow:auto;margin-top:6px}
|
|
127
|
+
.package-context .checkpoint{padding:6px 8px;margin:2px 0}
|
|
128
|
+
.package-context section{margin-top:10px}
|
|
129
|
+
.left-panel .parts-explorer{border:0;padding:0;background:transparent;overflow:visible}
|
|
130
|
+
.source-files{margin-top:12px}
|
|
131
|
+
.source-files .eyebrow{margin-bottom:4px}
|
|
132
|
+
|
|
133
|
+
/* View bar: layer toggles keep native checkboxes; the label carries the pressed look. */
|
|
134
|
+
.layer-toggles{display:flex;gap:3px;padding:3px;margin:0;min-width:0;border:1px solid var(--border-strong);border-radius:8px;background:var(--rv-segmented)}
|
|
135
|
+
.layer-toggle{position:relative;display:inline-flex;align-items:center;gap:6px;padding:5px 9px;border:1px solid transparent;border-radius:6px;font-size:11px;color:var(--fg-soft);cursor:pointer;white-space:nowrap}
|
|
136
|
+
.layer-toggle input{position:absolute;opacity:0;width:1px;height:1px;margin:0;pointer-events:none}
|
|
137
|
+
.layer-toggle:has(input:checked){background:var(--rv-pressed-bg);border-color:var(--rv-pressed-border);color:var(--rv-pressed-fg)}
|
|
138
|
+
.layer-toggle:has(input:disabled){opacity:.45;cursor:default}
|
|
139
|
+
.layer-toggle:has(input:focus-visible){outline:2px solid var(--rv-focus);outline-offset:2px}
|
|
140
|
+
.layer-toggle kbd{font:10px var(--font-mono);padding:0 4px;border:1px solid var(--border-strong);border-radius:3px;color:var(--fg-muted)}
|
|
141
|
+
.view-options{display:flex;align-items:center;gap:6px 10px;flex-wrap:wrap;font-size:11px;margin-left:auto}
|
|
142
|
+
.view-options label{display:inline-flex;align-items:center;gap:5px;white-space:nowrap}
|
|
143
|
+
.view-options select{width:auto;padding:4px;font-size:11px}
|
|
144
|
+
.view-options input[type=checkbox]{width:auto;margin:0}
|
|
145
|
+
.view-options button{padding:4px 8px;font-size:11px}
|
|
146
|
+
|
|
147
|
+
/* Footer is the one status area: message | readouts | view region and source hash. */
|
|
148
|
+
footer.status-bar{flex-wrap:nowrap}
|
|
149
|
+
.status-message{flex:1 1 180px;min-width:120px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
150
|
+
.status-readouts{flex:0 1 auto;max-width:60%;display:flex;align-items:center;justify-content:flex-end;gap:2px 12px;flex-wrap:wrap;min-width:0}
|
|
151
|
+
.status-readouts .draft-notice{padding:0}
|
|
152
|
+
.status-readouts .draft-notice #draft-recovery{margin-left:6px}
|
|
153
|
+
.status-source{display:flex;align-items:center;gap:12px;margin-left:auto;flex:none;white-space:nowrap}
|
|
154
|
+
.view-region{position:relative}
|
|
155
|
+
.view-region>summary{cursor:pointer}
|
|
156
|
+
.view-region-body{position:absolute;right:0;bottom:calc(100% + 10px);width:min(520px,90vw);max-height:60dvh;overflow:auto;padding:10px;z-index:10;background:var(--rv-panel);border:1px solid var(--border);border-radius:7px}
|
|
157
|
+
.view-region-body .numeric-view{margin:8px 0}
|
|
158
|
+
|
package/web/view-controls.ts
CHANGED
|
@@ -28,6 +28,38 @@ export function bindStageLayout(area:HTMLElement,changed:()=>void):()=>void {
|
|
|
28
28
|
const render=()=>{if(syncStageLayout(area))changed();};
|
|
29
29
|
const observer=new ResizeObserver(render);observer.observe(area);render();return ()=>observer.disconnect();
|
|
30
30
|
}
|
|
31
|
+
/** Single-key layer toggles. Free of Space (pan), Enter (frame entry), arrows/Home/End (tabs, separators, part list). */
|
|
32
|
+
export const layerShortcuts=Object.freeze([
|
|
33
|
+
Object.freeze({key:'i',id:'layer-images',label:'Images'}),
|
|
34
|
+
Object.freeze({key:'m',id:'layer-mesh',label:'Mesh edges'}),
|
|
35
|
+
Object.freeze({key:'b',id:'layer-bones',label:'Bone points'}),
|
|
36
|
+
] as const);
|
|
37
|
+
const nonTextInputs=new Set(['checkbox','radio','button','submit','reset','range','color','file','image']);
|
|
38
|
+
/** A focused text field, textarea, select or contenteditable keeps its keys. */
|
|
39
|
+
export function textEntryFocused(target:unknown):boolean {
|
|
40
|
+
const node=target as {closest?:(selector:string)=>unknown;isContentEditable?:boolean}|null;if(!node||typeof node.closest!=='function')return false;
|
|
41
|
+
if(node.isContentEditable||node.closest('textarea,select,[contenteditable]:not([contenteditable="false"])'))return true;
|
|
42
|
+
const input=node.closest('input') as {type?:string}|null;return !!input&&!nonTextInputs.has(String(input.type??'text').toLowerCase());
|
|
43
|
+
}
|
|
44
|
+
interface ShortcutInput {disabled:boolean;click():void}
|
|
45
|
+
/** `I`/`M`/`B` click the actual checkbox, so its change handler, checked state and draft input stay the only state. */
|
|
46
|
+
export function bindLayerShortcuts(root:Pick<Document,'addEventListener'|'removeEventListener'>&{getElementById(id:string):unknown}):()=>void {
|
|
47
|
+
const keyDown=(event:Event)=>{
|
|
48
|
+
const e=event as KeyboardEvent;if(e.defaultPrevented||e.repeat||e.ctrlKey||e.metaKey||e.altKey||typeof e.key!=='string'||textEntryFocused(e.target))return;
|
|
49
|
+
const shortcut=layerShortcuts.find(item=>item.key===e.key.toLowerCase());if(!shortcut)return;
|
|
50
|
+
const input=root.getElementById(shortcut.id) as ShortcutInput|null;if(!input||input.disabled)return;
|
|
51
|
+
e.preventDefault();input.click();
|
|
52
|
+
};
|
|
53
|
+
root.addEventListener('keydown',keyDown);return ()=>root.removeEventListener('keydown',keyDown);
|
|
54
|
+
}
|
|
55
|
+
/** Disabled toggles name their reason as a tooltip; enabled ones name their key. */
|
|
56
|
+
export function describeLayerToggles(root:{getElementById(id:string):unknown},reason:string):void {
|
|
57
|
+
for(const item of layerShortcuts){
|
|
58
|
+
const input=root.getElementById(item.id) as (ShortcutInput&{closest(selector:string):{title:string}|null})|null;const label=input?.closest('label');
|
|
59
|
+
if(input&&label)label.title=input.disabled?reason:`${item.label} · key ${item.key.toUpperCase()}`;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
31
63
|
// Nested or separately disposed pan binders must not restore another binder's override.
|
|
32
64
|
const panVideoOverrides=new WeakMap<HTMLVideoElement,{owners:number;value:string;priority:string}>();
|
|
33
65
|
export function bindPan(viewport:HTMLElement,move:(dx:number,dy:number)=>void,enabled:()=>boolean=()=>true):()=>void {
|
|
@@ -67,3 +99,4 @@ export function bindInspectorTabs(changed:()=>void,beforeChange:()=>void=()=>{})
|
|
|
67
99
|
const activate=(tab:HTMLButtonElement)=>{beforeChange();for(const item of tabs){const active=item===tab;item.setAttribute('aria-selected',String(active));item.tabIndex=active?0:-1;}for(const panel of document.querySelectorAll<HTMLElement>('[data-inspector-panel]'))panel.hidden=panel.dataset.inspectorPanel!==tab.dataset.inspector;changed();};
|
|
68
100
|
for(const [index,tab] of tabs.entries()){tab.onclick=()=>activate(tab);tab.onkeydown=event=>{const next=event.key==='ArrowRight'?(index+1)%tabs.length:event.key==='ArrowLeft'?(index+tabs.length-1)%tabs.length:event.key==='Home'?0:event.key==='End'?tabs.length-1:null;if(next===null)return;event.preventDefault();const target=tabs[next];if(target){activate(target);target.focus();}};}
|
|
69
101
|
}
|
|
102
|
+
|
package/web/workspace-layout.ts
CHANGED
|
@@ -3,29 +3,49 @@ import {parseWorkspacePanels,panelBounds,type WorkspacePanels} from './workspace
|
|
|
3
3
|
export type WorkspacePanel=keyof WorkspacePanels;
|
|
4
4
|
export const workspaceMinimums=Object.freeze({stageWidth:240,stageHeight:180,separator:8});
|
|
5
5
|
export interface SeparatorRange {min:number;max:number;value:number;enabled:boolean}
|
|
6
|
-
export
|
|
6
|
+
export type WorkspaceColumnKey=Exclude<WorkspacePanel,'inspectorHeight'>;
|
|
7
|
+
/** Which saved width each side column uses; null means the page has no column on that side.
|
|
8
|
+
* The compare page keeps sources left and parts right; the single review page shows the parts
|
|
9
|
+
* explorer (its saved `rightWidth`) on the left and has no right column. */
|
|
10
|
+
export interface WorkspaceColumns {left:WorkspaceColumnKey|null;right:WorkspaceColumnKey|null}
|
|
11
|
+
export const defaultWorkspaceColumns:Readonly<WorkspaceColumns>=Object.freeze({left:'leftWidth',right:'rightWidth'});
|
|
12
|
+
export type WorkspacePanelMode='panel'|'drawer'|'absent';
|
|
13
|
+
/** Sizes and modes are keyed by saved panel, not by side; `absent` panels have width 0 and no separator. */
|
|
14
|
+
export interface EffectiveWorkspaceLayout {leftWidth:number;rightWidth:number;inspectorHeight:number;stageWidth:number;stageHeight:number;leftMode:WorkspacePanelMode;rightMode:WorkspacePanelMode;inspectorMode:'panel'|'drawer';stageMinimumSatisfied:boolean;separators:Record<WorkspacePanel,SeparatorRange>}
|
|
7
15
|
const clamp=(value:number,min:number,max:number)=>Math.max(min,Math.min(max,value));
|
|
16
|
+
export function parseWorkspaceColumns(value:WorkspaceColumns):WorkspaceColumns {
|
|
17
|
+
const ok=(v:unknown)=>v===null||v==='leftWidth'||v==='rightWidth';
|
|
18
|
+
if(!value||!ok(value.left)||!ok(value.right)||value.left!==null&&value.left===value.right)throw new Error(`WORKSPACE_COLUMNS: left ${String(value?.left)} / right ${String(value?.right)}; required distinct leftWidth, rightWidth or null`);
|
|
19
|
+
return {left:value.left,right:value.right};
|
|
20
|
+
}
|
|
8
21
|
/** Input rectangle excludes app chrome. Drawer triggers must remain outside collapsed panel content.
|
|
9
|
-
* Collapse left, then right when minima cannot coexist; Inspector becomes a drawer below its minimum.
|
|
22
|
+
* Collapse the left column, then the right one when minima cannot coexist; Inspector becomes a drawer below its minimum.
|
|
10
23
|
* Returned clamps never replace the user's desired desktop sizes. */
|
|
11
|
-
export function allocateWorkspaceLayout(available:{width:number;height:number},desired:WorkspacePanels):EffectiveWorkspaceLayout {
|
|
12
|
-
const panels=parseWorkspacePanels(desired),{width,height}=available,{stageWidth:minWidth,stageHeight:minHeight,separator}=workspaceMinimums;
|
|
24
|
+
export function allocateWorkspaceLayout(available:{width:number;height:number},desired:WorkspacePanels,columns:WorkspaceColumns=defaultWorkspaceColumns):EffectiveWorkspaceLayout {
|
|
25
|
+
const panels=parseWorkspacePanels(desired),{left:leftKey,right:rightKey}=parseWorkspaceColumns(columns),{width,height}=available,{stageWidth:minWidth,stageHeight:minHeight,separator}=workspaceMinimums;
|
|
13
26
|
if(![width,height].every(n=>Number.isFinite(n)&&n>=0&&n<=65536))throw new Error('WORKSPACE_RECTANGLE: finite 0..65536 CSS pixels required');
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
27
|
+
const boundLeft=leftKey?panelBounds[leftKey].min:0,boundRight=rightKey?panelBounds[rightKey].min:0;
|
|
28
|
+
let left=leftKey!==null,right=rightKey!==null;
|
|
29
|
+
if(left&&right&&width<boundLeft+boundRight+minWidth+2*separator)left=false;
|
|
30
|
+
if(right&&width<(left?boundLeft+separator:0)+boundRight+minWidth+separator)right=false;
|
|
31
|
+
if(left&&!right&&width<boundLeft+minWidth+separator)left=false;
|
|
32
|
+
const count=Number(left)+Number(right),budget=Math.max(0,width-minWidth-count*separator),leftMin=left?boundLeft:0,rightMin=right?boundRight:0,wantLeft=left?panels[leftKey!]:0,wantRight=right?panels[rightKey!]:0;
|
|
33
|
+
const slack=wantLeft+wantRight-leftMin-rightMin,ratio=slack>0?Math.min(1,Math.max(0,(budget-leftMin-rightMin)/slack)):1,leftSize=leftMin+(wantLeft-leftMin)*ratio,rightSize=rightMin+(wantRight-rightMin)*ratio;
|
|
17
34
|
const inspector=height>=minHeight+panelBounds.inspectorHeight.min+separator,inspectorHeight=inspector?Math.min(panels.inspectorHeight,height-minHeight-separator):0;
|
|
18
|
-
const stageWidth=Math.max(0,width-
|
|
35
|
+
const stageWidth=Math.max(0,width-leftSize-rightSize-count*separator),stageHeight=Math.max(0,height-inspectorHeight-(inspector?separator:0));
|
|
19
36
|
const range=(visible:boolean,key:WorkspacePanel,value:number,max:number):SeparatorRange=>visible?{min:panelBounds[key].min,max:Math.max(panelBounds[key].min,Math.min(panelBounds[key].max,max)),value,enabled:true}:{min:0,max:0,value:0,enabled:false};
|
|
20
|
-
|
|
37
|
+
const size:Record<WorkspaceColumnKey,number>={leftWidth:0,rightWidth:0},mode:Record<WorkspaceColumnKey,WorkspacePanelMode>={leftWidth:'absent',rightWidth:'absent'},separators:Record<WorkspacePanel,SeparatorRange>={leftWidth:range(false,'leftWidth',0,0),rightWidth:range(false,'rightWidth',0,0),inspectorHeight:range(inspector,'inspectorHeight',inspectorHeight,height-minHeight-separator)};
|
|
38
|
+
if(leftKey){size[leftKey]=leftSize;mode[leftKey]=left?'panel':'drawer';separators[leftKey]=range(left,leftKey,leftSize,width-rightSize-count*separator-minWidth);}
|
|
39
|
+
if(rightKey){size[rightKey]=rightSize;mode[rightKey]=right?'panel':'drawer';separators[rightKey]=range(right,rightKey,rightSize,width-leftSize-count*separator-minWidth);}
|
|
40
|
+
return {leftWidth:size.leftWidth,rightWidth:size.rightWidth,inspectorHeight,stageWidth,stageHeight,leftMode:mode.leftWidth,rightMode:mode.rightWidth,inspectorMode:inspector?'panel':'drawer',stageMinimumSatisfied:width>=minWidth&&height>=minHeight,separators};
|
|
21
41
|
}
|
|
22
|
-
export interface WorkspaceSeparatorOptions {element:HTMLElement;panel:WorkspacePanel;controls:string;label:string;range:()=>SeparatorRange;resize:(desired:number)=>void;desired?:()=>number;resolveDesired?:(effective:number,context:{range:SeparatorRange;desired:number})=>number;commit?:(desired:number)=>void;events?:Pick<EventTarget,'addEventListener'|'removeEventListener'>|null;step?:number;coarseStep?:number}
|
|
42
|
+
export interface WorkspaceSeparatorOptions {element:HTMLElement;panel:WorkspacePanel;controls:string;label:string;range:()=>SeparatorRange;resize:(desired:number)=>void;desired?:()=>number;resolveDesired?:(effective:number,context:{range:SeparatorRange;desired:number})=>number;commit?:(desired:number)=>void;side?:'left'|'right';events?:Pick<EventTarget,'addEventListener'|'removeEventListener'>|null;step?:number;coarseStep?:number}
|
|
23
43
|
/** Arrow keys act only on the separator itself. Right/bottom boundaries grow inward.
|
|
24
44
|
* Pointer cancellation, capture loss, Escape and blur restore the prior desired size.
|
|
25
45
|
* Coupled allocators supply their reachable effective range and resolveDesired inverse;
|
|
26
46
|
* resize/commit receive desired sizes, including when effective geometry is clamped. */
|
|
27
47
|
export function bindWorkspaceSeparator(options:WorkspaceSeparatorOptions):{refresh():void;dispose():void} {
|
|
28
|
-
const {element,panel}=options,vertical=panel!=='inspectorHeight',direction=panel==='leftWidth'?1:-1,events=options.events===undefined?(typeof window==='undefined'?null:window):options.events;
|
|
48
|
+
const {element,panel}=options,vertical=panel!=='inspectorHeight',direction=(options.side??(panel==='leftWidth'?'left':'right'))==='left'?1:-1,events=options.events===undefined?(typeof window==='undefined'?null:window):options.events;
|
|
29
49
|
if(!options.controls||!options.label||/[\x00-\x1f]/.test(options.controls+options.label))throw new Error('WORKSPACE_SEPARATOR: controls and accessible label required');
|
|
30
50
|
const step=options.step??8,coarse=options.coarseStep??40;if(![step,coarse].every(n=>Number.isFinite(n)&&n>0))throw new Error('WORKSPACE_SEPARATOR: positive finite keyboard steps required');
|
|
31
51
|
const attrs=['role','aria-orientation','aria-controls','aria-label','aria-valuemin','aria-valuemax','aria-valuenow','aria-valuetext','aria-disabled'],saved=new Map(attrs.map(key=>[key,element.getAttribute(key)])),tabIndex=element.tabIndex,touchAction=element.style.touchAction;
|
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
export const workspacePreferenceKey='rig-review-preferences/1';
|
|
3
3
|
export const legacyAppearanceKey='rig-review-appearance/1';
|
|
4
4
|
export type WorkspaceTheme='system'|'dark'|'light';
|
|
5
|
+
/** Saved field names are part of the stored record and do not move with the panels.
|
|
6
|
+
* `rightWidth` is the parts explorer's width wherever the page shows it (the single review page shows
|
|
7
|
+
* it on the left); `leftWidth` is the compare page's sources column. */
|
|
5
8
|
export interface WorkspacePanels {leftWidth:number;rightWidth:number;inspectorHeight:number}
|
|
6
9
|
export const panelBounds=Object.freeze({leftWidth:Object.freeze({min:120,max:720}),rightWidth:Object.freeze({min:180,max:900}),inspectorHeight:Object.freeze({min:160,max:900})});
|
|
7
10
|
export const defaultWorkspacePanels:Readonly<WorkspacePanels>=Object.freeze({leftWidth:170,rightWidth:260,inspectorHeight:280});
|
package/web/workspace-ui.ts
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
/** Browser workspace presentation. Source selection, player clocks and review decisions stay with the page. */
|
|
2
2
|
import {WorkspacePreferenceStore,panelBounds,type WorkspacePanels,type WorkspacePreferences,type WorkspaceTheme} from './workspace-preferences.ts';
|
|
3
|
-
import {allocateWorkspaceLayout,bindWorkspaceSeparator,bindWorkspaceDrawer,InspectorScrollMemory,inspectorTabs,type InspectorTab,type WorkspacePanel,type SeparatorRange} from './workspace-layout.ts';
|
|
3
|
+
import {allocateWorkspaceLayout,bindWorkspaceSeparator,bindWorkspaceDrawer,defaultWorkspaceColumns,InspectorScrollMemory,inspectorTabs,type InspectorTab,type WorkspaceColumns,type WorkspacePanel,type SeparatorRange} from './workspace-layout.ts';
|
|
4
4
|
|
|
5
5
|
/** Reachable effective bounds, accounting for the other panel's saved desired size. */
|
|
6
|
-
export function workspaceResizeRange(available:{width:number;height:number},desired:WorkspacePanels,panel:WorkspacePanel):SeparatorRange {
|
|
7
|
-
const base=allocateWorkspaceLayout(available,desired),range=base.separators[panel];if(!range.enabled)return range;
|
|
8
|
-
const low=allocateWorkspaceLayout(available,{...desired,[panel]:panelBounds[panel].min})[panel],high=allocateWorkspaceLayout(available,{...desired,[panel]:panelBounds[panel].max})[panel];
|
|
6
|
+
export function workspaceResizeRange(available:{width:number;height:number},desired:WorkspacePanels,panel:WorkspacePanel,columns:WorkspaceColumns=defaultWorkspaceColumns):SeparatorRange {
|
|
7
|
+
const base=allocateWorkspaceLayout(available,desired,columns),range=base.separators[panel];if(!range.enabled)return range;
|
|
8
|
+
const low=allocateWorkspaceLayout(available,{...desired,[panel]:panelBounds[panel].min},columns)[panel],high=allocateWorkspaceLayout(available,{...desired,[panel]:panelBounds[panel].max},columns)[panel];
|
|
9
9
|
return {min:low,max:high,value:base[panel],enabled:high>low};
|
|
10
10
|
}
|
|
11
11
|
/** Invert the allocator instead of storing an already-clamped pixel value as a desktop desire. */
|
|
12
|
-
export function workspaceResizeTarget(available:{width:number;height:number},desired:WorkspacePanels,panel:WorkspacePanel,target:number):number {
|
|
13
|
-
const range=workspaceResizeRange(available,desired,panel);if(!range.enabled)return desired[panel];
|
|
12
|
+
export function workspaceResizeTarget(available:{width:number;height:number},desired:WorkspacePanels,panel:WorkspacePanel,target:number,columns:WorkspaceColumns=defaultWorkspaceColumns):number {
|
|
13
|
+
const range=workspaceResizeRange(available,desired,panel,columns);if(!range.enabled)return desired[panel];
|
|
14
14
|
if(target<=range.min)return panelBounds[panel].min;if(target>=range.max)return panelBounds[panel].max;
|
|
15
15
|
let low:number=panelBounds[panel].min,high:number=panelBounds[panel].max;
|
|
16
|
-
for(let i=0;i<48;i++){const mid=(low+high)/2,value=allocateWorkspaceLayout(available,{...desired,[panel]:mid})[panel];if(value<target)low=mid;else high=mid;}
|
|
16
|
+
for(let i=0;i<48;i++){const mid=(low+high)/2,value=allocateWorkspaceLayout(available,{...desired,[panel]:mid},columns)[panel];if(value<target)low=mid;else high=mid;}
|
|
17
17
|
return (low+high)/2;
|
|
18
18
|
}
|
|
19
19
|
export interface WorkspaceUI {
|
|
@@ -22,9 +22,13 @@ export interface WorkspaceUI {
|
|
|
22
22
|
}
|
|
23
23
|
export function bindWorkspaceUI(options:{changed:()=>void;defaultTheme:(theme:WorkspaceTheme)=>void}):WorkspaceUI {
|
|
24
24
|
const require=<T extends HTMLElement=HTMLElement>(id:string)=>{const node=document.getElementById(id);if(!node)throw new Error('Missing workspace element: '+id);return node as T;};
|
|
25
|
-
const layout=document.querySelector<HTMLElement>('.layout')!,main=layout.querySelector<HTMLElement>('main')!,stage=require('stage-layout'),inspector=document.querySelector<HTMLElement>('.inspector')!,
|
|
25
|
+
const layout=document.querySelector<HTMLElement>('.layout')!,main=layout.querySelector<HTMLElement>('main')!,stage=require('stage-layout'),inspector=document.querySelector<HTMLElement>('.inspector')!,explorer=require('parts-explorer');
|
|
26
26
|
layout.classList.add('workspace-layout');
|
|
27
27
|
let left=layout.querySelector<HTMLElement>('.left-panel');if(!left){left=document.createElement('aside');left.className='left-panel';left.innerHTML='<h2>INPUTS</h2><p class="hint">A and B retain their fixed source bindings. The reference video and shared animation are named above the stage.</p>';layout.prepend(left);}left.id='workspace-left';inspector.id='workspace-inspector';
|
|
28
|
+
// A parts explorer placed inside the left column makes that column the explorer: it keeps the explorer's
|
|
29
|
+
// saved width and the page has no right column. Otherwise sources stay left and the explorer stays right.
|
|
30
|
+
const explorerLeft=left.contains(explorer),columns:WorkspaceColumns=explorerLeft?{left:'rightWidth',right:null}:defaultWorkspaceColumns;
|
|
31
|
+
const tabs=inspectorTabs.filter(tab=>document.getElementById('inspector-'+tab));if(!tabs.length)throw new Error('Missing workspace element: inspector-'+inspectorTabs.join('|inspector-'));
|
|
28
32
|
const sourceList=document.createElement('div');sourceList.className='workspace-sources';sourceList.append(...left.childNodes);left.append(sourceList);
|
|
29
33
|
const settings=document.createElement('section');settings.className='workspace-settings';settings.setAttribute('aria-label','Settings');
|
|
30
34
|
settings.innerHTML='<h2>Settings</h2><label>Theme <select id="settings-theme" data-app-preference><option value="system">System</option><option value="dark">Dark</option><option value="light">Light</option></select></label><label><input id="settings-sync" data-app-preference type="checkbox">Sync cameras by default</label><label><input id="settings-idle" data-app-preference type="checkbox">Repeat idle by default</label><button id="settings-reset" type="button">Reset layout</button><p class="hint">Defaults apply to new inspections. Links and source drafts retain their explicit view.</p><p id="settings-state" class="hint" role="status"></p>';
|
|
@@ -33,36 +37,40 @@ export function bindWorkspaceUI(options:{changed:()=>void;defaultTheme:(theme:Wo
|
|
|
33
37
|
const store=new WorkspacePreferenceStore({onStatus:value=>{status.textContent=value.detail;}});let desired={...store.get().panels};
|
|
34
38
|
const stack=document.createElement('div');stack.className='workspace-stage-stack';for(const child of [...main.children])if(child!==inspector)stack.append(child);main.prepend(stack);
|
|
35
39
|
const triggers=document.createElement('div');triggers.className='workspace-drawer-triggers';triggers.setAttribute('aria-label','Workspace panels');stack.prepend(triggers);
|
|
36
|
-
const panels:Record<WorkspacePanel,HTMLElement
|
|
37
|
-
const
|
|
40
|
+
const panels:Partial<Record<WorkspacePanel,HTMLElement>>=explorerLeft?{rightWidth:left,inspectorHeight:inspector}:{leftWidth:left,rightWidth:explorer,inspectorHeight:inspector};
|
|
41
|
+
const sides:Partial<Record<WorkspacePanel,'left'|'right'>>=explorerLeft?{rightWidth:'left'}:{leftWidth:'left',rightWidth:'right'};
|
|
42
|
+
const labels:Record<WorkspacePanel,string>=explorerLeft?{leftWidth:'Sources and settings',rightWidth:'Parts and settings',inspectorHeight:'Inspector'}:{leftWidth:'Sources and settings',rightWidth:'Parts explorer',inspectorHeight:'Inspector'};
|
|
38
43
|
const handles={} as Record<WorkspacePanel,HTMLElement>,dialogs={} as Record<WorkspacePanel,HTMLDialogElement>,buttons={} as Record<WorkspacePanel,HTMLButtonElement>,disposers:(()=>void)[]=[];
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
44
|
+
const present=Object.keys(panels) as WorkspacePanel[],node=(panel:WorkspacePanel)=>panels[panel]!;
|
|
45
|
+
for(const panel of present){
|
|
46
|
+
const handle=document.createElement('div');handle.className='workspace-separator workspace-'+panel+(sides[panel]?' workspace-side-'+sides[panel]:'');handles[panel]=handle;
|
|
47
|
+
if(sides[panel]==='left')node(panel).after(handle);else if(sides[panel]==='right')node(panel).before(handle);else inspector.before(handle);
|
|
42
48
|
const trigger=document.createElement('button');trigger.type='button';trigger.textContent=labels[panel];trigger.hidden=true;triggers.append(trigger);buttons[panel]=trigger;
|
|
43
49
|
const dialog=document.createElement('dialog');dialog.className='workspace-drawer';dialog.id='workspace-drawer-'+panel;dialog.dataset.panel=panel;const close=document.createElement('button');close.type='button';close.textContent='Close '+labels[panel];close.className='workspace-drawer-close';dialog.append(close);document.body.append(dialog);dialogs[panel]=dialog;
|
|
44
50
|
disposers.push(bindWorkspaceDrawer({trigger,drawer:dialog,label:labels[panel],close}));
|
|
45
51
|
}
|
|
46
|
-
const memory=new InspectorScrollMemory();const active=():InspectorTab=>
|
|
47
|
-
let disposed=false,frame=0,applying=false,available={width:0,height:0},effective=allocateWorkspaceLayout(available,desired);
|
|
52
|
+
const memory=new InspectorScrollMemory();const active=():InspectorTab=>{const tab=document.querySelector<HTMLElement>('[data-inspector][aria-selected=true]')?.dataset.inspector as InspectorTab|undefined;return tab&&tabs.includes(tab)?tab:tabs[0]!;};
|
|
53
|
+
let disposed=false,frame=0,applying=false,available={width:0,height:0},effective=allocateWorkspaceLayout(available,desired,columns);
|
|
48
54
|
const panelFor=(tab:InspectorTab)=>require('inspector-'+tab);
|
|
49
55
|
const capture=()=>{const tab=active(),panel=panelFor(tab),saved=memory.snapshot()[tab];if(!panel.hidden&&(!saved||saved.x<=Math.max(0,panel.scrollWidth-panel.clientWidth)&&saved.y<=Math.max(0,panel.scrollHeight-panel.clientHeight)))memory.capture(tab,panel);};
|
|
50
56
|
const restore=()=>{if(!disposed)memory.restore(active(),panelFor(active()));};
|
|
51
57
|
const schedule=()=>{if(!disposed&&!frame)frame=requestAnimationFrame(()=>{frame=0;apply();});};
|
|
52
58
|
const dock=(panel:WorkspacePanel,drawer:boolean)=>{
|
|
53
|
-
const node
|
|
54
|
-
if(drawer&&
|
|
55
|
-
if(!drawer&&
|
|
59
|
+
const element=node(panel),dialog=dialogs[panel],trigger=buttons[panel];trigger.hidden=!drawer;handles[panel].hidden=drawer;
|
|
60
|
+
if(drawer&&element.parentElement!==dialog){const focus=element.contains(document.activeElement);dialog.append(element);if(focus)trigger.focus({preventScroll:true});}
|
|
61
|
+
if(!drawer&&element.parentElement===dialog){const focused=element.contains(document.activeElement)||document.activeElement===trigger;if(dialog.open)dialog.close();trigger.setAttribute('aria-expanded','false');if(sides[panel]==='left')layout.prepend(element);else if(sides[panel]==='right')layout.append(element);else main.append(element);if(focused){element.tabIndex=-1;element.focus({preventScroll:true});}}
|
|
56
62
|
};
|
|
57
|
-
const bindings=
|
|
63
|
+
const bindings=present.map(panel=>bindWorkspaceSeparator({element:handles[panel],panel,...(sides[panel]?{side:sides[panel]}:{}),controls:node(panel).id,label:'Resize '+labels[panel],range:()=>workspaceResizeRange(available,desired,panel,columns),resolveDesired:value=>workspaceResizeTarget(available,desired,panel,value,columns),desired:()=>desired[panel],resize:value=>{desired={...desired,[panel]:value};apply();},commit:value=>store.update({panels:{[panel]:value}})}));
|
|
58
64
|
function apply():void {
|
|
59
65
|
if(disposed||applying)return;applying=true;
|
|
60
66
|
try{
|
|
61
67
|
// Chrome shares the center width. It is not charged against the actual stage minimum.
|
|
62
68
|
const chrome=[...stack.children].filter(node=>node!==stage&&!((node as HTMLElement).hidden)).reduce((sum,node)=>sum+(node as HTMLElement).getBoundingClientRect().height,0);
|
|
63
|
-
available={width:Math.max(0,layout.clientWidth),height:Math.max(0,layout.clientHeight-chrome)};effective=allocateWorkspaceLayout(available,desired);
|
|
64
|
-
|
|
65
|
-
|
|
69
|
+
available={width:Math.max(0,layout.clientWidth),height:Math.max(0,layout.clientHeight-chrome)};effective=allocateWorkspaceLayout(available,desired,columns);
|
|
70
|
+
const mode=(panel:WorkspacePanel)=>effective[panel==='leftWidth'?'leftMode':panel==='rightWidth'?'rightMode':'inspectorMode'];
|
|
71
|
+
for(const panel of present)dock(panel,mode(panel)==='drawer');
|
|
72
|
+
const side=(key:WorkspaceColumns['left'])=>key&&mode(key)==='panel'?{width:effective[key],separator:8}:{width:0,separator:0},l=side(columns.left),r=side(columns.right);
|
|
73
|
+
layout.style.setProperty('--workspace-columns',`${l.width}px ${l.separator}px minmax(0,1fr) ${r.separator}px ${r.width}px`);
|
|
66
74
|
main.style.gridTemplateRows=`minmax(0,1fr) ${effective.inspectorMode==='panel'?8:0}px ${effective.inspectorHeight}px`;
|
|
67
75
|
layout.dataset.stageMinimum=String(effective.stageMinimumSatisfied);for(const binding of bindings)binding.refresh();options.changed();restore();
|
|
68
76
|
}finally{applying=false;}
|
|
@@ -70,7 +78,7 @@ export function bindWorkspaceUI(options:{changed:()=>void;defaultTheme:(theme:Wo
|
|
|
70
78
|
const renderSettings=()=>{const value=store.get();theme.value=value.theme;sync.checked=value.defaultCameraSync;idle.checked=value.defaultIdleRepeat;status.textContent=store.status().detail;};
|
|
71
79
|
theme.onchange=()=>{store.update({theme:theme.value as WorkspaceTheme});options.defaultTheme(store.get().theme);};sync.onchange=()=>{store.update({defaultCameraSync:sync.checked});};idle.onchange=()=>{store.update({defaultIdleRepeat:idle.checked});};require<HTMLButtonElement>('settings-reset').onclick=()=>store.resetLayout();
|
|
72
80
|
const unsubscribe=store.subscribe(value=>{desired={...value.panels};renderSettings();schedule();});renderSettings();
|
|
73
|
-
for(const tab of
|
|
81
|
+
for(const tab of tabs){
|
|
74
82
|
const panel=panelFor(tab);let user=false,timer:ReturnType<typeof setTimeout>|undefined;
|
|
75
83
|
const intent=()=>{user=true;if(timer)clearTimeout(timer);timer=setTimeout(()=>{user=false;},600);};const scrolled=()=>{
|
|
76
84
|
if(!user)return;const saved=memory.snapshot()[tab]??{x:0,y:0};
|
|
@@ -83,7 +91,7 @@ export function bindWorkspaceUI(options:{changed:()=>void;defaultTheme:(theme:Wo
|
|
|
83
91
|
let extent='';const content=new MutationObserver(()=>{const next=`${panel.scrollWidth}:${panel.scrollHeight}:${panel.clientWidth}:${panel.clientHeight}`;if(next!==extent){extent=next;if(tab===active())restore();}});content.observe(panel,{childList:true,subtree:true});
|
|
84
92
|
disposers.push(()=>{if(timer)clearTimeout(timer);for(const type of ['wheel','pointerdown','touchstart','keydown'])panel.removeEventListener(type,intent);panel.removeEventListener('scroll',scrolled);content.disconnect();});
|
|
85
93
|
}
|
|
86
|
-
const observer=new ResizeObserver(schedule);observer.observe(layout);for(const node of [...stack.children])if(node!==stage)observer.observe(node);for(const tab of
|
|
94
|
+
const observer=new ResizeObserver(schedule);observer.observe(layout);for(const node of [...stack.children])if(node!==stage)observer.observe(node);for(const tab of tabs)observer.observe(panelFor(tab));
|
|
87
95
|
const resumed=()=>{if(!document.hidden)store.refresh();};document.addEventListener('visibilitychange',resumed);schedule();
|
|
88
96
|
return {defaults:()=>store.get(),beforeTab:capture,afterTab:restore,scroll:()=>{capture();return JSON.stringify(memory.snapshot());},restoreScroll(value){if(typeof value!=='string')return;memory.replace(JSON.parse(value));restore();},revealInspector(){if(effective.inspectorMode==='drawer'&&!dialogs.inspectorHeight.open){for(const dialog of Object.values(dialogs))if(dialog.open)dialog.close();buttons.inspectorHeight.click();}},refresh:schedule,
|
|
89
97
|
dispose(){if(disposed)return;capture();disposed=true;if(frame)cancelAnimationFrame(frame);observer.disconnect();document.removeEventListener('visibilitychange',resumed);unsubscribe();for(const binding of bindings)binding.dispose();for(const dispose of disposers)dispose();memory.dispose();store.dispose();}
|