screengraft 0.59.1 → 0.60.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "screengraft",
3
- "version": "0.59.1",
3
+ "version": "0.60.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.59):** 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.
8
+ **What ships (v0.60):** 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
@@ -362,7 +362,7 @@
362
362
  .stepper button:first-child{border-top-left-radius:var(--r-sm);border-bottom-left-radius:var(--r-sm)}
363
363
  .stepper button:last-child{border-top-right-radius:var(--r-sm);border-bottom-right-radius:var(--r-sm)}
364
364
  /* One border between neighbours rather than two stacked. */
365
- .stepper button + button{margin-left:-1px}
365
+ .stepper button + button{margin-left:calc(-1 * var(--bw))}
366
366
  .stepper button:hover,.stepper button:focus-visible{position:relative;z-index:1}
367
367
  /* The dock's stepper is Sm height but keeps 12px padding (Figma 8:31/8:33). */
368
368
  .dock-step button{padding:0 var(--s3)}
@@ -544,7 +544,7 @@
544
544
 
545
545
  /* ------------------------------------------------------------------
546
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
547
+ 138:255). A rail is a Button Group: a 32px-wide column of 32px segment
548
548
  buttons on the raised surface, one 0.5px translucent stroke and one
549
549
  shadow around the whole group — the stroke is the only translucent one
550
550
  in the system, because the group sits over a photograph of any tone.
@@ -555,8 +555,12 @@
555
555
  background:var(--raise);border:var(--bw) solid var(--group-edge);
556
556
  border-radius:var(--r-xs);box-shadow:var(--group-shadow);overflow:hidden}
557
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);
558
+ /* Segment Button re-read 14 Sep 2026: 32px tall, 32px minimum, 8px side
559
+ padding; a text label sits in its own 6px-padded box, so a text button
560
+ pads 14px. Was 28 / 6. */
561
+ .segbtn{height:32px;min-width:32px;padding:0 8px;font-size:12px;color:var(--ink);
559
562
  background:var(--raise);display:inline-flex;align-items:center;justify-content:center;gap:4px}
563
+ .segbtn.txt{padding:0 14px}
560
564
  .segbtn:hover{background:var(--raise-mid)}
561
565
  .segbtn:active{background:var(--raise-low)}
562
566
  .segbtn.sel,.segbtn[aria-pressed="true"]{background:var(--raise-hi)}
@@ -565,8 +569,8 @@
565
569
  .segbtn svg{width:16px;height:16px;display:block;flex:none}
566
570
  .segbtn svg text{font:600 8px/1 -apple-system,BlinkMacSystemFont,"SF Pro",system-ui,sans-serif;fill:var(--ink)}
567
571
  .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;
572
+ .lrail .corners{margin-top:var(--s6)} /* 8 + 24 = the design's 32px between rails */
573
+ .brail{position:absolute;left:0;right:0;bottom:8px;z-index:3;display:flex;justify-content:center;
570
574
  align-items:center;gap:4px;pointer-events:none;flex-wrap:wrap;padding:0 var(--s4)}
571
575
  .brail .bgroup{pointer-events:auto}
572
576
  /* The status pill (Status pill 5:7 / 5:10): floats top-centre, its own
@@ -1122,10 +1126,10 @@
1122
1126
  rail — it is navigation, not an action on the fit. Rotate is not in
1123
1127
  the design frame and is kept until it is ruled on. -->
1124
1128
  <div class="brail" id="cvbar">
1125
- <div class="bgroup"><button class="segbtn" id="redetect">Re-detect</button></div>
1126
- <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>
1127
- <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>
1128
- <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>
1129
+ <div class="bgroup"><button class="segbtn txt" id="redetect">Re-detect</button></div>
1130
+ <div class="bgroup"><button class="segbtn txt" 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>
1131
+ <div class="bgroup"><button class="segbtn txt" 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>
1132
+ <div class="bgroup"><button class="segbtn txt" 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>
1129
1133
  </div>
1130
1134
  </section>
1131
1135
 
@@ -1279,7 +1283,7 @@
1279
1283
  <footer class="dock" id="dock">
1280
1284
  <div class="striphead">
1281
1285
  <span class="lbl">Edge view</span>
1282
- <span class="stepper dock-step"><button class="sm" id="stripOut">&minus;</button><button class="sm" id="stripIn">+</button></span>
1286
+ <span class="stepper dock-step"><button id="stripOut" aria-label="Zoom the edge view out">&minus;</button><button id="stripIn" aria-label="Zoom the edge view in">+</button></span>
1283
1287
  <span class="sm" id="stripZ" style="color:var(--mute);font-variant-numeric:tabular-nums"></span>
1284
1288
  <button class="sm" id="stripHC" aria-pressed="false" title="Stretch the strip's own tonal range to full contrast. For a dark screen on a dark frame, where the boundary is a few levels apart. The strip only — the composite is untouched.">Contrast</button>
1285
1289
  <span class="spacer"></span>
@@ -2885,7 +2889,7 @@ function paintGizmo(){
2885
2889
  // it, each keeping its distance, the way a tilt-shift band moves as one thing.
2886
2890
  // It used to stop at the far line (start <= end - 0.05), so with the dashed
2887
2891
  // line sitting on the phone's near edge the sharp line could not be placed
2888
- // over the phone at all: it hit the dashed line and stayed (Darek's recording,
2892
+ // over the phone at all: it hit the dashed line and stayed (a screen recording,
2889
2893
  // 14 Sep 2026). The dashed lines are what change the ramp's length; the sharp
2890
2894
  // line only ever changes where the ramp is. Reach: -0.5 .. 0.95 along the
2891
2895
  // screen, the same distance past the glass the far line has -- on a steep,