screengraft 0.64.1 → 0.65.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 +195 -57
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "screengraft",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.65.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.65):** 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** (the images you have used before, 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
|
@@ -9,10 +9,12 @@
|
|
|
9
9
|
SURFACES STEP: well (recessed) < bg < sunk < card < raise < float. The
|
|
10
10
|
previous build used one tone for the top bar, rail, dock, popovers AND
|
|
11
11
|
buttons, so nothing read as a control sitting on a panel — it read as
|
|
12
|
-
regions of one sheet divided by lines. Depth
|
|
13
|
-
|
|
14
|
-
a raised control from a div
|
|
15
|
-
|
|
12
|
+
regions of one sheet divided by lines. Depth comes from tone steps alone
|
|
13
|
+
now; the 1px top inner highlight (--hi) that visual-language.md documents
|
|
14
|
+
as load-bearing for reading a raised control apart from a div was
|
|
15
|
+
removed app-wide by request, 16 Sep 2026 — a deliberate departure
|
|
16
|
+
from that doc, not a regression. Shadow is reserved for things that
|
|
17
|
+
genuinely float.
|
|
16
18
|
Status colours keep three distinct LUMINANCE steps, not three hues —
|
|
17
19
|
measured, --ok and --warn once differed by 1.01:1, invisible in greyscale
|
|
18
20
|
and to red-green colour blindness. --line divides; --edge bounds anything
|
|
@@ -57,6 +59,10 @@
|
|
|
57
59
|
primary button is therefore the cursor and the press, as designed. */
|
|
58
60
|
--acc:#f23b0d; --acc-btn-rest:#f23b0d; --acc-press:#bb2b00;
|
|
59
61
|
--acc-stroke:#ff6833; --acc-ink:#fbf9f9; --acc-soft:rgba(242,59,13,.14);
|
|
62
|
+
/* slider/handle/ring/focus, read as a solid at 40% over the card in
|
|
63
|
+
Figma's Focus state (216:470) -- rgba(255,104,51,.4) is --acc-stroke's
|
|
64
|
+
own channels, not a separate pick. */
|
|
65
|
+
--acc-glow:rgba(255,104,51,.4);
|
|
60
66
|
/* Neutral button hover/pressed are explicit surfaces in Figma, not a
|
|
61
67
|
brightness filter on the rest state. Re-picked 11 Sep 2026 against the
|
|
62
68
|
polished Button matrix (4:14): hover's border is border/edge-mid, and
|
|
@@ -107,6 +113,13 @@
|
|
|
107
113
|
--group-edge-lift:rgba(255,255,255,.4);
|
|
108
114
|
/* The Button Group's stroke weight (0.75, set 14 Sep); the pills keep 1px. */
|
|
109
115
|
--group-bw:.75px;
|
|
116
|
+
/* Segment Button 138:255, re-read 16 Sep 2026: a horizontal group now
|
|
117
|
+
carries its own divider between segments (button/secondary/label/default
|
|
118
|
+
at 10% -- exactly --ink at 10%), a real element in the file, on by
|
|
119
|
+
default and turned off per-instance where it isn't wanted (the last
|
|
120
|
+
segment, mainly -- there's nothing after it to divide from). Vertical
|
|
121
|
+
groups still have no divider variant in the file; not added here. */
|
|
122
|
+
--seg-divider:rgba(233,236,241,.1);
|
|
110
123
|
--group-shadow:0 2px 4px rgba(0,0,0,.08), 0 3px 10px rgba(0,0,0,.12), 0 4px 24px rgba(0,0,0,.25);
|
|
111
124
|
--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);
|
|
112
125
|
|
|
@@ -136,7 +149,11 @@
|
|
|
136
149
|
Don't "fix" either by moving --ok back; that reopens the pair that counts. */
|
|
137
150
|
--ok:#4fbf8b; --err:#e5534b;
|
|
138
151
|
|
|
139
|
-
--hi
|
|
152
|
+
/* --hi (inset 0 1px 0 rgba(255,255,255,.06), the top inner highlight on
|
|
153
|
+
every raised control) removed by request, 16 Sep 2026 -- was
|
|
154
|
+
documented in visual-language.md as deliberate ('most of the distance
|
|
155
|
+
between div and control'). Left here as the reason it is gone, not
|
|
156
|
+
just that it is. */
|
|
140
157
|
--shadow:0 16px 40px rgba(0,0,0,.55), 0 3px 10px rgba(0,0,0,.4);
|
|
141
158
|
--r-sm:6px; --r-md:8px; --r-lg:12px;
|
|
142
159
|
--s2:8px; --s3:12px; --s4:16px; --s6:24px; --s9:48px;
|
|
@@ -222,7 +239,7 @@
|
|
|
222
239
|
where every variant carries the same radius. Md had been 8 since 11 Sep. */
|
|
223
240
|
button{font:inherit;color:var(--ink);background:var(--raise);
|
|
224
241
|
border:var(--bw) solid var(--edge);border-radius:var(--r-sm);
|
|
225
|
-
padding:0 var(--s3);height:28px;font-size:13px;cursor:pointer;
|
|
242
|
+
padding:0 var(--s3);height:28px;font-size:13px;cursor:pointer;
|
|
226
243
|
transition:var(--t-all), transform var(--t)}
|
|
227
244
|
button:hover{background:var(--btn-hover);border-color:var(--btn-hover-edge)}
|
|
228
245
|
button:active{background:var(--btn-press);border-color:var(--btn-press-edge);
|
|
@@ -260,7 +277,7 @@
|
|
|
260
277
|
as an ordinary neutral control. */
|
|
261
278
|
button.accent:not(:disabled):not(.primary){
|
|
262
279
|
background:var(--raise); color:var(--ink); border-color:var(--edge);
|
|
263
|
-
font-weight:400
|
|
280
|
+
font-weight:400}
|
|
264
281
|
/* Radius is deliberately NOT restated in these three rules any more. It is the
|
|
265
282
|
base button's, so .sm keeps winning on size — an accent .sm button used to
|
|
266
283
|
be handed --r-md by specificity alone. */
|
|
@@ -331,7 +348,7 @@
|
|
|
331
348
|
for the next action. The old accent ring is gone for that reason.
|
|
332
349
|
------------------------------------------------------------------ */
|
|
333
350
|
.chip{border-radius:999px;padding:0 var(--s3);height:28px;font-size:12px;
|
|
334
|
-
background:var(--raise);border-color:var(--edge)
|
|
351
|
+
background:var(--raise);border-color:var(--edge)}
|
|
335
352
|
/* Chip state matrix, ported from the Chip component set. Every state now
|
|
336
353
|
steps in the direction it should: hover UP from rest, pressed DOWN from it,
|
|
337
354
|
and selected-hover up again from selected-rest. Before the new tokens
|
|
@@ -340,7 +357,7 @@
|
|
|
340
357
|
.chip:hover{background:var(--raise-mid);border-color:var(--edge-mid)}
|
|
341
358
|
.chip:active{background:var(--raise);border-color:var(--edge-mid)}
|
|
342
359
|
.chip.on{background:var(--raise-hi);border-color:var(--edge-hi);
|
|
343
|
-
color:var(--ink);font-weight:600
|
|
360
|
+
color:var(--ink);font-weight:600}
|
|
344
361
|
.chip.on:hover{background:var(--raise-highest);border-color:var(--edge-hi)}
|
|
345
362
|
.chip.on:active{background:var(--raise-mid);border-color:var(--edge-hi)}
|
|
346
363
|
.chip[disabled]{opacity:.5;color:var(--mute);background:var(--raise);border-color:var(--edge)}
|
|
@@ -452,7 +469,7 @@
|
|
|
452
469
|
chip: accent stroke and label on the empty one, accent fill on the filled. */
|
|
453
470
|
.inputchip{display:inline-flex;align-items:center;gap:var(--s2);max-width:220px;
|
|
454
471
|
height:32px;padding:0 12px 0 6px;border-radius:999px;color:var(--ink);
|
|
455
|
-
background:var(--raise-hi);border:var(--bw) solid var(--edge-hi)
|
|
472
|
+
background:var(--raise-hi);border:var(--bw) solid var(--edge-hi)}
|
|
456
473
|
.inputchip:hover{background:var(--raise-highest);border-color:var(--edge-hi)}
|
|
457
474
|
.inputchip:active{background:var(--raise-mid);border-color:var(--edge-hi)}
|
|
458
475
|
.inputchip.over,.inputchip.over:hover{background:var(--acc);border-color:var(--acc-stroke);
|
|
@@ -616,19 +633,56 @@
|
|
|
616
633
|
inset:calc(-1 * var(--group-bw));border:var(--group-bw) solid transparent;
|
|
617
634
|
border-radius:calc(var(--r-sm) + var(--group-bw))}
|
|
618
635
|
.noblend .pillwrap > .ring.lift{mix-blend-mode:normal;border-color:transparent}
|
|
619
|
-
|
|
636
|
+
/* flex-wrap:nowrap and align-items:stretch are declared here, not just on
|
|
637
|
+
.bgroup, because a generic .row utility (line ~908, unrelated -- it wraps
|
|
638
|
+
chip lists in the sidebar) happens to share this class name and sets
|
|
639
|
+
flex-wrap:wrap/align-items:center at the same specificity as .bgroup's
|
|
640
|
+
own base rule. .bgroup.row's two classes outrank the generic one-class
|
|
641
|
+
.row, so declaring them here wins outright rather than depending on
|
|
642
|
+
which rule the cascade happens to read last. Latent since the class was
|
|
643
|
+
first reused for orientation (11 Sep) -- every existing horizontal group
|
|
644
|
+
happened to fit its container without wrapping, so nothing ever forced
|
|
645
|
+
it to matter until the video bar's Button Group (16 Sep, 7:38) hit a
|
|
646
|
+
narrower parent and visibly split into two rows. */
|
|
647
|
+
/* gap:0 for the same reason as the two properties above: the generic .row
|
|
648
|
+
utility sets gap:8px (line ~908), and .bgroup never declared its own, so
|
|
649
|
+
it leaked in here too -- found 16 Sep when Play and Preview turned out to
|
|
650
|
+
have an 8px gap between them that the file doesn't show (they're meant
|
|
651
|
+
to touch; the divider is the only separation). */
|
|
652
|
+
.bgroup.row{flex-direction:row;flex-wrap:nowrap;align-items:stretch;gap:0}
|
|
620
653
|
.bgroup > .segbtn:first-of-type{border-top-left-radius:var(--r-sm);border-top-right-radius:var(--r-sm)}
|
|
621
654
|
.bgroup > .segbtn:last-of-type{border-bottom-left-radius:var(--r-sm);border-bottom-right-radius:var(--r-sm)}
|
|
622
655
|
.bgroup.row > .segbtn:first-of-type{border-radius:var(--r-sm) 0 0 var(--r-sm)}
|
|
623
656
|
.bgroup.row > .segbtn:last-of-type{border-radius:0 var(--r-sm) var(--r-sm) 0}
|
|
624
657
|
.bgroup > .segbtn:only-of-type{border-radius:var(--r-sm)}
|
|
625
658
|
.segbtn,.segbtn:hover,.segbtn:active{border:0;box-shadow:none;transform:none}
|
|
626
|
-
.segbtn{border-radius:0}
|
|
659
|
+
.segbtn{border-radius:0;position:relative}
|
|
660
|
+
/* The divider is an absolutely-positioned 1px slice, not a border -- a
|
|
661
|
+
border would add a device pixel to the segment's own measured width
|
|
662
|
+
(this file keeps those to the pixel against the Figma frame). Only
|
|
663
|
+
.row (horizontal) groups get it, matching the file: Vertical's variants
|
|
664
|
+
carry no divider prop at all. :last-of-type is the "off where needed" --
|
|
665
|
+
the last segment has nothing to its right to divide from. */
|
|
666
|
+
/* Height corrected 16 Sep: the file's own Divider node (226:5252) measures
|
|
667
|
+
1x20 at y=6 inside a 32-tall button -- a 6px inset top and bottom, not
|
|
668
|
+
the button's full height. First pass used top:0/bottom:0 (32px) without
|
|
669
|
+
checking the node's own geometry. */
|
|
670
|
+
.bgroup.row > .segbtn:not(:last-of-type)::after{content:"";position:absolute;
|
|
671
|
+
top:6px;bottom:6px;right:0;width:1px;background:var(--seg-divider);pointer-events:none}
|
|
627
672
|
/* Segment Button re-read 14 Sep 2026: 32px tall, 32px minimum, 8px side
|
|
628
673
|
padding; a text label sits in its own 6px-padded box, so a text button
|
|
629
674
|
pads 14px. Was 28 / 6. */
|
|
675
|
+
/* flex-shrink:0 -- min-width:32px above is a floor for an ICON button's
|
|
676
|
+
square, not a licence to crush a text button's label below readable
|
|
677
|
+
width. A .segbtn's min-width:auto default (content-based) would already
|
|
678
|
+
have protected text buttons in a flex row; setting an explicit min-width
|
|
679
|
+
removes that protection, so it has to come back explicitly. Surfaced by
|
|
680
|
+
the video bar's Button Group (16 Sep, 7:38): "Preview" shrank to a single
|
|
681
|
+
"P" in a narrow pane before this. The slider's own input keeps its
|
|
682
|
+
min-width:140 floor and is what gives way first (see #vframe below). */
|
|
630
683
|
.segbtn{height:32px;min-width:32px;padding:0 8px;font-size:12px;color:var(--ink);
|
|
631
|
-
background:var(--raise);display:inline-flex;align-items:center;justify-content:center;gap:4px
|
|
684
|
+
background:var(--raise);display:inline-flex;align-items:center;justify-content:center;gap:4px;
|
|
685
|
+
flex-shrink:0}
|
|
632
686
|
.segbtn.txt{padding:0 14px}
|
|
633
687
|
.segbtn:hover{background:var(--raise-mid)}
|
|
634
688
|
.segbtn:active{background:var(--raise-low)}
|
|
@@ -688,21 +742,56 @@
|
|
|
688
742
|
keeps it where the eye expects while the picture moves under it. */
|
|
689
743
|
/* max-width so a narrow pane (compare view on a small window) can never
|
|
690
744
|
push the bar outside the frame it is anchored to. */
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
745
|
+
/* Positioning only -- the look (background, the four-blend-ring stroke,
|
|
746
|
+
radius, shadow) now comes from the .bgroup.row inside it, same split as
|
|
747
|
+
.brail/.bgroup on the fit pane. Centred with left:0/right:0 + justify-
|
|
748
|
+
content:center, NOT left:50%+transform: a transform (like the z-index
|
|
749
|
+
this replaced) opens its own stacking context, and mix-blend-mode cannot
|
|
750
|
+
cross a stacking-context boundary -- the group's four rings would blend
|
|
751
|
+
against .cvbar's own (empty) background instead of the video/photo
|
|
752
|
+
behind it. Exactly the gotcha .brail was already written to avoid;
|
|
753
|
+
.cvbar just hadn't been, since it predates the group being inside it.
|
|
754
|
+
pointer-events follows .brail's pattern for the same reason: the bar
|
|
755
|
+
now spans the full width, so clicks on the canvas either side of the
|
|
756
|
+
group must still pass through. */
|
|
757
|
+
/* overflow-x:auto is gone -- CSS forces the OTHER axis to compute as
|
|
758
|
+
'auto' too whenever one of overflow-x/overflow-y is non-visible (you
|
|
759
|
+
cannot actually get overflow-x:auto with a truly visible overflow-y),
|
|
760
|
+
so it was clipping .bgroup's own drop shadow and the top/bottom of its
|
|
761
|
+
blend rings to exactly the bar's 32px height -- invisible instead of a
|
|
762
|
+
shadow, and the ring only partly there. Found by comparing this rail to
|
|
763
|
+
.brail (fit pane), which has never set any overflow and shows both
|
|
764
|
+
fully. .brail also never needed to scroll: it doesn't set overflow at
|
|
765
|
+
all and simply lets its content overflow visibly, so a narrow pane's
|
|
766
|
+
residual clipping (rare -- a normal window is comfortably wider than
|
|
767
|
+
either rail) now happens at #outWrap's own overflow:auto instead of a
|
|
768
|
+
dedicated scrollbar on the bar itself. Trading a rare, already-degraded
|
|
769
|
+
edge case for a shadow and stroke that are always correct. */
|
|
770
|
+
.cvbar{position:absolute;left:0;right:0;bottom:16px;display:flex;justify-content:center;
|
|
771
|
+
padding:0 var(--s4);pointer-events:none;white-space:nowrap}
|
|
772
|
+
.cvbar .bgroup{pointer-events:auto}
|
|
773
|
+
/* The slider cell: label, track, frame counter, in the Segment Button's own
|
|
774
|
+
32px height and 12px side padding (227:5326). Same background as the
|
|
775
|
+
group it sits in, so it needs no radius of its own to look seamless at
|
|
776
|
+
rest -- given one anyway, for the corner .bgroup itself doesn't clip. */
|
|
777
|
+
/* Right padding 8px, not the symmetric 12 the Figma frame's own Slider
|
|
778
|
+
wrapper carries: with .bgroup.row's gap now 0, the space from the frame
|
|
779
|
+
count to the play icon is this padding PLUS .segbtn's own 8px left
|
|
780
|
+
padding, and 12+8=20 read too wide against the group's own -- 8+8=16
|
|
781
|
+
matches --s4 and the file's row of Play/Preview. Left padding (before
|
|
782
|
+
"Fit on frame") is unaffected. */
|
|
783
|
+
.cvslider{display:flex;align-items:center;gap:var(--s2);height:32px;box-sizing:border-box;
|
|
784
|
+
padding:0 8px 0 12px;background:var(--raise);border-radius:var(--r-sm) 0 0 var(--r-sm)}
|
|
785
|
+
/* .bgroup.row > .segbtn:first-of-type rounds the first BUTTON's left
|
|
786
|
+
corners -- wrong here, since the slider cell (a <div>, not a .segbtn) is
|
|
787
|
+
what actually sits leftmost. Cancelled by ID specificity. */
|
|
788
|
+
#vidbar .bgroup > .segbtn:first-of-type{border-radius:0}
|
|
699
789
|
/* The scrubber needs a floor, or the bar "fits" by crushing it. Moving the
|
|
700
790
|
format control out stopped the bar overflowing at 742px — and the space it
|
|
701
791
|
freed went to the other controls while the slider collapsed to FOUR pixels,
|
|
702
|
-
measured. The bar may scroll (
|
|
792
|
+
measured. The bar may scroll (.cvbar keeps overflow-x:auto); the one
|
|
703
793
|
control that is useless when small must not be the one that gives way. */
|
|
704
794
|
#vframe{min-width:140px;flex:1 1 180px}
|
|
705
|
-
.cvbar{white-space:nowrap}
|
|
706
795
|
/* `hidden` is a UA display:none, which any explicit display beats — and
|
|
707
796
|
.cvbar sets display:flex. Without this the bar stays visible while the
|
|
708
797
|
photo is chosen but the screenshot is not, offering corners to fit
|
|
@@ -863,15 +952,9 @@
|
|
|
863
952
|
reason a true-black UI stops reading as a hole. It is NOT the same maths as
|
|
864
953
|
compose()'s emissive, and the page says so. */
|
|
865
954
|
#liveVid.emissive{mix-blend-mode:screen}
|
|
866
|
-
/*
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
/* `flex:none` and a min-width, not just a width: the bar is a nowrap flex row
|
|
870
|
-
that overflows at narrow widths, and a flex item with a width will happily
|
|
871
|
-
shrink to its content. Measured at 742px before this: the button was 13px
|
|
872
|
-
wide — the glyph and its border — which is a target nobody can hit. */
|
|
873
|
-
.sm.icon{display:inline-flex;align-items:center;justify-content:center;
|
|
874
|
-
flex:none;width:28px;min-width:28px;padding:0}
|
|
955
|
+
/* .sm.icon (the square icon-only small button) is gone -- #playlive is a
|
|
956
|
+
.segbtn now, part of the video bar's Button Group (7:38, 16 Sep), and
|
|
957
|
+
nothing else used the class. */
|
|
875
958
|
.empty{color:var(--mute);font-size:12px;padding:24px;text-align:center;align-self:center;justify-self:center;max-width:320px}
|
|
876
959
|
|
|
877
960
|
.rail{min-width:0;overflow:auto;padding:0;display:grid;align-content:start;background:var(--card)}
|
|
@@ -936,7 +1019,7 @@
|
|
|
936
1019
|
.tip{position:fixed;width:264px;max-width:calc(100vw - 32px);
|
|
937
1020
|
padding:8px 10px;border-radius:var(--r-md);
|
|
938
1021
|
background:var(--float);border:1px solid var(--edge);
|
|
939
|
-
box-shadow:var(--shadow)
|
|
1022
|
+
box-shadow:var(--shadow);
|
|
940
1023
|
color:var(--ink);font-size:12px;font-weight:400;line-height:1.45;
|
|
941
1024
|
letter-spacing:normal;text-transform:none;text-align:left;white-space:normal;
|
|
942
1025
|
z-index:70;opacity:0;visibility:hidden;pointer-events:none;
|
|
@@ -975,15 +1058,61 @@
|
|
|
975
1058
|
land, since the thumb travels inside the track's content box. */
|
|
976
1059
|
input[type=range]{-webkit-appearance:none;appearance:none;width:100%;height:20px;
|
|
977
1060
|
margin:0;padding-inline:2px;background:transparent;box-sizing:border-box}
|
|
1061
|
+
/* Ported from Figma 216:470 (16 Sep). Rail is surface/float, not --sunk --
|
|
1062
|
+
the two read identically at a glance but are different tokens (the old
|
|
1063
|
+
port had borrowed --sunk). Thumb is a squircle-radius square (r 3px),
|
|
1064
|
+
not a pill -- the one visible shape change from the previous slider. */
|
|
978
1065
|
input[type=range]::-webkit-slider-runnable-track{height:4px;border-radius:999px;transition:var(--t-all);
|
|
979
|
-
background:linear-gradient(to right, var(--acc) 0 calc(var(--p,0) * 100%), var(--
|
|
1066
|
+
background:linear-gradient(to right, var(--acc) 0 calc(var(--p,0) * 100%), var(--float) calc(var(--p,0) * 100%) 100%);
|
|
980
1067
|
box-shadow:inset 0 0 0 1px var(--line)}
|
|
1068
|
+
/* slider/track/pressed is a darker accent (--acc-press), not the rest
|
|
1069
|
+
colour dimmed -- the fill itself recedes while the thumb comes forward. */
|
|
1070
|
+
input[type=range]:active::-webkit-slider-runnable-track{
|
|
1071
|
+
background:linear-gradient(to right, var(--acc-press) 0 calc(var(--p,0) * 100%), var(--float) calc(var(--p,0) * 100%) 100%)}
|
|
1072
|
+
/* All five states in the file carry a crisp 1px --card ring around the
|
|
1073
|
+
handle, sampled at (22,22,24) on every one of them (default, disabled,
|
|
1074
|
+
hover, pressed) -- not just where the glow needed it. It is the casing
|
|
1075
|
+
that separates the handle from whatever is under the rail, same idea as
|
|
1076
|
+
the corner-loupe / quad-overlay casing elsewhere in this file. State
|
|
1077
|
+
changes use their own 150ms transition rather than --t-all's 180ms --
|
|
1078
|
+
the handle is dragged continuously, so it reads better a touch snappier
|
|
1079
|
+
than a click target does. */
|
|
981
1080
|
input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;width:14px;height:14px;
|
|
982
|
-
margin-top:-5px;border-radius:
|
|
983
|
-
box-shadow:
|
|
984
|
-
|
|
1081
|
+
margin-top:-5px;border-radius:3px;background:var(--raise);border:1px solid var(--edge-hi);
|
|
1082
|
+
box-shadow:0 1px 3px rgba(0,0,0,.4), 0 0 0 1px var(--card);
|
|
1083
|
+
transition:background-color .15s cubic-bezier(.2,0,0,1), border-color .15s cubic-bezier(.2,0,0,1),
|
|
1084
|
+
box-shadow .15s cubic-bezier(.2,0,0,1)}
|
|
1085
|
+
/* Hover/Pressed in the file are rasterised because the ring carries a soft
|
|
1086
|
+
outer glow (blur), which Figma exports as an image rather than a
|
|
1087
|
+
translatable box-shadow. The glow's own blur radius is judged against
|
|
1088
|
+
the two PNGs' bounding boxes (44px/52px tall on a 26px track --
|
|
1089
|
+
symmetric ~9px/~13px halos) rather than measured to the pixel. */
|
|
1090
|
+
input[type=range]:hover::-webkit-slider-thumb{background:var(--raise-mid);border-color:var(--acc-stroke);
|
|
1091
|
+
box-shadow:0 1px 3px rgba(0,0,0,.4), 0 0 0 1px var(--card), 0 0 6px 1px var(--acc-glow)}
|
|
1092
|
+
input[type=range]:active::-webkit-slider-thumb{background:var(--acc);border-color:var(--acc-stroke);
|
|
1093
|
+
box-shadow:0 1px 3px rgba(0,0,0,.4), 0 0 0 1px var(--card), 0 0 9px 2px rgba(255,104,51,.6)}
|
|
1094
|
+
/* Focus keeps the default handle colours and adds a second, wider ring
|
|
1095
|
+
(the same --card gap, at 1.5px so it clears the thicker glow) plus the
|
|
1096
|
+
accent glow, same language as the switch's focus ring (line ~792) --
|
|
1097
|
+
not a browser outline. */
|
|
985
1098
|
input[type=range]:focus-visible{outline:none}
|
|
986
|
-
input[type=range]:focus-visible::-webkit-slider-thumb{
|
|
1099
|
+
input[type=range]:focus-visible::-webkit-slider-thumb{
|
|
1100
|
+
box-shadow:0 1px 3px rgba(0,0,0,.4), 0 0 0 1.5px var(--card), 0 0 0 3px var(--acc-glow)}
|
|
1101
|
+
/* Disabled reads as the handle's own colours at roughly 50% opacity over
|
|
1102
|
+
--card in the file (sampled fill ~(33,33,36), which is --raise blended
|
|
1103
|
+
~52% over --card, and the track's ~(105,33,12) is --acc-dis at the same
|
|
1104
|
+
~50%) -- Figma's own Disabled variants are usually built as a group
|
|
1105
|
+
opacity rather than a recolour. Kept as solid tokens instead: the rest
|
|
1106
|
+
of this system deliberately does NOT fade disabled controls (see the
|
|
1107
|
+
accent-disabled note above --acc-dis) so it can stay readable, and a
|
|
1108
|
+
lone opacity-faded slider would be the one control that fades. The ring
|
|
1109
|
+
stays -- it is the one thing every state agrees on. */
|
|
1110
|
+
input[type=range][disabled]{cursor:not-allowed}
|
|
1111
|
+
input[type=range][disabled]::-webkit-slider-runnable-track{
|
|
1112
|
+
background:linear-gradient(to right, var(--acc-dis) 0 calc(var(--p,0) * 100%), var(--raise-low) calc(var(--p,0) * 100%) 100%);
|
|
1113
|
+
box-shadow:inset 0 0 0 1px var(--line)}
|
|
1114
|
+
input[type=range][disabled]::-webkit-slider-thumb{background:var(--raise);border-color:var(--edge-low);
|
|
1115
|
+
box-shadow:0 0 0 1px var(--card)}
|
|
987
1116
|
/* Captions are text/faint in Figma. The port had quietly used --mute
|
|
988
1117
|
instead; now they follow the token as designed. At the brightened
|
|
989
1118
|
#8f939ab2 that measures 3.48:1 on card — up from 2.42:1, still under the
|
|
@@ -1034,7 +1163,7 @@
|
|
|
1034
1163
|
background:var(--well);box-shadow:inset 0 0 0 1px var(--line), inset 0 1px 3px rgba(0,0,0,.5)}
|
|
1035
1164
|
/* Floating mode: the loupe follows the edge instead of taking a permanent band. */
|
|
1036
1165
|
.floatloupe{position:fixed;z-index:40;display:none;padding:8px;border-radius:var(--r-lg);
|
|
1037
|
-
background:var(--float);border:1px solid var(--edge);box-shadow:var(--shadow)
|
|
1166
|
+
background:var(--float);border:1px solid var(--edge);box-shadow:var(--shadow)}
|
|
1038
1167
|
.floatloupe.on{display:block}
|
|
1039
1168
|
.floatloupe canvas{display:block;width:340px;height:104px;border-radius:var(--r-sm);
|
|
1040
1169
|
background:var(--well);box-shadow:inset 0 0 0 1px var(--line)}
|
|
@@ -1063,7 +1192,7 @@
|
|
|
1063
1192
|
.toast{position:absolute;left:0;top:0;width:100%;box-sizing:border-box;
|
|
1064
1193
|
display:flex;align-items:flex-start;gap:8px;padding:10px 12px;
|
|
1065
1194
|
border-radius:var(--r-md);background:var(--float);
|
|
1066
|
-
border:1px solid var(--edge);box-shadow:var(--shadow)
|
|
1195
|
+
border:1px solid var(--edge);box-shadow:var(--shadow);
|
|
1067
1196
|
font-size:13px;line-height:1.45;color:var(--ink);
|
|
1068
1197
|
pointer-events:auto;touch-action:none;transform-origin:50% 0;
|
|
1069
1198
|
will-change:transform,opacity;
|
|
@@ -1127,7 +1256,7 @@
|
|
|
1127
1256
|
.pop{position:fixed;z-index:51;width:640px;max-width:calc(100vw - 24px);
|
|
1128
1257
|
max-height:78vh;overflow:auto;background:var(--float-glass);border:var(--bw) solid var(--edge);
|
|
1129
1258
|
backdrop-filter:blur(5px);-webkit-backdrop-filter:blur(5px);
|
|
1130
|
-
border-radius:var(--r-lg);box-shadow:var(--shadow)
|
|
1259
|
+
border-radius:var(--r-lg);box-shadow:var(--shadow);padding:16px;
|
|
1131
1260
|
opacity:0;visibility:hidden;transform:translateY(6px) scale(.985);
|
|
1132
1261
|
transform-origin:50% 0;
|
|
1133
1262
|
transition:opacity var(--t-pop), transform var(--t-pop),
|
|
@@ -1291,28 +1420,37 @@
|
|
|
1291
1420
|
unchanged — you match the edges on one frame and every frame gets the
|
|
1292
1421
|
same geometry, because the photo does not move. -->
|
|
1293
1422
|
<div class="cvbar" id="vidbar" hidden>
|
|
1294
|
-
<!--
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
<
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1423
|
+
<!-- Ported from Figma 7:38 (16 Sep): the bar is now one Button Group
|
|
1424
|
+
(solid --raise, the same four-blend-ring stroke every other rail
|
|
1425
|
+
uses), not a frosted floating pill — the slider and the two
|
|
1426
|
+
actions read as one joined control instead of a popover sitting
|
|
1427
|
+
over the video. .cvbar itself keeps only the positioning
|
|
1428
|
+
(centred, 16px off the bottom, scrolls if it doesn't fit) — same
|
|
1429
|
+
split as .brail/.bgroup on the fit pane. -->
|
|
1430
|
+
<div class="bgroup row"><i class="ring" aria-hidden="true"></i><i class="ring lift" aria-hidden="true"></i>
|
|
1431
|
+
<!-- What is left in this bar is only what the RESULT pane is for:
|
|
1432
|
+
which moment of the clip you are looking at, and the two ways of
|
|
1433
|
+
looking at it. The output format left for the top bar, beside
|
|
1434
|
+
Render, because deciding how a file is written is not judging a
|
|
1435
|
+
fit — and as the fifth control here it was squeezing the scrubber
|
|
1436
|
+
to nothing at 742px. -->
|
|
1437
|
+
<div class="cvslider">
|
|
1438
|
+
<span class="sm" style="color:var(--mute)">Fit on frame</span>
|
|
1439
|
+
<input type="range" id="vframe" min="0" max="0" value="0" step="1"
|
|
1440
|
+
aria-label="Which frame of the clip to match the edges on">
|
|
1441
|
+
<span class="sm" id="vframeLbl" style="color:var(--mute);min-width:9ch">0</span>
|
|
1442
|
+
</div>
|
|
1305
1443
|
<!-- Viewing controls sit with the scrubber, which is the other viewing
|
|
1306
1444
|
control; the format stepper is about the OUTPUT and stays at the
|
|
1307
1445
|
end. Icon-only, so each carries an aria-label — a title alone
|
|
1308
1446
|
names it for a mouse and not for a screen reader. -->
|
|
1309
|
-
<button class="
|
|
1447
|
+
<button class="segbtn" id="playlive" aria-label="Play the clip on the photo"
|
|
1310
1448
|
title="Play the clip on the photo, right now. The browser applies the same four corners and corner radius, and approximates emissive with screen blending; the colour grade, grain and depth of field are not in this view.">
|
|
1311
|
-
<svg viewBox="0 0
|
|
1449
|
+
<svg viewBox="0 0 16 16" aria-hidden="true" focusable="false"><path d="M5 3.5 12.5 8 5 12.5Z" fill="currentColor"/></svg>
|
|
1312
1450
|
</button>
|
|
1313
|
-
<button class="
|
|
1451
|
+
<button class="segbtn txt" id="playclip"
|
|
1314
1452
|
title="Composite a few seconds from the fitted frame through the real pipeline and play that. Slower — it is a render — but it is the only view with the grade, the grain and the true emissive blend.">Preview</button>
|
|
1315
|
-
</
|
|
1453
|
+
</div>
|
|
1316
1454
|
</div>
|
|
1317
1455
|
</section>
|
|
1318
1456
|
|
|
@@ -1742,7 +1880,7 @@ const ROTATE_CURSOR = 'url("data:image/svg+xml;utf8,' + encodeURIComponent(
|
|
|
1742
1880
|
'<path d="M15.6 2.6l0.4 4.2-4.2 0.3" fill="none" stroke="rgba(0,0,0,.85)" stroke-width="4.2" stroke-linecap="round" stroke-linejoin="round"/>' +
|
|
1743
1881
|
'<path d="M15.6 2.6l0.4 4.2-4.2 0.3" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>' +
|
|
1744
1882
|
'</svg>') + '") 11 11, crosshair';
|
|
1745
|
-
const QUAD_W =
|
|
1883
|
+
const QUAD_W = 1, QUAD_ACTIVE_W = 1, GUIDE_W = 1, CASE_PAD = 2.0; // every line 1px, 14 Sep 2026 — the active edge no longer thickens
|
|
1746
1884
|
const GUIDE_DASH = [6, 5];
|
|
1747
1885
|
function cased(c, path, coreW, coreColor, dash){
|
|
1748
1886
|
c.save();
|