screengraft 0.56.1 → 0.58.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/package.json +1 -1
- package/skills/inject-screenshot/SKILL.md +1 -1
- package/ui/index.html +165 -74
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "screengraft",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.58.0",
|
|
4
4
|
"description": "Put a UI screenshot or screen recording onto a photographed device screen with the perspective exactly right \u2014 a homography you confirm by hand, not a generative guess.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"mockup",
|
|
@@ -5,7 +5,7 @@ description: Injects a UI screenshot OR a screen recording onto a photographed d
|
|
|
5
5
|
|
|
6
6
|
# Inject a screenshot onto a photographed device
|
|
7
7
|
|
|
8
|
-
**What ships (v0.
|
|
8
|
+
**What ships (v0.58):** a local browser UI (`scripts/ui.py`) that walks the designer through the whole job — pick the photo and the screen source, which may be an image **or a video** (recent Desktop/Downloads images, drag-drop, browse, path, or a **Figma frame link**), auto-detect the screen as a starting position — and when detection cannot tell which region is a screen, **Point at screen**: one click inside it and the detector uses that point — or, if this photograph has been fitted before, **the fit it was saved with comes back** as the starting position instead of a detection, recognised by the photo's own pixels so a rename or a drag-drop still match — and every save also writes a **portable `.fit.json` beside the mockup** that can be dropped back onto the page later, which is how a fit survives a re-export, another machine, or someone else's hands — then **match the four edges** (drag an edge's middle to slide it, near an end to pivot; corners still draggable) with canvas navigation that follows the usual conventions — **hold ⌘ and scroll to zoom to the pointer, hold space and drag to pan** — and a rectified strip loupe. The fit and the composite sit **side by side and always have** — the result pane re-renders as you drag, which is how a corner gets judged, so it is the layout rather than a mode you can switch off. Then an on-by-default realism pass that colour-matches the source to the photo's light, **Save** (or **Render**, for a video) into the project folder (`--out-dir`), and a **Send to Claude** button that reaches you through the plugin's own MCP server. The UI is a hand port of the project's Figma design file — dark only.
|
|
9
9
|
|
|
10
10
|
The geometry is exact (`warp.py`); the detection is advisory (`detect.py`) and the human corrects it. **When a detection is wrong and you want to know why**, ask for the candidate list: `POST /api/detect {"trace": true}` writes `<session>/candidates.json`, or run `python3 scripts/detect.py --photo P --out-corners /tmp/c.json --trace /tmp/t.json` (add `--click X,Y`). Every candidate quad is in there with its score and whether it was accepted, rejected, never reached, or filtered out by the click — which is what separates "the screen was never proposed" from "it was proposed and something else won".
|
|
11
11
|
|
package/ui/index.html
CHANGED
|
@@ -49,7 +49,11 @@
|
|
|
49
49
|
accent at 85%, not solid — solid is the hover state. */
|
|
50
50
|
/* accent/acc + accent/acc_btn_rest re-picked in Figma 5 Sep:
|
|
51
51
|
#ec4c13 -> #f23b0d. --acc-soft is derived from the accent, so it follows. */
|
|
52
|
-
|
|
52
|
+
/* Primary at REST is the solid accent — re-read from Button 4:14 on
|
|
53
|
+
13 Sep 2026: container/default and container/hover are both #f23b0d, so
|
|
54
|
+
the 85% rest that used to sit under hover is gone. Hover feedback on the
|
|
55
|
+
primary button is therefore the cursor and the press, as designed. */
|
|
56
|
+
--acc:#f23b0d; --acc-btn-rest:#f23b0d; --acc-press:#bb2b00;
|
|
53
57
|
--acc-stroke:#ff6833; --acc-ink:#fbf9f9; --acc-soft:rgba(242,59,13,.14);
|
|
54
58
|
/* Neutral button hover/pressed are explicit surfaces in Figma, not a
|
|
55
59
|
brightness filter on the rest state. Re-picked 11 Sep 2026 against the
|
|
@@ -60,7 +64,31 @@
|
|
|
60
64
|
--btn-press:#1c1c20; --btn-press-edge:#3a3b41;
|
|
61
65
|
/* Accent disabled keeps the label READABLE rather than fading the whole
|
|
62
66
|
button — the Button component in Figma says so explicitly. */
|
|
63
|
-
|
|
67
|
+
/* Re-read 13 Sep 2026: primary/container/disabled is the SOLID press
|
|
68
|
+
colour with the solid stroke and the solid label — the design fades
|
|
69
|
+
nothing here, it recesses. Was three alpha variants. */
|
|
70
|
+
--acc-dis:#bb2b00; --acc-dis-edge:#ff6833; --acc-dis-ink:#fbf9f9;
|
|
71
|
+
/* Strokes are 0.5px throughout the component sets (button, chip, input
|
|
72
|
+
chip, segmented, switch, button group). A half pixel snaps to one device
|
|
73
|
+
pixel on any screen, so it never vanishes; on a Retina Mac it is the
|
|
74
|
+
hairline the design shows. Colour contrast of the stroke is unchanged. */
|
|
75
|
+
--bw:.5px;
|
|
76
|
+
/* radius/xs — the segment button and the segmented thumb. */
|
|
77
|
+
--r-xs:4px;
|
|
78
|
+
/* color/foreground/icon-construction — the 16px icon glyph in a segment
|
|
79
|
+
button; quieter than --ink on purpose. */
|
|
80
|
+
--icon:#d9d9d9;
|
|
81
|
+
/* input-chip/unselected/label — the empty chip's label sits between
|
|
82
|
+
--ink and --mute. input-chip/unselected/container/default shares
|
|
83
|
+
neutral/cool/650 with border/line; it gets its own name here so a line
|
|
84
|
+
colour change cannot silently repaint a surface. */
|
|
85
|
+
--inchip-ink:#c2c3cb; --inchip-empty:#1d1e20;
|
|
86
|
+
/* button-group/default/border — a 20% cool grey, the ONLY translucent
|
|
87
|
+
stroke in the system; it sits over the photograph, so it has to read on
|
|
88
|
+
any tone. The two shadows are the Button Group's and the Status pill's. */
|
|
89
|
+
--group-edge:rgba(194,195,203,.2);
|
|
90
|
+
--group-shadow:0 2px 4px rgba(0,0,0,.03), 0 3px 10px rgba(0,0,0,.08), 0 4px 24px rgba(0,0,0,.25);
|
|
91
|
+
--pill-shadow:0 2px 2px rgba(0,0,0,.03), 0 3px 5px rgba(0,0,0,.08), 0 4px 12px rgba(0,0,0,.25);
|
|
64
92
|
|
|
65
93
|
--warn:#f0c375;
|
|
66
94
|
/* --ok is status/ok, re-picked in the design file (#8fe3c0 -> #4fbf8b).
|
|
@@ -170,12 +198,10 @@
|
|
|
170
198
|
filter over the rest state — a filter shifts hue and cannot be measured
|
|
171
199
|
against a token.
|
|
172
200
|
------------------------------------------------------------------ */
|
|
173
|
-
/*
|
|
174
|
-
|
|
175
|
-
each other in the top bar were not the same shape. Settled in Figma
|
|
176
|
-
11 Sep 2026: Md = 8, Sm = 6. */
|
|
201
|
+
/* Both sizes are radius/sm (6px) — re-read from Button 4:14 on 13 Sep 2026,
|
|
202
|
+
where every variant carries the same radius. Md had been 8 since 11 Sep. */
|
|
177
203
|
button{font:inherit;color:var(--ink);background:var(--raise);
|
|
178
|
-
border:
|
|
204
|
+
border:var(--bw) solid var(--edge);border-radius:var(--r-sm);
|
|
179
205
|
padding:0 var(--s3);height:28px;font-size:13px;cursor:pointer;box-shadow:var(--hi);
|
|
180
206
|
transition:var(--t-all), transform var(--t)}
|
|
181
207
|
button:hover{background:var(--btn-hover);border-color:var(--btn-hover-edge)}
|
|
@@ -198,7 +224,7 @@
|
|
|
198
224
|
dimmest text in the tool. The description wins until that is
|
|
199
225
|
settled in the file. */
|
|
200
226
|
button[disabled]{background:var(--raise-low);border-color:var(--edge-low);
|
|
201
|
-
color:var(--
|
|
227
|
+
color:var(--mute);cursor:default;font-weight:400;
|
|
202
228
|
box-shadow:none;transform:none}
|
|
203
229
|
button[disabled]:hover{background:var(--raise-low);border-color:var(--edge-low)}
|
|
204
230
|
button.sm{height:24px;padding:0 9px;font-size:12px;border-radius:var(--r-sm)}
|
|
@@ -357,14 +383,18 @@
|
|
|
357
383
|
the stepper was 24px against 28px neighbours), so the thumb is 22px here so the track lands on 28px exactly. Everything else
|
|
358
384
|
— padding, gap, border, both radii — is as drawn.
|
|
359
385
|
------------------------------------------------------------------ */
|
|
386
|
+
/* Segmented control 84:177, re-read 13 Sep 2026: the track is
|
|
387
|
+
surface/raise-low (not --sunk) inside a 6px radius with a 0.5px stroke;
|
|
388
|
+
the thumb is 24px tall at radius/xs. Hover lifts the thumb to raise-hi
|
|
389
|
+
and the track's stroke to edge-mid; pressed drops the thumb to raise-mid. */
|
|
360
390
|
.seg{display:inline-flex;align-items:center;gap:2px;padding:2px;
|
|
361
|
-
background:var(--
|
|
362
|
-
border-radius:var(--r-
|
|
391
|
+
background:var(--raise-low);border:var(--bw) solid var(--edge);
|
|
392
|
+
border-radius:var(--r-sm);transition:var(--t-all)}
|
|
363
393
|
.seg:hover{border-color:var(--edge-mid)}
|
|
364
394
|
.seg:active{border-color:var(--edge-mid)}
|
|
365
395
|
/* The track carries the border and the elevation, so a segment is a label
|
|
366
396
|
until it is selected: no border, no shadow, no lift on press. */
|
|
367
|
-
.seg button{height:
|
|
397
|
+
.seg button{height:24px;padding:0 11px;font-size:12px;border-radius:var(--r-xs);
|
|
368
398
|
background:transparent;border:0;box-shadow:none;color:var(--mute);
|
|
369
399
|
font-weight:400;display:inline-flex;flex-direction:column;
|
|
370
400
|
align-items:center;justify-content:center}
|
|
@@ -382,8 +412,8 @@
|
|
|
382
412
|
point of the component: one raised thing, behaving like every other raised
|
|
383
413
|
thing on the page. */
|
|
384
414
|
.seg button.sel{background:var(--raise);color:var(--ink);font-weight:600}
|
|
385
|
-
.seg button.sel:hover{background:var(--raise-
|
|
386
|
-
.seg button.sel:active{background:var(--raise-
|
|
415
|
+
.seg button.sel:hover{background:var(--raise-hi)}
|
|
416
|
+
.seg button.sel:active{background:var(--raise-mid);transform:none}
|
|
387
417
|
/* A toggle in the strip head. On = the raised surface every selected thing
|
|
388
418
|
on this page has (the segmented thumb, the device chip), off = a label. */
|
|
389
419
|
.striphead button[aria-pressed],#dofBoth[aria-pressed]{background:transparent;border-color:transparent;box-shadow:none;color:var(--mute)}
|
|
@@ -395,17 +425,30 @@
|
|
|
395
425
|
NOTE the modifier is `is-empty`, not `empty`: `.empty` is already the
|
|
396
426
|
centred placeholder-paragraph class (padding:24px), and the chip was
|
|
397
427
|
silently inheriting it and rendering 50px tall instead of 32. */
|
|
428
|
+
/* Input chip 159:4117, re-read 13 Sep 2026. Filled = the SELECTED chip's
|
|
429
|
+
surfaces (raise-hi / edge-hi, hover raise-highest, pressed raise-mid): a
|
|
430
|
+
chosen source reads as a selection. Empty = its own recessed, dashed
|
|
431
|
+
surface with a mid-grey label. Active = a file is being dragged OVER the
|
|
432
|
+
chip: accent stroke and label on the empty one, accent fill on the filled. */
|
|
398
433
|
.inputchip{display:inline-flex;align-items:center;gap:var(--s2);max-width:220px;
|
|
399
|
-
height:32px;padding:0 12px 0 6px;border-radius:999px;
|
|
400
|
-
background:var(--raise);border:
|
|
434
|
+
height:32px;padding:0 12px 0 6px;border-radius:999px;color:var(--ink);
|
|
435
|
+
background:var(--raise-hi);border:var(--bw) solid var(--edge-hi);box-shadow:var(--hi)}
|
|
436
|
+
.inputchip:hover{background:var(--raise-highest);border-color:var(--edge-hi)}
|
|
437
|
+
.inputchip:active{background:var(--raise-mid);border-color:var(--edge-hi)}
|
|
438
|
+
.inputchip.over,.inputchip.over:hover{background:var(--acc);border-color:var(--acc-stroke);
|
|
439
|
+
color:var(--acc-ink)}
|
|
401
440
|
.inputchip .sw{width:22px;height:22px;border-radius:999px;flex:none;
|
|
402
441
|
background:var(--sunk);box-shadow:inset 0 0 0 1px rgba(0,0,0,.35)}
|
|
403
442
|
.inputchip .nm{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:12px}
|
|
404
443
|
/* Empty is the SAME box as filled — 32px tall, same padding, same 22px
|
|
405
444
|
avatar. Only the surface, border style and label colour change. It was
|
|
406
445
|
reading oversized because it had no surface to sit in. */
|
|
407
|
-
.inputchip.is-empty{background:var(--
|
|
408
|
-
color:var(--
|
|
446
|
+
.inputchip.is-empty{background:var(--inchip-empty);border-style:dashed;
|
|
447
|
+
border-color:var(--edge);color:var(--inchip-ink);box-shadow:none}
|
|
448
|
+
.inputchip.is-empty:hover{background:var(--float);border-color:var(--edge-mid)}
|
|
449
|
+
.inputchip.is-empty:active{background:var(--raise-low);border-color:var(--edge-mid)}
|
|
450
|
+
.inputchip.is-empty.over,.inputchip.is-empty.over:hover{background:var(--float);
|
|
451
|
+
border-color:var(--acc-stroke);color:var(--acc-stroke)}
|
|
409
452
|
.inputchip.is-empty .sw{box-shadow:inset 0 0 0 1px var(--line)}
|
|
410
453
|
/* The clear control is a SIBLING of the chip in the markup, not a child:
|
|
411
454
|
the chip is one <button>, and a button inside a button is invalid HTML
|
|
@@ -484,29 +527,58 @@
|
|
|
484
527
|
min-width:0 lets the PANE shrink; it does nothing for the auto track
|
|
485
528
|
inside it, which was still sizing to the header's min-content — 798px
|
|
486
529
|
wide in a 398px column. */
|
|
487
|
-
|
|
530
|
+
/* One row: neither pane has a header any more (Figma 7:12 / 7:38, 13 Sep
|
|
531
|
+
2026). Everything that used to live in the header floats over the well as
|
|
532
|
+
a rail, so the picture gets the whole pane and the two wells line up. */
|
|
533
|
+
.pane{min-width:0;min-height:0;display:grid;grid-template-rows:minmax(0,1fr);
|
|
488
534
|
grid-template-columns:minmax(0,1fr);
|
|
489
|
-
position:relative; /* the frame the
|
|
535
|
+
position:relative; /* the frame the rails are anchored to */
|
|
490
536
|
border-right:1px solid var(--line)}
|
|
491
537
|
.panehead{display:flex;align-items:center;gap:var(--s2);padding:var(--s2) var(--s4);background:var(--card);
|
|
492
538
|
box-shadow:inset 0 -1px 0 var(--line);font-size:12px;flex-wrap:wrap;min-height:48px}
|
|
493
|
-
/* The Fit header pads 12px vertically around 28px controls (Figma 7:13),
|
|
494
|
-
which makes it 52px — deliberately taller than the Result header's 48. */
|
|
495
|
-
/* nowrap matches the design, but a long detection message must not be able
|
|
496
|
-
to widen the grid column — that is the v0.3.1 blowout, where an
|
|
497
|
-
unconstrained child forced the fixed rail off-screen. The pill truncates
|
|
498
|
-
instead; min-width:0 is what actually lets it. */
|
|
499
|
-
.fit-head{justify-content:space-between;flex-wrap:nowrap}
|
|
500
|
-
/* Both pane headers are the same height: they sit side by side, and 4px of
|
|
501
|
-
difference reads as a misalignment rather than as a hierarchy. */
|
|
502
539
|
.panehead{padding:var(--s3) var(--s4);min-height:52px}
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
.
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
540
|
+
/* The zoom readout has no place on the icon rail; it is kept for assistive
|
|
541
|
+
tech, announced on change. */
|
|
542
|
+
.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;
|
|
543
|
+
clip:rect(0,0,0,0);white-space:nowrap;border:0}
|
|
544
|
+
|
|
545
|
+
/* ------------------------------------------------------------------
|
|
546
|
+
Rails over the well (Figma 7:12, Button Group 145:372, Segment Button
|
|
547
|
+
138:255). A rail is a Button Group: a 28px-wide column of 28px segment
|
|
548
|
+
buttons on the raised surface, one 0.5px translucent stroke and one
|
|
549
|
+
shadow around the whole group — the stroke is the only translucent one
|
|
550
|
+
in the system, because the group sits over a photograph of any tone.
|
|
551
|
+
Three groups on the left (zoom, fit, corners), three on the bottom, and
|
|
552
|
+
the status pill top-centre; the well underneath keeps every pixel.
|
|
553
|
+
------------------------------------------------------------------ */
|
|
554
|
+
.bgroup{display:inline-flex;flex-direction:column;align-items:stretch;flex:none;
|
|
555
|
+
background:var(--raise);border:var(--bw) solid var(--group-edge);
|
|
556
|
+
border-radius:var(--r-xs);box-shadow:var(--group-shadow);overflow:hidden}
|
|
557
|
+
.segbtn,.segbtn:hover,.segbtn:active{border:0;border-radius:0;box-shadow:none;transform:none}
|
|
558
|
+
.segbtn{height:28px;min-width:28px;padding:0 6px;font-size:12px;color:var(--ink);
|
|
559
|
+
background:var(--raise);display:inline-flex;align-items:center;justify-content:center;gap:4px}
|
|
560
|
+
.segbtn:hover{background:var(--raise-mid)}
|
|
561
|
+
.segbtn:active{background:var(--raise-low)}
|
|
562
|
+
.segbtn.sel,.segbtn[aria-pressed="true"]{background:var(--raise-hi)}
|
|
563
|
+
.segbtn[disabled],.segbtn[disabled]:hover{background:var(--raise-low);color:var(--mute)}
|
|
564
|
+
.segbtn:focus-visible{outline-offset:-2px}
|
|
565
|
+
.segbtn svg{width:16px;height:16px;display:block;flex:none}
|
|
566
|
+
.segbtn svg text{font:600 8px/1 -apple-system,BlinkMacSystemFont,"SF Pro",system-ui,sans-serif;fill:var(--ink)}
|
|
567
|
+
.lrail{position:absolute;left:24px;top:48px;z-index:3;display:flex;flex-direction:column;gap:var(--s2)}
|
|
568
|
+
.lrail .corners{margin-top:var(--s3)} /* 8 + 12 = the design's 20px */
|
|
569
|
+
.brail{position:absolute;left:0;right:0;bottom:12px;z-index:3;display:flex;justify-content:center;
|
|
570
|
+
align-items:center;gap:4px;pointer-events:none;flex-wrap:wrap;padding:0 var(--s4)}
|
|
571
|
+
.brail .bgroup{pointer-events:auto}
|
|
572
|
+
/* The status pill (Status pill 5:7 / 5:10): floats top-centre, its own
|
|
573
|
+
recessed surface and the group stroke, hidden while it has nothing to say.
|
|
574
|
+
Glyph + weight + luminance encoding unchanged (see .status.ok/.warn/.err). */
|
|
575
|
+
/* Keyed on the ids, not a class: the JS rewrites className on both pills. */
|
|
576
|
+
#detSt,#outSt{position:absolute;top:12px;left:50%;transform:translateX(-50%);z-index:3;
|
|
577
|
+
height:22px;padding:0 10px;border-radius:999px;background:var(--inchip-empty);
|
|
578
|
+
border:1px solid var(--group-edge);box-shadow:var(--pill-shadow);
|
|
579
|
+
max-width:calc(100% - 96px);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
|
|
580
|
+
display:inline-flex;line-height:20px}
|
|
581
|
+
#detSt:empty,#outSt:empty{display:none}
|
|
510
582
|
.lbl{color:var(--ink);text-transform:uppercase;letter-spacing:.06em;font-size:11px;font-weight:600}
|
|
511
583
|
.scroller{overflow:auto;background:var(--well);display:grid;
|
|
512
584
|
min-height:0;min-width:0;
|
|
@@ -587,30 +659,30 @@
|
|
|
587
659
|
/* Track 48×20 inside a 48×28 hit area (Figma 17:61): the button keeps the
|
|
588
660
|
full 28px for the pointer, the drawn track is 20px. Was 22px. */
|
|
589
661
|
.sw-track{position:relative;display:block;width:48px;height:20px;border-radius:6px;
|
|
590
|
-
background:var(--bg);border:
|
|
662
|
+
background:var(--bg);border:var(--bw) solid var(--edge);
|
|
591
663
|
transition:var(--t-all), box-shadow var(--t)}
|
|
592
|
-
.sw-handle{position:absolute;top:
|
|
664
|
+
.sw-handle{position:absolute;top:1.5px;bottom:1.5px;left:1.5px;width:24px;border-radius:4px;
|
|
593
665
|
background:var(--raise-hi);
|
|
594
666
|
transition:left var(--t), right var(--t), width var(--t), background-color var(--t), opacity var(--t)}
|
|
595
667
|
.sw-grip{position:absolute;border-radius:1px;background:var(--sunk);
|
|
596
|
-
left:
|
|
668
|
+
left:11.5px;width:4px;top:8.5px;bottom:8.5px;
|
|
597
669
|
transition:left var(--t), width var(--t), top var(--t), bottom var(--t), background-color var(--t)}
|
|
598
670
|
|
|
599
671
|
/* --- OFF ------------------------------------------------------------- */
|
|
600
|
-
button.switch:hover .sw-handle{background:var(--
|
|
672
|
+
button.switch:hover .sw-handle{background:var(--raise-mid)}
|
|
601
673
|
button.switch:active .sw-handle{width:26px;background:var(--raise-hi)}
|
|
602
|
-
button.switch:active .sw-grip{left:
|
|
674
|
+
button.switch:active .sw-grip{left:12.5px}
|
|
603
675
|
|
|
604
676
|
/* --- ON -------------------------------------------------------------- */
|
|
605
677
|
button.switch[aria-checked="true"] .sw-track{background:var(--acc-btn-rest);border-color:var(--acc-stroke)}
|
|
606
|
-
button.switch[aria-checked="true"] .sw-handle{left:auto;right:
|
|
607
|
-
button.switch[aria-checked="true"] .sw-grip{left:
|
|
678
|
+
button.switch[aria-checked="true"] .sw-handle{left:auto;right:1.5px;background:var(--acc-ink)}
|
|
679
|
+
button.switch[aria-checked="true"] .sw-grip{left:32.5px;width:2px;top:5.5px;bottom:5.5px;
|
|
608
680
|
background:var(--acc-btn-rest)}
|
|
609
681
|
button.switch[aria-checked="true"]:hover .sw-track{background:var(--acc);border-color:var(--acc-stroke)}
|
|
610
682
|
button.switch[aria-checked="true"]:hover .sw-grip{background:var(--acc)}
|
|
611
683
|
button.switch[aria-checked="true"]:active .sw-track{background:var(--acc-press);border-color:var(--acc-stroke)}
|
|
612
684
|
button.switch[aria-checked="true"]:active .sw-handle{width:26px;opacity:.8}
|
|
613
|
-
button.switch[aria-checked="true"]:active .sw-grip{left:
|
|
685
|
+
button.switch[aria-checked="true"]:active .sw-grip{left:31.5px;background:var(--acc-press)}
|
|
614
686
|
|
|
615
687
|
/* --- disabled --------------------------------------------------------
|
|
616
688
|
The component uses opacity here, against the project's usual rule. Kept as
|
|
@@ -1006,49 +1078,52 @@
|
|
|
1006
1078
|
100%), with the status pill pushed to the far end by
|
|
1007
1079
|
justify-between. Buttons are Md — the same size as every other
|
|
1008
1080
|
button on screen, which is the point of the change. -->
|
|
1009
|
-
<div class="panehead fit-head">
|
|
1010
|
-
<div class="ph-left">
|
|
1011
|
-
<div class="ph-left-1">
|
|
1012
|
-
<span class="lbl">Fit</span>
|
|
1013
|
-
<span class="stepper"><button id="czOut">−</button><button id="czIn">+</button></span>
|
|
1014
|
-
<span id="czSt" class="zoomval"></span>
|
|
1015
|
-
<button class="info" type="button" aria-label="About canvas navigation"><svg viewBox="0 0 16 16" fill="none" aria-hidden="true"><path fill="currentColor" d="M8.36 1.01C12.06 1.2 15 4.26 15 8l-.01.36C14.8 12.06 11.74 15 8 15l-.36-.01C4.06 14.81 1.19 11.94 1.01 8.36L1 8c0-3.87 3.13-7 7-7l.36.01ZM8 2C4.69 2 2 4.69 2 8c0 3.31 2.69 6 6 6 3.31 0 6-2.69 6-6 0-3.31-2.69-6-6-6Zm.5 8l.5 0v1L7 11v-1h.5V7H7V6h1.5v4ZM7.9 4c.33 0 .6.27.6.6a.6.6 0 1 1-1.21 0c0-.33.28-.6.61-.6Z"/></svg><span class="tip" role="tooltip">Hold ⌘ and scroll to zoom to the pointer — the pixel under the cursor stays under the cursor, which is how you keep a corner in view while you magnify it. Hold space and drag to pan. Scrolling on its own pans too; the Result pane follows either way.</span></button>
|
|
1016
|
-
</div>
|
|
1017
|
-
<div class="ph-left-2">
|
|
1018
|
-
<button id="czFit">Fit</button>
|
|
1019
|
-
<button id="cz100">100%</button>
|
|
1020
|
-
</div>
|
|
1021
|
-
</div>
|
|
1022
|
-
<span class="status" id="detSt"></span>
|
|
1023
|
-
</div>
|
|
1024
1081
|
<div class="scroller" id="scroller">
|
|
1025
1082
|
<canvas id="c" tabindex="-1"></canvas>
|
|
1026
1083
|
</div>
|
|
1084
|
+
<span class="status" id="detSt"></span>
|
|
1085
|
+
<span id="czSt" class="sr-only" aria-live="polite"></span>
|
|
1086
|
+
<!-- Left rail (Figma 137:4025): the zoom group, the fit button, then the
|
|
1087
|
+
corner rail. Icon-only, so every button carries an aria-label. The
|
|
1088
|
+
corner glyphs light the corner they name; the one currently picked
|
|
1089
|
+
on the canvas is also raised (kept in step from draw()). -->
|
|
1090
|
+
<div class="lrail" id="lrail">
|
|
1091
|
+
<div class="bgroup" role="group" aria-label="Zoom">
|
|
1092
|
+
<button class="segbtn" id="czIn" aria-label="Zoom in" title="Zoom in (⌘ + scroll zooms to the pointer)"><svg viewBox="0 0 16 16" aria-hidden="true"><path fill="var(--icon)" d="M8.5 7.5H12.5V8.5H8.5V12.5H7.5V8.5H3.5V7.5H7.5V3.5H8.5V7.5Z"/></svg></button>
|
|
1093
|
+
<button class="segbtn" id="cz100" aria-label="Actual size, 100%" title="Actual size — one photo pixel per screen pixel"><svg viewBox="0 0 16 16" aria-hidden="true"><path fill="var(--mute)" d="M14.1025 1.00488C14.6067 1.05621 15 1.48232 15 2V14L14.9951 14.1025C14.9472 14.573 14.573 14.9472 14.1025 14.9951L14 15H2C1.48232 15 1.05621 14.6067 1.00488 14.1025L1 14V2C1 1.44772 1.44772 1 2 1H14L14.1025 1.00488ZM2 14H14V2H2V14Z"/><text x="8" y="10.8" text-anchor="middle">1x</text></svg></button>
|
|
1094
|
+
<button class="segbtn" id="czOut" aria-label="Zoom out" title="Zoom out"><svg viewBox="0 0 16 16" aria-hidden="true"><path fill="var(--icon)" d="M12.5 8.5H3.5V7.5H12.5V8.5Z"/></svg></button>
|
|
1095
|
+
</div>
|
|
1096
|
+
<div class="bgroup">
|
|
1097
|
+
<button class="segbtn" id="czFit" aria-label="Fit the photo to the pane" title="Fit the whole photo in the pane"><svg viewBox="0 0 16 16" fill="none" aria-hidden="true"><path fill="var(--mute)" d="M14.1025 1.00488C14.6067 1.05621 15 1.48232 15 2V14L14.9951 14.1025C14.9472 14.573 14.573 14.9472 14.1025 14.9951L14 15H2C1.48232 15 1.05621 14.6067 1.00488 14.1025L1 14V2C1 1.44772 1.44772 1 2 1H14L14.1025 1.00488ZM2 14H14V2H2V14Z"/><path stroke="var(--icon)" d="M7 11.5H4.5V9M4.5 11.5L7.5 8.5"/><path stroke="var(--icon)" d="M9 4.5H11.5V7M11.5 4.5L8.5 7.5"/></svg></button>
|
|
1098
|
+
</div>
|
|
1099
|
+
<div class="bgroup corners" role="group" aria-label="Select corner">
|
|
1100
|
+
<button class="segbtn" data-jump="0" aria-label="Top-left corner" title="Jump to the top-left corner"><svg viewBox="0 0 16 16" aria-hidden="true"><path fill="var(--mute)" d="M2 13C2 13.5523 2.44772 14 3 14H6V15H3L2.7959 14.9893C1.85435 14.8938 1.1062 14.1457 1.01074 13.2041L1 13V10H2V13ZM15 13C15 14.0357 14.2128 14.887 13.2041 14.9893L13 15H10V14H13C13.5523 14 14 13.5523 14 13V10H15V13ZM6 2H3C2.44772 2 2 2.44772 2 3V6H1V3C1 1.89543 1.89543 1 3 1H6V2ZM13 1C14.1046 1 15 1.89543 15 3V6H14V3C14 2.44772 13.5523 2 13 2H10V1H13Z"/><path fill="var(--acc-ink)" d="M6 2H3C2.44772 2 2 2.44772 2 3V6H1V3C1 1.89543 1.89543 1 3 1H6V2Z"/></svg></button>
|
|
1101
|
+
<button class="segbtn" data-jump="1" aria-label="Top-right corner" title="Jump to the top-right corner"><svg viewBox="0 0 16 16" aria-hidden="true"><path fill="var(--mute)" d="M2 13C2 13.5523 2.44772 14 3 14H6V15H3L2.7959 14.9893C1.85435 14.8938 1.1062 14.1457 1.01074 13.2041L1 13V10H2V13ZM15 13C15 14.0357 14.2128 14.887 13.2041 14.9893L13 15H10V14H13C13.5523 14 14 13.5523 14 13V10H15V13ZM6 2H3C2.44772 2 2 2.44772 2 3V6H1V3C1 1.89543 1.89543 1 3 1H6V2ZM13 1C14.1046 1 15 1.89543 15 3V6H14V3C14 2.44772 13.5523 2 13 2H10V1H13Z"/><path fill="var(--acc-ink)" d="M10 2H13C13.5523 2 14 2.44772 14 3V6H15V3C15 1.89543 14.1046 1 13 1H10V2Z"/></svg></button>
|
|
1102
|
+
<button class="segbtn" data-jump="3" aria-label="Bottom-left corner" title="Jump to the bottom-left corner"><svg viewBox="0 0 16 16" aria-hidden="true"><path fill="var(--mute)" d="M2 13C2 13.5523 2.44772 14 3 14H6V15H3L2.7959 14.9893C1.85435 14.8938 1.1062 14.1457 1.01074 13.2041L1 13V10H2V13ZM15 13C15 14.0357 14.2128 14.887 13.2041 14.9893L13 15H10V14H13C13.5523 14 14 13.5523 14 13V10H15V13ZM6 2H3C2.44772 2 2 2.44772 2 3V6H1V3C1 1.89543 1.89543 1 3 1H6V2ZM13 1C14.1046 1 15 1.89543 15 3V6H14V3C14 2.44772 13.5523 2 13 2H10V1H13Z"/><path fill="var(--acc-ink)" d="M6 14H3C2.44772 14 2 13.5523 2 13V10H1V13C1 14.1046 1.89543 15 3 15H6V14Z"/></svg></button>
|
|
1103
|
+
<button class="segbtn" data-jump="2" aria-label="Bottom-right corner" title="Jump to the bottom-right corner"><svg viewBox="0 0 16 16" aria-hidden="true"><path fill="var(--mute)" d="M2 13C2 13.5523 2.44772 14 3 14H6V15H3L2.7959 14.9893C1.85435 14.8938 1.1062 14.1457 1.01074 13.2041L1 13V10H2V13ZM15 13C15 14.0357 14.2128 14.887 13.2041 14.9893L13 15H10V14H13C13.5523 14 14 13.5523 14 13V10H15V13ZM6 2H3C2.44772 2 2 2.44772 2 3V6H1V3C1 1.89543 1.89543 1 3 1H6V2ZM13 1C14.1046 1 15 1.89543 15 3V6H14V3C14 2.44772 13.5523 2 13 2H10V1H13Z"/><path fill="var(--acc-ink)" d="M10 14H13C13.5523 14 14 13.5523 14 13V10H15V13C15 14.1046 14.1046 15 13 15H10V14Z"/></svg></button>
|
|
1104
|
+
</div>
|
|
1105
|
+
</div>
|
|
1027
1106
|
<!-- Corner jump. It is a SIBLING of the scroller, not a child: anything
|
|
1028
1107
|
positioned inside a scroll container scrolls with the content, so
|
|
1029
1108
|
when it lived over the canvas it slid out of view the moment you
|
|
1030
1109
|
zoomed in. Anchored to the pane it stays put while the picture moves
|
|
1031
1110
|
underneath, which is what a floating control should do. -->
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
</
|
|
1111
|
+
<!-- Bottom rail (Figma 113:2851): the actions on the FIT as a whole, each
|
|
1112
|
+
its own one-button group, centred. Corner selection moved to the left
|
|
1113
|
+
rail — it is navigation, not an action on the fit. Rotate is not in
|
|
1114
|
+
the design frame and is kept until it is ruled on. -->
|
|
1115
|
+
<div class="brail" id="cvbar">
|
|
1116
|
+
<div class="bgroup"><button class="segbtn" id="redetect">Re-detect</button></div>
|
|
1117
|
+
<div class="bgroup"><button class="segbtn" id="pointat" title="Click once inside the screen and the detector will use that point. The detectors usually do find the screen — they just cannot tell which region IS one, and that is the part you can answer instantly.">Point at screen</button></div>
|
|
1118
|
+
<div class="bgroup"><button class="segbtn" id="rotatequad" title="Turn the screenshot inside the same four edges. The edges stay where they are; only which one is the top changes. A portrait screenshot fits a phone only two ways, so on a phone this is a half turn; a quarter turn where the screen is near-square or the screenshot fits the other way.">Rotate</button></div>
|
|
1119
|
+
<div class="bgroup"><button class="segbtn" id="resetquad" title="Put the four edges back to a rectangle in the middle of the photo. Use this if a corner has ended up off the picture where you cannot grab it.">Reset</button></div>
|
|
1041
1120
|
</div>
|
|
1042
1121
|
</section>
|
|
1043
1122
|
|
|
1044
1123
|
<section class="pane" id="outPane">
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
<span class="sm" id="outSt" style="color:var(--mute)">Press Preview</span>
|
|
1049
|
-
<span class="spacer"></span>
|
|
1050
|
-
<span class="sm" style="color:var(--mute)">shares zoom & pan</span>
|
|
1051
|
-
</div>
|
|
1124
|
+
<!-- No header (Figma 7:38): the well fills the pane so it lines up with
|
|
1125
|
+
the fit well beside it, and the render state floats as a pill. -->
|
|
1126
|
+
<span class="status" id="outSt">Press Preview</span>
|
|
1052
1127
|
<div id="outWrap"><span class="empty" id="outEmpty">The composite appears here and updates as you drag.</span><img id="outImg" alt="" hidden><video id="outVid" hidden loop muted playsinline controls></video><svg id="dofGizmo" hidden aria-label="Depth of field: drag the solid line to place focus, its ends to turn it, the dashed line for strength"></svg><!--
|
|
1053
1128
|
The live layer: the PHOTOGRAPH with the source clip warped onto the
|
|
1054
1129
|
fitted quad by the browser. Not the composite — the composite already
|
|
@@ -1735,6 +1810,13 @@ for (const [n, role] of [[1,'photo'],[2,'screenshot']]){
|
|
|
1735
1810
|
drop.ondragleave = () => drop.classList.remove('over');
|
|
1736
1811
|
drop.ondrop = e => { e.preventDefault(); drop.classList.remove('over'); const f = e.dataTransfer.files[0]; if (f) upload(role, f); };
|
|
1737
1812
|
$('#chip'+n).onclick = () => openPop(n);
|
|
1813
|
+
// The chip itself takes a drop (Input chip's Active state): drag a file
|
|
1814
|
+
// onto "Choose a photo" and it lands in that role without opening the
|
|
1815
|
+
// popover. Same upload path as the popover's drop zone.
|
|
1816
|
+
const chip = $('#chip'+n);
|
|
1817
|
+
chip.ondragover = e => { if ([...(e.dataTransfer?.items || [])].some(i => i.kind === 'file')) { e.preventDefault(); chip.classList.add('over'); } };
|
|
1818
|
+
chip.ondragleave = () => chip.classList.remove('over');
|
|
1819
|
+
chip.ondrop = e => { e.preventDefault(); chip.classList.remove('over'); const f = e.dataTransfer.files[0]; if (f) upload(role, f); };
|
|
1738
1820
|
}
|
|
1739
1821
|
/* One popover mechanic for every popover: anchor it under whatever opened it,
|
|
1740
1822
|
clamped into the viewport. The pickers and the preview both use it. */
|
|
@@ -2289,7 +2371,16 @@ $('#resetquad').onclick = () => {
|
|
|
2289
2371
|
draw(); drawStrip(); autoPreview();
|
|
2290
2372
|
};
|
|
2291
2373
|
|
|
2374
|
+
function syncCornerRail(){
|
|
2375
|
+
const i = pick && pick.kind === 'corner' ? pick.i : -1;
|
|
2376
|
+
document.querySelectorAll('#lrail [data-jump]').forEach(b => {
|
|
2377
|
+
const on = +b.dataset.jump === i;
|
|
2378
|
+
b.classList.toggle('sel', on);
|
|
2379
|
+
b.setAttribute('aria-pressed', on ? 'true' : 'false');
|
|
2380
|
+
});
|
|
2381
|
+
}
|
|
2292
2382
|
function draw(){
|
|
2383
|
+
syncCornerRail();
|
|
2293
2384
|
// The clip follows the quad while it is being dragged: the live layer IS the
|
|
2294
2385
|
// fit, so leaving it on the old corners would make the two panes disagree
|
|
2295
2386
|
// mid-drag — which is the one thing the side-by-side exists to prevent.
|