Changeset 3736872
- Timestamp:
- 10/09/2026 02:34:13 PM (2 weeks ago)
- Location:
- stillframe
- Files:
-
- 6 added
- 33 edited
- 1 copied
-
assets/screenshot-1.png (modified) ( previous)
-
assets/screenshot-2.png (modified) ( previous)
-
assets/screenshot-3.png (modified) ( previous)
-
assets/screenshot-4.png (modified) ( previous)
-
assets/screenshot-5.png (modified) ( previous)
-
tags/0.2.5 (copied) (copied from stillframe/trunk )
-
tags/0.2.5/assets/css/stillframe-capture-panel.css (modified) (4 diffs)
-
tags/0.2.5/assets/css/stillframe-tools.css (modified) (1 diff)
-
tags/0.2.5/assets/js/stillframe-admin-bar.js (modified) (2 diffs)
-
tags/0.2.5/assets/js/stillframe-annotation-editor-v2.js (added)
-
tags/0.2.5/assets/js/stillframe-annotation-editor.js (modified) (4 diffs)
-
tags/0.2.5/assets/js/stillframe-capture-panel.js (modified) (37 diffs)
-
tags/0.2.5/assets/js/stillframe-tools.js (modified) (5 diffs)
-
tags/0.2.5/assets/js/vendor/LICENSE-fabric.txt (added)
-
tags/0.2.5/assets/js/vendor/fabric.min.js (added)
-
tags/0.2.5/includes/class-stillframe-asset-loader.php (modified) (17 diffs)
-
tags/0.2.5/includes/class-stillframe-media.php (modified) (7 diffs)
-
tags/0.2.5/includes/class-stillframe-plugin.php (modified) (1 diff)
-
tags/0.2.5/includes/class-stillframe-tools-page.php (modified) (3 diffs)
-
tags/0.2.5/languages/stillframe.pot (modified) (3 diffs)
-
tags/0.2.5/readme.txt (modified) (3 diffs)
-
tags/0.2.5/stillframe.php (modified) (2 diffs)
-
tags/0.2.5/uninstall.php (modified) (1 diff)
-
trunk/assets/css/stillframe-capture-panel.css (modified) (4 diffs)
-
trunk/assets/css/stillframe-tools.css (modified) (1 diff)
-
trunk/assets/js/stillframe-admin-bar.js (modified) (2 diffs)
-
trunk/assets/js/stillframe-annotation-editor-v2.js (added)
-
trunk/assets/js/stillframe-annotation-editor.js (modified) (4 diffs)
-
trunk/assets/js/stillframe-capture-panel.js (modified) (37 diffs)
-
trunk/assets/js/stillframe-tools.js (modified) (5 diffs)
-
trunk/assets/js/vendor/LICENSE-fabric.txt (added)
-
trunk/assets/js/vendor/fabric.min.js (added)
-
trunk/includes/class-stillframe-asset-loader.php (modified) (17 diffs)
-
trunk/includes/class-stillframe-media.php (modified) (7 diffs)
-
trunk/includes/class-stillframe-plugin.php (modified) (1 diff)
-
trunk/includes/class-stillframe-tools-page.php (modified) (3 diffs)
-
trunk/languages/stillframe.pot (modified) (3 diffs)
-
trunk/readme.txt (modified) (3 diffs)
-
trunk/stillframe.php (modified) (2 diffs)
-
trunk/uninstall.php (modified) (1 diff)
Legend:
- Unmodified
- Added
- Removed
-
stillframe/tags/0.2.5/assets/css/stillframe-capture-panel.css
r3724658 r3736872 1595 1595 align-items: center; 1596 1596 gap: 10px; 1597 width: max-content; 1598 max-width: calc(100% - 32px); 1597 width: fit-content; 1598 max-width: min(520px, calc(100% - 32px)); 1599 white-space: normal; 1599 1600 margin: 0 auto; 1600 1601 padding: 8px 14px; … … 1717 1718 } 1718 1719 1719 /* Neutral accents ------------------------------------------------------- */ 1720 /* The brand colour is reserved for primary buttons (Capture, Download, Save). 1721 Selected and active states use plain greys so nothing else competes. */ 1720 /* Brand accents --------------------------------------------------------- */ 1721 /* Selected and active states use the Stillframe petrol colour. */ 1722 1722 1723 1723 .stillframe-snip, 1724 1724 .stillframe-result { 1725 --sf-ink: # 1f2937;1726 --sf-pick-bg: # f3f4f6;1727 --sf-pick-line: # 9ca3af;1725 --sf-ink: #005976; 1726 --sf-pick-bg: #e6f4f8; 1727 --sf-pick-line: #7fb9cc; 1728 1728 } 1729 1729 … … 1851 1851 .stillframe-snip, 1852 1852 .stillframe-result { 1853 --sf-pick-line: # 6b7280;1853 --sf-pick-line: #7fb9cc; 1854 1854 --sf-edge: #b4bac4; 1855 1855 } … … 2070 2070 } 2071 2071 .stillframe-snip__bar .stillframe-hub-sec { margin-bottom: 0; } 2072 .stillframe-snip__direct-menu { width: 480px; padding: 12px; gap: 8px; } 2072 .stillframe-snip__direct-menu { width: 480px; min-width: 360px; padding: 12px; gap: 8px; max-height: min(80vh, 640px); overflow-y: auto; } 2073 .stillframe-snip__recent-menu { max-height: min(80vh, 640px); overflow-y: auto; } 2074 2075 .stillframe-hub-help { 2076 font-size: 11.5px; 2077 line-height: 1.4; 2078 color: var(--sf-muted, #6b7280); 2079 } 2080 2081 /* Recent screenshots: two-column card grid */ 2082 .stillframe-snip__recent-menu { 2083 width: 520px; 2084 } 2085 2086 .stillframe-snip .stillframe-recent-list { 2087 display: grid; 2088 grid-template-columns: repeat(2, minmax(0, 1fr)); 2089 gap: 10px; 2090 max-height: none; 2091 overflow: visible; 2092 } 2093 2094 .stillframe-snip .stillframe-recent-card { 2095 flex-direction: column; 2096 gap: 8px; 2097 padding: 8px; 2098 background: var(--sf-surface); 2099 border: 1px solid var(--sf-line); 2100 border-radius: 10px; 2101 } 2102 2103 .stillframe-snip .stillframe-recent-card:last-child { 2104 border-bottom: 1px solid var(--sf-line); 2105 } 2106 2107 .stillframe-snip .stillframe-recent-card:hover { 2108 background: var(--sf-brand-muted); 2109 border-color: var(--sf-brand); 2110 } 2111 2112 .stillframe-snip .stillframe-recent-thumbwrap { 2113 flex: 0 0 auto; 2114 width: 100%; 2115 height: auto; 2116 aspect-ratio: 16 / 10; 2117 } 2118 2119 .stillframe-snip .stillframe-recent-info { 2120 gap: 4px; 2121 } 2122 2123 .stillframe-snip .stillframe-recent-actions { 2124 margin-top: 2px; 2125 } 2126 2127 @media (max-width: 560px) { 2128 .stillframe-snip .stillframe-recent-list { 2129 grid-template-columns: minmax(0, 1fr); 2130 } 2131 } 2132 2133 .stillframe-result .stillframe-format-select { 2134 height: 36px; 2135 margin: 0 4px 0 0; 2136 padding: 0 24px 0 10px; 2137 font-size: 13px; 2138 font-weight: 600; 2139 color: var(--sf-text-2, #374151); 2140 background-color: var(--sf-surface, #ffffff); 2141 border: 1px solid var(--sf-line-strong, #d1d5db); 2142 border-radius: 8px; 2143 box-shadow: none; 2144 cursor: pointer; 2145 } 2146 2147 .stillframe-result .stillframe-format-select:hover, 2148 .stillframe-result .stillframe-format-select:focus { 2149 color: var(--sf-brand, #005976); 2150 border-color: var(--sf-brand, #005976); 2151 outline: none; 2152 } 2153 2154 /* Device preview frames */ 2155 .stillframe-dev-card__frame { 2156 display: flex; 2157 align-items: center; 2158 justify-content: center; 2159 margin-bottom: 8px; 2160 background: var(--sf-well); 2161 border: 1.5px solid var(--sf-line-strong); 2162 border-radius: 5px; 2163 box-sizing: border-box; 2164 color: var(--sf-muted); 2165 } 2166 2167 .stillframe-dev-card.is-selected .stillframe-dev-card__frame { 2168 background: var(--sf-surface); 2169 border-color: var(--sf-brand); 2170 color: var(--sf-brand); 2171 } 2172 2173 .stillframe-dev-card__frame .stillframe-dev-card__icon { 2174 margin: 0; 2175 color: inherit; 2176 } 2177 2178 .stillframe-hub-size { 2179 font-size: 11.5px; 2180 line-height: 1.4; 2181 color: var(--sf-text-2); 2182 } 2183 2184 /* Sticky capture button */ 2185 .stillframe-hub-footer { 2186 position: sticky; 2187 bottom: -12px; 2188 z-index: 2; 2189 margin: 4px -12px -12px; 2190 padding: 10px 12px 12px; 2191 background: var(--sf-surface); 2192 border-top: 1px solid var(--sf-line); 2193 border-radius: 0 0 10px 10px; 2194 } 2195 2196 @media (max-width: 560px) { 2197 .stillframe-snip__direct-menu { 2198 width: calc(100vw - 24px); 2199 min-width: 0; 2200 } 2201 } 2202 2203 /* Fabric annotation layer sits exactly over the captured image */ 2204 .stillframe-result .stillframe-annotation-wrapper { 2205 z-index: 2; 2206 } 2207 2208 .stillframe-result .stillframe-annotation-wrapper canvas { 2209 touch-action: none; 2210 } 2211 2212 /* ========================================================================== 2213 Stillframe interface layer 2214 Flat, compact controls in the Stillframe petrol colour. These rules sit last 2215 and use !important on colours so a site theme's button styles cannot recolour them. 2216 ========================================================================== */ 2217 2218 .stillframe-snip, 2219 .stillframe-result { 2220 --sf-brand: #005976; 2221 --sf-brand-hover: #004a61; 2222 --sf-brand-muted: #f1f6f8; 2223 --sf-brand-border: #c3d3da; 2224 } 2225 2226 .stillframe-snip button, 2227 .stillframe-result button, 2228 .sfp button { 2229 appearance: none; 2230 margin: 0; 2231 font-family: inherit; 2232 letter-spacing: normal; 2233 text-transform: none; 2234 text-shadow: none; 2235 } 2236 2237 /* ---- Top selection bar ---- */ 2238 .stillframe-snip .stillframe-snip__bar button { 2239 height: 32px; 2240 gap: 6px; 2241 padding: 0 10px; 2242 font-size: 12.5px; 2243 font-weight: 600; 2244 border-radius: 6px; 2245 } 2246 2247 .stillframe-snip .stillframe-snip__bar svg { 2248 width: 16px; 2249 height: 16px; 2250 } 2251 2252 .stillframe-snip .stillframe-snip__bar .stillframe-snip__chevron { 2253 width: 10px; 2254 height: 10px; 2255 } 2256 2257 .stillframe-snip .stillframe-snip__bar .stillframe-snip__mode:hover, 2258 .stillframe-snip .stillframe-snip__bar .stillframe-snip__close:hover { 2259 color: var(--sf-brand) !important; 2260 background: #f3f5f6 !important; 2261 } 2262 2263 .stillframe-snip .stillframe-snip__bar .stillframe-snip__mode[aria-pressed="true"], 2264 .stillframe-snip.has-menu-open .stillframe-snip__bar .stillframe-snip__mode[aria-expanded="true"] { 2265 color: var(--sf-brand) !important; 2266 background: var(--sf-brand-muted) !important; 2267 border-color: var(--sf-brand-border) !important; 2268 } 2269 2270 .stillframe-snip .stillframe-snip__go, 2271 .stillframe-snip .stillframe-snip__go:focus { 2272 color: #ffffff !important; 2273 background: var(--sf-brand) !important; 2274 border: 0 !important; 2275 box-shadow: none !important; 2276 } 2277 2278 .stillframe-snip .stillframe-snip__go:hover { 2279 color: #ffffff !important; 2280 background: var(--sf-brand-hover) !important; 2281 } 2282 2283 /* ---- Menus that open from the bar ---- */ 2284 .stillframe-snip .stillframe-snip__direct-menu { 2285 width: 400px; 2286 max-width: calc(100vw - 24px); 2287 padding: 12px 14px 14px; 2288 gap: 10px; 2289 max-height: none; 2290 overflow: visible; 2291 } 2292 2293 @media (max-height: 640px) { 2294 .stillframe-snip .stillframe-snip__direct-menu { 2295 max-height: calc(100vh - 110px); 2296 overflow-y: auto; 2297 } 2298 } 2299 2300 .stillframe-snip .stillframe-hub-sec-title { 2301 margin: 0; 2302 font-size: 11px; 2303 font-weight: 700; 2304 letter-spacing: 0.06em; 2305 text-transform: uppercase; 2306 color: var(--sf-muted); 2307 } 2308 2309 .sfd-row { 2310 display: grid; 2311 grid-template-columns: 64px minmax(0, 1fr); 2312 align-items: center; 2313 gap: 8px; 2314 } 2315 2316 .sfd-label { 2317 font-size: 12px; 2318 font-weight: 600; 2319 color: var(--sf-text-2, #374151); 2320 } 2321 2322 .sfd-chips { 2323 display: flex; 2324 gap: 6px; 2325 min-width: 0; 2326 } 2327 2328 .stillframe-snip .sfd-chip { 2329 flex: 1 1 0; 2330 display: inline-flex; 2331 align-items: center; 2332 justify-content: center; 2333 gap: 5px; 2334 min-width: 0; 2335 height: 30px; 2336 padding: 0 6px; 2337 font-size: 12px; 2338 font-weight: 600; 2339 color: var(--sf-text-2, #374151) !important; 2340 background: #ffffff !important; 2341 border: 1px solid var(--sf-line-strong, #d1d5db) !important; 2342 border-radius: 6px; 2343 box-shadow: none !important; 2344 cursor: pointer; 2345 } 2346 2347 .stillframe-snip .sfd-chip em { 2348 font-style: normal; 2349 font-weight: 500; 2350 font-size: 11px; 2351 color: var(--sf-muted, #6b7280); 2352 } 2353 2354 .stillframe-snip .sfd-chip:hover { 2355 color: var(--sf-brand) !important; 2356 border-color: var(--sf-brand-border) !important; 2357 } 2358 2359 .stillframe-snip .sfd-chip.is-selected { 2360 color: var(--sf-brand) !important; 2361 background: var(--sf-brand-muted) !important; 2362 border-color: var(--sf-brand) !important; 2363 } 2364 2365 .stillframe-snip .sfd-chip.is-selected em { 2366 color: var(--sf-brand); 2367 } 2368 2369 .sfd-seg { 2370 display: inline-flex; 2371 width: fit-content; 2372 overflow: hidden; 2373 border: 1px solid var(--sf-line-strong, #d1d5db); 2374 border-radius: 6px; 2375 } 2376 2377 .stillframe-snip .sfd-seg__btn { 2378 height: 28px; 2379 padding: 0 14px; 2380 font-size: 12px; 2381 font-weight: 600; 2382 color: var(--sf-text-2, #374151) !important; 2383 background: #ffffff !important; 2384 border: 0 !important; 2385 border-right: 1px solid var(--sf-line-strong, #d1d5db) !important; 2386 border-radius: 0; 2387 box-shadow: none !important; 2388 cursor: pointer; 2389 } 2390 2391 .stillframe-snip .sfd-seg__btn:last-child { 2392 border-right: 0 !important; 2393 } 2394 2395 .stillframe-snip .sfd-seg__btn:hover { 2396 color: var(--sf-brand) !important; 2397 background: var(--sf-brand-muted) !important; 2398 } 2399 2400 .stillframe-snip .sfd-seg__btn.is-active, 2401 .stillframe-snip .sfd-seg__btn.is-active:hover { 2402 color: #ffffff !important; 2403 background: var(--sf-brand) !important; 2404 } 2405 2406 .sfd-help { 2407 margin: -4px 0 0; 2408 padding-left: 72px; 2409 font-size: 11.5px; 2410 line-height: 1.4; 2411 color: var(--sf-muted, #6b7280); 2412 } 2413 2414 .stillframe-snip .stillframe-hub-toggle { 2415 gap: 8px; 2416 font-size: 12.5px; 2417 color: var(--sf-text-2, #374151); 2418 } 2419 2420 .stillframe-snip .stillframe-hub-toggle__track { 2421 width: 30px; 2422 height: 17px; 2423 background: #cbd5e1; 2424 } 2425 2426 .stillframe-snip .stillframe-hub-toggle__track::before { 2427 top: 2px; 2428 left: 2px; 2429 width: 13px; 2430 height: 13px; 2431 } 2432 2433 .stillframe-snip .stillframe-hub-toggle__input:checked + .stillframe-hub-toggle__track { 2434 background: var(--sf-brand) !important; 2435 } 2436 2437 .stillframe-snip .stillframe-hub-toggle__input:checked + .stillframe-hub-toggle__track::before { 2438 transform: translateX(13px); 2439 } 2440 2441 .stillframe-snip .stillframe-hub-toggle__input:focus-visible + .stillframe-hub-toggle__track { 2442 outline: 2px solid var(--sf-brand); 2443 } 2444 2445 .sfd-engine { 2446 align-items: flex-start; 2447 padding-top: 10px; 2448 margin-top: 4px; 2449 border-top: 1px solid var(--sf-line, #e5e7eb); 2450 } 2451 2452 .sfd-engine .stillframe-hub-toggle__track { 2453 margin-top: 2px; 2454 } 2455 2456 .sfd-engine__text { 2457 display: flex; 2458 flex-direction: column; 2459 gap: 2px; 2460 min-width: 0; 2461 } 2462 2463 .sfd-engine__text b { 2464 font-size: 12.5px; 2465 font-weight: 600; 2466 color: var(--sf-text, #111827); 2467 } 2468 2469 .sfd-engine__text small { 2470 font-size: 11.5px; 2471 line-height: 1.4; 2472 color: var(--sf-muted, #6b7280); 2473 } 2474 2475 .stillframe-snip .sfd-submit { 2476 display: flex; 2477 align-items: center; 2478 justify-content: center; 2479 gap: 8px; 2480 width: 100%; 2481 height: 34px; 2482 padding: 0 12px; 2483 font-size: 13px; 2484 font-weight: 600; 2485 color: #ffffff !important; 2486 background: var(--sf-brand) !important; 2487 border: 1px solid var(--sf-brand) !important; 2488 border-radius: 6px; 2489 box-shadow: none !important; 2490 cursor: pointer; 2491 } 2492 2493 .stillframe-snip .sfd-submit:hover { 2494 color: #ffffff !important; 2495 background: var(--sf-brand-hover) !important; 2496 border-color: var(--sf-brand-hover) !important; 2497 } 2498 2499 /* ---- Page dropdown ---- */ 2500 .sfp { 2501 position: relative; 2502 min-width: 0; 2503 width: 100%; 2504 font-size: 12.5px; 2505 } 2506 2507 .sfp *, 2508 .sfp *::before, 2509 .sfp *::after { 2510 box-sizing: border-box; 2511 } 2512 2513 .sfp .sfp__trigger { 2514 display: flex; 2515 align-items: center; 2516 justify-content: space-between; 2517 gap: 8px; 2518 width: 100%; 2519 height: 30px; 2520 padding: 0 10px; 2521 font-size: 12.5px; 2522 font-weight: 500; 2523 line-height: 1; 2524 text-align: left; 2525 color: var(--sf-text, #111827) !important; 2526 background: #ffffff !important; 2527 border: 1px solid var(--sf-line-strong, #d1d5db) !important; 2528 border-radius: 6px; 2529 box-shadow: none !important; 2530 cursor: pointer; 2531 } 2532 2533 .sfp .sfp__trigger:hover, 2534 .sfp.is-open .sfp__trigger { 2535 border-color: var(--sf-brand) !important; 2536 } 2537 2538 .sfp .sfp__trigger:focus-visible { 2539 outline: 2px solid var(--sf-brand); 2540 outline-offset: 1px; 2541 } 2542 2543 .sfp .sfp__summary { 2544 flex: 1 1 auto; 2545 min-width: 0; 2546 overflow: hidden; 2547 text-overflow: ellipsis; 2548 white-space: nowrap; 2549 } 2550 2551 .sfp .sfp__trigger svg { 2552 flex: 0 0 auto; 2553 color: var(--sf-muted, #6b7280); 2554 } 2555 2556 .sfp.is-open .sfp__trigger svg { 2557 transform: rotate(180deg); 2558 } 2559 2560 .sfp .sfp__drop { 2561 position: absolute; 2562 z-index: 40; 2563 top: calc(100% + 4px); 2564 left: 0; 2565 right: 0; 2566 padding: 6px; 2567 background: #ffffff; 2568 border: 1px solid var(--sf-line-strong, #d1d5db); 2569 border-radius: 8px; 2570 box-shadow: 0 10px 28px rgba(15, 23, 42, 0.18), 0 2px 6px rgba(15, 23, 42, 0.08); 2571 } 2572 2573 .sfp .sfp__drop[hidden] { 2574 display: none !important; 2575 } 2576 2577 .sfp .sfp__search { 2578 display: block; 2579 width: 100%; 2580 height: 30px; 2581 margin: 0 0 6px; 2582 padding: 0 10px; 2583 font-family: inherit; 2584 font-size: 12.5px; 2585 color: var(--sf-text, #111827); 2586 background: #ffffff; 2587 border: 1px solid var(--sf-line-strong, #d1d5db); 2588 border-radius: 6px; 2589 box-shadow: none; 2590 } 2591 2592 .sfp .sfp__search:focus { 2593 border-color: var(--sf-brand); 2594 box-shadow: 0 0 0 1px var(--sf-brand); 2595 outline: none; 2596 } 2597 2598 .sfp .sfp__list { 2599 max-height: 220px; 2600 overflow-y: auto; 2601 overscroll-behavior: contain; 2602 } 2603 2604 .sfp .sfp__group { 2605 position: sticky; 2606 top: 0; 2607 z-index: 1; 2608 padding: 6px 8px 4px; 2609 font-size: 10.5px; 2610 font-weight: 700; 2611 letter-spacing: 0.05em; 2612 text-transform: uppercase; 2613 color: var(--sf-muted, #6b7280); 2614 background: #ffffff; 2615 } 2616 2617 .sfp .sfp__row { 2618 display: grid; 2619 grid-template-columns: minmax(0, 1fr) auto; 2620 column-gap: 8px; 2621 align-items: center; 2622 padding: 5px 8px; 2623 border-radius: 5px; 2624 cursor: pointer; 2625 } 2626 2627 .sfp .sfp__row:hover, 2628 .sfp .sfp__row.is-active { 2629 background: var(--sf-well, #f3f4f6); 2630 } 2631 2632 .sfp .sfp__row.is-selected { 2633 background: var(--sf-brand-muted, #e6f4f8); 2634 } 2635 2636 .sfp .sfp__row.is-selected .sfp__title { 2637 color: var(--sf-brand); 2638 } 2639 2640 .sfp .sfp__title { 2641 grid-column: 1; 2642 overflow: hidden; 2643 font-weight: 600; 2644 color: var(--sf-text, #111827); 2645 text-overflow: ellipsis; 2646 white-space: nowrap; 2647 } 2648 2649 .sfp .sfp__path { 2650 grid-column: 1; 2651 overflow: hidden; 2652 font-size: 11.5px; 2653 color: var(--sf-muted, #6b7280); 2654 text-overflow: ellipsis; 2655 white-space: nowrap; 2656 } 2657 2658 .sfp .sfp__chip { 2659 grid-column: 2; 2660 grid-row: 1 / span 2; 2661 padding: 1px 6px; 2662 font-size: 10px; 2663 font-weight: 600; 2664 color: var(--sf-muted, #6b7280); 2665 background: var(--sf-well, #f3f4f6); 2666 border-radius: 999px; 2667 } 2668 2669 .sfp .sfp__note { 2670 padding: 10px 8px; 2671 font-size: 12px; 2672 color: var(--sf-muted, #6b7280); 2673 } 2674 2675 /* ---- Editor top bar ---- */ 2676 .stillframe-result .stillframe-result__toolbar { 2677 display: grid; 2678 grid-template-columns: auto minmax(0, 1fr) auto; 2679 grid-template-areas: "brand tools actions"; 2680 align-items: center; 2681 gap: 8px 16px; 2682 padding: 8px 12px; 2683 } 2684 2685 .stillframe-result .stillframe-result__brand { 2686 grid-area: brand; 2687 } 2688 2689 .stillframe-result .stillframe-result__editing { 2690 grid-area: tools; 2691 flex-wrap: wrap; 2692 justify-content: flex-start; 2693 gap: 6px 12px; 2694 } 2695 2696 .stillframe-result .stillframe-result__actions { 2697 grid-area: actions; 2698 justify-self: end; 2699 flex-shrink: 0; 2700 gap: 6px; 2701 margin-left: 0; 2702 } 2703 2704 @media (max-width: 1180px) { 2705 .stillframe-result .stillframe-result__toolbar { 2706 grid-template-columns: minmax(0, 1fr) auto; 2707 grid-template-areas: "brand actions" "tools tools"; 2708 } 2709 } 2710 2711 .stillframe-result .stillframe-result__tools, 2712 .stillframe-result .stillframe-result__history, 2713 .stillframe-result .stillframe-result__sizes, 2714 .stillframe-result .stillframe-result__swatches { 2715 gap: 2px; 2716 padding: 0; 2717 background: transparent; 2718 border-radius: 0; 2719 } 2720 2721 .stillframe-result .stillframe-result__editing > * + * { 2722 padding-left: 12px; 2723 border-left: 1px solid var(--sf-line, #e5e7eb); 2724 } 2725 2726 .stillframe-result .stillframe-result__swatches { 2727 gap: 7px; 2728 } 2729 2730 .stillframe-result .stillframe-tool, 2731 .stillframe-result .stillframe-icon-btn, 2732 .stillframe-result .stillframe-size { 2733 width: 30px; 2734 height: 30px; 2735 color: var(--sf-text-2, #374151) !important; 2736 background: transparent !important; 2737 border: 1px solid transparent !important; 2738 border-radius: 6px; 2739 box-shadow: none !important; 2740 } 2741 2742 .stillframe-result .stillframe-tool svg, 2743 .stillframe-result .stillframe-icon-btn svg { 2744 width: 16px; 2745 height: 16px; 2746 } 2747 2748 .stillframe-result .stillframe-tool:hover, 2749 .stillframe-result .stillframe-icon-btn:hover, 2750 .stillframe-result .stillframe-size:hover { 2751 color: var(--sf-brand) !important; 2752 background: var(--sf-brand-muted) !important; 2753 } 2754 2755 .stillframe-result .stillframe-tool[aria-pressed="true"], 2756 .stillframe-result .stillframe-size[aria-pressed="true"] { 2757 color: var(--sf-brand) !important; 2758 background: var(--sf-brand-muted) !important; 2759 border-color: var(--sf-brand-border) !important; 2760 } 2761 2762 .stillframe-result .stillframe-swatch { 2763 width: 16px; 2764 height: 16px; 2765 } 2766 2767 .stillframe-result .stillframe-btn { 2768 height: 32px; 2769 gap: 6px; 2770 padding: 0 11px; 2771 font-size: 12.5px; 2772 font-weight: 600; 2773 color: var(--sf-text-2, #374151) !important; 2774 background: #ffffff !important; 2775 border: 1px solid var(--sf-line-strong, #d1d5db) !important; 2776 border-radius: 6px; 2777 box-shadow: none !important; 2778 } 2779 2780 .stillframe-result .stillframe-btn svg { 2781 width: 16px; 2782 height: 16px; 2783 } 2784 2785 .stillframe-result .stillframe-btn:hover { 2786 color: var(--sf-brand) !important; 2787 background: var(--sf-brand-muted) !important; 2788 border-color: var(--sf-brand-border) !important; 2789 } 2790 2791 .stillframe-result .stillframe-btn.stillframe-btn-primary, 2792 .stillframe-result .stillframe-btn.stillframe-btn-primary:hover { 2793 color: #ffffff !important; 2794 background: var(--sf-brand) !important; 2795 border-color: var(--sf-brand) !important; 2796 } 2797 2798 .stillframe-result .stillframe-btn.stillframe-btn-primary:hover { 2799 background: var(--sf-brand-hover) !important; 2800 } 2801 2802 .stillframe-result .stillframe-btn.stillframe-result__close { 2803 width: 32px; 2804 padding: 0; 2805 color: var(--sf-muted, #6b7280) !important; 2806 } 2807 2808 .stillframe-result .stillframe-format-select { 2809 height: 32px; 2810 margin: 0; 2811 border-radius: 6px; 2812 } 2813 2814 /* ---- Recent cards ---- */ 2815 .stillframe-snip .stillframe-recent-btn:hover, 2816 .stillframe-result .stillframe-recent-btn:hover { 2817 color: var(--sf-brand) !important; 2818 background: var(--sf-brand-muted) !important; 2819 border-color: var(--sf-brand-border) !important; 2820 } 2821 2822 /* Download keeps its label at every width */ 2823 .stillframe-result .stillframe-btn.stillframe-btn-primary { 2824 width: auto; 2825 min-width: 0; 2826 padding: 0 14px; 2827 } 2828 2829 .stillframe-result .stillframe-btn.stillframe-btn-primary .stillframe-btn__label { 2830 display: inline !important; 2831 } 2832 2833 /* ---- Direct Page tweaks ---- */ 2834 .stillframe-snip .stillframe-snip__direct-menu { 2835 width: 420px; 2836 gap: 12px; 2837 } 2838 2839 .stillframe-snip .sfd-chip { 2840 height: 32px; 2841 } 2842 2843 .stillframe-snip .sfd-chip svg { 2844 width: 17px; 2845 height: 17px; 2846 } 2847 2848 .sfd-error { 2849 margin: -6px 0 0; 2850 padding-left: 72px; 2851 font-size: 12px; 2852 font-weight: 600; 2853 color: #b42318; 2854 } 2855 2856 .sfd-error:empty { 2857 display: none; 2858 } 2859 2860 .stillframe-snip .sfd-row + .stillframe-hub-toggle, 2861 .stillframe-snip .stillframe-snip__direct-menu > .stillframe-hub-toggle { 2862 margin: 6px 0 4px; 2863 } 2864 2865 /* Page list opens across the full width of the menu and nothing in it is sticky */ 2866 .sfd-row .sfp .sfp__drop { 2867 left: -72px; 2868 } 2869 2870 .sfp .sfp__search { 2871 height: 26px; 2872 margin-bottom: 4px; 2873 font-size: 12px; 2874 } 2875 2876 .sfp .sfp__group { 2877 position: static; 2878 } 2879 2880 .sfp .sfp__list { 2881 max-height: 260px; 2882 } 2883 2884 /* ---- Recent cards: quiet hover, smaller buttons ---- */ 2885 .stillframe-snip .stillframe-recent-card:hover, 2886 .stillframe-result .stillframe-recent-card:hover { 2887 background: #ffffff; 2888 border-color: var(--sf-line-strong, #d1d5db); 2889 box-shadow: 0 1px 3px rgba(15, 23, 42, 0.08); 2890 } 2891 2892 .stillframe-snip .stillframe-recent-actions, 2893 .stillframe-result .stillframe-recent-actions { 2894 flex-wrap: nowrap; 2895 gap: 6px; 2896 } 2897 2898 .stillframe-snip .stillframe-recent-btn, 2899 .stillframe-result .stillframe-recent-btn { 2900 height: 26px !important; 2901 gap: 5px !important; 2902 padding: 0 9px !important; 2903 font-size: 12px !important; 2904 font-weight: 600 !important; 2905 border-radius: 6px !important; 2906 } 2907 2908 .stillframe-snip .stillframe-recent-btn svg, 2909 .stillframe-result .stillframe-recent-btn svg { 2910 width: 13px; 2911 height: 13px; 2912 } 2913 2914 .stillframe-snip .stillframe-recent-btn:hover, 2915 .stillframe-result .stillframe-recent-btn:hover { 2916 color: var(--sf-text, #111827) !important; 2917 background: #f3f4f6 !important; 2918 border-color: #9ca3af !important; 2919 } 2920 2921 .stillframe-snip .stillframe-recent-btn.stillframe-recent-btn--del, 2922 .stillframe-result .stillframe-recent-btn.stillframe-recent-btn--del { 2923 width: 26px !important; 2924 padding: 0 !important; 2925 } 2926 2927 .stillframe-snip .stillframe-recent-btn.stillframe-recent-btn--del:hover, 2928 .stillframe-result .stillframe-recent-btn.stillframe-recent-btn--del:hover { 2929 color: #b42318 !important; 2930 background: #fef2f2 !important; 2931 border-color: #f1b5b0 !important; 2932 } 2933 2934 /* ---- Lighter states, calmer menus ---- */ 2935 .stillframe-snip.has-menu-open .stillframe-snip__bar .stillframe-snip__mode[aria-pressed="true"]:not([aria-expanded="true"]) { 2936 color: var(--sf-text-2) !important; 2937 background: transparent !important; 2938 border-color: transparent !important; 2939 } 2940 2941 /* Both menus open centred under the bar */ 2942 .stillframe-snip .stillframe-snip__devices-wrap, 2943 .stillframe-snip .stillframe-snip__direct-wrap, 2944 .stillframe-snip .stillframe-snip__recent-wrap { 2945 position: static; 2946 } 2947 2948 .stillframe-snip .stillframe-snip__direct-menu, 2949 .stillframe-snip .stillframe-snip__recent-menu { 2950 top: calc(100% + 8px); 2951 right: auto; 2952 left: 50%; 2953 transform: translateX(-50%); 2954 } 2955 2956 .stillframe-snip .stillframe-snip__recent-menu { 2957 width: 480px; 2958 max-height: min(75vh, 600px); 2959 padding: 14px; 2960 } 2961 2962 .stillframe-snip .stillframe-snip__direct-menu { 2963 gap: 14px; 2964 padding: 14px 16px 16px; 2965 } 2966 2967 .stillframe-snip .sfd-label { 2968 display: block; 2969 padding: 0; 2970 margin: 0; 2971 font-size: 12px; 2972 font-weight: 600; 2973 line-height: 1.2; 2974 color: var(--sf-text-2, #374151); 2975 background: none !important; 2976 border: 0; 2977 border-radius: 0; 2978 } 2979 2980 .stillframe-snip .sfd-chips { 2981 gap: 8px; 2982 } 2983 2984 .stillframe-snip .sfd-chip:hover { 2985 color: var(--sf-brand) !important; 2986 background: #f6f8f9 !important; 2987 border-color: #aebcc2 !important; 2988 } 2989 2990 .stillframe-snip .sfd-chip.is-selected, 2991 .stillframe-snip .sfd-chip.is-selected:hover { 2992 color: var(--sf-brand) !important; 2993 background: #f4f8fa !important; 2994 border-color: var(--sf-brand) !important; 2995 } 2996 2997 .stillframe-snip .sfd-seg { 2998 gap: 8px; 2999 overflow: visible; 3000 border: 0; 3001 border-radius: 0; 3002 } 3003 3004 .stillframe-snip .sfd-seg__btn { 3005 height: 30px; 3006 padding: 0 16px; 3007 border: 1px solid var(--sf-line-strong, #d1d5db) !important; 3008 border-radius: 6px; 3009 } 3010 3011 .stillframe-snip .sfd-seg__btn:last-child { 3012 border-right: 1px solid var(--sf-line-strong, #d1d5db) !important; 3013 } 3014 3015 .stillframe-snip .sfd-seg__btn:hover { 3016 color: var(--sf-brand) !important; 3017 background: #f6f8f9 !important; 3018 border-color: #aebcc2 !important; 3019 } 3020 3021 .stillframe-snip .sfd-seg__btn.is-active, 3022 .stillframe-snip .sfd-seg__btn.is-active:hover { 3023 color: var(--sf-brand) !important; 3024 background: #f4f8fa !important; 3025 border-color: var(--sf-brand) !important; 3026 } 3027 3028 .stillframe-snip .sfd-submit, 3029 .stillframe-snip .sfd-submit:focus { 3030 color: #ffffff !important; 3031 background: var(--sf-brand) !important; 3032 border-color: var(--sf-brand) !important; 3033 } 3034 3035 .stillframe-snip .sfd-submit:hover { 3036 background: var(--sf-brand-hover) !important; 3037 border-color: var(--sf-brand-hover) !important; 3038 } 3039 3040 .stillframe-snip .stillframe-snip__direct-menu > .stillframe-hub-toggle { 3041 margin: 4px 0 2px; 3042 padding: 4px 0; 3043 } 3044 3045 /* Page list: lighter rows, no type badge */ 3046 .sfp .sfp__chip { 3047 display: none; 3048 } 3049 3050 .sfp .sfp__list { 3051 display: flex; 3052 flex-direction: column; 3053 gap: 2px; 3054 } 3055 3056 .sfp .sfp__row { 3057 padding: 6px 10px; 3058 } 3059 3060 .sfp .sfp__title { 3061 font-size: 12.5px; 3062 font-weight: 500; 3063 } 3064 3065 .sfp .sfp__path { 3066 font-size: 11px; 3067 } 3068 3069 .sfp .sfp__group { 3070 padding: 8px 10px 3px; 3071 font-size: 10px; 3072 } 3073 3074 .sfp .sfp__row:hover, 3075 .sfp .sfp__row.is-active { 3076 background: #f5f6f7; 3077 } 3078 3079 .sfp .sfp__row.is-selected { 3080 background: #f4f8fa; 3081 } 3082 3083 .sfp .sfp__trigger:hover, 3084 .sfp.is-open .sfp__trigger { 3085 background: #ffffff !important; 3086 } 3087 3088 /* ---- Editor: one toolbar row, compact side panel ---- */ 3089 .stillframe-result .stillframe-result__tools { order: 1; } 3090 .stillframe-result .stillframe-result__history { order: 2; } 3091 .stillframe-result .stillframe-result__shape-opts { order: 3; display: inline-flex; align-items: center; gap: 6px; } 3092 .stillframe-result .stillframe-result__swatches { order: 4; } 3093 .stillframe-result .stillframe-result__sizes { order: 5; } 3094 3095 .stillframe-result .stillframe-result__editing { 3096 display: flex; 3097 align-items: center; 3098 flex-wrap: nowrap; 3099 gap: 8px 10px; 3100 } 3101 3102 .stillframe-result .stillframe-result__editing > * + * { 3103 padding-left: 10px; 3104 } 3105 3106 .stillframe-result .stillframe-result__shape-opts[style*="display: none"] { 3107 display: none !important; 3108 padding-left: 0 !important; 3109 border-left: none !important; 3110 } 3111 3112 .stillframe-result .stillframe-tool, 3113 .stillframe-result .stillframe-icon-btn, 3114 .stillframe-result .stillframe-size { 3115 width: 28px; 3116 height: 28px; 3117 } 3118 3119 .stillframe-result .stillframe-tool:hover, 3120 .stillframe-result .stillframe-icon-btn:hover, 3121 .stillframe-result .stillframe-size:hover, 3122 .stillframe-result .stillframe-btn:hover { 3123 color: var(--sf-brand) !important; 3124 background: #f3f5f6 !important; 3125 } 3126 3127 .stillframe-result .stillframe-tool[aria-pressed="true"], 3128 .stillframe-result .stillframe-size[aria-pressed="true"] { 3129 background: #eef3f5 !important; 3130 border-color: transparent !important; 3131 } 3132 3133 @media (max-width: 1500px) { 3134 .stillframe-result .stillframe-result__actions .stillframe-btn:not(.stillframe-btn-primary):not(.stillframe-result__close) .stillframe-btn__label { 3135 display: none !important; 3136 } 3137 3138 .stillframe-result .stillframe-result__actions .stillframe-btn:not(.stillframe-btn-primary):not(.stillframe-result__close) { 3139 width: 32px; 3140 padding: 0; 3141 } 3142 } 3143 3144 @media (max-width: 1180px) { 3145 .stillframe-result .stillframe-result__editing { 3146 flex-wrap: wrap; 3147 } 3148 } 3149 3150 .stillframe-result .stillframe-result__sidebar { 3151 width: 184px; 3152 } 3153 3154 .stillframe-result .stillframe-result__sidebar-head { 3155 font-size: 10.5px !important; 3156 padding: 12px 12px 6px; 3157 } 3158 3159 .stillframe-result .stillframe-device-tab__name { 3160 font-size: 12.5px !important; 3161 font-weight: 600 !important; 3162 line-height: 1.3; 3163 } 3164 3165 .stillframe-result .stillframe-device-tab__res { 3166 font-size: 11px !important; 3167 } 3168 3169 .stillframe-result .stillframe-sidebar-action-btn { 3170 height: 30px; 3171 font-size: 12px !important; 3172 font-weight: 600 !important; 3173 box-shadow: none !important; 3174 } 3175 3176 .stillframe-result .stillframe-sidebar-action-btn:not(.stillframe-sidebar-action-btn--primary):hover { 3177 color: var(--sf-brand) !important; 3178 background: #f3f5f6 !important; 3179 border-color: #aebcc2 !important; 3180 } 3181 3182 .stillframe-result .stillframe-sidebar-action-btn--primary, 3183 .stillframe-result .stillframe-sidebar-action-btn--primary:hover { 3184 color: #ffffff !important; 3185 background: var(--sf-brand) !important; 3186 border-color: var(--sf-brand) !important; 3187 } 3188 3189 .stillframe-result .stillframe-sidebar-action-btn--primary:hover { 3190 background: var(--sf-brand-hover) !important; 3191 } 3192 3193 .stillframe-result .stillframe-device-tab.is-active { 3194 background: #f4f8fa; 3195 border-color: var(--sf-brand); 3196 } 3197 3198 /* ---- Round close buttons everywhere ---- */ 3199 .stillframe-result .stillframe-btn.stillframe-result__close, 3200 .stillframe-result .stillframe-result__drawer-close, 3201 .stillframe-snip .stillframe-snip__reset, 3202 .stillframe-snip .stillframe-snip__bar .stillframe-snip__close { 3203 width: 34px; 3204 height: 34px; 3205 padding: 0; 3206 border-radius: 50% !important; 3207 } 3208 3209 .stillframe-result .stillframe-result__drawer-close { 3210 width: 28px; 3211 height: 28px; 3212 } 3213 3214 /* ---- Editor top bar: every control on the right shares one height and corner ---- */ 3215 .stillframe-result .stillframe-result__actions { 3216 align-items: center; 3217 gap: 8px; 3218 } 3219 3220 .stillframe-result .stillframe-result__actions .stillframe-btn, 3221 .stillframe-result .stillframe-result__actions .stillframe-format-select { 3222 height: 34px; 3223 margin: 0; 3224 border-radius: 8px; 3225 } 3226 3227 .stillframe-result .stillframe-result__actions .stillframe-btn:not(.stillframe-btn-primary):not(.stillframe-result__close) { 3228 min-width: 34px; 3229 } 3230 3231 .stillframe-result .stillframe-result__actions .stillframe-format-select { 3232 appearance: none; 3233 -webkit-appearance: none; 3234 padding: 0 28px 0 12px; 3235 font-size: 12.5px; 3236 font-weight: 600; 3237 line-height: 32px; 3238 background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%236b7280' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'><path d='M6 9l6 6 6-6'/></svg>"); 3239 background-repeat: no-repeat; 3240 background-position: right 9px center; 3241 } 3242 3243 /* ---- Page list: the search box scrolls away with the list ---- */ 3244 .sfp .sfp__drop { 3245 max-height: 300px; 3246 overflow-y: auto; 3247 overscroll-behavior: contain; 3248 } 3249 3250 .sfp .sfp__list { 3251 max-height: none; 3252 overflow: visible; 3253 } 3254 3255 /* ---- Hover help next to Quality and Height ---- */ 3256 .sfd-label { 3257 display: inline-flex; 3258 align-items: center; 3259 gap: 5px; 3260 } 3261 3262 .sfd-info { 3263 position: relative; 3264 display: inline-flex; 3265 align-items: center; 3266 justify-content: center; 3267 color: var(--sf-muted, #6b7280); 3268 cursor: help; 3269 border-radius: 50%; 3270 } 3271 3272 .sfd-info:hover, 3273 .sfd-info:focus-visible { 3274 color: var(--sf-brand, #005976); 3275 outline: none; 3276 } 3277 3278 .sfd-info__tip { 3279 position: absolute; 3280 z-index: 50; 3281 left: -6px; 3282 bottom: calc(100% + 8px); 3283 display: none; 3284 width: max-content; 3285 max-width: 230px; 3286 padding: 8px 10px; 3287 font-size: 12px; 3288 font-weight: 400; 3289 line-height: 1.45; 3290 color: #ffffff; 3291 background: #14313c; 3292 border-radius: 8px; 3293 box-shadow: 0 6px 18px rgba(15, 23, 42, 0.25); 3294 pointer-events: none; 3295 } 3296 3297 .sfd-info__tip::after { 3298 content: ""; 3299 position: absolute; 3300 left: 10px; 3301 top: 100%; 3302 border: 5px solid transparent; 3303 border-top-color: #14313c; 3304 } 3305 3306 .sfd-info:hover .sfd-info__tip, 3307 .sfd-info:focus-visible .sfd-info__tip { 3308 display: block; 3309 } 3310 3311 .sfd-info__line { 3312 display: block; 3313 } 3314 3315 .sfd-info__line b { 3316 font-weight: 700; 3317 } 3318 3319 /* ---- Toast ---- */ 3320 .stillframe-toast { 3321 position: fixed; 3322 z-index: 1000001; 3323 left: 50%; 3324 bottom: 24px; 3325 display: flex; 3326 align-items: center; 3327 gap: 11px; 3328 max-width: min(520px, calc(100vw - 32px)); 3329 padding: 10px 16px 10px 11px; 3330 font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; 3331 font-size: 13px; 3332 font-weight: 500; 3333 line-height: 1.35; 3334 color: #1f2937; 3335 background: #ffffff; 3336 border: 1px solid #e5e7eb; 3337 border-radius: 12px; 3338 box-shadow: 0 12px 32px rgba(15, 23, 42, 0.14), 0 2px 6px rgba(15, 23, 42, 0.06); 3339 opacity: 0; 3340 transform: translate(-50%, 10px); 3341 pointer-events: none; 3342 transition: opacity 0.2s ease, transform 0.2s ease; 3343 } 3344 3345 .stillframe-toast.is-visible { 3346 opacity: 1; 3347 transform: translate(-50%, 0); 3348 pointer-events: auto; 3349 } 3350 3351 .stillframe-toast__icon { 3352 flex: none; 3353 width: 24px; 3354 height: 24px; 3355 border-radius: 50%; 3356 background-color: #e6f4f8; 3357 background-repeat: no-repeat; 3358 background-position: center; 3359 background-size: 14px 14px; 3360 background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23005976' stroke-width='2.6' stroke-linecap='round' stroke-linejoin='round'><path d='M12 8v.01M12 12v5'/></svg>"); 3361 } 3362 3363 .stillframe-toast[data-tone="success"] .stillframe-toast__icon { 3364 background-color: #005976; 3365 background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23ffffff' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'><path d='M5 12.5l4.5 4.5L19 7.5'/></svg>"); 3366 } 3367 3368 .stillframe-toast[data-tone="error"] .stillframe-toast__icon { 3369 background-color: #fdecea; 3370 background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23b42318' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'><path d='M12 6v7M12 18v.01'/></svg>"); 3371 } 3372 3373 .stillframe-toast[data-tone="busy"] .stillframe-toast__icon { 3374 background-image: none; 3375 background-color: transparent; 3376 border: 2.5px solid #cfe5ec; 3377 border-top-color: #005976; 3378 animation: stillframe-toast-spin 0.8s linear infinite; 3379 } 3380 3381 .stillframe-toast__link { 3382 padding-left: 12px; 3383 font-weight: 600; 3384 color: #005976; 3385 text-decoration: none; 3386 white-space: nowrap; 3387 border-left: 1px solid #e5e7eb; 3388 } 3389 3390 .stillframe-toast__link:hover { 3391 text-decoration: underline; 3392 } 3393 3394 @keyframes stillframe-toast-spin { 3395 to { 3396 transform: rotate(360deg); 3397 } 3398 } 3399 3400 /* ---- Options for the selected mark ---- */ 3401 .stillframe-result .stillframe-result__props { 3402 display: flex; 3403 flex-wrap: wrap; 3404 align-items: center; 3405 gap: 6px 18px; 3406 padding: 6px 16px; 3407 background: #f9fafb; 3408 border-bottom: 1px solid var(--sf-line, #e5e7eb); 3409 } 3410 3411 .stillframe-result .stillframe-result__props[hidden] { 3412 display: none; 3413 } 3414 3415 .stillframe-result .stillframe-prop { 3416 display: inline-flex; 3417 align-items: center; 3418 gap: 8px; 3419 } 3420 3421 .stillframe-result .stillframe-prop__label { 3422 font-size: 11px; 3423 font-weight: 700; 3424 letter-spacing: 0.04em; 3425 text-transform: uppercase; 3426 color: var(--sf-muted, #6b7280); 3427 } 3428 3429 .stillframe-result .stillframe-prop__select { 3430 box-sizing: border-box !important; 3431 height: 28px !important; 3432 max-height: 28px !important; 3433 line-height: 26px !important; 3434 padding: 0 8px !important; 3435 font-size: 12px !important; 3436 font-weight: 500 !important; 3437 color: var(--sf-text-2, #374151) !important; 3438 background: #ffffff !important; 3439 border: 1px solid var(--sf-line-strong, #d1d5db) !important; 3440 border-radius: 6px !important; 3441 cursor: pointer; 3442 outline: none; 3443 } 3444 3445 .stillframe-result .stillframe-prop__select:hover { 3446 border-color: var(--sf-brand, #005976) !important; 3447 } 3448 3449 .stillframe-result .stillframe-prop__color { 3450 width: 28px; 3451 height: 28px; 3452 padding: 2px; 3453 background: #ffffff; 3454 border: 1px solid var(--sf-line-strong, #d1d5db); 3455 border-radius: 50%; 3456 cursor: pointer; 3457 } 3458 3459 .stillframe-result .stillframe-prop__toggle, 3460 .stillframe-result .stillframe-prop__seg-btn { 3461 box-sizing: border-box !important; 3462 height: 28px !important; 3463 max-height: 28px !important; 3464 min-width: 28px !important; 3465 line-height: 26px !important; 3466 padding: 0 8px !important; 3467 font-size: 12px !important; 3468 font-weight: 700 !important; 3469 color: var(--sf-text-2, #374151) !important; 3470 background: #ffffff !important; 3471 border: 1px solid var(--sf-line-strong, #d1d5db) !important; 3472 border-radius: 6px !important; 3473 cursor: pointer; 3474 transition: background 0.15s, color 0.15s, border-color 0.15s; 3475 } 3476 3477 .stillframe-result .stillframe-prop__seg { 3478 display: inline-flex; 3479 } 3480 3481 .stillframe-result .stillframe-prop__seg-btn:first-child { 3482 border-radius: 6px 0 0 6px; 3483 } 3484 3485 .stillframe-result .stillframe-prop__seg-btn:last-child { 3486 margin-left: -1px; 3487 border-radius: 0 6px 6px 0; 3488 } 3489 3490 .stillframe-result .stillframe-prop__toggle[aria-pressed="true"], 3491 .stillframe-result .stillframe-prop__seg-btn[aria-pressed="true"] { 3492 color: #ffffff; 3493 background: var(--sf-brand, #005976); 3494 border-color: var(--sf-brand, #005976); 3495 } 3496 3497 /* ---- Custom colour and thickness slider ---- */ 3498 .stillframe-result .stillframe-swatch-custom { 3499 appearance: none; 3500 -webkit-appearance: none; 3501 width: 18px; 3502 height: 18px; 3503 padding: 0; 3504 cursor: pointer; 3505 background: conic-gradient(#ef4444, #facc15, #22c55e, #06b6d4, #3b82f6, #a855f7, #ef4444); 3506 border: 0; 3507 border-radius: 50%; 3508 box-shadow: inset 0 0 0 1px rgba(17, 24, 39, 0.18); 3509 } 3510 3511 .stillframe-result .stillframe-swatch-custom::-webkit-color-swatch-wrapper { 3512 padding: 0; 3513 opacity: 0; 3514 } 3515 3516 .stillframe-result .stillframe-swatch-custom::-moz-color-swatch { 3517 opacity: 0; 3518 } 3519 3520 .stillframe-result .stillframe-size-slider { 3521 width: 92px; 3522 height: 4px; 3523 margin: 0 4px; 3524 cursor: pointer; 3525 accent-color: var(--sf-brand, #005976); 3526 } 3527 3528 /* ---- Text size control ---- */ 3529 .stillframe-result .stillframe-prop__size { 3530 display: inline-flex; 3531 align-items: center; 3532 gap: 8px; 3533 } 3534 3535 .stillframe-result .stillframe-prop__range { 3536 width: 140px; 3537 accent-color: var(--sf-brand, #005976); 3538 } 3539 3540 .stillframe-result .stillframe-prop__number { 3541 width: 58px; 3542 height: 28px; 3543 padding: 0 6px; 3544 font-size: 12.5px; 3545 color: var(--sf-text-2, #374151); 3546 background: #ffffff; 3547 border: 1px solid var(--sf-line-strong, #d1d5db); 3548 border-radius: 6px; 3549 } 3550 3551 /* ---- Top bar controls: exactly one height, whatever WordPress does to selects ---- */ 3552 .stillframe-result .stillframe-result__actions > .stillframe-btn, 3553 .stillframe-result .stillframe-result__actions > .stillframe-format-select { 3554 box-sizing: border-box !important; 3555 height: 36px !important; 3556 min-height: 36px !important; 3557 max-height: 36px !important; 3558 margin: 0 !important; 3559 line-height: 1 !important; 3560 border-radius: 8px; 3561 } 3562 3563 .stillframe-result .stillframe-result__actions > .stillframe-format-select { 3564 padding: 0 28px 0 12px !important; 3565 line-height: 34px !important; 3566 } 3567 3568 .stillframe-result .stillframe-result__actions > .stillframe-btn.stillframe-result__close { 3569 width: 36px !important; 3570 border-radius: 50% !important; 3571 } 3572 3573 .stillframe-result .stillframe-result__actions > .stillframe-btn:not(.stillframe-btn-primary):not(.stillframe-result__close) { 3574 min-width: 36px; 3575 } 3576 3577 /* Marks may hang past the picture, so the wrapper must not clip them */ 3578 .stillframe-result .stillframe-capture-panel__canvas-wrap:has(img) { 3579 overflow: visible; 3580 } 3581 3582 .stillframe-result .stillframe-capture-panel__canvas-wrap > img { 3583 border-radius: 4px; 3584 } 3585 3586 /* ---- Device sizes shown in Window mode ---- */ 3587 .stillframe-snip__devstrip { 3588 position: fixed; 3589 z-index: 7; 3590 top: 74px; 3591 left: 50%; 3592 transform: translateX(-50%); 3593 display: flex; 3594 align-items: center; 3595 gap: 6px; 3596 width: max-content; 3597 max-width: calc(100vw - 16px); 3598 margin: 0; 3599 padding: 5px 12px; 3600 background: var(--sf-surface, #ffffff); 3601 border: 1px solid var(--sf-line, #e5e7eb); 3602 border-radius: 10px; 3603 box-shadow: 0 8px 24px rgba(15, 23, 42, 0.16), 0 1px 3px rgba(15, 23, 42, 0.08); 3604 pointer-events: auto; 3605 } 3606 3607 .stillframe-snip__devstrip[hidden] { 3608 display: none; 3609 } 3610 3611 .stillframe-snip__devstrip-label { 3612 margin-right: 4px; 3613 font-size: 12px; 3614 font-weight: 600; 3615 color: var(--sf-muted, #6b7280); 3616 white-space: nowrap; 3617 } 3618 3619 .stillframe-snip .stillframe-snip__devstrip-btn { 3620 display: inline-flex; 3621 align-items: center; 3622 gap: 6px; 3623 height: 30px; 3624 padding: 0 10px; 3625 font-size: 12.5px; 3626 font-weight: 600; 3627 color: var(--sf-text-2, #374151); 3628 background: transparent; 3629 border: 1px solid transparent; 3630 border-radius: 8px; 3631 box-shadow: none; 3632 } 3633 3634 .stillframe-snip .stillframe-snip__devstrip-btn small { 3635 font-size: 11px; 3636 font-weight: 500; 3637 color: var(--sf-muted, #6b7280); 3638 } 3639 3640 .stillframe-snip .stillframe-snip__devstrip-btn:hover, 3641 .stillframe-snip .stillframe-snip__devstrip-btn[aria-pressed="true"] { 3642 color: var(--sf-brand, #005976); 3643 background: var(--sf-brand-muted, #e6f4f8); 3644 } -
stillframe/tags/0.2.5/assets/css/stillframe-tools.css
r3724658 r3736872 473 473 } 474 474 475 /* Neutral accents: the brand colour is only for the primary Capture button. */476 :root {477 --sf-ink: #1d2327;478 }479 480 .stillframe-select:focus,481 .stillframe-input:focus {482 border-color: var(--sf-ink) !important;483 box-shadow: none !important;484 }485 486 .stillframe-device-tile.is-active {487 border-color: var(--sf-ink);488 background: var(--sf-well);489 }490 491 .stillframe-device-tile.is-active .stillframe-device-tile__icon {492 color: var(--sf-ink);493 }494 495 .stillframe-device-tile.is-active .stillframe-device-tile__check {496 background: var(--sf-ink);497 border-color: var(--sf-ink);498 }499 500 .stillframe-device-tile.is-active .stillframe-device-tile__res {501 color: var(--sf-muted);502 font-weight: 400;503 }504 505 .stillframe-pill.is-active,506 .stillframe-pill.is-active:hover {507 background: #f0f0f1;508 border-color: var(--sf-ink);509 color: var(--sf-ink);510 font-weight: 600;511 }512 513 .stillframe-switch-wrap input.stillframe-switch-input:checked + .stillframe-switch-slider {514 background: var(--sf-ink);515 }516 517 /* Softer, button-like choices */518 .stillframe-pill {519 background: #fafafa;520 border-color: #b4bac4;521 }522 523 .stillframe-pill:hover {524 background: #f0f0f1;525 border-color: #6b7280;526 }527 528 .stillframe-pill.is-active,529 .stillframe-pill.is-active:hover {530 background: #eceef1;531 border-color: #6b7280;532 color: var(--sf-ink);533 }534 535 .stillframe-device-tile {536 border-width: 1px;537 border-color: #b4bac4;538 }539 540 .stillframe-device-tile.is-active {541 background: #f3f4f6;542 border-color: #6b7280;543 box-shadow: inset 0 0 0 1px #6b7280;544 }545 546 .stillframe-device-tile.is-active .stillframe-device-tile__check {547 background: #6b7280;548 border-color: #6b7280;549 }550 551 .stillframe-switch-wrap input.stillframe-switch-input:checked + .stillframe-switch-slider {552 background: #6b7280;553 }554 555 475 .stillframe-box__header { 556 476 padding: 18px 28px; -
stillframe/tags/0.2.5/assets/js/stillframe-admin-bar.js
r3724658 r3736872 35 35 throw new Error('origin'); 36 36 } 37 if (config.version) { 38 parsed.searchParams.set('ver', String(config.version)); 37 var ver = config.assetVer || config.version; 38 if (ver) { 39 parsed.searchParams.set('ver', String(ver)); 39 40 } 40 41 return parsed.toString(); … … 93 94 return loading; 94 95 } 96 var useFabric = config.useFabric !== false; 95 97 loading = Promise.resolve().then(function () { 96 return Promise.all([98 var initial = [ 97 99 loadStylesheet(assetUrl('assets/css/stillframe-capture-panel.css'), 'stillframe-capture-panel-css'), 98 loadScript(assetUrl('assets/js/stillframe-annotation-editor.js'), 'stillframe-annotation-editor-js'),99 100 loadScript(assetUrl('assets/js/vendor/modern-screenshot.js'), 'stillframe-modern-screenshot-js') 100 ]); 101 ]; 102 if (useFabric) { 103 initial.push(loadScript(assetUrl('assets/js/vendor/fabric.min.js'), 'stillframe-fabric-js')); 104 } 105 return Promise.all(initial); 106 }).then(function () { 107 var editorScript = useFabric ? 'assets/js/stillframe-annotation-editor-v2.js' : 'assets/js/stillframe-annotation-editor.js'; 108 return loadScript(assetUrl(editorScript), 'stillframe-annotation-editor-js'); 101 109 }).then(function () { 102 110 return loadScript(assetUrl('assets/js/stillframe-capture-panel.js'), 'stillframe-capture-panel-js'); -
stillframe/tags/0.2.5/assets/js/stillframe-annotation-editor.js
r3724658 r3736872 11 11 var BOX_TYPES = { circle: 1, rect: 1, highlight: 1, blur: 1 }; 12 12 var NO_ROTATE = { arrow: 1, blur: 1, step: 1 }; 13 var UNIFORM = { text: 1,step: 1 };13 var UNIFORM = { step: 1 }; 14 14 var MAX_MARKS = 400; 15 15 var MAX_PEN_POINTS = 3000; 16 var HANDLE_PX = 9;17 var ROTATE_OFFSET = 26;16 var HANDLE_PX = 6; 17 var ROTATE_OFFSET = 16; 18 18 var measureCtx = null; 19 19 … … 580 580 return; 581 581 } 582 var wShape = b.r - b.l; 583 var hShape = b.b - b.t; 584 var minShape = Math.min(wShape, hShape); 585 var hRad = minShape < 35 ? 2.5 : (minShape < 70 ? 3 : 3.5); 582 586 ctx.fillStyle = '#ffffff'; 583 ctx.strokeStyle = '#374151'; 587 ctx.strokeStyle = '#005976'; 588 ctx.lineWidth = 1.5; 584 589 ctx.beginPath(); 585 590 if (hd.id === 'rot') { … … 588 593 ctx.stroke(); 589 594 ctx.beginPath(); 590 ctx.arc(hd.x, hd.y, 5, 0, Math.PI * 2);595 ctx.arc(hd.x, hd.y, hRad, 0, Math.PI * 2); 591 596 } else { 592 ctx. rect(hd.x - 4, hd.y - 4, 8, 8);597 ctx.arc(hd.x, hd.y, hRad, 0, Math.PI * 2); 593 598 } 594 599 ctx.fill(); … … 970 975 m.x1 = cw.x / d.w; 971 976 m.y1 = cw.y / d.h; 977 } 978 } else if (o.type === 'text') { 979 var isSide = h === 'w' || h === 'e' || h === 'ml' || h === 'mr'; 980 if (isSide) { 981 var rawW = (h.indexOf('w') > -1 || h === 'ml') ? b.r - local.x : local.x - b.l; 982 m.w = Math.max(0.04, Math.min(1.5, rawW / d.w)); 972 983 } else { 973 m.fs = Math.max(8, Math.min(240, fontSize(o) * s)); 984 var rawW = h.indexOf('w') > -1 ? b.r - local.x : local.x - b.l; 985 var rawH = h.indexOf('n') > -1 ? b.b - local.y : local.y - b.t; 986 var s = Math.max(rawW / ow, rawH / oh, min / Math.min(ow, oh)); 987 m.fs = Math.max(8, Math.min(240, Math.round(fontSize(o) * s))); 974 988 if (o.w) { 975 m.w = Math.min(1 , o.w * s);989 m.w = Math.min(1.5, o.w * s); 976 990 } 977 var lay = textLayout(m, d.w);978 m.x1 = (cw.x - lay.width / 2) / d.w;979 m.y1 = (cw.y - lay.height / 2) / d.h;980 991 } 981 992 return; -
stillframe/tags/0.2.5/assets/js/stillframe-capture-panel.js
r3727291 r3736872 14 14 var returnFocus = null; 15 15 var captured = null; 16 var resultDevices = null; 16 17 var statusNode = null; 17 18 var propsNode = null; … … 56 57 }; 57 58 58 // Above this many elements, or after a render slower than SLOW_RENDER_MS, 59 // the page is rendered only after the selection is released. A background 60 // render freezes the main thread and would make the drag stutter. 61 var HEAVY_PAGE_NODES = 1500; 59 // A background render freezes the main thread, so it waits until the pointer 60 // has been idle. Full screen starts it immediately. 62 61 var SLOW_RENDER_MS = 700; 63 62 var IDLE_BEFORE_RENDER_MS = 1500; 64 63 65 64 function canPrerender() { 66 // Do not prerender in the background during active capture — it freezes the main thread67 // on pages with many DOM nodes, causing dropped clicks and sluggish interaction.68 return false;65 // The redraw is heavy, so it starts only after the pointer has been idle. 66 // That way a full-screen shot can already be underway before Capture is clicked. 67 return true; 69 68 } 70 69 … … 305 304 } 306 305 306 // Read-only actions such as "edit" stay. These values change data if replayed. 307 var MUTATING_ACTIONS = [ 308 'trash', 'untrash', 'delete', 'delete-all', 'editpost', 'postajaxpost', 'post-quickdraft-save', 309 'deletecomment', 'trashcomment', 'untrashcomment', 'spamcomment', 'unspamcomment', 'approvecomment', 'unapprovecomment', 310 'approve', 'unapprove', 'spam', 'unspam', 'dim-comment', 311 'activate', 'deactivate', 'activate-selected', 'deactivate-selected', 'update-selected', 'delete-selected', 312 'enable-auto-update', 'disable-auto-update', 'error_scrape', 'resume', 313 'upgrade-plugin', 'upgrade-theme', 'do-plugin-upgrade', 'do-theme-upgrade', 'do-core-upgrade', 'do-translation-upgrade', 314 'install-plugin', 'install-theme', 'upload-plugin', 'upload-theme', 315 'logout', 'remove', 'deleteblog', 'spamblog' 316 ]; 317 318 function mutatingActions() { 319 return Array.isArray(config.mutatingActions) && config.mutatingActions.length 320 ? config.mutatingActions 321 : MUTATING_ACTIONS; 322 } 323 324 function actionIsMutating(value) { 325 var list = mutatingActions(); 326 if (Array.isArray(value)) { 327 return value.some(actionIsMutating); 328 } 329 var name = String(value || '').toLowerCase(); 330 if (!name) { 331 return false; 332 } 333 return list.indexOf(name) !== -1; 334 } 335 336 // Drop nonces and state-changing actions from any URL the capture iframe will open. 337 // Read-only actions such as edit stay, so a pasted editor link still opens the editor. 338 function scrubCaptureUrl(urlString) { 339 var current = new URL(window.location.href); 340 var target; 341 try { 342 target = new URL(String(urlString || current.href), current.href); 343 } catch (error) { 344 target = new URL(current.href); 345 } 346 var strip = Array.isArray(config.stripArgs) && config.stripArgs.length 347 ? config.stripArgs 348 : ['_wpnonce', 'nonce', '_wp_http_referer', 'wp_customize']; 349 strip.forEach(function (key) { 350 if (String(key) === 'action') { 351 return; 352 } 353 target.searchParams.delete(String(key)); 354 }); 355 var actions = target.searchParams.getAll('action'); 356 if (actions.some(actionIsMutating)) { 357 target.searchParams.delete('action'); 358 } 359 target.hash = ''; 360 return target.toString(); 361 } 362 307 363 function captureTargetUrl() { 308 364 var current = new URL(window.location.href); … … 316 372 target = new URL(current.href); 317 373 } 318 var strip = Array.isArray(config.stripArgs) && config.stripArgs.length 319 ? config.stripArgs 320 : ['action', '_wpnonce', 'nonce', '_wp_http_referer', 'wp_customize']; 321 strip.forEach(function (key) { 322 target.searchParams.delete(String(key)); 323 }); 324 target.hash = ''; 325 return target.toString(); 374 return scrubCaptureUrl(target.toString()); 326 375 } 327 376 … … 1478 1527 } 1479 1528 1529 // The redraw keeps each box at its on-screen width but can draw text a 1530 // fraction wider, so a row that fit on one line (pagination, the admin bar 1531 // avatar) wraps in the shot. Rows that sit on one line now are marked so the 1532 // copy cannot wrap them; multi-line boxes inside them keep wrapping. 1533 var ATOMIC_TAGS = { IMG: true, INPUT: true, SELECT: true, TEXTAREA: true, BUTTON: true, SVG: true, svg: true, CANVAS: true, VIDEO: true, IFRAME: true, OBJECT: true, EMBED: true }; 1534 1535 // Boxes that sit on the lines of el. Text and inline boxes count; block 1536 // children count as one box each. Out-of-flow content such as WordPress's 1537 // screen-reader-text labels (absolute, clipped to 1px) is ignored. 1538 function lineRects(el, win, range) { 1539 var rects = []; 1540 var stack = [el]; 1541 while (stack.length) { 1542 var node = stack.pop(); 1543 var child; 1544 for (child = node.lastChild; child; child = child.previousSibling) { 1545 if (child.nodeType === 3) { 1546 if (/\S/.test(child.data)) { 1547 range.selectNodeContents(child); 1548 Array.prototype.push.apply(rects, range.getClientRects()); 1549 } 1550 continue; 1551 } 1552 if (child.nodeType !== 1) { 1553 continue; 1554 } 1555 var cs = win.getComputedStyle(child); 1556 if (cs.display === 'none' || cs.position === 'absolute' || cs.position === 'fixed') { 1557 continue; 1558 } 1559 if (cs.display === 'contents' || (cs.display === 'inline' && !ATOMIC_TAGS[child.tagName])) { 1560 stack.push(child); 1561 continue; 1562 } 1563 rects.push(child.getBoundingClientRect()); 1564 } 1565 } 1566 return rects; 1567 } 1568 1569 function markSingleLines(doc, crop) { 1570 var marked = []; 1571 var win = doc.defaultView; 1572 var all = doc.body ? doc.body.getElementsByTagName('*') : []; 1573 var range = doc.createRange(); 1574 var i; 1575 for (i = 0; i < all.length; i++) { 1576 var el = all[i]; 1577 var box = el.getBoundingClientRect(); 1578 if (!box.width || !box.height || box.height > 160) { 1579 continue; 1580 } 1581 if (crop && (box.bottom < crop.y || box.top > crop.y + crop.height || box.right < crop.x || box.left > crop.x + crop.width)) { 1582 continue; 1583 } 1584 var style; 1585 try { 1586 style = win.getComputedStyle(el); 1587 } catch (error) { 1588 continue; 1589 } 1590 if (style.whiteSpace !== 'normal' || style.display === 'inline' || style.display === 'contents' || style.display === 'none') { 1591 continue; 1592 } 1593 var rects = lineRects(el, win, range); 1594 var top = -Infinity; 1595 var bottom = Infinity; 1596 var count = 0; 1597 var r; 1598 for (r = 0; r < rects.length; r++) { 1599 if (!rects[r].width || !rects[r].height) { 1600 continue; 1601 } 1602 top = Math.max(top, rects[r].top); 1603 bottom = Math.min(bottom, rects[r].bottom); 1604 count++; 1605 } 1606 if (!count) { 1607 continue; 1608 } 1609 var name = top < bottom ? 'data-stillframe-line' : 'data-stillframe-wrap'; 1610 if (name === 'data-stillframe-wrap' && !(el.parentElement && el.parentElement.closest('[data-stillframe-line]'))) { 1611 continue; 1612 } 1613 el.setAttribute(name, '1'); 1614 marked.push([el, name]); 1615 } 1616 if (range.detach) { 1617 range.detach(); 1618 } 1619 return function () { 1620 marked.forEach(function (item) { 1621 item[0].removeAttribute(item[1]); 1622 }); 1623 }; 1624 } 1625 1480 1626 function prepareClone(root, view, crop) { 1481 1627 if (!root || root.nodeType !== 1) { … … 1483 1629 } 1484 1630 var doc = view.document; 1631 // The copy writes text-wrap onto every element, so a one-line row is 1632 // held on one line by setting it on each element inside it, except 1633 // inside boxes that wrap on screen. text-wrap-mode leaves spacing alone. 1634 var wrapProp = view.CSS && view.CSS.supports && view.CSS.supports('text-wrap-mode', 'nowrap') ? 'text-wrap-mode' : 'white-space'; 1635 Array.prototype.forEach.call(root.querySelectorAll('[data-stillframe-line], [data-stillframe-line] *'), function (node) { 1636 var owner = node.closest('[data-stillframe-line], [data-stillframe-wrap]'); 1637 if (owner && owner.hasAttribute('data-stillframe-line') && node.style) { 1638 node.style.setProperty(wrapProp, 'nowrap', 'important'); 1639 } 1640 }); 1485 1641 Array.prototype.forEach.call(root.querySelectorAll('[data-stillframe-off]'), function (node) { 1486 1642 if (node.tagName === 'IMG') { … … 1550 1706 } 1551 1707 1708 function fontNames(value) { 1709 return String(value || '').toLowerCase().split(',').map(function (part) { 1710 return part.trim().replace(/^["']|["']$/g, ''); 1711 }).filter(Boolean); 1712 } 1713 1714 // Drop the CSS alt-text separator in values such as "\f148" / "". 1715 function stripContentAlt(text) { 1716 var quote = ''; 1717 var i; 1718 for (i = 0; i < text.length; i++) { 1719 var ch = text.charAt(i); 1720 if (quote) { 1721 if (ch === '\\') { 1722 i++; 1723 continue; 1724 } 1725 if (ch === quote) { 1726 quote = ''; 1727 } 1728 continue; 1729 } 1730 if (ch === '"' || ch === "'") { 1731 quote = ch; 1732 continue; 1733 } 1734 if (ch === '/' && i > 0 && text.charAt(i - 1) === ' ' && text.charAt(i + 1) === ' ') { 1735 return text.slice(0, i - 1).trim(); 1736 } 1737 } 1738 return text; 1739 } 1740 1741 function quotedStrings(text) { 1742 var out = []; 1743 var i = 0; 1744 while (i < text.length) { 1745 var ch = text.charAt(i); 1746 if (ch !== '"' && ch !== "'") { 1747 i++; 1748 continue; 1749 } 1750 var quote = ch; 1751 var buf = ''; 1752 i++; 1753 while (i < text.length) { 1754 var c = text.charAt(i); 1755 if (c === '\\') { 1756 i++; 1757 if (i < text.length) { 1758 buf += text.charAt(i); 1759 } 1760 i++; 1761 continue; 1762 } 1763 if (c === quote) { 1764 i++; 1765 break; 1766 } 1767 buf += c; 1768 i++; 1769 } 1770 out.push(buf); 1771 } 1772 return out; 1773 } 1774 1775 // getComputedStyle returns a quoted string for content, or the keyword none. 1776 function pseudoText(content) { 1777 if (!content || content === 'none' || content === 'normal') { 1778 return ''; 1779 } 1780 var text = stripContentAlt(String(content).trim()); 1781 if (/^(?:url|attr|counter|counters|image-set|-webkit-image-set|[a-z-]*gradient)\(/i.test(text)) { 1782 return ''; 1783 } 1784 var parts = quotedStrings(text); 1785 if (parts.length) { 1786 text = parts.join(''); 1787 } 1788 if (!text || text === 'none' || text === 'normal') { 1789 return ''; 1790 } 1791 return text; 1792 } 1793 1794 function usesIconFont(fontFamily) { 1795 return /dashicons|fontawesome|font-awesome|font awesome|material-icons|material icons|glyphicons|icomoon|ionicons|bootstrap-icons|feather|remixicon|tabler/i.test(fontFamily || ''); 1796 } 1797 1798 // A slash in the shot is a private-use icon character written into a CSS ::before. 1799 // Real text in that same font draws; the pseudo's character does not. 1800 function needsRealGlyph(text, fontFamily) { 1801 var i; 1802 if (!text || /^\s+$/.test(text)) { 1803 return false; 1804 } 1805 if (usesIconFont(fontFamily)) { 1806 return true; 1807 } 1808 for (i = 0; i < text.length; i++) { 1809 if (text.charCodeAt(i) > 126) { 1810 return true; 1811 } 1812 } 1813 return false; 1814 } 1815 1816 function iconContentSelectors(doc) { 1817 var list = []; 1818 var seen = {}; 1819 function add(selectorText) { 1820 if (!selectorText) { 1821 return; 1822 } 1823 selectorText.split(',').forEach(function (raw) { 1824 var sel = raw.replace(/::(?:before|after)/gi, '').replace(/:(?:before|after)/gi, '').trim(); 1825 if (!sel || seen[sel]) { 1826 return; 1827 } 1828 seen[sel] = true; 1829 list.push(sel); 1830 }); 1831 } 1832 function walk(rules) { 1833 var i; 1834 for (i = 0; i < rules.length; i++) { 1835 var rule = rules[i]; 1836 if (rule.cssRules && rule.cssRules.length) { 1837 try { 1838 walk(rule.cssRules); 1839 } catch (error) {} 1840 } 1841 if (!rule.style || !rule.selectorText) { 1842 continue; 1843 } 1844 if (rule.selectorText.indexOf('before') === -1 && rule.selectorText.indexOf('after') === -1) { 1845 continue; 1846 } 1847 var content = rule.style.getPropertyValue('content'); 1848 if (!content || content === 'none' || content === 'normal' || content === '""' || content === "''") { 1849 continue; 1850 } 1851 if (/counter\(|url\(|attr\(|gradient|image-set|open-quote|close-quote/i.test(content)) { 1852 continue; 1853 } 1854 if (!needsRealGlyph(pseudoText(content), rule.style.getPropertyValue('font-family'))) { 1855 continue; 1856 } 1857 add(rule.selectorText); 1858 } 1859 } 1860 var sheets = doc.styleSheets; 1861 var s; 1862 for (s = 0; s < sheets.length; s++) { 1863 var rules; 1864 try { 1865 rules = sheets[s].cssRules; 1866 } catch (error2) { 1867 continue; 1868 } 1869 if (rules) { 1870 walk(rules); 1871 } 1872 } 1873 return list; 1874 } 1875 1876 function iconCandidates(root) { 1877 var doc = root.ownerDocument || document; 1878 var found = []; 1879 var seen = typeof WeakSet === 'function' ? new WeakSet() : null; 1880 function add(el) { 1881 if (!el || el.nodeType !== 1) { 1882 return; 1883 } 1884 if (seen) { 1885 if (seen.has(el)) { 1886 return; 1887 } 1888 seen.add(el); 1889 } 1890 found.push(el); 1891 } 1892 function take(list) { 1893 var i; 1894 for (i = 0; i < list.length; i++) { 1895 add(list[i]); 1896 } 1897 } 1898 try { 1899 take(root.querySelectorAll('[class*="dashicons"], .wp-menu-image, .ab-icon, .view-switch a, .collapse-button-icon')); 1900 } catch (error) { 1901 return found; 1902 } 1903 var selectors = iconContentSelectors(doc); 1904 var batch = []; 1905 function flush() { 1906 if (!batch.length) { 1907 return; 1908 } 1909 var query = batch.join(','); 1910 batch = []; 1911 try { 1912 take(root.querySelectorAll(query)); 1913 } catch (error2) { 1914 query.split(',').forEach(function (sel) { 1915 try { 1916 take(root.querySelectorAll(sel)); 1917 } catch (error3) { 1918 return; 1919 } 1920 }); 1921 } 1922 } 1923 var i; 1924 for (i = 0; i < selectors.length; i++) { 1925 batch.push(selectors[i]); 1926 if (batch.length === 20) { 1927 flush(); 1928 } 1929 } 1930 flush(); 1931 return found; 1932 } 1933 1934 // Icon fonts such as dashicons live on ::before / ::after. The redraw turns 1935 // those characters into slashes, so the glyph is painted as real text instead. 1936 // The live page is only marked. The glyphs go into the redraw's copy, where 1937 // no page stylesheet applies, so nothing on screen moves while the shot is 1938 // taken and no theme rule (span, :first-child, :empty) can restyle them. 1939 var GLYPH_ATTR = 'data-stillframe-glyph'; 1940 1941 function pinIconFonts(root) { 1942 var marked = []; 1943 var glyphs = {}; 1944 var families = []; 1945 var seenFamily = {}; 1946 var doc = root && root.ownerDocument; 1947 var none = { 1948 undo: function () {}, 1949 apply: function () {}, 1950 families: families 1951 }; 1952 if (!root || root.nodeType !== 1 || !root.querySelectorAll || !doc) { 1953 return none; 1954 } 1955 var view = doc.defaultView || window; 1956 var skip = { 1957 IMG: true, 1958 INPUT: true, 1959 TEXTAREA: true, 1960 SELECT: true, 1961 OPTION: true, 1962 BR: true, 1963 HR: true, 1964 SVG: true, 1965 CANVAS: true, 1966 VIDEO: true, 1967 IFRAME: true, 1968 SCRIPT: true, 1969 STYLE: true 1970 }; 1971 var nodes = iconCandidates(root); 1972 var i; 1973 for (i = 0; i < nodes.length; i++) { 1974 var el = nodes[i]; 1975 if (!el || skip[el.tagName]) { 1976 continue; 1977 } 1978 if (el.namespaceURI && el.namespaceURI !== 'http://www.w3.org/1999/xhtml') { 1979 continue; 1980 } 1981 if (el.closest && el.closest('.stillframe-snip, .stillframe-result, .stillframe-capture-panel, .stillframe-capture-frame')) { 1982 continue; 1983 } 1984 var elStyle; 1985 try { 1986 elStyle = view.getComputedStyle(el); 1987 } catch (error) { 1988 continue; 1989 } 1990 if (elStyle.display === 'none' || elStyle.visibility === 'hidden') { 1991 continue; 1992 } 1993 var pseudos = ['::before', '::after']; 1994 var p; 1995 for (p = 0; p < pseudos.length; p++) { 1996 var pseudo = pseudos[p]; 1997 var cs; 1998 try { 1999 cs = view.getComputedStyle(el, pseudo); 2000 } catch (error2) { 2001 continue; 2002 } 2003 var text = pseudoText(cs.content); 2004 if (!needsRealGlyph(text, cs.fontFamily)) { 2005 continue; 2006 } 2007 // Every computed property, so borders, min-width, flex and float 2008 // match the pseudo-element exactly. 2009 var style = []; 2010 var c; 2011 for (c = 0; c < cs.length; c++) { 2012 var prop = cs.item(c); 2013 if (prop !== 'content') { 2014 style.push([prop, cs.getPropertyValue(prop)]); 2015 } 2016 } 2017 var id = el.getAttribute(GLYPH_ATTR); 2018 if (!id) { 2019 id = String(marked.length); 2020 el.setAttribute(GLYPH_ATTR, id); 2021 marked.push(el); 2022 glyphs[id] = {}; 2023 } 2024 glyphs[id][pseudo] = { text: text, style: style }; 2025 var fam = cs.fontFamily; 2026 if (fam && !seenFamily[fam]) { 2027 seenFamily[fam] = true; 2028 families.push(fam); 2029 } 2030 } 2031 } 2032 if (!marked.length) { 2033 return none; 2034 } 2035 2036 function makeGlyph(cloneDoc, entry) { 2037 var span = cloneDoc.createElement('span'); 2038 span.setAttribute('aria-hidden', 'true'); 2039 entry.style.forEach(function (pair) { 2040 span.style.setProperty(pair[0], pair[1]); 2041 }); 2042 span.textContent = entry.text; 2043 return span; 2044 } 2045 2046 return { 2047 // Swap the pseudo-elements for real glyphs inside the cloned tree. 2048 apply: function (cloned) { 2049 if (!cloned || cloned.nodeType !== 1 || !cloned.querySelectorAll) { 2050 return; 2051 } 2052 var cloneDoc = cloned.ownerDocument || doc; 2053 var nodes = Array.prototype.slice.call(cloned.querySelectorAll('[' + GLYPH_ATTR + ']')); 2054 if (cloned.hasAttribute(GLYPH_ATTR)) { 2055 nodes.unshift(cloned); 2056 } 2057 if (!nodes.length) { 2058 return; 2059 } 2060 nodes.forEach(function (host) { 2061 var entry = glyphs[host.getAttribute(GLYPH_ATTR)]; 2062 host.removeAttribute(GLYPH_ATTR); 2063 if (!entry) { 2064 return; 2065 } 2066 if (entry['::before']) { 2067 host.insertBefore(makeGlyph(cloneDoc, entry['::before']), host.firstChild); 2068 host.classList.add('stillframe-icon-before'); 2069 } 2070 if (entry['::after']) { 2071 host.appendChild(makeGlyph(cloneDoc, entry['::after'])); 2072 host.classList.add('stillframe-icon-after'); 2073 } 2074 }); 2075 var styleTag = cloneDoc.createElement('style'); 2076 styleTag.textContent = '.stillframe-icon-before::before{content:none!important;}.stillframe-icon-after::after{content:none!important;}'; 2077 (cloned.querySelector('body') || cloned).appendChild(styleTag); 2078 }, 2079 undo: function () { 2080 marked.forEach(function (el) { 2081 el.removeAttribute(GLYPH_ATTR); 2082 }); 2083 }, 2084 families: families 2085 }; 2086 } 2087 2088 function loadIconFonts(view, families) { 2089 var fonts = view && view.document && view.document.fonts; 2090 if (!fonts || typeof fonts.load !== 'function' || !families || !families.length) { 2091 return Promise.resolve(); 2092 } 2093 return Promise.all(families.map(function (family) { 2094 return fonts.load('20px ' + family).catch(function () { 2095 return null; 2096 }); 2097 })).then(function () { 2098 return fonts.ready; 2099 }).catch(function () { 2100 return null; 2101 }); 2102 } 2103 1552 2104 function renderCanvas(node, scale, bounds, view) { 1553 2105 view = view || window; … … 1558 2110 } 1559 2111 var crop = bounds && bounds.crop ? bounds.crop : null; 1560 var unmark = crop ? markElements(view.document, crop) : function () {}; 2112 var unmarkCrop = crop ? markElements(view.document, crop) : function () {}; 2113 var unmarkLines = function () {}; 2114 try { 2115 unmarkLines = markSingleLines(view.document, crop); 2116 } catch (lineError) { 2117 unmarkLines = function () {}; 2118 } 2119 var unmark = function () { 2120 unmarkCrop(); 2121 unmarkLines(); 2122 }; 2123 var pinned = { undo: function () {}, apply: function () {}, families: [] }; 2124 try { 2125 pinned = pinIconFonts(node); 2126 } catch (pinError) { 2127 pinned = { undo: function () {}, apply: function () {}, families: [] }; 2128 } 1561 2129 try { 1562 2130 var options = { … … 1573 2141 onCloneNode: function (cloned) { 1574 2142 try { 2143 pinned.apply(cloned); 2144 } catch (glyphError) { 2145 // The shot still renders; icons may show as slashes. 2146 } 2147 try { 1575 2148 prepareClone(cloned, view, crop); 1576 2149 } catch (cloneError) { … … 1583 2156 options.height = bounds.height; 1584 2157 } 1585 return window.modernScreenshot.domToCanvas(node, options).then(function (canvas) { 2158 var nodeView = (node.ownerDocument && node.ownerDocument.defaultView) || view; 2159 return loadIconFonts(nodeView, pinned.families).then(function () { 2160 return window.modernScreenshot.domToCanvas(node, options); 2161 }).then(function (canvas) { 2162 pinned.undo(); 1586 2163 unmark(); 1587 2164 return canvas; 1588 2165 }, function (error) { 2166 pinned.undo(); 1589 2167 unmark(); 1590 2168 throw error; 1591 2169 }); 1592 2170 } catch (error) { 2171 pinned.undo(); 1593 2172 unmark(); 1594 2173 if (error && !error.code) { … … 1749 2328 var token = session; 1750 2329 1751 function compatCapture() { 2330 function compatNote(error) { 2331 var name = error && error.name ? String(error.name) : ''; 2332 var message = error && error.message ? String(error.message) : ''; 2333 if (name === 'NotAllowedError' || name === 'AbortError') { 2334 return tr('exactCancelled', 'Sharing was cancelled, so this shot was redrawn from the page. The next capture will ask to share the tab again.'); 2335 } 2336 if (message === 'surface') { 2337 return tr('exactSurface', 'That share was not this browser tab, so this shot was redrawn from the page. The next capture will ask again.'); 2338 } 2339 if (message === 'mismatch') { 2340 return tr('exactMismatch', 'The tab capture did not match this window, so this shot was redrawn from the page. The next capture will ask again.'); 2341 } 2342 return tr('exactFallback', 'Pixel-perfect capture did not finish, so this shot was redrawn from the page. The next capture will ask again.'); 2343 } 2344 2345 function compatCapture(note) { 1752 2346 var work = getViewCanvas().then(function (canvas) { 1753 2347 return cropCanvasToBlob(canvas, rect, viewShot.scale); 1754 2348 }); 2349 if (note) { 2350 rect.compatNote = note; 2351 } 1755 2352 openResult(rect, work, token); 1756 2353 destroySnip(); … … 1768 2365 openResult(rect, Promise.resolve(result.blob), token); 1769 2366 destroySnip(); 1770 }).catch(function ( ) {2367 }).catch(function (error) { 1771 2368 if (hideEl) { 1772 2369 hideEl.style.visibility = ''; … … 1776 2373 return; 1777 2374 } 1778 // Denied or unsupported: use the compatible DOM render for the rest of this page view. 1779 exact.failed = true; 2375 // One failure does not turn pixel-perfect off. The next capture asks again. 1780 2376 stopExact(); 1781 compatCapture(); 1782 }); 2377 compatCapture(compatNote(error)); 2378 }); 2379 return; 2380 } 2381 if (exactPref()) { 2382 compatCapture(tr('exactUnsupported', 'This browser cannot share the tab, so this shot was redrawn from the page.')); 1783 2383 return; 1784 2384 } … … 2116 2716 } 2117 2717 2118 // Drop originals that are no longer in the list, and the oldest ones past the size cap. 2119 function recentPrune(list) { 2718 // A blob can land in IndexedDB before its card is in the list. Don't delete it in that window. 2719 var RECENT_GRACE_MS = 120000; 2720 2721 function commitRecentList(build) { 2722 var next = []; 2723 var pass; 2724 for (pass = 0; pass < 3; pass++) { 2725 next = build(getRecentCaptures().slice()); 2726 if (!Array.isArray(next)) { 2727 next = []; 2728 } 2729 if (next.length > RECENT_MAX) { 2730 next = next.slice(0, RECENT_MAX); 2731 } 2732 while (next.length) { 2733 try { 2734 window.localStorage.setItem(RECENT_KEY, JSON.stringify(next)); 2735 break; 2736 } catch (error) { 2737 next.pop(); 2738 } 2739 } 2740 if (!next.length) { 2741 try { 2742 window.localStorage.removeItem(RECENT_KEY); 2743 } catch (removeError) { 2744 return []; 2745 } 2746 } 2747 } 2748 return getRecentCaptures(); 2749 } 2750 2751 function bitmapScale(cssWidth, pixelWidth, fallback) { 2752 var css = Number(cssWidth); 2753 var pixels = Number(pixelWidth); 2754 if (pixels > 0 && css > 0) { 2755 var ratio = pixels / css; 2756 if (ratio >= 0.5 && ratio <= 6) { 2757 return Math.round(ratio * 100) / 100; 2758 } 2759 } 2760 var scale = Number(fallback); 2761 if (isFinite(scale) && scale > 0) { 2762 return Math.round(scale * 100) / 100; 2763 } 2764 return 1; 2765 } 2766 2767 function recentScaleOf(item) { 2768 return bitmapScale(item && item.width, item && item.pixelWidth, item && item.scale); 2769 } 2770 2771 function cleanRecentTitle(title, url) { 2772 var cleanTitle = title || ''; 2773 if (!cleanTitle || cleanTitle.indexOf('http') === 0) { 2774 try { 2775 cleanTitle = new URL(url || window.location.href).pathname || 'Screenshot'; 2776 if (cleanTitle === '/') { 2777 cleanTitle = 'Home Page'; 2778 } 2779 } catch (error) { 2780 cleanTitle = 'Screenshot'; 2781 } 2782 } 2783 return cleanTitle; 2784 } 2785 2786 // Drop originals that fell off the list, and the oldest ones past the size cap. 2787 // Re-reads the list inside IndexedDB so a second capture is not deleted first. 2788 function recentPrune() { 2120 2789 return idbRun('readwrite', function (store) { 2121 2790 var all = store.getAll(); … … 2123 2792 var byId = {}; 2124 2793 (all.result || []).forEach(function (row) { 2125 byId[row.id] = row; 2126 }); 2127 var total = 0; 2128 var keep = {}; 2129 list.forEach(function (item) { 2130 var row = byId[item.id]; 2131 if (row && total + (row.size || 0) <= RECENT_BYTES) { 2132 total += row.size || 0; 2133 keep[item.id] = true; 2794 if (row && row.id) { 2795 byId[row.id] = row; 2134 2796 } 2135 2797 }); 2798 var list = getRecentCaptures(); 2799 var now = Date.now(); 2800 var total = 0; 2801 var over = false; 2802 var keep = {}; 2803 var evict = {}; 2804 list.forEach(function (item) { 2805 if (!item || !item.id) { 2806 return; 2807 } 2808 var row = byId[item.id]; 2809 var size = row && row.size ? row.size : (item.size || 0); 2810 if (!over && total + size <= RECENT_BYTES) { 2811 total += size; 2812 keep[item.id] = true; 2813 return; 2814 } 2815 over = true; 2816 evict[item.id] = true; 2817 }); 2136 2818 Object.keys(byId).forEach(function (id) { 2137 if (!keep[id]) { 2819 if (keep[id]) { 2820 return; 2821 } 2822 if (evict[id]) { 2138 2823 store.delete(id); 2824 return; 2139 2825 } 2826 var row = byId[id]; 2827 if (row && row.timestamp && (now - row.timestamp) < RECENT_GRACE_MS) { 2828 return; 2829 } 2830 store.delete(id); 2140 2831 }); 2832 if (over) { 2833 commitRecentList(function (latest) { 2834 return latest.filter(function (item) { 2835 return item && !evict[item.id]; 2836 }); 2837 }); 2838 } 2141 2839 }; 2142 2840 return all; … … 2200 2898 } 2201 2899 2202 function saveCaptureToRecent(blob, width, height, title, url ) {2900 function saveCaptureToRecent(blob, width, height, title, url, scale) { 2203 2901 if (!blob || !width || !height) return; 2204 2902 var id = 'sf_' + Date.now() + '_' + Math.random().toString(36).substr(2, 5); 2903 var storedScale = Number(scale); 2904 if (!isFinite(storedScale) || storedScale <= 0) { 2905 storedScale = 1; 2906 } 2907 storedScale = Math.round(storedScale * 100) / 100; 2908 var item = { 2909 id: id, 2910 title: cleanRecentTitle(title, url), 2911 url: url || window.location.href, 2912 width: width, 2913 height: height, 2914 scale: storedScale, 2915 timestamp: Date.now(), 2916 size: blob.size, 2917 thumb: '' 2918 }; 2205 2919 recentBlobs[id] = blob; 2920 // The card is in the list before the thumbnail or the prune, so a second capture cannot be deleted first. 2921 commitRecentList(function (list) { 2922 var next = list.filter(function (entry) { 2923 return entry && entry.id !== id; 2924 }); 2925 next.unshift(item); 2926 return next; 2927 }); 2206 2928 idbRun('readwrite', function (store) { 2207 return store.put({ id: id, blob: blob, size: blob.size, timestamp: Date.now()});2929 return store.put({ id: id, blob: blob, size: blob.size, timestamp: item.timestamp }); 2208 2930 }).catch(function () { 2209 2931 return null; … … 2222 2944 ctx.drawImage(img, 0, 0, thumbCanvas.width, thumbCanvas.height); 2223 2945 var thumbData = thumbCanvas.toDataURL('image/jpeg', 0.85); 2224 2225 var cleanTitle = title || ''; 2226 if (!cleanTitle || cleanTitle.indexOf('http') === 0) { 2227 try { 2228 cleanTitle = new URL(url || window.location.href).pathname || 'Screenshot'; 2229 if (cleanTitle === '/') cleanTitle = 'Home Page'; 2230 } catch (e) { 2231 cleanTitle = 'Screenshot'; 2232 } 2233 } 2234 2235 var item = { 2236 id: id, 2237 title: cleanTitle, 2238 url: url || window.location.href, 2239 width: width, 2240 height: height, 2241 timestamp: Date.now(), 2242 size: blob.size, 2243 pixelWidth: img.naturalWidth || width, 2244 thumb: thumbData 2245 }; 2246 2247 var list = getRecentCaptures(); 2248 list.unshift(item); 2249 if (list.length > RECENT_MAX) list = list.slice(0, RECENT_MAX); 2250 saveRecentList(list); 2251 recentPrune(list); 2946 var pixelWidth = img.naturalWidth || width; 2947 var pixelHeight = img.naturalHeight || height; 2948 var usedScale = bitmapScale(width, pixelWidth, storedScale); 2949 commitRecentList(function (list) { 2950 list.forEach(function (entry) { 2951 if (!entry || entry.id !== id) { 2952 return; 2953 } 2954 entry.thumb = thumbData; 2955 entry.pixelWidth = pixelWidth; 2956 entry.pixelHeight = pixelHeight; 2957 entry.scale = usedScale; 2958 entry.height = Math.max(1, Math.round(pixelHeight / usedScale)); 2959 entry.size = blob.size; 2960 }); 2961 // A missing card was removed or pruned. Do not put it back onto a deleted file. 2962 return list; 2963 }); 2964 showRecentThumb(id, thumbData); 2965 recentPrune(); 2252 2966 } catch (err) { 2253 2967 console.warn('Stillframe recent save error', err); … … 2258 2972 img.onerror = function () { 2259 2973 URL.revokeObjectURL(objUrl); 2974 recentPrune(); 2260 2975 }; 2261 2976 img.src = objUrl; 2262 2977 } 2263 2978 2979 function unsavedMarks() { 2980 if (editor && typeof editor.hasMarks === 'function' && editor.hasMarks()) { 2981 return true; 2982 } 2983 if (!resultDevices) { 2984 return false; 2985 } 2986 var i; 2987 for (i = 0; i < resultDevices.length; i++) { 2988 var marks = resultDevices[i] && resultDevices[i].marks; 2989 if (marks && marks.length) { 2990 return true; 2991 } 2992 } 2993 return false; 2994 } 2995 2264 2996 function openRecentCaptureInEditor(item) { 2997 if (!item) { 2998 return; 2999 } 3000 if (unsavedMarks()) { 3001 var proceed = window.confirm(tr('discardMarks', 'This capture has unsaved marks. Opening another screenshot will discard them. Continue?')); 3002 if (!proceed) { 3003 return; 3004 } 3005 } 2265 3006 var token = ++session; 2266 3007 var rect = { … … 2269 3010 width: item.width || 1440, 2270 3011 height: item.height || 900, 2271 scale: 1,3012 scale: recentScaleOf(item), 2272 3013 url: item.url || window.location.href, 2273 3014 fromRecent: true … … 2325 3066 } 2326 3067 3068 function showRecentThumb(id, thumbData) { 3069 if (!id || !thumbData) { 3070 return; 3071 } 3072 var cards = document.querySelectorAll('.stillframe-recent-card[data-id="' + id + '"]'); 3073 Array.prototype.forEach.call(cards, function (card) { 3074 var wrap = card.querySelector('.stillframe-recent-thumbwrap'); 3075 if (!wrap) { 3076 return; 3077 } 3078 var img = wrap.querySelector('img'); 3079 if (img) { 3080 img.src = thumbData; 3081 return; 3082 } 3083 wrap.appendChild(element('img', { 3084 className: 'stillframe-recent-thumb', 3085 alt: '', 3086 src: thumbData 3087 })); 3088 }); 3089 } 3090 2327 3091 function renderRecentList(container, onSelect, onAction) { 2328 3092 while (container.firstChild) { … … 2340 3104 var listWrap = element('div', { className: 'stillframe-recent-list' }); 2341 3105 list.forEach(function (item) { 2342 var card = element('div', { className: 'stillframe-recent-card' });3106 var card = element('div', { className: 'stillframe-recent-card', 'data-id': item.id || '' }); 2343 3107 var thumbWrap = element('div', { className: 'stillframe-recent-thumbwrap' }); 2344 3108 if (item.thumb) { … … 2588 3352 sizeGroup.appendChild(sizeSlider); 2589 3353 2590 var textOptGroup = element('div', {2591 className: 'stillframe-result__text-opts',2592 role: 'group',2593 'aria-label': 'Text options'2594 });2595 textOptGroup.style.display = 'none';2596 2597 var fontSelect = element('select', { className: 'stillframe-prop__select', 'aria-label': 'Font family', title: 'Font family' });2598 FONT_CHOICES.forEach(function (choice) {2599 fontSelect.appendChild(element('option', { value: choice[0], text: choice[1] }));2600 });2601 fontSelect.value = currentFont || 'sans';2602 fontSelect.addEventListener('change', function () {2603 currentFont = fontSelect.value;2604 if (editor && editor.setFont) {2605 editor.setFont(currentFont);2606 }2607 });2608 textOptGroup.appendChild(fontSelect);2609 2610 var boldBtn = element('button', {2611 type: 'button',2612 className: 'stillframe-prop__toggle',2613 'aria-pressed': currentBold ? 'true' : 'false',2614 text: 'B',2615 title: 'Bold (toggle)'2616 });2617 boldBtn.addEventListener('click', function () {2618 currentBold = boldBtn.getAttribute('aria-pressed') !== 'true';2619 boldBtn.setAttribute('aria-pressed', currentBold ? 'true' : 'false');2620 if (editor && editor.setBold) {2621 editor.setBold(currentBold);2622 }2623 });2624 textOptGroup.appendChild(boldBtn);2625 2626 3354 var shapeOptGroup = element('div', { 2627 3355 className: 'stillframe-result__shape-opts', … … 2687 3415 if (isText) { 2688 3416 currentSliderMode = 'text'; 2689 textOptGroup.style.display = 'inline-flex';2690 3417 shapeOptGroup.style.display = 'none'; 2691 3418 sizeSlider.min = '12'; … … 2698 3425 sizeSlider.title = 'Text size (' + sizeSlider.value + 'px)'; 2699 3426 sizeSlider.setAttribute('aria-label', 'Text size'); 2700 if (info && info.font) {2701 currentFont = info.font;2702 fontSelect.value = currentFont;2703 }2704 if (info && typeof info.bold === 'boolean') {2705 currentBold = info.bold;2706 boldBtn.setAttribute('aria-pressed', currentBold ? 'true' : 'false');2707 }2708 3427 } else if (isShape) { 2709 3428 currentSliderMode = 'stroke'; 2710 textOptGroup.style.display = 'none';2711 3429 shapeOptGroup.style.display = 'inline-flex'; 2712 3430 sizeSlider.min = '1'; … … 2726 3444 } else { 2727 3445 currentSliderMode = 'stroke'; 2728 textOptGroup.style.display = 'none';2729 3446 shapeOptGroup.style.display = 'none'; 2730 3447 sizeSlider.min = '1'; … … 2768 3485 var toolsWrap = element('div', { className: 'stillframe-result__editing' }); 2769 3486 toolsWrap.appendChild(markGroup); 3487 toolsWrap.appendChild(historyGroup); 3488 toolsWrap.appendChild(shapeOptGroup); 2770 3489 toolsWrap.appendChild(colorGroup); 2771 3490 toolsWrap.appendChild(sizeGroup); 2772 toolsWrap.appendChild(textOptGroup);2773 toolsWrap.appendChild(shapeOptGroup);2774 toolsWrap.appendChild(historyGroup);2775 3491 2776 3492 var saveGroup = element('div', { className: 'stillframe-result__actions' }); … … 2859 3575 var hasMulti = multiOptions && Array.isArray(multiOptions.devices) && multiOptions.devices.length > 1; 2860 3576 var devicesList = (multiOptions && multiOptions.devices) || []; 3577 resultDevices = devicesList.length ? devicesList : null; 2861 3578 var currentDeviceId = multiOptions && multiOptions.activeDeviceId ? multiOptions.activeDeviceId : (devicesList[0] ? devicesList[0].id : ''); 2862 3579 var currentDevice = devicesList.find(function (d) { return d.id === currentDeviceId; }) || devicesList[0] || null; … … 3256 3973 setStatus(''); 3257 3974 flushPending(); 3975 if (rect && rect.compatNote) { 3976 setStatus(rect.compatNote); 3977 } 3258 3978 if (captured.blob && !(rect && rect.fromRecent)) { 3259 saveCaptureToRecent(captured.blob, captured.width, captured.height, document.title, captured.url );3979 saveCaptureToRecent(captured.blob, captured.width, captured.height, document.title, captured.url, captured.scale); 3260 3980 } 3261 3981 }).catch(function (error) { … … 3271 3991 text: error && error.code === 'library' ? text('libraryMissing') : text('captureFailed') 3272 3992 })); 3273 setStatus( '');3993 setStatus(rect && rect.compatNote ? rect.compatNote : ''); 3274 3994 if (dialog) { 3275 3995 dialog.setAttribute('aria-busy', 'false'); … … 3286 4006 if (dev.id === currentDeviceId) { 3287 4007 captured.blob = blob; 4008 captured.scale = dev.scale || captured.scale || 1; 3288 4009 captured.width = dev.width; 3289 4010 captured.height = dev.height; … … 3342 4063 3343 4064 function openMultiDirectCapture(sourceUrl, selectedDevices, scale, hideAdminBar, isFullPage) { 4065 sourceUrl = scrubCaptureUrl(sourceUrl); 3344 4066 var token = ++session; 3345 4067 var scaleVal = scale || 1; … … 3406 4128 dev.image = img; 3407 4129 dev.status = 'ready'; 3408 dev.height = Math.round(img.naturalHeight / scaleVal); 4130 dev.scale = bitmapScale(dev.width, img.naturalWidth, scaleVal); 4131 dev.height = Math.max(1, Math.round((img.naturalHeight || 1) / dev.scale)); 3409 4132 controller.onDeviceReady(dev, blob, img); 3410 saveCaptureToRecent(blob, dev.width, dev.height, document.title + ' (' + dev.label + ')', sourceUrl );4133 saveCaptureToRecent(blob, dev.width, dev.height, document.title + ' (' + dev.label + ')', sourceUrl, dev.scale); 3411 4134 }); 3412 4135 }).catch(function (err) { … … 3563 4286 sel = { x: 0, y: 0, width: sizeNow.width, height: sizeNow.height }; 3564 4287 showSelection(sel); 3565 scheduleShot(IDLE_BEFORE_RENDER_MS); 4288 // Start the redraw now, while the selection is on screen, so Capture 4289 // does not wait for the whole page to be built from scratch. 4290 if (!viewShot.canvas && !viewShot.work) { 4291 startViewShot().catch(function () { 4292 return null; 4293 }); 4294 } 3566 4295 }); 3567 4296 … … 3710 4439 var engineText = element('span', { className: 'sfd-engine__text' }); 3711 4440 engineText.appendChild(element('b', { text: tr('engineTitle', 'Pixel-perfect capture') })); 3712 engineText.appendChild(element('small', { text: tr('engineHelp', ' Your browser asks to share this tab, so the screenshot matches the screen exactly. Nothing is uploaded.') }));4441 engineText.appendChild(element('small', { text: tr('engineHelp', 'Area, Window, and Full screen on this tab in Chrome or Edge. The browser asks to share the tab, and nothing is uploaded. Other browsers, Direct Page, and Tools redraw the page.') })); 3713 4442 engineLabel.appendChild(engineCb); 3714 4443 engineLabel.appendChild(element('span', { className: 'stillframe-hub-toggle__track' })); … … 4015 4744 bar.appendChild(windowModeButton); 4016 4745 bar.appendChild(fullButton); 4746 bar.appendChild(devWrap); 4017 4747 bar.appendChild(directWrap); 4018 4748 bar.appendChild(recentWrap); … … 4434 5164 4435 5165 function captureInFrame(url, width, scale, isCancelled, options) { 5166 url = scrubCaptureUrl(url); 4436 5167 options = options || {}; 4437 5168 return new Promise(function (resolve, reject) { … … 5032 5763 } 5033 5764 captured = null; 5765 resultDevices = null; 5034 5766 compactPanel = false; 5035 5767 resultReady = false; -
stillframe/tags/0.2.5/assets/js/stillframe-tools.js
r3724658 r3736872 2 2 'use strict'; 3 3 4 var p ageSelect = document.getElementById('stillframe-target-page');5 var customUrlWrap = document.getElementById('stillframe-custom-url-wrap');6 var customUrlInput = document.getElementById('stillframe-custom-url');4 var pickerMount = document.getElementById('stillframe-target-page'); 5 var toolsData = window.StillframeToolsData || {}; 6 var pagePicker = null; 7 7 var startBtn = document.getElementById('stillframe-start-multicapture'); 8 8 … … 41 41 }); 42 42 43 if (pageSelect) { 44 pageSelect.addEventListener('change', function () { 45 var isCustom = pageSelect.value === '__custom__'; 46 if (customUrlWrap) { 47 customUrlWrap.hidden = !isCustom; 48 customUrlWrap.style.display = isCustom ? 'block' : 'none'; 49 } 50 if (isCustom && customUrlInput) { 51 customUrlInput.focus(); 52 } 43 if (pickerMount && window.StillframeCapture && typeof window.StillframeCapture.createPagePicker === 'function') { 44 pagePicker = window.StillframeCapture.createPagePicker({ 45 pages: toolsData.pages, 46 homeUrl: toolsData.homeUrl || '' 53 47 }); 48 pickerMount.appendChild(pagePicker.root); 54 49 } 55 50 56 51 function getTargetUrl() { 57 if (!pageSelect) { 58 return ''; 59 } 60 if (pageSelect.value === '__custom__') { 61 return customUrlInput ? customUrlInput.value.trim() : ''; 62 } 63 return pageSelect.value.trim(); 52 return pagePicker ? pagePicker.getValue() : ''; 64 53 } 65 54 … … 86 75 // The capture opens the same pop-up editor as the top-bar capture, 87 76 // with every chosen device ready to switch between. 77 // Tools always redraws the page in an iframe. Pixel-perfect tab capture 78 // is the admin-bar Area, Window, and Full screen control, not this screen. 79 var errorNode = null; 80 81 function showError(message) { 82 if (!errorNode) { 83 errorNode = document.createElement('p'); 84 errorNode.className = 'stillframe-tools-error'; 85 errorNode.setAttribute('role', 'alert'); 86 errorNode.style.cssText = 'margin:8px 0 0;color:#b42318;font-size:13px;font-weight:600'; 87 if (startBtn && startBtn.parentNode) { 88 startBtn.parentNode.insertBefore(errorNode, startBtn); 89 } 90 } 91 errorNode.textContent = message; 92 errorNode.hidden = !message; 93 } 94 88 95 function startCapture() { 96 showError(''); 89 97 var url = getTargetUrl(); 90 98 if (!url) { 91 window.alert('Please select a target page or enter a URL to capture.');92 if ( customUrlInput && pageSelect && pageSelect.value === '__custom__') {93 customUrlInput.focus();99 showError('Please select a target page or enter a URL to capture.'); 100 if (pagePicker) { 101 pagePicker.focusCustom(); 94 102 } 95 103 return; … … 98 106 var devices = getSelectedDevices(); 99 107 if (!devices.length) { 100 window.alert('Please select at least one device to capture.');108 showError('Please select at least one device to capture.'); 101 109 return; 102 110 } … … 104 112 var engine = window.StillframeCapture || {}; 105 113 if (typeof engine.openMultiCapture !== 'function') { 106 window.alert('Stillframe is still loading. Please try again in a moment.');114 showError('Stillframe is still loading. Please try again in a moment.'); 107 115 return; 108 116 } -
stillframe/tags/0.2.5/includes/class-stillframe-asset-loader.php
r3724658 r3736872 20 20 * @var string 21 21 */ 22 public const PAGES_TRANSIENT = 'stillframe_site_pages ';22 public const PAGES_TRANSIENT = 'stillframe_site_pages_v2'; 23 23 24 24 /** 25 25 * Query arguments that must not be replayed inside the capture frame. 26 26 * 27 * `action` is not in this list. Screens such as post.php?action=edit need it 28 * to stay on the same screen. Mutating values are removed separately. 29 * 27 30 * @var string[] 28 31 */ 29 32 public const STRIPPED_QUERY_ARGS = array( 30 'action',31 33 '_wpnonce', 32 34 'nonce', … … 36 38 37 39 /** 40 * Admin `action` values that change data when loaded with GET. 41 * 42 * Read-only values such as `edit` are kept so the capture iframe opens the 43 * screen the user was on. 44 * 45 * @var string[] 46 */ 47 public const MUTATING_ACTIONS = array( 48 'trash', 49 'untrash', 50 'delete', 51 'delete-all', 52 'editpost', 53 'postajaxpost', 54 'post-quickdraft-save', 55 'deletecomment', 56 'trashcomment', 57 'untrashcomment', 58 'spamcomment', 59 'unspamcomment', 60 'approvecomment', 61 'unapprovecomment', 62 'approve', 63 'unapprove', 64 'spam', 65 'unspam', 66 'dim-comment', 67 'activate', 68 'deactivate', 69 'activate-selected', 70 'deactivate-selected', 71 'update-selected', 72 'delete-selected', 73 'enable-auto-update', 74 'disable-auto-update', 75 'error_scrape', 76 'resume', 77 'upgrade-plugin', 78 'upgrade-theme', 79 'do-plugin-upgrade', 80 'do-theme-upgrade', 81 'do-core-upgrade', 82 'do-translation-upgrade', 83 'install-plugin', 84 'install-theme', 85 'upload-plugin', 86 'upload-theme', 87 'logout', 88 'remove', 89 'deleteblog', 90 'spamblog', 91 ); 92 93 /** 38 94 * Enqueue the admin bar script and its small style, for administrators only. 39 95 * … … 46 102 47 103 $style_url = false; 48 wp_register_style( 'stillframe-admin-bar', $style_url, array(), STILLFRAME_ VERSION);104 wp_register_style( 'stillframe-admin-bar', $style_url, array(), STILLFRAME_ASSET_VER ); 49 105 wp_enqueue_style( 'stillframe-admin-bar' ); 50 106 wp_add_inline_style( 'stillframe-admin-bar', $this->admin_bar_css() ); … … 56 112 $base . 'assets/css/stillframe-capture-panel.css', 57 113 array(), 58 STILLFRAME_VERSION 59 ); 60 61 wp_register_script( 62 'stillframe-annotation-editor', 63 $base . 'assets/js/stillframe-annotation-editor.js', 64 array(), 65 STILLFRAME_VERSION, 66 true 67 ); 114 STILLFRAME_ASSET_VER 115 ); 116 117 /** 118 * Filters whether the Fabric.js annotation editor is used. Return false for the legacy editor. 119 * 120 * @param bool $use_fabric Whether to use the Fabric.js editor. 121 */ 122 if ( apply_filters( 'stillframe_use_fabric_editor', true ) ) { 123 wp_register_script( 124 'stillframe-fabric', 125 $base . 'assets/js/vendor/fabric.min.js', 126 array(), 127 '6.7.1', 128 true 129 ); 130 wp_register_script( 131 'stillframe-annotation-editor', 132 $base . 'assets/js/stillframe-annotation-editor-v2.js', 133 array( 'stillframe-fabric' ), 134 STILLFRAME_ASSET_VER, 135 true 136 ); 137 } else { 138 wp_register_script( 139 'stillframe-annotation-editor', 140 $base . 'assets/js/stillframe-annotation-editor.js', 141 array(), 142 STILLFRAME_ASSET_VER, 143 true 144 ); 145 } 68 146 69 147 wp_register_script( … … 71 149 $base . 'assets/js/stillframe-capture-panel.js', 72 150 array( 'stillframe-annotation-editor' ), 73 STILLFRAME_ VERSION,151 STILLFRAME_ASSET_VER, 74 152 true 75 153 ); … … 79 157 $base . 'assets/js/stillframe-admin-bar.js', 80 158 array(), 81 STILLFRAME_ VERSION,159 STILLFRAME_ASSET_VER, 82 160 true 83 161 ); … … 86 164 'pluginUrl' => $base, 87 165 'version' => STILLFRAME_VERSION, 166 'assetVer' => STILLFRAME_ASSET_VER, 167 'useFabric' => apply_filters( 'stillframe_use_fabric_editor', true ), 88 168 'currentUrl' => $this->current_screen_url(), 89 169 'presets' => $this->device_width_presets(), … … 97 177 'maxWidth' => 2560, 98 178 'homeUrl' => home_url( '/' ), 99 'sitePages' => $this->site_pages(),179 'sitePages' => self::site_pages(), 100 180 'toolsUrl' => admin_url( 'tools.php?page=stillframe' ), 101 'stripArgs' => self::STRIPPED_QUERY_ARGS, 102 'i18n' => $this->script_strings(), 181 'stripArgs' => self::STRIPPED_QUERY_ARGS, 182 'mutatingActions' => self::MUTATING_ACTIONS, 183 'i18n' => $this->script_strings(), 103 184 ); 104 185 … … 115 196 $base . 'assets/css/stillframe-tools.css', 116 197 array(), 117 STILLFRAME_ VERSION198 STILLFRAME_ASSET_VER 118 199 ); 119 200 wp_enqueue_style( 'stillframe-tools' ); … … 123 204 $base . 'assets/js/vendor/modern-screenshot.js', 124 205 array(), 125 STILLFRAME_ VERSION,206 STILLFRAME_ASSET_VER, 126 207 true 127 208 ); … … 134 215 $base . 'assets/js/stillframe-tools.js', 135 216 array( 'stillframe-capture-panel', 'stillframe-modern-screenshot' ), 136 STILLFRAME_ VERSION,217 STILLFRAME_ASSET_VER, 137 218 true 138 219 ); … … 145 226 'mediaNonce' => wp_create_nonce( 'stillframe_save_media' ), 146 227 'canUpload' => current_user_can( 'upload_files' ), 147 'pages' => $this->site_pages(),228 'pages' => self::site_pages(), 148 229 ); 149 230 wp_add_inline_script( … … 164 245 165 246 /** 166 * Retrieve a list of published pages and posts for quick selection. 167 * 168 * @return array<int, array{id: int, title: string, type: string, url: string}> 169 */ 170 private function site_pages() { 247 * Retrieve published content from every public post type, for the page picker. 248 * 249 * Shared by the capture panel and the Tools screen. 250 * 251 * @return array<int, array{id: int, title: string, type: string, url: string, path: string, parent: string, status: string, home: bool}> 252 */ 253 public static function site_pages() { 171 254 $cached = get_transient( self::PAGES_TRANSIENT ); 172 255 if ( is_array( $cached ) ) { … … 174 257 } 175 258 176 $items = array(); 259 $types = array_values( array_diff( get_post_types( array( 'public' => true ) ), array( 'attachment' ) ) ); 260 261 /** 262 * Filters the maximum number of items listed in the page picker. 263 * 264 * @param int $max Maximum number of items. 265 */ 266 $max = (int) apply_filters( 'stillframe_max_pages', 500 ); 267 if ( $max < 1 ) { 268 $max = 500; 269 } 270 177 271 $pages = get_posts( 178 272 array( 179 'post_type' => array( 'page', 'post' ),273 'post_type' => $types, 180 274 'post_status' => 'publish', 181 'posts_per_page' => 25, 182 'orderby' => 'title', 183 'order' => 'ASC', 275 'posts_per_page' => $max, 276 'no_found_rows' => true, 277 'orderby' => array( 278 'type' => 'ASC', 279 'menu_order' => 'ASC', 280 'title' => 'ASC', 281 ), 184 282 ) 185 283 ); 284 285 $home_ids = array_values( 286 array_filter( 287 array( 288 (int) get_option( 'page_on_front' ), 289 (int) get_option( 'page_for_posts' ), 290 ) 291 ) 292 ); 293 294 foreach ( array_reverse( $home_ids ) as $home_id ) { 295 $home_post = get_post( $home_id ); 296 if ( $home_post && 'publish' === $home_post->post_status ) { 297 array_unshift( $pages, $home_post ); 298 } 299 } 300 301 $items = array(); 302 $seen = array(); 186 303 foreach ( $pages as $p ) { 304 if ( isset( $seen[ $p->ID ] ) ) { 305 continue; 306 } 307 $seen[ $p->ID ] = true; 308 187 309 $permalink = get_permalink( $p ); 188 if ( $permalink ) { 189 $items[] = array( 190 'id' => (int) $p->ID, 191 'title' => $p->post_title ? $p->post_title : __( '(No title)', 'stillframe' ), 192 'type' => $p->post_type, 193 'url' => $permalink, 194 ); 195 } 196 } 310 if ( ! $permalink ) { 311 continue; 312 } 313 314 $path = wp_parse_url( $permalink, PHP_URL_PATH ); 315 $items[] = array( 316 'id' => (int) $p->ID, 317 'title' => $p->post_title ? $p->post_title : __( '(No title)', 'stillframe' ), 318 'type' => $p->post_type, 319 'url' => $permalink, 320 'path' => is_string( $path ) && '' !== $path ? $path : '/', 321 'parent' => $p->post_parent ? get_the_title( $p->post_parent ) : '', 322 'status' => $p->post_status, 323 'home' => in_array( (int) $p->ID, $home_ids, true ), 324 ); 325 } 326 197 327 set_transient( self::PAGES_TRANSIENT, $items, 12 * HOUR_IN_SECONDS ); 198 328 return $items; … … 411 541 'recent' => __( 'Recent', 'stillframe' ), 412 542 'devices' => __( 'Devices', 'stillframe' ), 543 'pickCurrent' => __( 'Current screen', 'stillframe' ), 544 'pickHome' => __( 'Home', 'stillframe' ), 545 'pickPages' => __( 'Pages', 'stillframe' ), 546 'pickPosts' => __( 'Posts', 'stillframe' ), 547 'pickCustomPlaceholder' => __( 'Custom URL: paste any address...', 'stillframe' ), 548 'pickCustom' => __( 'Custom URL', 'stillframe' ), 549 'pickUse' => __( 'Use', 'stillframe' ), 550 'pickSearchPlaceholder' => __( 'Search pages or paste a URL from this site', 'stillframe' ), 551 'pickSearch' => __( 'Search pages', 'stillframe' ), 552 'pickEnterUrl' => __( 'Enter a URL below', 'stillframe' ), 553 'pickNoMatch' => __( 'No matching pages. Paste a full URL to use it.', 'stillframe' ), 554 'directTitle' => __( 'Capture a page', 'stillframe' ), 555 'targetPage' => __( 'Page', 'stillframe' ), 556 'currentAdmin' => __( 'Current Admin Screen', 'stillframe' ), 557 'currentScreen' => __( 'Current Screen', 'stillframe' ), 558 'selectDevices' => __( 'Devices', 'stillframe' ), 559 'desktopOnly' => __( 'Desktop Only', 'stillframe' ), 560 'selectAll' => __( 'Select All (3)', 'stillframe' ), 561 'hideAdminBar' => __( 'Hide the admin bar in the screenshot', 'stillframe' ), 562 'resQuality' => __( 'Quality', 'stillframe' ), 563 'captureHeight' => __( 'Height', 'stillframe' ), 564 'res1' => __( '1x', 'stillframe' ), 565 'res2' => __( '2x', 'stillframe' ), 566 'res3' => __( '3x', 'stillframe' ), 567 'res1Help' => __( 'Same pixels as the screen. Smallest files.', 'stillframe' ), 568 'res2Help' => __( 'Sharp on high-density displays. Recommended.', 'stillframe' ), 569 'res3Help' => __( 'Maximum detail. Large files; very long pages may be reduced.', 'stillframe' ), 570 'heightViewport' => __( 'First screen', 'stillframe' ), 571 'heightFull' => __( 'Full Page', 'stillframe' ), 572 'heightViewportHelp' => __( 'Only the first screen (above the fold).', 'stillframe' ), 573 'heightFullHelp' => __( 'The entire page, top to bottom.', 'stillframe' ), 574 'captureAll' => __( 'Capture 3 devices', 'stillframe' ), 575 'dev_desktop' => __( 'Desktop', 'stillframe' ), 576 'dev_tablet' => __( 'iPad', 'stillframe' ), 577 'dev_mobile' => __( 'Mobile', 'stillframe' ), 578 /* translators: 1: rows shown, 2: rows matched. */ 579 'pickShowing' => __( 'Showing %1$d of %2$d. Keep typing to narrow the list.', 'stillframe' ), 580 'pickChoose' => __( 'Choose a page', 'stillframe' ), 581 'captureNone' => __( 'Capture', 'stillframe' ), 582 'chooseDevice' => __( 'Choose at least one device to capture.', 'stillframe' ), 583 'pickUseUrl' => __( 'Use this URL', 'stillframe' ), 584 'engineTitle' => __( 'Pixel-perfect capture', 'stillframe' ), 585 'engineHelp' => __( 'Area, Window, and Full screen on this tab in Chrome or Edge. The browser asks to share the tab, and nothing is uploaded. Other browsers, Direct Page, and Tools redraw the page.', 'stillframe' ), 586 'exactUnsupported' => __( 'This browser cannot share the tab, so this shot was redrawn from the page.', 'stillframe' ), 587 'exactCancelled' => __( 'Sharing was cancelled, so this shot was redrawn from the page. The next capture will ask to share the tab again.', 'stillframe' ), 588 'exactSurface' => __( 'That share was not this browser tab, so this shot was redrawn from the page. The next capture will ask again.', 'stillframe' ), 589 'exactMismatch' => __( 'The tab capture did not match this window, so this shot was redrawn from the page. The next capture will ask again.', 'stillframe' ), 590 'exactFallback' => __( 'Pixel-perfect capture did not finish, so this shot was redrawn from the page. The next capture will ask again.', 'stillframe' ), 591 'discardMarks' => __( 'This capture has unsaved marks. Opening another screenshot will discard them. Continue?', 'stillframe' ), 592 /* translators: %d: number of devices. */ 593 'captureN' => __( 'Capture %d devices', 'stillframe' ), 594 /* translators: %s: device name. */ 595 'captureOne' => __( 'Capture %s', 'stillframe' ), 413 596 ); 414 597 } … … 424 607 425 608 /** 609 * Whether an admin `action` value changes data when the URL is opened. 610 * 611 * Empty and read-only values, including `edit`, are safe to keep. 612 * 613 * @param mixed $action Action value from the query string. 614 * @return bool 615 */ 616 public static function action_is_mutating( $action ) { 617 if ( is_array( $action ) ) { 618 foreach ( $action as $item ) { 619 if ( self::action_is_mutating( $item ) ) { 620 return true; 621 } 622 } 623 return false; 624 } 625 626 if ( ! is_string( $action ) || '' === $action ) { 627 return false; 628 } 629 630 return in_array( strtolower( $action ), self::MUTATING_ACTIONS, true ); 631 } 632 633 /** 634 * Remove nonces and state-changing actions from a capture URL. 635 * 636 * @param string $url Absolute or root-relative URL. 637 * @return string 638 */ 639 public static function scrub_capture_url( $url ) { 640 $clean = remove_query_arg( self::STRIPPED_QUERY_ARGS, $url ); 641 $action = null; 642 $parts = wp_parse_url( $clean ); 643 644 if ( is_array( $parts ) && isset( $parts['query'] ) && is_string( $parts['query'] ) ) { 645 $query = array(); 646 parse_str( $parts['query'], $query ); 647 if ( isset( $query['action'] ) ) { 648 $action = $query['action']; 649 } 650 } 651 652 if ( self::action_is_mutating( $action ) ) { 653 $clean = remove_query_arg( 'action', $clean ); 654 } 655 656 return $clean; 657 } 658 659 /** 426 660 * URL of the screen being viewed, safe to load with a same-origin GET. 427 661 * 428 * The host comes from home_url(). Query arguments that can replay a 429 * state-changing request are removed before the value ever reaches JavaScript. 662 * The host comes from home_url(). Nonces and state-changing actions are 663 * removed before the value ever reaches JavaScript. Read-only actions such 664 * as edit stay, so post.php?action=edit reloads the editor. 430 665 * 431 666 * @return string … … 434 669 $target = $this->request_path_and_query(); 435 670 $origin = $this->home_origin(); 436 $url = remove_query_arg( self::STRIPPED_QUERY_ARGS,$origin . $target );671 $url = self::scrub_capture_url( $origin . $target ); 437 672 $clean = esc_url_raw( $url ); 438 673 -
stillframe/tags/0.2.5/includes/class-stillframe-media.php
r3724658 r3736872 11 11 12 12 /** 13 * Accepts one PNG from an administrator and stores it as an attachment.13 * Accepts one PNG, JPEG or WebP image from an administrator and stores it as an attachment. 14 14 */ 15 15 class Stillframe_Media { … … 25 25 26 26 /** 27 * Store the uploaded PNGand return its attachment URL.27 * Store the uploaded image and return its attachment URL. 28 28 * 29 29 * @return void … … 66 66 } 67 67 68 $name = $this-> png_filename( wp_unslash( $file['name'] ) );68 $name = $this->image_filename( wp_unslash( $file['name'] ) ); 69 69 70 70 require_once ABSPATH . 'wp-admin/includes/file.php'; … … 76 76 $name, 77 77 array( 78 'png' => 'image/png', 78 'png' => 'image/png', 79 'jpg|jpeg' => 'image/jpeg', 80 'webp' => 'image/webp', 79 81 ) 80 82 ); 81 83 82 if ( empty( $checked['ext'] ) || 'png' !== $checked['ext'] || 'image/png' !== $checked['type']) {84 if ( empty( $checked['ext'] ) || empty( $checked['type'] ) || ! in_array( $checked['type'], array( 'image/png', 'image/jpeg', 'image/webp' ), true ) ) { 83 85 $this->fail(); 84 86 } … … 91 93 'test_form' => false, 92 94 'mimes' => array( 93 'png' => 'image/png', 95 'png' => 'image/png', 96 'jpg|jpeg' => 'image/jpeg', 97 'webp' => 'image/webp', 94 98 ), 95 99 ) … … 104 108 $attachment_id = wp_insert_attachment( 105 109 array( 106 'post_mime_type' => 'image/png',110 'post_mime_type' => $checked['type'], 107 111 'post_title' => sanitize_text_field( pathinfo( $name, PATHINFO_FILENAME ) ), 108 112 'post_content' => '', … … 141 145 142 146 /** 143 * Keep a safe PNGfilename, or fall back to a generated one.147 * Keep a safe image filename, or fall back to a generated one. 144 148 * 145 149 * @param mixed $raw Original upload name. 146 150 * @return string 147 151 */ 148 private function png_filename( $raw ) {152 private function image_filename( $raw ) { 149 153 $name = is_string( $raw ) ? sanitize_file_name( $raw ) : ''; 150 154 $name = strtolower( $name ); 151 155 152 if ( ! preg_match( '/^[a-z0-9][a-z0-9._-]*\. png$/', $name ) ) {156 if ( ! preg_match( '/^[a-z0-9][a-z0-9._-]*\.(png|jpe?g|webp)$/', $name ) ) { 153 157 $name = 'stillframe-' . gmdate( 'Ymd-His' ) . '.png'; 154 158 } -
stillframe/tags/0.2.5/includes/class-stillframe-plugin.php
r3724658 r3736872 66 66 add_action( 'save_post', array( 'Stillframe_Asset_Loader', 'flush_pages_cache' ) ); 67 67 add_action( 'deleted_post', array( 'Stillframe_Asset_Loader', 'flush_pages_cache' ) ); 68 add_action( 'trashed_post', array( 'Stillframe_Asset_Loader', 'flush_pages_cache' ) ); 69 add_action( 'transition_post_status', array( 'Stillframe_Asset_Loader', 'flush_pages_cache' ) ); 68 70 } 69 71 } -
stillframe/tags/0.2.5/includes/class-stillframe-tools-page.php
r3724658 r3736872 49 49 } 50 50 51 $home_url = home_url( '/' );52 $site_pages = get_posts(53 array(54 'post_type' => array( 'page', 'post' ),55 'post_status' => 'publish',56 'posts_per_page' => 30,57 'orderby' => 'title',58 'order' => 'ASC',59 )60 );61 51 ?> 62 52 <div class="wrap stillframe-tools-wrap"> … … 72 62 <div class="stillframe-field-group"> 73 63 <div class="stillframe-field-group__header"> 74 < label class="stillframe-field-group__title" for="stillframe-target-page"><?php esc_html_e( 'Target Page', 'stillframe' ); ?></label>64 <span class="stillframe-field-group__title"><?php esc_html_e( 'Target Page', 'stillframe' ); ?></span> 75 65 <span class="stillframe-field-group__hint"><?php esc_html_e( 'Select a site page or enter a custom URL to capture.', 'stillframe' ); ?></span> 76 66 </div> 77 67 <div class="stillframe-page-picker"> 78 <select id="stillframe-target-page" class="stillframe-select"> 79 <option value="<?php echo esc_url( $home_url ); ?>"> 80 <?php echo esc_html__( 'Home Page', 'stillframe' ) . ' (' . esc_html( $home_url ) . ')'; ?> 81 </option> 82 <?php foreach ( $site_pages as $page ) : ?> 83 <?php 84 $page_url = get_permalink( $page ); 85 if ( ! $page_url || $page_url === $home_url ) { 86 continue; 87 } 88 $title = $page->post_title ? $page->post_title : __( '(No title)', 'stillframe' ); 89 ?> 90 <option value="<?php echo esc_url( $page_url ); ?>"> 91 <?php echo esc_html( $title . ' (' . ucfirst( $page->post_type ) . ')' ); ?> 92 </option> 93 <?php endforeach; ?> 94 <option value="__custom__"><?php esc_html_e( 'Custom URL...', 'stillframe' ); ?></option> 95 </select> 96 <div id="stillframe-custom-url-wrap" class="stillframe-custom-url-wrap" hidden style="display: none;"> 97 <input type="url" id="stillframe-custom-url" class="stillframe-input" placeholder="https://example.com/" /> 98 </div> 68 <div id="stillframe-target-page"></div> 99 69 </div> 100 70 </div> … … 174 144 <div class="stillframe-field-group__header"> 175 145 <span class="stillframe-field-group__title"><?php esc_html_e( 'Capture Preferences', 'stillframe' ); ?></span> 176 <span class="stillframe-field-group__hint"><?php esc_html_e( ' Configure resolution quality and display settings.', 'stillframe' ); ?></span>146 <span class="stillframe-field-group__hint"><?php esc_html_e( 'These shots redraw the page. Pixel-perfect capture is Area, Window, and Full screen from the admin bar.', 'stillframe' ); ?></span> 177 147 </div> 178 148 <div class="stillframe-settings-grid"> -
stillframe/tags/0.2.5/languages/stillframe.pot
r3724658 r3736872 1 # Copyright (C) 2026 Stillframe1 # Copyright (C) 2026 Grafucci 2 2 # This file is distributed under the GPL-2.0-or-later. 3 3 msgid "" 4 4 msgstr "" 5 "Project-Id-Version: Stillframe 0.2.0\n"6 "Report-Msgid-Bugs-To: \n"5 "Project-Id-Version: Stillframe – Screenshot Capture & Markup 0.2.5\n" 6 "Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/stillframe\n" 7 7 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" 8 8 "Language-Team: LANGUAGE <LL@li.org>\n" … … 10 10 "Content-Type: text/plain; charset=UTF-8\n" 11 11 "Content-Transfer-Encoding: 8bit\n" 12 "POT-Creation-Date: 2026- 09-29T08:26:04+00:00\n"12 "POT-Creation-Date: 2026-10-09T14:33:09+00:00\n" 13 13 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 14 14 "X-Generator: WP-CLI 2.12.0\n" 15 15 "X-Domain: stillframe\n" 16 16 17 17 #. Plugin Name of the plugin 18 18 #: stillframe.php 19 #: includes/class-stillframe-admin-bar-menu.php:32 20 msgid "Stillframe" 19 msgid "Stillframe – Screenshot Capture & Markup" 20 msgstr "" 21 22 #. Plugin URI of the plugin 23 #: stillframe.php 24 msgid "https://grafucci.com/stillframe/" 21 25 msgstr "" 22 26 23 27 #. Description of the plugin 24 28 #: stillframe.php 25 msgid "Capture a plugin screen, a theme, or any page at phone, tablet, or a custom width, mark it up, and download a PNG."29 msgid "Capture multi-device screenshots (Desktop, Tablet, Mobile) or custom regions, mark them up with rich annotation tools, and export to PNG or Media Library." 26 30 msgstr "" 27 31 … … 31 35 msgstr "" 32 36 37 #. Author URI of the plugin 38 #: stillframe.php 39 msgid "https://grafucci.com/" 40 msgstr "" 41 42 #: includes/class-stillframe-admin-bar-menu.php:32 33 43 #: includes/class-stillframe-admin-bar-menu.php:36 34 #: includes/class-stillframe-asset-loader.php: 23344 #: includes/class-stillframe-asset-loader.php:487 35 45 msgid "Capture this screen" 36 46 msgstr "" 37 47 38 #: includes/class-stillframe-asset-loader.php:125 39 #: includes/class-stillframe-asset-loader.php:185 48 #: includes/class-stillframe-asset-loader.php:317 49 msgid "(No title)" 50 msgstr "" 51 52 #: includes/class-stillframe-asset-loader.php:379 53 #: includes/class-stillframe-asset-loader.php:439 40 54 msgid "Phone" 41 55 msgstr "" 42 56 43 #: includes/class-stillframe-asset-loader.php:130 44 #: includes/class-stillframe-asset-loader.php:190 57 #: includes/class-stillframe-asset-loader.php:384 58 #: includes/class-stillframe-asset-loader.php:444 59 #: includes/class-stillframe-asset-loader.php:576 45 60 msgid "iPad" 46 61 msgstr "" 47 62 48 #: includes/class-stillframe-asset-loader.php:135 49 #: includes/class-stillframe-asset-loader.php:195 63 #: includes/class-stillframe-asset-loader.php:389 64 #: includes/class-stillframe-asset-loader.php:449 65 #: includes/class-stillframe-asset-loader.php:575 66 #: includes/class-stillframe-tools-page.php:95 50 67 msgid "Desktop" 51 68 msgstr "" 52 69 53 #: includes/class-stillframe-asset-loader.php:213 70 #: includes/class-stillframe-asset-loader.php:467 71 #: includes/class-stillframe-asset-loader.php:564 54 72 msgid "1x" 55 73 msgstr "" 56 74 57 #: includes/class-stillframe-asset-loader.php:217 75 #: includes/class-stillframe-asset-loader.php:471 76 #: includes/class-stillframe-asset-loader.php:565 58 77 msgid "2x" 59 78 msgstr "" 60 79 61 #: includes/class-stillframe-asset-loader.php:221 80 #: includes/class-stillframe-asset-loader.php:475 81 #: includes/class-stillframe-asset-loader.php:566 62 82 msgid "3x" 63 83 msgstr "" 64 84 65 #: includes/class-stillframe-asset-loader.php: 23485 #: includes/class-stillframe-asset-loader.php:488 66 86 msgid "Close" 67 87 msgstr "" 68 88 69 #: includes/class-stillframe-asset-loader.php:235 89 #: includes/class-stillframe-asset-loader.php:489 90 msgid "This screen" 91 msgstr "" 92 93 #: includes/class-stillframe-asset-loader.php:490 70 94 msgid "Width" 71 95 msgstr "" 72 96 73 #: includes/class-stillframe-asset-loader.php: 23697 #: includes/class-stillframe-asset-loader.php:491 74 98 msgid "Custom" 75 99 msgstr "" 76 100 77 #: includes/class-stillframe-asset-loader.php: 237101 #: includes/class-stillframe-asset-loader.php:492 78 102 msgid "Custom width" 79 103 msgstr "" 80 104 81 #: includes/class-stillframe-asset-loader.php: 238105 #: includes/class-stillframe-asset-loader.php:493 82 106 msgid "Export scale" 83 107 msgstr "" 84 108 85 #: includes/class-stillframe-asset-loader.php:239 109 #: includes/class-stillframe-asset-loader.php:494 110 #: includes/class-stillframe-asset-loader.php:581 86 111 msgid "Capture" 87 112 msgstr "" 88 113 89 #: includes/class-stillframe-asset-loader.php: 240114 #: includes/class-stillframe-asset-loader.php:495 90 115 msgid "Capture current window" 91 116 msgstr "" 92 117 93 #: includes/class-stillframe-asset-loader.php:241 94 msgid "Download PNG" 95 msgstr "" 96 97 #: includes/class-stillframe-asset-loader.php:242 118 #: includes/class-stillframe-asset-loader.php:496 119 msgid "Reloading this page at the chosen width." 120 msgstr "" 121 122 #: includes/class-stillframe-asset-loader.php:497 123 msgid "Download" 124 msgstr "" 125 126 #: includes/class-stillframe-asset-loader.php:498 127 msgid "Save to Media" 128 msgstr "" 129 130 #: includes/class-stillframe-asset-loader.php:499 131 msgid "Saving to the Media Library." 132 msgstr "" 133 134 #: includes/class-stillframe-asset-loader.php:500 135 msgid "Saved to Media" 136 msgstr "" 137 138 #: includes/class-stillframe-asset-loader.php:501 139 msgid "View in Media" 140 msgstr "" 141 142 #: includes/class-stillframe-asset-loader.php:502 143 #: includes/class-stillframe-media.php:171 144 msgid "The media save failed. The page was not changed." 145 msgstr "" 146 147 #: includes/class-stillframe-asset-loader.php:503 98 148 msgid "Pen" 99 149 msgstr "" 100 150 101 #: includes/class-stillframe-asset-loader.php: 243151 #: includes/class-stillframe-asset-loader.php:504 102 152 msgid "Circle" 103 153 msgstr "" 104 154 105 #: includes/class-stillframe-asset-loader.php: 244155 #: includes/class-stillframe-asset-loader.php:505 106 156 msgid "Arrow" 107 157 msgstr "" 108 158 109 #: includes/class-stillframe-asset-loader.php: 245159 #: includes/class-stillframe-asset-loader.php:506 110 160 msgid "Undo" 111 161 msgstr "" 112 162 113 #: includes/class-stillframe-asset-loader.php: 246163 #: includes/class-stillframe-asset-loader.php:507 114 164 msgid "Clear" 115 165 msgstr "" 116 166 117 #: includes/class-stillframe-asset-loader.php: 247167 #: includes/class-stillframe-asset-loader.php:508 118 168 msgid "Annotation tools" 119 169 msgstr "" 120 170 121 #: includes/class-stillframe-asset-loader.php: 248171 #: includes/class-stillframe-asset-loader.php:509 122 172 msgid "Captured screen" 123 173 msgstr "" 124 174 125 #: includes/class-stillframe-asset-loader.php: 249175 #: includes/class-stillframe-asset-loader.php:510 126 176 msgid "Stillframe capture frame" 127 177 msgstr "" 128 178 129 #: includes/class-stillframe-asset-loader.php: 250179 #: includes/class-stillframe-asset-loader.php:511 130 180 msgid "Capturing this screen." 131 181 msgstr "" 132 182 133 183 #. translators: 1: width in CSS pixels, 2: export scale. 134 #: includes/class-stillframe-asset-loader.php: 252184 #: includes/class-stillframe-asset-loader.php:513 135 185 #, php-format 136 186 msgid "Captured at %1$d pixels wide and %2$dx. Draw on the picture, then download the PNG." 137 187 msgstr "" 138 188 139 #: includes/class-stillframe-asset-loader.php: 253189 #: includes/class-stillframe-asset-loader.php:514 140 190 msgid "Preparing the PNG." 141 191 msgstr "" 142 192 143 #: includes/class-stillframe-asset-loader.php: 254193 #: includes/class-stillframe-asset-loader.php:515 144 194 msgid "Stillframe could not load the capture tools. The page was not changed." 145 195 msgstr "" 146 196 147 #: includes/class-stillframe-asset-loader.php: 255197 #: includes/class-stillframe-asset-loader.php:516 148 198 msgid "The capture failed. The page was not changed." 149 199 msgstr "" 150 200 151 #: includes/class-stillframe-asset-loader.php: 256201 #: includes/class-stillframe-asset-loader.php:517 152 202 msgid "The capture timed out. The page was not changed." 153 203 msgstr "" 154 204 155 #: includes/class-stillframe-asset-loader.php: 257205 #: includes/class-stillframe-asset-loader.php:518 156 206 msgid "The frame was blocked, so only the current window can be captured. The page was not changed." 157 207 msgstr "" 158 208 159 #: includes/class-stillframe-asset-loader.php: 258209 #: includes/class-stillframe-asset-loader.php:519 160 210 msgid "Enter a whole number from 320 to 2560. The page was not changed." 161 211 msgstr "" 162 212 163 #: includes/class-stillframe-asset-loader.php: 259213 #: includes/class-stillframe-asset-loader.php:520 164 214 msgid "The download failed. The page was not changed." 165 215 msgstr "" 166 216 167 #: includes/class-stillframe-asset-loader.php: 260217 #: includes/class-stillframe-asset-loader.php:521 168 218 msgid "The capture tool did not load. The page was not changed." 169 219 msgstr "" 220 221 #: includes/class-stillframe-asset-loader.php:522 222 msgid "Drag to select an area" 223 msgstr "" 224 225 #: includes/class-stillframe-asset-loader.php:523 226 msgid "Click a section of the page to capture it" 227 msgstr "" 228 229 #: includes/class-stillframe-asset-loader.php:524 230 msgid "Area" 231 msgstr "" 232 233 #: includes/class-stillframe-asset-loader.php:525 234 msgid "Window" 235 msgstr "" 236 237 #: includes/class-stillframe-asset-loader.php:526 238 msgid "Full screen" 239 msgstr "" 240 241 #: includes/class-stillframe-asset-loader.php:527 242 msgid "Capturing…" 243 msgstr "" 244 245 #: includes/class-stillframe-asset-loader.php:528 246 msgid "Almost there. This will finish as soon as the capture is ready." 247 msgstr "" 248 249 #: includes/class-stillframe-asset-loader.php:529 250 msgid "Downloaded." 251 msgstr "" 252 253 #: includes/class-stillframe-asset-loader.php:530 254 msgid "Color" 255 msgstr "" 256 257 #: includes/class-stillframe-asset-loader.php:531 258 msgid "Line thickness" 259 msgstr "" 260 261 #: includes/class-stillframe-asset-loader.php:532 262 msgid "Top bar" 263 msgstr "" 264 265 #: includes/class-stillframe-asset-loader.php:533 266 msgid "Side menu" 267 msgstr "" 268 269 #: includes/class-stillframe-asset-loader.php:534 270 msgid "Hide other plugins" 271 msgstr "" 272 273 #: includes/class-stillframe-asset-loader.php:535 274 msgid "Admin notices" 275 msgstr "" 276 277 #: includes/class-stillframe-asset-loader.php:536 278 msgid "Cancel" 279 msgstr "" 280 281 #: includes/class-stillframe-asset-loader.php:537 282 msgid "More" 283 msgstr "" 284 285 #: includes/class-stillframe-asset-loader.php:538 286 msgid "Include the top bar" 287 msgstr "" 288 289 #: includes/class-stillframe-asset-loader.php:539 290 msgid "Include the side menu" 291 msgstr "" 292 293 #: includes/class-stillframe-asset-loader.php:540 294 msgid "Direct Page" 295 msgstr "" 296 297 #: includes/class-stillframe-asset-loader.php:541 298 msgid "Recent" 299 msgstr "" 300 301 #: includes/class-stillframe-asset-loader.php:542 302 #: includes/class-stillframe-asset-loader.php:558 303 msgid "Devices" 304 msgstr "" 305 306 #: includes/class-stillframe-asset-loader.php:543 307 msgid "Current screen" 308 msgstr "" 309 310 #: includes/class-stillframe-asset-loader.php:544 311 msgid "Home" 312 msgstr "" 313 314 #: includes/class-stillframe-asset-loader.php:545 315 msgid "Pages" 316 msgstr "" 317 318 #: includes/class-stillframe-asset-loader.php:546 319 msgid "Posts" 320 msgstr "" 321 322 #: includes/class-stillframe-asset-loader.php:547 323 msgid "Custom URL: paste any address..." 324 msgstr "" 325 326 #: includes/class-stillframe-asset-loader.php:548 327 msgid "Custom URL" 328 msgstr "" 329 330 #: includes/class-stillframe-asset-loader.php:549 331 msgid "Use" 332 msgstr "" 333 334 #: includes/class-stillframe-asset-loader.php:550 335 msgid "Search pages or paste a URL from this site" 336 msgstr "" 337 338 #: includes/class-stillframe-asset-loader.php:551 339 msgid "Search pages" 340 msgstr "" 341 342 #: includes/class-stillframe-asset-loader.php:552 343 msgid "Enter a URL below" 344 msgstr "" 345 346 #: includes/class-stillframe-asset-loader.php:553 347 msgid "No matching pages. Paste a full URL to use it." 348 msgstr "" 349 350 #: includes/class-stillframe-asset-loader.php:554 351 msgid "Capture a page" 352 msgstr "" 353 354 #: includes/class-stillframe-asset-loader.php:555 355 msgid "Page" 356 msgstr "" 357 358 #: includes/class-stillframe-asset-loader.php:556 359 msgid "Current Admin Screen" 360 msgstr "" 361 362 #: includes/class-stillframe-asset-loader.php:557 363 msgid "Current Screen" 364 msgstr "" 365 366 #: includes/class-stillframe-asset-loader.php:559 367 msgid "Desktop Only" 368 msgstr "" 369 370 #: includes/class-stillframe-asset-loader.php:560 371 msgid "Select All (3)" 372 msgstr "" 373 374 #: includes/class-stillframe-asset-loader.php:561 375 msgid "Hide the admin bar in the screenshot" 376 msgstr "" 377 378 #: includes/class-stillframe-asset-loader.php:562 379 msgid "Quality" 380 msgstr "" 381 382 #: includes/class-stillframe-asset-loader.php:563 383 msgid "Height" 384 msgstr "" 385 386 #: includes/class-stillframe-asset-loader.php:567 387 msgid "Same pixels as the screen. Smallest files." 388 msgstr "" 389 390 #: includes/class-stillframe-asset-loader.php:568 391 msgid "Sharp on high-density displays. Recommended." 392 msgstr "" 393 394 #: includes/class-stillframe-asset-loader.php:569 395 msgid "Maximum detail. Large files; very long pages may be reduced." 396 msgstr "" 397 398 #: includes/class-stillframe-asset-loader.php:570 399 msgid "First screen" 400 msgstr "" 401 402 #: includes/class-stillframe-asset-loader.php:571 403 msgid "Full Page" 404 msgstr "" 405 406 #: includes/class-stillframe-asset-loader.php:572 407 msgid "Only the first screen (above the fold)." 408 msgstr "" 409 410 #: includes/class-stillframe-asset-loader.php:573 411 msgid "The entire page, top to bottom." 412 msgstr "" 413 414 #: includes/class-stillframe-asset-loader.php:574 415 msgid "Capture 3 devices" 416 msgstr "" 417 418 #: includes/class-stillframe-asset-loader.php:577 419 msgid "Mobile" 420 msgstr "" 421 422 #. translators: 1: rows shown, 2: rows matched. 423 #: includes/class-stillframe-asset-loader.php:579 424 #, php-format 425 msgid "Showing %1$d of %2$d. Keep typing to narrow the list." 426 msgstr "" 427 428 #: includes/class-stillframe-asset-loader.php:580 429 msgid "Choose a page" 430 msgstr "" 431 432 #: includes/class-stillframe-asset-loader.php:582 433 msgid "Choose at least one device to capture." 434 msgstr "" 435 436 #: includes/class-stillframe-asset-loader.php:583 437 msgid "Use this URL" 438 msgstr "" 439 440 #: includes/class-stillframe-asset-loader.php:584 441 msgid "Pixel-perfect capture" 442 msgstr "" 443 444 #: includes/class-stillframe-asset-loader.php:585 445 msgid "Area, Window, and Full screen on this tab in Chrome or Edge. The browser asks to share the tab, and nothing is uploaded. Other browsers, Direct Page, and Tools redraw the page." 446 msgstr "" 447 448 #: includes/class-stillframe-asset-loader.php:586 449 msgid "This browser cannot share the tab, so this shot was redrawn from the page." 450 msgstr "" 451 452 #: includes/class-stillframe-asset-loader.php:587 453 msgid "Sharing was cancelled, so this shot was redrawn from the page. The next capture will ask to share the tab again." 454 msgstr "" 455 456 #: includes/class-stillframe-asset-loader.php:588 457 msgid "That share was not this browser tab, so this shot was redrawn from the page. The next capture will ask again." 458 msgstr "" 459 460 #: includes/class-stillframe-asset-loader.php:589 461 msgid "The tab capture did not match this window, so this shot was redrawn from the page. The next capture will ask again." 462 msgstr "" 463 464 #: includes/class-stillframe-asset-loader.php:590 465 msgid "Pixel-perfect capture did not finish, so this shot was redrawn from the page. The next capture will ask again." 466 msgstr "" 467 468 #: includes/class-stillframe-asset-loader.php:591 469 msgid "This capture has unsaved marks. Opening another screenshot will discard them. Continue?" 470 msgstr "" 471 472 #. translators: %d: number of devices. 473 #: includes/class-stillframe-asset-loader.php:593 474 #, php-format 475 msgid "Capture %d devices" 476 msgstr "" 477 478 #. translators: %s: device name. 479 #: includes/class-stillframe-asset-loader.php:595 480 #, php-format 481 msgid "Capture %s" 482 msgstr "" 483 484 #: includes/class-stillframe-media.php:35 485 msgid "You cannot save this capture." 486 msgstr "" 487 488 #: includes/class-stillframe-tools-page.php:33 489 #: includes/class-stillframe-tools-page.php:34 490 #: includes/class-stillframe-tools-page.php:53 491 #: includes/class-stillframe-tools-page.php:57 492 msgid "Stillframe" 493 msgstr "" 494 495 #: includes/class-stillframe-tools-page.php:48 496 msgid "You cannot capture this screen." 497 msgstr "" 498 499 #: includes/class-stillframe-tools-page.php:64 500 msgid "Target Page" 501 msgstr "" 502 503 #: includes/class-stillframe-tools-page.php:65 504 msgid "Select a site page or enter a custom URL to capture." 505 msgstr "" 506 507 #: includes/class-stillframe-tools-page.php:75 508 msgid "Device Viewports" 509 msgstr "" 510 511 #: includes/class-stillframe-tools-page.php:76 512 msgid "Select viewports to capture simultaneously." 513 msgstr "" 514 515 #: includes/class-stillframe-tools-page.php:115 516 msgid "iPad / Tablet" 517 msgstr "" 518 519 #: includes/class-stillframe-tools-page.php:135 520 msgid "Mobile Phone" 521 msgstr "" 522 523 #: includes/class-stillframe-tools-page.php:145 524 msgid "Capture Preferences" 525 msgstr "" 526 527 #: includes/class-stillframe-tools-page.php:146 528 msgid "These shots redraw the page. Pixel-perfect capture is Area, Window, and Full screen from the admin bar." 529 msgstr "" 530 531 #: includes/class-stillframe-tools-page.php:151 532 msgid "Resolution Quality" 533 msgstr "" 534 535 #: includes/class-stillframe-tools-page.php:170 536 msgid "Capture Height" 537 msgstr "" 538 539 #: includes/class-stillframe-tools-page.php:185 540 msgid "Clean Presentation" 541 msgstr "" 542 543 #: includes/class-stillframe-tools-page.php:189 544 msgid "Hide WordPress Admin Bar" 545 msgstr "" 546 547 #: includes/class-stillframe-tools-page.php:202 548 msgid "Capture Selected Devices" 549 msgstr "" -
stillframe/tags/0.2.5/readme.txt
r3724668 r3736872 4 4 Requires at least: 6.4 5 5 Tested up to: 7.1 6 Stable tag: 0.2. 06 Stable tag: 0.2.5 7 7 Requires PHP: 7.4 8 8 License: GPL-2.0-or-later … … 58 58 == Screenshots == 59 59 60 1. Clean floating capture toolbar with Area selection mode and tooltipon the WordPress dashboard.61 2. Window & element capture mode with live pixel dimensions and instant capture button.62 3. Devices dropdown: frame and capture standard Desktop (1440px), iPad (834px), and Mobile (390px) viewportson screen.63 4. Direct Page capture dropdown: choose target pages, select viewports, toggle clean presentation mode, and select Retina resolution.64 5. Annotation Editor Suite: full canvas markup tools including shapes, arrows, text, numbered steps, blur redaction, and instant export.60 1. Clean floating capture toolbar with Area selection and the drag hint on the WordPress dashboard. 61 2. Window capture: hover any section, see its size, then capture it. 62 3. Window mode device bar: frame Desktop (1440px), iPad (834px), or Mobile (390px) on screen. 63 4. Direct Page: pick a page, choose devices, set quality and height, hide the admin bar, and capture. 64 5. Annotation editor: shapes, arrows, text, numbered steps, blur, and download or save to the Media Library. 65 65 66 66 == Frequently Asked Questions == … … 86 86 Stillframe bundles modern-screenshot 4.7.0 (MIT license) as assets/js/vendor/modern-screenshot.js. Source: https://github.com/qq15725/modern-screenshot. The license text is in assets/js/vendor/LICENSE-modern-screenshot.txt. 87 87 88 Stillframe also bundles Fabric.js 6.7.1 (MIT license) as assets/js/vendor/fabric.min.js. Source: https://github.com/fabricjs/fabric.js (npm package "fabric"). The license text is in assets/js/vendor/LICENSE-fabric.txt. 89 88 90 == Changelog == 91 92 = 0.2.5 = 93 * fix the editor failing to open a capture when no text size was set 94 * keep the current admin screen when capturing it, including the post editor, and still drop links that would delete or change data 95 * ask before another screenshot replaces one that still has unsaved marks 96 * keep one-line rows, such as pagination and the admin bar, on a single line in redrawn screenshots 97 * load dashboard icons before a redraw so they stay visible in the screenshot 98 * change text boxes so the side handles set the line width, the corners scale the letters, and the height follows the text 99 * shrink selection handles on small marks so they no longer cover the shape 100 * let marks and their handles sit in the grey area around the picture without being cut off 101 * show Desktop, iPad, and Mobile on a solid bar under the capture toolbar in Window mode 102 * wait until the pointer is idle before redrawing the page, so selecting and drawing stay responsive 103 * start a full-screen redraw as soon as the area is selected, so Capture does not wait for the page to be built 104 * load the annotation editor from the admin bar using the current file version, so an update is not served from an old cached copy 105 * explain that pixel-perfect capture is Area, Window, and Full screen in Chrome or Edge, and that Direct Page and Tools redraw the page 106 * say when tab sharing is cancelled or does not match this window, then redraw the page instead 107 108 = 0.2.1 = 109 * Text marks stretch freely: the corner handles scale the text evenly, the side handles stretch it wider or narrower, and the top and bottom handles make it taller or shorter. The Size control keeps showing the real letter height. 110 * Marks can now be dragged and resized out over the grey area around the picture. The part outside the picture is shown faded, and only the picture itself is saved. 111 * Every handle of a mark stays visible and reachable: a mark can no longer be pushed or resized past the edge of the working area. 112 * Smaller, lighter transform handles and a closer rotate knob on every mark; a thinner white outline on large text. 113 * Device frame: Desktop, iPad and Mobile frames keep their proportions on small screens, no longer sit under the top bar, and the device buttons stay available so the size can be switched after framing. 114 * The device buttons in Window mode now hang directly under the top bar, centred on it. 115 * Scripts and styles are versioned by file date, so an updated plugin never serves a stale cached copy. 89 116 90 117 = 0.2.0 = -
stillframe/tags/0.2.5/stillframe.php
r3724658 r3736872 4 4 * Plugin URI: https://grafucci.com/stillframe/ 5 5 * Description: Capture multi-device screenshots (Desktop, Tablet, Mobile) or custom regions, mark them up with rich annotation tools, and export to PNG or Media Library. 6 * Version: 0.2. 06 * Version: 0.2.5 7 7 * Requires at least: 6.4 8 8 * Requires PHP: 7.4 … … 21 21 } 22 22 23 define( 'STILLFRAME_VERSION', '0.2. 0' );23 define( 'STILLFRAME_VERSION', '0.2.5' ); 24 24 define( 'STILLFRAME_FILE', __FILE__ ); 25 25 define( 'STILLFRAME_DIR', plugin_dir_path( __FILE__ ) ); 26 // Asset query string follows the newest file, so updated scripts and styles are never served from a stale browser cache. 27 define( 'STILLFRAME_ASSET_VER', STILLFRAME_VERSION . '.' . max( (int) filemtime( STILLFRAME_DIR . 'assets/js/stillframe-capture-panel.js' ), (int) filemtime( STILLFRAME_DIR . 'assets/js/stillframe-annotation-editor-v2.js' ), (int) filemtime( STILLFRAME_DIR . 'assets/js/stillframe-annotation-editor.js' ), (int) filemtime( STILLFRAME_DIR . 'assets/js/stillframe-admin-bar.js' ), (int) filemtime( STILLFRAME_DIR . 'assets/css/stillframe-capture-panel.css' ) ) ); 26 28 27 29 require_once STILLFRAME_DIR . 'includes/class-stillframe-capability-check.php'; -
stillframe/tags/0.2.5/uninstall.php
r3724658 r3736872 19 19 20 20 delete_transient( 'stillframe_site_pages' ); 21 delete_transient( 'stillframe_site_pages_v2' ); -
stillframe/trunk/assets/css/stillframe-capture-panel.css
r3724658 r3736872 1595 1595 align-items: center; 1596 1596 gap: 10px; 1597 width: max-content; 1598 max-width: calc(100% - 32px); 1597 width: fit-content; 1598 max-width: min(520px, calc(100% - 32px)); 1599 white-space: normal; 1599 1600 margin: 0 auto; 1600 1601 padding: 8px 14px; … … 1717 1718 } 1718 1719 1719 /* Neutral accents ------------------------------------------------------- */ 1720 /* The brand colour is reserved for primary buttons (Capture, Download, Save). 1721 Selected and active states use plain greys so nothing else competes. */ 1720 /* Brand accents --------------------------------------------------------- */ 1721 /* Selected and active states use the Stillframe petrol colour. */ 1722 1722 1723 1723 .stillframe-snip, 1724 1724 .stillframe-result { 1725 --sf-ink: # 1f2937;1726 --sf-pick-bg: # f3f4f6;1727 --sf-pick-line: # 9ca3af;1725 --sf-ink: #005976; 1726 --sf-pick-bg: #e6f4f8; 1727 --sf-pick-line: #7fb9cc; 1728 1728 } 1729 1729 … … 1851 1851 .stillframe-snip, 1852 1852 .stillframe-result { 1853 --sf-pick-line: # 6b7280;1853 --sf-pick-line: #7fb9cc; 1854 1854 --sf-edge: #b4bac4; 1855 1855 } … … 2070 2070 } 2071 2071 .stillframe-snip__bar .stillframe-hub-sec { margin-bottom: 0; } 2072 .stillframe-snip__direct-menu { width: 480px; padding: 12px; gap: 8px; } 2072 .stillframe-snip__direct-menu { width: 480px; min-width: 360px; padding: 12px; gap: 8px; max-height: min(80vh, 640px); overflow-y: auto; } 2073 .stillframe-snip__recent-menu { max-height: min(80vh, 640px); overflow-y: auto; } 2074 2075 .stillframe-hub-help { 2076 font-size: 11.5px; 2077 line-height: 1.4; 2078 color: var(--sf-muted, #6b7280); 2079 } 2080 2081 /* Recent screenshots: two-column card grid */ 2082 .stillframe-snip__recent-menu { 2083 width: 520px; 2084 } 2085 2086 .stillframe-snip .stillframe-recent-list { 2087 display: grid; 2088 grid-template-columns: repeat(2, minmax(0, 1fr)); 2089 gap: 10px; 2090 max-height: none; 2091 overflow: visible; 2092 } 2093 2094 .stillframe-snip .stillframe-recent-card { 2095 flex-direction: column; 2096 gap: 8px; 2097 padding: 8px; 2098 background: var(--sf-surface); 2099 border: 1px solid var(--sf-line); 2100 border-radius: 10px; 2101 } 2102 2103 .stillframe-snip .stillframe-recent-card:last-child { 2104 border-bottom: 1px solid var(--sf-line); 2105 } 2106 2107 .stillframe-snip .stillframe-recent-card:hover { 2108 background: var(--sf-brand-muted); 2109 border-color: var(--sf-brand); 2110 } 2111 2112 .stillframe-snip .stillframe-recent-thumbwrap { 2113 flex: 0 0 auto; 2114 width: 100%; 2115 height: auto; 2116 aspect-ratio: 16 / 10; 2117 } 2118 2119 .stillframe-snip .stillframe-recent-info { 2120 gap: 4px; 2121 } 2122 2123 .stillframe-snip .stillframe-recent-actions { 2124 margin-top: 2px; 2125 } 2126 2127 @media (max-width: 560px) { 2128 .stillframe-snip .stillframe-recent-list { 2129 grid-template-columns: minmax(0, 1fr); 2130 } 2131 } 2132 2133 .stillframe-result .stillframe-format-select { 2134 height: 36px; 2135 margin: 0 4px 0 0; 2136 padding: 0 24px 0 10px; 2137 font-size: 13px; 2138 font-weight: 600; 2139 color: var(--sf-text-2, #374151); 2140 background-color: var(--sf-surface, #ffffff); 2141 border: 1px solid var(--sf-line-strong, #d1d5db); 2142 border-radius: 8px; 2143 box-shadow: none; 2144 cursor: pointer; 2145 } 2146 2147 .stillframe-result .stillframe-format-select:hover, 2148 .stillframe-result .stillframe-format-select:focus { 2149 color: var(--sf-brand, #005976); 2150 border-color: var(--sf-brand, #005976); 2151 outline: none; 2152 } 2153 2154 /* Device preview frames */ 2155 .stillframe-dev-card__frame { 2156 display: flex; 2157 align-items: center; 2158 justify-content: center; 2159 margin-bottom: 8px; 2160 background: var(--sf-well); 2161 border: 1.5px solid var(--sf-line-strong); 2162 border-radius: 5px; 2163 box-sizing: border-box; 2164 color: var(--sf-muted); 2165 } 2166 2167 .stillframe-dev-card.is-selected .stillframe-dev-card__frame { 2168 background: var(--sf-surface); 2169 border-color: var(--sf-brand); 2170 color: var(--sf-brand); 2171 } 2172 2173 .stillframe-dev-card__frame .stillframe-dev-card__icon { 2174 margin: 0; 2175 color: inherit; 2176 } 2177 2178 .stillframe-hub-size { 2179 font-size: 11.5px; 2180 line-height: 1.4; 2181 color: var(--sf-text-2); 2182 } 2183 2184 /* Sticky capture button */ 2185 .stillframe-hub-footer { 2186 position: sticky; 2187 bottom: -12px; 2188 z-index: 2; 2189 margin: 4px -12px -12px; 2190 padding: 10px 12px 12px; 2191 background: var(--sf-surface); 2192 border-top: 1px solid var(--sf-line); 2193 border-radius: 0 0 10px 10px; 2194 } 2195 2196 @media (max-width: 560px) { 2197 .stillframe-snip__direct-menu { 2198 width: calc(100vw - 24px); 2199 min-width: 0; 2200 } 2201 } 2202 2203 /* Fabric annotation layer sits exactly over the captured image */ 2204 .stillframe-result .stillframe-annotation-wrapper { 2205 z-index: 2; 2206 } 2207 2208 .stillframe-result .stillframe-annotation-wrapper canvas { 2209 touch-action: none; 2210 } 2211 2212 /* ========================================================================== 2213 Stillframe interface layer 2214 Flat, compact controls in the Stillframe petrol colour. These rules sit last 2215 and use !important on colours so a site theme's button styles cannot recolour them. 2216 ========================================================================== */ 2217 2218 .stillframe-snip, 2219 .stillframe-result { 2220 --sf-brand: #005976; 2221 --sf-brand-hover: #004a61; 2222 --sf-brand-muted: #f1f6f8; 2223 --sf-brand-border: #c3d3da; 2224 } 2225 2226 .stillframe-snip button, 2227 .stillframe-result button, 2228 .sfp button { 2229 appearance: none; 2230 margin: 0; 2231 font-family: inherit; 2232 letter-spacing: normal; 2233 text-transform: none; 2234 text-shadow: none; 2235 } 2236 2237 /* ---- Top selection bar ---- */ 2238 .stillframe-snip .stillframe-snip__bar button { 2239 height: 32px; 2240 gap: 6px; 2241 padding: 0 10px; 2242 font-size: 12.5px; 2243 font-weight: 600; 2244 border-radius: 6px; 2245 } 2246 2247 .stillframe-snip .stillframe-snip__bar svg { 2248 width: 16px; 2249 height: 16px; 2250 } 2251 2252 .stillframe-snip .stillframe-snip__bar .stillframe-snip__chevron { 2253 width: 10px; 2254 height: 10px; 2255 } 2256 2257 .stillframe-snip .stillframe-snip__bar .stillframe-snip__mode:hover, 2258 .stillframe-snip .stillframe-snip__bar .stillframe-snip__close:hover { 2259 color: var(--sf-brand) !important; 2260 background: #f3f5f6 !important; 2261 } 2262 2263 .stillframe-snip .stillframe-snip__bar .stillframe-snip__mode[aria-pressed="true"], 2264 .stillframe-snip.has-menu-open .stillframe-snip__bar .stillframe-snip__mode[aria-expanded="true"] { 2265 color: var(--sf-brand) !important; 2266 background: var(--sf-brand-muted) !important; 2267 border-color: var(--sf-brand-border) !important; 2268 } 2269 2270 .stillframe-snip .stillframe-snip__go, 2271 .stillframe-snip .stillframe-snip__go:focus { 2272 color: #ffffff !important; 2273 background: var(--sf-brand) !important; 2274 border: 0 !important; 2275 box-shadow: none !important; 2276 } 2277 2278 .stillframe-snip .stillframe-snip__go:hover { 2279 color: #ffffff !important; 2280 background: var(--sf-brand-hover) !important; 2281 } 2282 2283 /* ---- Menus that open from the bar ---- */ 2284 .stillframe-snip .stillframe-snip__direct-menu { 2285 width: 400px; 2286 max-width: calc(100vw - 24px); 2287 padding: 12px 14px 14px; 2288 gap: 10px; 2289 max-height: none; 2290 overflow: visible; 2291 } 2292 2293 @media (max-height: 640px) { 2294 .stillframe-snip .stillframe-snip__direct-menu { 2295 max-height: calc(100vh - 110px); 2296 overflow-y: auto; 2297 } 2298 } 2299 2300 .stillframe-snip .stillframe-hub-sec-title { 2301 margin: 0; 2302 font-size: 11px; 2303 font-weight: 700; 2304 letter-spacing: 0.06em; 2305 text-transform: uppercase; 2306 color: var(--sf-muted); 2307 } 2308 2309 .sfd-row { 2310 display: grid; 2311 grid-template-columns: 64px minmax(0, 1fr); 2312 align-items: center; 2313 gap: 8px; 2314 } 2315 2316 .sfd-label { 2317 font-size: 12px; 2318 font-weight: 600; 2319 color: var(--sf-text-2, #374151); 2320 } 2321 2322 .sfd-chips { 2323 display: flex; 2324 gap: 6px; 2325 min-width: 0; 2326 } 2327 2328 .stillframe-snip .sfd-chip { 2329 flex: 1 1 0; 2330 display: inline-flex; 2331 align-items: center; 2332 justify-content: center; 2333 gap: 5px; 2334 min-width: 0; 2335 height: 30px; 2336 padding: 0 6px; 2337 font-size: 12px; 2338 font-weight: 600; 2339 color: var(--sf-text-2, #374151) !important; 2340 background: #ffffff !important; 2341 border: 1px solid var(--sf-line-strong, #d1d5db) !important; 2342 border-radius: 6px; 2343 box-shadow: none !important; 2344 cursor: pointer; 2345 } 2346 2347 .stillframe-snip .sfd-chip em { 2348 font-style: normal; 2349 font-weight: 500; 2350 font-size: 11px; 2351 color: var(--sf-muted, #6b7280); 2352 } 2353 2354 .stillframe-snip .sfd-chip:hover { 2355 color: var(--sf-brand) !important; 2356 border-color: var(--sf-brand-border) !important; 2357 } 2358 2359 .stillframe-snip .sfd-chip.is-selected { 2360 color: var(--sf-brand) !important; 2361 background: var(--sf-brand-muted) !important; 2362 border-color: var(--sf-brand) !important; 2363 } 2364 2365 .stillframe-snip .sfd-chip.is-selected em { 2366 color: var(--sf-brand); 2367 } 2368 2369 .sfd-seg { 2370 display: inline-flex; 2371 width: fit-content; 2372 overflow: hidden; 2373 border: 1px solid var(--sf-line-strong, #d1d5db); 2374 border-radius: 6px; 2375 } 2376 2377 .stillframe-snip .sfd-seg__btn { 2378 height: 28px; 2379 padding: 0 14px; 2380 font-size: 12px; 2381 font-weight: 600; 2382 color: var(--sf-text-2, #374151) !important; 2383 background: #ffffff !important; 2384 border: 0 !important; 2385 border-right: 1px solid var(--sf-line-strong, #d1d5db) !important; 2386 border-radius: 0; 2387 box-shadow: none !important; 2388 cursor: pointer; 2389 } 2390 2391 .stillframe-snip .sfd-seg__btn:last-child { 2392 border-right: 0 !important; 2393 } 2394 2395 .stillframe-snip .sfd-seg__btn:hover { 2396 color: var(--sf-brand) !important; 2397 background: var(--sf-brand-muted) !important; 2398 } 2399 2400 .stillframe-snip .sfd-seg__btn.is-active, 2401 .stillframe-snip .sfd-seg__btn.is-active:hover { 2402 color: #ffffff !important; 2403 background: var(--sf-brand) !important; 2404 } 2405 2406 .sfd-help { 2407 margin: -4px 0 0; 2408 padding-left: 72px; 2409 font-size: 11.5px; 2410 line-height: 1.4; 2411 color: var(--sf-muted, #6b7280); 2412 } 2413 2414 .stillframe-snip .stillframe-hub-toggle { 2415 gap: 8px; 2416 font-size: 12.5px; 2417 color: var(--sf-text-2, #374151); 2418 } 2419 2420 .stillframe-snip .stillframe-hub-toggle__track { 2421 width: 30px; 2422 height: 17px; 2423 background: #cbd5e1; 2424 } 2425 2426 .stillframe-snip .stillframe-hub-toggle__track::before { 2427 top: 2px; 2428 left: 2px; 2429 width: 13px; 2430 height: 13px; 2431 } 2432 2433 .stillframe-snip .stillframe-hub-toggle__input:checked + .stillframe-hub-toggle__track { 2434 background: var(--sf-brand) !important; 2435 } 2436 2437 .stillframe-snip .stillframe-hub-toggle__input:checked + .stillframe-hub-toggle__track::before { 2438 transform: translateX(13px); 2439 } 2440 2441 .stillframe-snip .stillframe-hub-toggle__input:focus-visible + .stillframe-hub-toggle__track { 2442 outline: 2px solid var(--sf-brand); 2443 } 2444 2445 .sfd-engine { 2446 align-items: flex-start; 2447 padding-top: 10px; 2448 margin-top: 4px; 2449 border-top: 1px solid var(--sf-line, #e5e7eb); 2450 } 2451 2452 .sfd-engine .stillframe-hub-toggle__track { 2453 margin-top: 2px; 2454 } 2455 2456 .sfd-engine__text { 2457 display: flex; 2458 flex-direction: column; 2459 gap: 2px; 2460 min-width: 0; 2461 } 2462 2463 .sfd-engine__text b { 2464 font-size: 12.5px; 2465 font-weight: 600; 2466 color: var(--sf-text, #111827); 2467 } 2468 2469 .sfd-engine__text small { 2470 font-size: 11.5px; 2471 line-height: 1.4; 2472 color: var(--sf-muted, #6b7280); 2473 } 2474 2475 .stillframe-snip .sfd-submit { 2476 display: flex; 2477 align-items: center; 2478 justify-content: center; 2479 gap: 8px; 2480 width: 100%; 2481 height: 34px; 2482 padding: 0 12px; 2483 font-size: 13px; 2484 font-weight: 600; 2485 color: #ffffff !important; 2486 background: var(--sf-brand) !important; 2487 border: 1px solid var(--sf-brand) !important; 2488 border-radius: 6px; 2489 box-shadow: none !important; 2490 cursor: pointer; 2491 } 2492 2493 .stillframe-snip .sfd-submit:hover { 2494 color: #ffffff !important; 2495 background: var(--sf-brand-hover) !important; 2496 border-color: var(--sf-brand-hover) !important; 2497 } 2498 2499 /* ---- Page dropdown ---- */ 2500 .sfp { 2501 position: relative; 2502 min-width: 0; 2503 width: 100%; 2504 font-size: 12.5px; 2505 } 2506 2507 .sfp *, 2508 .sfp *::before, 2509 .sfp *::after { 2510 box-sizing: border-box; 2511 } 2512 2513 .sfp .sfp__trigger { 2514 display: flex; 2515 align-items: center; 2516 justify-content: space-between; 2517 gap: 8px; 2518 width: 100%; 2519 height: 30px; 2520 padding: 0 10px; 2521 font-size: 12.5px; 2522 font-weight: 500; 2523 line-height: 1; 2524 text-align: left; 2525 color: var(--sf-text, #111827) !important; 2526 background: #ffffff !important; 2527 border: 1px solid var(--sf-line-strong, #d1d5db) !important; 2528 border-radius: 6px; 2529 box-shadow: none !important; 2530 cursor: pointer; 2531 } 2532 2533 .sfp .sfp__trigger:hover, 2534 .sfp.is-open .sfp__trigger { 2535 border-color: var(--sf-brand) !important; 2536 } 2537 2538 .sfp .sfp__trigger:focus-visible { 2539 outline: 2px solid var(--sf-brand); 2540 outline-offset: 1px; 2541 } 2542 2543 .sfp .sfp__summary { 2544 flex: 1 1 auto; 2545 min-width: 0; 2546 overflow: hidden; 2547 text-overflow: ellipsis; 2548 white-space: nowrap; 2549 } 2550 2551 .sfp .sfp__trigger svg { 2552 flex: 0 0 auto; 2553 color: var(--sf-muted, #6b7280); 2554 } 2555 2556 .sfp.is-open .sfp__trigger svg { 2557 transform: rotate(180deg); 2558 } 2559 2560 .sfp .sfp__drop { 2561 position: absolute; 2562 z-index: 40; 2563 top: calc(100% + 4px); 2564 left: 0; 2565 right: 0; 2566 padding: 6px; 2567 background: #ffffff; 2568 border: 1px solid var(--sf-line-strong, #d1d5db); 2569 border-radius: 8px; 2570 box-shadow: 0 10px 28px rgba(15, 23, 42, 0.18), 0 2px 6px rgba(15, 23, 42, 0.08); 2571 } 2572 2573 .sfp .sfp__drop[hidden] { 2574 display: none !important; 2575 } 2576 2577 .sfp .sfp__search { 2578 display: block; 2579 width: 100%; 2580 height: 30px; 2581 margin: 0 0 6px; 2582 padding: 0 10px; 2583 font-family: inherit; 2584 font-size: 12.5px; 2585 color: var(--sf-text, #111827); 2586 background: #ffffff; 2587 border: 1px solid var(--sf-line-strong, #d1d5db); 2588 border-radius: 6px; 2589 box-shadow: none; 2590 } 2591 2592 .sfp .sfp__search:focus { 2593 border-color: var(--sf-brand); 2594 box-shadow: 0 0 0 1px var(--sf-brand); 2595 outline: none; 2596 } 2597 2598 .sfp .sfp__list { 2599 max-height: 220px; 2600 overflow-y: auto; 2601 overscroll-behavior: contain; 2602 } 2603 2604 .sfp .sfp__group { 2605 position: sticky; 2606 top: 0; 2607 z-index: 1; 2608 padding: 6px 8px 4px; 2609 font-size: 10.5px; 2610 font-weight: 700; 2611 letter-spacing: 0.05em; 2612 text-transform: uppercase; 2613 color: var(--sf-muted, #6b7280); 2614 background: #ffffff; 2615 } 2616 2617 .sfp .sfp__row { 2618 display: grid; 2619 grid-template-columns: minmax(0, 1fr) auto; 2620 column-gap: 8px; 2621 align-items: center; 2622 padding: 5px 8px; 2623 border-radius: 5px; 2624 cursor: pointer; 2625 } 2626 2627 .sfp .sfp__row:hover, 2628 .sfp .sfp__row.is-active { 2629 background: var(--sf-well, #f3f4f6); 2630 } 2631 2632 .sfp .sfp__row.is-selected { 2633 background: var(--sf-brand-muted, #e6f4f8); 2634 } 2635 2636 .sfp .sfp__row.is-selected .sfp__title { 2637 color: var(--sf-brand); 2638 } 2639 2640 .sfp .sfp__title { 2641 grid-column: 1; 2642 overflow: hidden; 2643 font-weight: 600; 2644 color: var(--sf-text, #111827); 2645 text-overflow: ellipsis; 2646 white-space: nowrap; 2647 } 2648 2649 .sfp .sfp__path { 2650 grid-column: 1; 2651 overflow: hidden; 2652 font-size: 11.5px; 2653 color: var(--sf-muted, #6b7280); 2654 text-overflow: ellipsis; 2655 white-space: nowrap; 2656 } 2657 2658 .sfp .sfp__chip { 2659 grid-column: 2; 2660 grid-row: 1 / span 2; 2661 padding: 1px 6px; 2662 font-size: 10px; 2663 font-weight: 600; 2664 color: var(--sf-muted, #6b7280); 2665 background: var(--sf-well, #f3f4f6); 2666 border-radius: 999px; 2667 } 2668 2669 .sfp .sfp__note { 2670 padding: 10px 8px; 2671 font-size: 12px; 2672 color: var(--sf-muted, #6b7280); 2673 } 2674 2675 /* ---- Editor top bar ---- */ 2676 .stillframe-result .stillframe-result__toolbar { 2677 display: grid; 2678 grid-template-columns: auto minmax(0, 1fr) auto; 2679 grid-template-areas: "brand tools actions"; 2680 align-items: center; 2681 gap: 8px 16px; 2682 padding: 8px 12px; 2683 } 2684 2685 .stillframe-result .stillframe-result__brand { 2686 grid-area: brand; 2687 } 2688 2689 .stillframe-result .stillframe-result__editing { 2690 grid-area: tools; 2691 flex-wrap: wrap; 2692 justify-content: flex-start; 2693 gap: 6px 12px; 2694 } 2695 2696 .stillframe-result .stillframe-result__actions { 2697 grid-area: actions; 2698 justify-self: end; 2699 flex-shrink: 0; 2700 gap: 6px; 2701 margin-left: 0; 2702 } 2703 2704 @media (max-width: 1180px) { 2705 .stillframe-result .stillframe-result__toolbar { 2706 grid-template-columns: minmax(0, 1fr) auto; 2707 grid-template-areas: "brand actions" "tools tools"; 2708 } 2709 } 2710 2711 .stillframe-result .stillframe-result__tools, 2712 .stillframe-result .stillframe-result__history, 2713 .stillframe-result .stillframe-result__sizes, 2714 .stillframe-result .stillframe-result__swatches { 2715 gap: 2px; 2716 padding: 0; 2717 background: transparent; 2718 border-radius: 0; 2719 } 2720 2721 .stillframe-result .stillframe-result__editing > * + * { 2722 padding-left: 12px; 2723 border-left: 1px solid var(--sf-line, #e5e7eb); 2724 } 2725 2726 .stillframe-result .stillframe-result__swatches { 2727 gap: 7px; 2728 } 2729 2730 .stillframe-result .stillframe-tool, 2731 .stillframe-result .stillframe-icon-btn, 2732 .stillframe-result .stillframe-size { 2733 width: 30px; 2734 height: 30px; 2735 color: var(--sf-text-2, #374151) !important; 2736 background: transparent !important; 2737 border: 1px solid transparent !important; 2738 border-radius: 6px; 2739 box-shadow: none !important; 2740 } 2741 2742 .stillframe-result .stillframe-tool svg, 2743 .stillframe-result .stillframe-icon-btn svg { 2744 width: 16px; 2745 height: 16px; 2746 } 2747 2748 .stillframe-result .stillframe-tool:hover, 2749 .stillframe-result .stillframe-icon-btn:hover, 2750 .stillframe-result .stillframe-size:hover { 2751 color: var(--sf-brand) !important; 2752 background: var(--sf-brand-muted) !important; 2753 } 2754 2755 .stillframe-result .stillframe-tool[aria-pressed="true"], 2756 .stillframe-result .stillframe-size[aria-pressed="true"] { 2757 color: var(--sf-brand) !important; 2758 background: var(--sf-brand-muted) !important; 2759 border-color: var(--sf-brand-border) !important; 2760 } 2761 2762 .stillframe-result .stillframe-swatch { 2763 width: 16px; 2764 height: 16px; 2765 } 2766 2767 .stillframe-result .stillframe-btn { 2768 height: 32px; 2769 gap: 6px; 2770 padding: 0 11px; 2771 font-size: 12.5px; 2772 font-weight: 600; 2773 color: var(--sf-text-2, #374151) !important; 2774 background: #ffffff !important; 2775 border: 1px solid var(--sf-line-strong, #d1d5db) !important; 2776 border-radius: 6px; 2777 box-shadow: none !important; 2778 } 2779 2780 .stillframe-result .stillframe-btn svg { 2781 width: 16px; 2782 height: 16px; 2783 } 2784 2785 .stillframe-result .stillframe-btn:hover { 2786 color: var(--sf-brand) !important; 2787 background: var(--sf-brand-muted) !important; 2788 border-color: var(--sf-brand-border) !important; 2789 } 2790 2791 .stillframe-result .stillframe-btn.stillframe-btn-primary, 2792 .stillframe-result .stillframe-btn.stillframe-btn-primary:hover { 2793 color: #ffffff !important; 2794 background: var(--sf-brand) !important; 2795 border-color: var(--sf-brand) !important; 2796 } 2797 2798 .stillframe-result .stillframe-btn.stillframe-btn-primary:hover { 2799 background: var(--sf-brand-hover) !important; 2800 } 2801 2802 .stillframe-result .stillframe-btn.stillframe-result__close { 2803 width: 32px; 2804 padding: 0; 2805 color: var(--sf-muted, #6b7280) !important; 2806 } 2807 2808 .stillframe-result .stillframe-format-select { 2809 height: 32px; 2810 margin: 0; 2811 border-radius: 6px; 2812 } 2813 2814 /* ---- Recent cards ---- */ 2815 .stillframe-snip .stillframe-recent-btn:hover, 2816 .stillframe-result .stillframe-recent-btn:hover { 2817 color: var(--sf-brand) !important; 2818 background: var(--sf-brand-muted) !important; 2819 border-color: var(--sf-brand-border) !important; 2820 } 2821 2822 /* Download keeps its label at every width */ 2823 .stillframe-result .stillframe-btn.stillframe-btn-primary { 2824 width: auto; 2825 min-width: 0; 2826 padding: 0 14px; 2827 } 2828 2829 .stillframe-result .stillframe-btn.stillframe-btn-primary .stillframe-btn__label { 2830 display: inline !important; 2831 } 2832 2833 /* ---- Direct Page tweaks ---- */ 2834 .stillframe-snip .stillframe-snip__direct-menu { 2835 width: 420px; 2836 gap: 12px; 2837 } 2838 2839 .stillframe-snip .sfd-chip { 2840 height: 32px; 2841 } 2842 2843 .stillframe-snip .sfd-chip svg { 2844 width: 17px; 2845 height: 17px; 2846 } 2847 2848 .sfd-error { 2849 margin: -6px 0 0; 2850 padding-left: 72px; 2851 font-size: 12px; 2852 font-weight: 600; 2853 color: #b42318; 2854 } 2855 2856 .sfd-error:empty { 2857 display: none; 2858 } 2859 2860 .stillframe-snip .sfd-row + .stillframe-hub-toggle, 2861 .stillframe-snip .stillframe-snip__direct-menu > .stillframe-hub-toggle { 2862 margin: 6px 0 4px; 2863 } 2864 2865 /* Page list opens across the full width of the menu and nothing in it is sticky */ 2866 .sfd-row .sfp .sfp__drop { 2867 left: -72px; 2868 } 2869 2870 .sfp .sfp__search { 2871 height: 26px; 2872 margin-bottom: 4px; 2873 font-size: 12px; 2874 } 2875 2876 .sfp .sfp__group { 2877 position: static; 2878 } 2879 2880 .sfp .sfp__list { 2881 max-height: 260px; 2882 } 2883 2884 /* ---- Recent cards: quiet hover, smaller buttons ---- */ 2885 .stillframe-snip .stillframe-recent-card:hover, 2886 .stillframe-result .stillframe-recent-card:hover { 2887 background: #ffffff; 2888 border-color: var(--sf-line-strong, #d1d5db); 2889 box-shadow: 0 1px 3px rgba(15, 23, 42, 0.08); 2890 } 2891 2892 .stillframe-snip .stillframe-recent-actions, 2893 .stillframe-result .stillframe-recent-actions { 2894 flex-wrap: nowrap; 2895 gap: 6px; 2896 } 2897 2898 .stillframe-snip .stillframe-recent-btn, 2899 .stillframe-result .stillframe-recent-btn { 2900 height: 26px !important; 2901 gap: 5px !important; 2902 padding: 0 9px !important; 2903 font-size: 12px !important; 2904 font-weight: 600 !important; 2905 border-radius: 6px !important; 2906 } 2907 2908 .stillframe-snip .stillframe-recent-btn svg, 2909 .stillframe-result .stillframe-recent-btn svg { 2910 width: 13px; 2911 height: 13px; 2912 } 2913 2914 .stillframe-snip .stillframe-recent-btn:hover, 2915 .stillframe-result .stillframe-recent-btn:hover { 2916 color: var(--sf-text, #111827) !important; 2917 background: #f3f4f6 !important; 2918 border-color: #9ca3af !important; 2919 } 2920 2921 .stillframe-snip .stillframe-recent-btn.stillframe-recent-btn--del, 2922 .stillframe-result .stillframe-recent-btn.stillframe-recent-btn--del { 2923 width: 26px !important; 2924 padding: 0 !important; 2925 } 2926 2927 .stillframe-snip .stillframe-recent-btn.stillframe-recent-btn--del:hover, 2928 .stillframe-result .stillframe-recent-btn.stillframe-recent-btn--del:hover { 2929 color: #b42318 !important; 2930 background: #fef2f2 !important; 2931 border-color: #f1b5b0 !important; 2932 } 2933 2934 /* ---- Lighter states, calmer menus ---- */ 2935 .stillframe-snip.has-menu-open .stillframe-snip__bar .stillframe-snip__mode[aria-pressed="true"]:not([aria-expanded="true"]) { 2936 color: var(--sf-text-2) !important; 2937 background: transparent !important; 2938 border-color: transparent !important; 2939 } 2940 2941 /* Both menus open centred under the bar */ 2942 .stillframe-snip .stillframe-snip__devices-wrap, 2943 .stillframe-snip .stillframe-snip__direct-wrap, 2944 .stillframe-snip .stillframe-snip__recent-wrap { 2945 position: static; 2946 } 2947 2948 .stillframe-snip .stillframe-snip__direct-menu, 2949 .stillframe-snip .stillframe-snip__recent-menu { 2950 top: calc(100% + 8px); 2951 right: auto; 2952 left: 50%; 2953 transform: translateX(-50%); 2954 } 2955 2956 .stillframe-snip .stillframe-snip__recent-menu { 2957 width: 480px; 2958 max-height: min(75vh, 600px); 2959 padding: 14px; 2960 } 2961 2962 .stillframe-snip .stillframe-snip__direct-menu { 2963 gap: 14px; 2964 padding: 14px 16px 16px; 2965 } 2966 2967 .stillframe-snip .sfd-label { 2968 display: block; 2969 padding: 0; 2970 margin: 0; 2971 font-size: 12px; 2972 font-weight: 600; 2973 line-height: 1.2; 2974 color: var(--sf-text-2, #374151); 2975 background: none !important; 2976 border: 0; 2977 border-radius: 0; 2978 } 2979 2980 .stillframe-snip .sfd-chips { 2981 gap: 8px; 2982 } 2983 2984 .stillframe-snip .sfd-chip:hover { 2985 color: var(--sf-brand) !important; 2986 background: #f6f8f9 !important; 2987 border-color: #aebcc2 !important; 2988 } 2989 2990 .stillframe-snip .sfd-chip.is-selected, 2991 .stillframe-snip .sfd-chip.is-selected:hover { 2992 color: var(--sf-brand) !important; 2993 background: #f4f8fa !important; 2994 border-color: var(--sf-brand) !important; 2995 } 2996 2997 .stillframe-snip .sfd-seg { 2998 gap: 8px; 2999 overflow: visible; 3000 border: 0; 3001 border-radius: 0; 3002 } 3003 3004 .stillframe-snip .sfd-seg__btn { 3005 height: 30px; 3006 padding: 0 16px; 3007 border: 1px solid var(--sf-line-strong, #d1d5db) !important; 3008 border-radius: 6px; 3009 } 3010 3011 .stillframe-snip .sfd-seg__btn:last-child { 3012 border-right: 1px solid var(--sf-line-strong, #d1d5db) !important; 3013 } 3014 3015 .stillframe-snip .sfd-seg__btn:hover { 3016 color: var(--sf-brand) !important; 3017 background: #f6f8f9 !important; 3018 border-color: #aebcc2 !important; 3019 } 3020 3021 .stillframe-snip .sfd-seg__btn.is-active, 3022 .stillframe-snip .sfd-seg__btn.is-active:hover { 3023 color: var(--sf-brand) !important; 3024 background: #f4f8fa !important; 3025 border-color: var(--sf-brand) !important; 3026 } 3027 3028 .stillframe-snip .sfd-submit, 3029 .stillframe-snip .sfd-submit:focus { 3030 color: #ffffff !important; 3031 background: var(--sf-brand) !important; 3032 border-color: var(--sf-brand) !important; 3033 } 3034 3035 .stillframe-snip .sfd-submit:hover { 3036 background: var(--sf-brand-hover) !important; 3037 border-color: var(--sf-brand-hover) !important; 3038 } 3039 3040 .stillframe-snip .stillframe-snip__direct-menu > .stillframe-hub-toggle { 3041 margin: 4px 0 2px; 3042 padding: 4px 0; 3043 } 3044 3045 /* Page list: lighter rows, no type badge */ 3046 .sfp .sfp__chip { 3047 display: none; 3048 } 3049 3050 .sfp .sfp__list { 3051 display: flex; 3052 flex-direction: column; 3053 gap: 2px; 3054 } 3055 3056 .sfp .sfp__row { 3057 padding: 6px 10px; 3058 } 3059 3060 .sfp .sfp__title { 3061 font-size: 12.5px; 3062 font-weight: 500; 3063 } 3064 3065 .sfp .sfp__path { 3066 font-size: 11px; 3067 } 3068 3069 .sfp .sfp__group { 3070 padding: 8px 10px 3px; 3071 font-size: 10px; 3072 } 3073 3074 .sfp .sfp__row:hover, 3075 .sfp .sfp__row.is-active { 3076 background: #f5f6f7; 3077 } 3078 3079 .sfp .sfp__row.is-selected { 3080 background: #f4f8fa; 3081 } 3082 3083 .sfp .sfp__trigger:hover, 3084 .sfp.is-open .sfp__trigger { 3085 background: #ffffff !important; 3086 } 3087 3088 /* ---- Editor: one toolbar row, compact side panel ---- */ 3089 .stillframe-result .stillframe-result__tools { order: 1; } 3090 .stillframe-result .stillframe-result__history { order: 2; } 3091 .stillframe-result .stillframe-result__shape-opts { order: 3; display: inline-flex; align-items: center; gap: 6px; } 3092 .stillframe-result .stillframe-result__swatches { order: 4; } 3093 .stillframe-result .stillframe-result__sizes { order: 5; } 3094 3095 .stillframe-result .stillframe-result__editing { 3096 display: flex; 3097 align-items: center; 3098 flex-wrap: nowrap; 3099 gap: 8px 10px; 3100 } 3101 3102 .stillframe-result .stillframe-result__editing > * + * { 3103 padding-left: 10px; 3104 } 3105 3106 .stillframe-result .stillframe-result__shape-opts[style*="display: none"] { 3107 display: none !important; 3108 padding-left: 0 !important; 3109 border-left: none !important; 3110 } 3111 3112 .stillframe-result .stillframe-tool, 3113 .stillframe-result .stillframe-icon-btn, 3114 .stillframe-result .stillframe-size { 3115 width: 28px; 3116 height: 28px; 3117 } 3118 3119 .stillframe-result .stillframe-tool:hover, 3120 .stillframe-result .stillframe-icon-btn:hover, 3121 .stillframe-result .stillframe-size:hover, 3122 .stillframe-result .stillframe-btn:hover { 3123 color: var(--sf-brand) !important; 3124 background: #f3f5f6 !important; 3125 } 3126 3127 .stillframe-result .stillframe-tool[aria-pressed="true"], 3128 .stillframe-result .stillframe-size[aria-pressed="true"] { 3129 background: #eef3f5 !important; 3130 border-color: transparent !important; 3131 } 3132 3133 @media (max-width: 1500px) { 3134 .stillframe-result .stillframe-result__actions .stillframe-btn:not(.stillframe-btn-primary):not(.stillframe-result__close) .stillframe-btn__label { 3135 display: none !important; 3136 } 3137 3138 .stillframe-result .stillframe-result__actions .stillframe-btn:not(.stillframe-btn-primary):not(.stillframe-result__close) { 3139 width: 32px; 3140 padding: 0; 3141 } 3142 } 3143 3144 @media (max-width: 1180px) { 3145 .stillframe-result .stillframe-result__editing { 3146 flex-wrap: wrap; 3147 } 3148 } 3149 3150 .stillframe-result .stillframe-result__sidebar { 3151 width: 184px; 3152 } 3153 3154 .stillframe-result .stillframe-result__sidebar-head { 3155 font-size: 10.5px !important; 3156 padding: 12px 12px 6px; 3157 } 3158 3159 .stillframe-result .stillframe-device-tab__name { 3160 font-size: 12.5px !important; 3161 font-weight: 600 !important; 3162 line-height: 1.3; 3163 } 3164 3165 .stillframe-result .stillframe-device-tab__res { 3166 font-size: 11px !important; 3167 } 3168 3169 .stillframe-result .stillframe-sidebar-action-btn { 3170 height: 30px; 3171 font-size: 12px !important; 3172 font-weight: 600 !important; 3173 box-shadow: none !important; 3174 } 3175 3176 .stillframe-result .stillframe-sidebar-action-btn:not(.stillframe-sidebar-action-btn--primary):hover { 3177 color: var(--sf-brand) !important; 3178 background: #f3f5f6 !important; 3179 border-color: #aebcc2 !important; 3180 } 3181 3182 .stillframe-result .stillframe-sidebar-action-btn--primary, 3183 .stillframe-result .stillframe-sidebar-action-btn--primary:hover { 3184 color: #ffffff !important; 3185 background: var(--sf-brand) !important; 3186 border-color: var(--sf-brand) !important; 3187 } 3188 3189 .stillframe-result .stillframe-sidebar-action-btn--primary:hover { 3190 background: var(--sf-brand-hover) !important; 3191 } 3192 3193 .stillframe-result .stillframe-device-tab.is-active { 3194 background: #f4f8fa; 3195 border-color: var(--sf-brand); 3196 } 3197 3198 /* ---- Round close buttons everywhere ---- */ 3199 .stillframe-result .stillframe-btn.stillframe-result__close, 3200 .stillframe-result .stillframe-result__drawer-close, 3201 .stillframe-snip .stillframe-snip__reset, 3202 .stillframe-snip .stillframe-snip__bar .stillframe-snip__close { 3203 width: 34px; 3204 height: 34px; 3205 padding: 0; 3206 border-radius: 50% !important; 3207 } 3208 3209 .stillframe-result .stillframe-result__drawer-close { 3210 width: 28px; 3211 height: 28px; 3212 } 3213 3214 /* ---- Editor top bar: every control on the right shares one height and corner ---- */ 3215 .stillframe-result .stillframe-result__actions { 3216 align-items: center; 3217 gap: 8px; 3218 } 3219 3220 .stillframe-result .stillframe-result__actions .stillframe-btn, 3221 .stillframe-result .stillframe-result__actions .stillframe-format-select { 3222 height: 34px; 3223 margin: 0; 3224 border-radius: 8px; 3225 } 3226 3227 .stillframe-result .stillframe-result__actions .stillframe-btn:not(.stillframe-btn-primary):not(.stillframe-result__close) { 3228 min-width: 34px; 3229 } 3230 3231 .stillframe-result .stillframe-result__actions .stillframe-format-select { 3232 appearance: none; 3233 -webkit-appearance: none; 3234 padding: 0 28px 0 12px; 3235 font-size: 12.5px; 3236 font-weight: 600; 3237 line-height: 32px; 3238 background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%236b7280' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'><path d='M6 9l6 6 6-6'/></svg>"); 3239 background-repeat: no-repeat; 3240 background-position: right 9px center; 3241 } 3242 3243 /* ---- Page list: the search box scrolls away with the list ---- */ 3244 .sfp .sfp__drop { 3245 max-height: 300px; 3246 overflow-y: auto; 3247 overscroll-behavior: contain; 3248 } 3249 3250 .sfp .sfp__list { 3251 max-height: none; 3252 overflow: visible; 3253 } 3254 3255 /* ---- Hover help next to Quality and Height ---- */ 3256 .sfd-label { 3257 display: inline-flex; 3258 align-items: center; 3259 gap: 5px; 3260 } 3261 3262 .sfd-info { 3263 position: relative; 3264 display: inline-flex; 3265 align-items: center; 3266 justify-content: center; 3267 color: var(--sf-muted, #6b7280); 3268 cursor: help; 3269 border-radius: 50%; 3270 } 3271 3272 .sfd-info:hover, 3273 .sfd-info:focus-visible { 3274 color: var(--sf-brand, #005976); 3275 outline: none; 3276 } 3277 3278 .sfd-info__tip { 3279 position: absolute; 3280 z-index: 50; 3281 left: -6px; 3282 bottom: calc(100% + 8px); 3283 display: none; 3284 width: max-content; 3285 max-width: 230px; 3286 padding: 8px 10px; 3287 font-size: 12px; 3288 font-weight: 400; 3289 line-height: 1.45; 3290 color: #ffffff; 3291 background: #14313c; 3292 border-radius: 8px; 3293 box-shadow: 0 6px 18px rgba(15, 23, 42, 0.25); 3294 pointer-events: none; 3295 } 3296 3297 .sfd-info__tip::after { 3298 content: ""; 3299 position: absolute; 3300 left: 10px; 3301 top: 100%; 3302 border: 5px solid transparent; 3303 border-top-color: #14313c; 3304 } 3305 3306 .sfd-info:hover .sfd-info__tip, 3307 .sfd-info:focus-visible .sfd-info__tip { 3308 display: block; 3309 } 3310 3311 .sfd-info__line { 3312 display: block; 3313 } 3314 3315 .sfd-info__line b { 3316 font-weight: 700; 3317 } 3318 3319 /* ---- Toast ---- */ 3320 .stillframe-toast { 3321 position: fixed; 3322 z-index: 1000001; 3323 left: 50%; 3324 bottom: 24px; 3325 display: flex; 3326 align-items: center; 3327 gap: 11px; 3328 max-width: min(520px, calc(100vw - 32px)); 3329 padding: 10px 16px 10px 11px; 3330 font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; 3331 font-size: 13px; 3332 font-weight: 500; 3333 line-height: 1.35; 3334 color: #1f2937; 3335 background: #ffffff; 3336 border: 1px solid #e5e7eb; 3337 border-radius: 12px; 3338 box-shadow: 0 12px 32px rgba(15, 23, 42, 0.14), 0 2px 6px rgba(15, 23, 42, 0.06); 3339 opacity: 0; 3340 transform: translate(-50%, 10px); 3341 pointer-events: none; 3342 transition: opacity 0.2s ease, transform 0.2s ease; 3343 } 3344 3345 .stillframe-toast.is-visible { 3346 opacity: 1; 3347 transform: translate(-50%, 0); 3348 pointer-events: auto; 3349 } 3350 3351 .stillframe-toast__icon { 3352 flex: none; 3353 width: 24px; 3354 height: 24px; 3355 border-radius: 50%; 3356 background-color: #e6f4f8; 3357 background-repeat: no-repeat; 3358 background-position: center; 3359 background-size: 14px 14px; 3360 background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23005976' stroke-width='2.6' stroke-linecap='round' stroke-linejoin='round'><path d='M12 8v.01M12 12v5'/></svg>"); 3361 } 3362 3363 .stillframe-toast[data-tone="success"] .stillframe-toast__icon { 3364 background-color: #005976; 3365 background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23ffffff' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'><path d='M5 12.5l4.5 4.5L19 7.5'/></svg>"); 3366 } 3367 3368 .stillframe-toast[data-tone="error"] .stillframe-toast__icon { 3369 background-color: #fdecea; 3370 background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23b42318' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'><path d='M12 6v7M12 18v.01'/></svg>"); 3371 } 3372 3373 .stillframe-toast[data-tone="busy"] .stillframe-toast__icon { 3374 background-image: none; 3375 background-color: transparent; 3376 border: 2.5px solid #cfe5ec; 3377 border-top-color: #005976; 3378 animation: stillframe-toast-spin 0.8s linear infinite; 3379 } 3380 3381 .stillframe-toast__link { 3382 padding-left: 12px; 3383 font-weight: 600; 3384 color: #005976; 3385 text-decoration: none; 3386 white-space: nowrap; 3387 border-left: 1px solid #e5e7eb; 3388 } 3389 3390 .stillframe-toast__link:hover { 3391 text-decoration: underline; 3392 } 3393 3394 @keyframes stillframe-toast-spin { 3395 to { 3396 transform: rotate(360deg); 3397 } 3398 } 3399 3400 /* ---- Options for the selected mark ---- */ 3401 .stillframe-result .stillframe-result__props { 3402 display: flex; 3403 flex-wrap: wrap; 3404 align-items: center; 3405 gap: 6px 18px; 3406 padding: 6px 16px; 3407 background: #f9fafb; 3408 border-bottom: 1px solid var(--sf-line, #e5e7eb); 3409 } 3410 3411 .stillframe-result .stillframe-result__props[hidden] { 3412 display: none; 3413 } 3414 3415 .stillframe-result .stillframe-prop { 3416 display: inline-flex; 3417 align-items: center; 3418 gap: 8px; 3419 } 3420 3421 .stillframe-result .stillframe-prop__label { 3422 font-size: 11px; 3423 font-weight: 700; 3424 letter-spacing: 0.04em; 3425 text-transform: uppercase; 3426 color: var(--sf-muted, #6b7280); 3427 } 3428 3429 .stillframe-result .stillframe-prop__select { 3430 box-sizing: border-box !important; 3431 height: 28px !important; 3432 max-height: 28px !important; 3433 line-height: 26px !important; 3434 padding: 0 8px !important; 3435 font-size: 12px !important; 3436 font-weight: 500 !important; 3437 color: var(--sf-text-2, #374151) !important; 3438 background: #ffffff !important; 3439 border: 1px solid var(--sf-line-strong, #d1d5db) !important; 3440 border-radius: 6px !important; 3441 cursor: pointer; 3442 outline: none; 3443 } 3444 3445 .stillframe-result .stillframe-prop__select:hover { 3446 border-color: var(--sf-brand, #005976) !important; 3447 } 3448 3449 .stillframe-result .stillframe-prop__color { 3450 width: 28px; 3451 height: 28px; 3452 padding: 2px; 3453 background: #ffffff; 3454 border: 1px solid var(--sf-line-strong, #d1d5db); 3455 border-radius: 50%; 3456 cursor: pointer; 3457 } 3458 3459 .stillframe-result .stillframe-prop__toggle, 3460 .stillframe-result .stillframe-prop__seg-btn { 3461 box-sizing: border-box !important; 3462 height: 28px !important; 3463 max-height: 28px !important; 3464 min-width: 28px !important; 3465 line-height: 26px !important; 3466 padding: 0 8px !important; 3467 font-size: 12px !important; 3468 font-weight: 700 !important; 3469 color: var(--sf-text-2, #374151) !important; 3470 background: #ffffff !important; 3471 border: 1px solid var(--sf-line-strong, #d1d5db) !important; 3472 border-radius: 6px !important; 3473 cursor: pointer; 3474 transition: background 0.15s, color 0.15s, border-color 0.15s; 3475 } 3476 3477 .stillframe-result .stillframe-prop__seg { 3478 display: inline-flex; 3479 } 3480 3481 .stillframe-result .stillframe-prop__seg-btn:first-child { 3482 border-radius: 6px 0 0 6px; 3483 } 3484 3485 .stillframe-result .stillframe-prop__seg-btn:last-child { 3486 margin-left: -1px; 3487 border-radius: 0 6px 6px 0; 3488 } 3489 3490 .stillframe-result .stillframe-prop__toggle[aria-pressed="true"], 3491 .stillframe-result .stillframe-prop__seg-btn[aria-pressed="true"] { 3492 color: #ffffff; 3493 background: var(--sf-brand, #005976); 3494 border-color: var(--sf-brand, #005976); 3495 } 3496 3497 /* ---- Custom colour and thickness slider ---- */ 3498 .stillframe-result .stillframe-swatch-custom { 3499 appearance: none; 3500 -webkit-appearance: none; 3501 width: 18px; 3502 height: 18px; 3503 padding: 0; 3504 cursor: pointer; 3505 background: conic-gradient(#ef4444, #facc15, #22c55e, #06b6d4, #3b82f6, #a855f7, #ef4444); 3506 border: 0; 3507 border-radius: 50%; 3508 box-shadow: inset 0 0 0 1px rgba(17, 24, 39, 0.18); 3509 } 3510 3511 .stillframe-result .stillframe-swatch-custom::-webkit-color-swatch-wrapper { 3512 padding: 0; 3513 opacity: 0; 3514 } 3515 3516 .stillframe-result .stillframe-swatch-custom::-moz-color-swatch { 3517 opacity: 0; 3518 } 3519 3520 .stillframe-result .stillframe-size-slider { 3521 width: 92px; 3522 height: 4px; 3523 margin: 0 4px; 3524 cursor: pointer; 3525 accent-color: var(--sf-brand, #005976); 3526 } 3527 3528 /* ---- Text size control ---- */ 3529 .stillframe-result .stillframe-prop__size { 3530 display: inline-flex; 3531 align-items: center; 3532 gap: 8px; 3533 } 3534 3535 .stillframe-result .stillframe-prop__range { 3536 width: 140px; 3537 accent-color: var(--sf-brand, #005976); 3538 } 3539 3540 .stillframe-result .stillframe-prop__number { 3541 width: 58px; 3542 height: 28px; 3543 padding: 0 6px; 3544 font-size: 12.5px; 3545 color: var(--sf-text-2, #374151); 3546 background: #ffffff; 3547 border: 1px solid var(--sf-line-strong, #d1d5db); 3548 border-radius: 6px; 3549 } 3550 3551 /* ---- Top bar controls: exactly one height, whatever WordPress does to selects ---- */ 3552 .stillframe-result .stillframe-result__actions > .stillframe-btn, 3553 .stillframe-result .stillframe-result__actions > .stillframe-format-select { 3554 box-sizing: border-box !important; 3555 height: 36px !important; 3556 min-height: 36px !important; 3557 max-height: 36px !important; 3558 margin: 0 !important; 3559 line-height: 1 !important; 3560 border-radius: 8px; 3561 } 3562 3563 .stillframe-result .stillframe-result__actions > .stillframe-format-select { 3564 padding: 0 28px 0 12px !important; 3565 line-height: 34px !important; 3566 } 3567 3568 .stillframe-result .stillframe-result__actions > .stillframe-btn.stillframe-result__close { 3569 width: 36px !important; 3570 border-radius: 50% !important; 3571 } 3572 3573 .stillframe-result .stillframe-result__actions > .stillframe-btn:not(.stillframe-btn-primary):not(.stillframe-result__close) { 3574 min-width: 36px; 3575 } 3576 3577 /* Marks may hang past the picture, so the wrapper must not clip them */ 3578 .stillframe-result .stillframe-capture-panel__canvas-wrap:has(img) { 3579 overflow: visible; 3580 } 3581 3582 .stillframe-result .stillframe-capture-panel__canvas-wrap > img { 3583 border-radius: 4px; 3584 } 3585 3586 /* ---- Device sizes shown in Window mode ---- */ 3587 .stillframe-snip__devstrip { 3588 position: fixed; 3589 z-index: 7; 3590 top: 74px; 3591 left: 50%; 3592 transform: translateX(-50%); 3593 display: flex; 3594 align-items: center; 3595 gap: 6px; 3596 width: max-content; 3597 max-width: calc(100vw - 16px); 3598 margin: 0; 3599 padding: 5px 12px; 3600 background: var(--sf-surface, #ffffff); 3601 border: 1px solid var(--sf-line, #e5e7eb); 3602 border-radius: 10px; 3603 box-shadow: 0 8px 24px rgba(15, 23, 42, 0.16), 0 1px 3px rgba(15, 23, 42, 0.08); 3604 pointer-events: auto; 3605 } 3606 3607 .stillframe-snip__devstrip[hidden] { 3608 display: none; 3609 } 3610 3611 .stillframe-snip__devstrip-label { 3612 margin-right: 4px; 3613 font-size: 12px; 3614 font-weight: 600; 3615 color: var(--sf-muted, #6b7280); 3616 white-space: nowrap; 3617 } 3618 3619 .stillframe-snip .stillframe-snip__devstrip-btn { 3620 display: inline-flex; 3621 align-items: center; 3622 gap: 6px; 3623 height: 30px; 3624 padding: 0 10px; 3625 font-size: 12.5px; 3626 font-weight: 600; 3627 color: var(--sf-text-2, #374151); 3628 background: transparent; 3629 border: 1px solid transparent; 3630 border-radius: 8px; 3631 box-shadow: none; 3632 } 3633 3634 .stillframe-snip .stillframe-snip__devstrip-btn small { 3635 font-size: 11px; 3636 font-weight: 500; 3637 color: var(--sf-muted, #6b7280); 3638 } 3639 3640 .stillframe-snip .stillframe-snip__devstrip-btn:hover, 3641 .stillframe-snip .stillframe-snip__devstrip-btn[aria-pressed="true"] { 3642 color: var(--sf-brand, #005976); 3643 background: var(--sf-brand-muted, #e6f4f8); 3644 } -
stillframe/trunk/assets/css/stillframe-tools.css
r3724658 r3736872 473 473 } 474 474 475 /* Neutral accents: the brand colour is only for the primary Capture button. */476 :root {477 --sf-ink: #1d2327;478 }479 480 .stillframe-select:focus,481 .stillframe-input:focus {482 border-color: var(--sf-ink) !important;483 box-shadow: none !important;484 }485 486 .stillframe-device-tile.is-active {487 border-color: var(--sf-ink);488 background: var(--sf-well);489 }490 491 .stillframe-device-tile.is-active .stillframe-device-tile__icon {492 color: var(--sf-ink);493 }494 495 .stillframe-device-tile.is-active .stillframe-device-tile__check {496 background: var(--sf-ink);497 border-color: var(--sf-ink);498 }499 500 .stillframe-device-tile.is-active .stillframe-device-tile__res {501 color: var(--sf-muted);502 font-weight: 400;503 }504 505 .stillframe-pill.is-active,506 .stillframe-pill.is-active:hover {507 background: #f0f0f1;508 border-color: var(--sf-ink);509 color: var(--sf-ink);510 font-weight: 600;511 }512 513 .stillframe-switch-wrap input.stillframe-switch-input:checked + .stillframe-switch-slider {514 background: var(--sf-ink);515 }516 517 /* Softer, button-like choices */518 .stillframe-pill {519 background: #fafafa;520 border-color: #b4bac4;521 }522 523 .stillframe-pill:hover {524 background: #f0f0f1;525 border-color: #6b7280;526 }527 528 .stillframe-pill.is-active,529 .stillframe-pill.is-active:hover {530 background: #eceef1;531 border-color: #6b7280;532 color: var(--sf-ink);533 }534 535 .stillframe-device-tile {536 border-width: 1px;537 border-color: #b4bac4;538 }539 540 .stillframe-device-tile.is-active {541 background: #f3f4f6;542 border-color: #6b7280;543 box-shadow: inset 0 0 0 1px #6b7280;544 }545 546 .stillframe-device-tile.is-active .stillframe-device-tile__check {547 background: #6b7280;548 border-color: #6b7280;549 }550 551 .stillframe-switch-wrap input.stillframe-switch-input:checked + .stillframe-switch-slider {552 background: #6b7280;553 }554 555 475 .stillframe-box__header { 556 476 padding: 18px 28px; -
stillframe/trunk/assets/js/stillframe-admin-bar.js
r3724658 r3736872 35 35 throw new Error('origin'); 36 36 } 37 if (config.version) { 38 parsed.searchParams.set('ver', String(config.version)); 37 var ver = config.assetVer || config.version; 38 if (ver) { 39 parsed.searchParams.set('ver', String(ver)); 39 40 } 40 41 return parsed.toString(); … … 93 94 return loading; 94 95 } 96 var useFabric = config.useFabric !== false; 95 97 loading = Promise.resolve().then(function () { 96 return Promise.all([98 var initial = [ 97 99 loadStylesheet(assetUrl('assets/css/stillframe-capture-panel.css'), 'stillframe-capture-panel-css'), 98 loadScript(assetUrl('assets/js/stillframe-annotation-editor.js'), 'stillframe-annotation-editor-js'),99 100 loadScript(assetUrl('assets/js/vendor/modern-screenshot.js'), 'stillframe-modern-screenshot-js') 100 ]); 101 ]; 102 if (useFabric) { 103 initial.push(loadScript(assetUrl('assets/js/vendor/fabric.min.js'), 'stillframe-fabric-js')); 104 } 105 return Promise.all(initial); 106 }).then(function () { 107 var editorScript = useFabric ? 'assets/js/stillframe-annotation-editor-v2.js' : 'assets/js/stillframe-annotation-editor.js'; 108 return loadScript(assetUrl(editorScript), 'stillframe-annotation-editor-js'); 101 109 }).then(function () { 102 110 return loadScript(assetUrl('assets/js/stillframe-capture-panel.js'), 'stillframe-capture-panel-js'); -
stillframe/trunk/assets/js/stillframe-annotation-editor.js
r3724658 r3736872 11 11 var BOX_TYPES = { circle: 1, rect: 1, highlight: 1, blur: 1 }; 12 12 var NO_ROTATE = { arrow: 1, blur: 1, step: 1 }; 13 var UNIFORM = { text: 1,step: 1 };13 var UNIFORM = { step: 1 }; 14 14 var MAX_MARKS = 400; 15 15 var MAX_PEN_POINTS = 3000; 16 var HANDLE_PX = 9;17 var ROTATE_OFFSET = 26;16 var HANDLE_PX = 6; 17 var ROTATE_OFFSET = 16; 18 18 var measureCtx = null; 19 19 … … 580 580 return; 581 581 } 582 var wShape = b.r - b.l; 583 var hShape = b.b - b.t; 584 var minShape = Math.min(wShape, hShape); 585 var hRad = minShape < 35 ? 2.5 : (minShape < 70 ? 3 : 3.5); 582 586 ctx.fillStyle = '#ffffff'; 583 ctx.strokeStyle = '#374151'; 587 ctx.strokeStyle = '#005976'; 588 ctx.lineWidth = 1.5; 584 589 ctx.beginPath(); 585 590 if (hd.id === 'rot') { … … 588 593 ctx.stroke(); 589 594 ctx.beginPath(); 590 ctx.arc(hd.x, hd.y, 5, 0, Math.PI * 2);595 ctx.arc(hd.x, hd.y, hRad, 0, Math.PI * 2); 591 596 } else { 592 ctx. rect(hd.x - 4, hd.y - 4, 8, 8);597 ctx.arc(hd.x, hd.y, hRad, 0, Math.PI * 2); 593 598 } 594 599 ctx.fill(); … … 970 975 m.x1 = cw.x / d.w; 971 976 m.y1 = cw.y / d.h; 977 } 978 } else if (o.type === 'text') { 979 var isSide = h === 'w' || h === 'e' || h === 'ml' || h === 'mr'; 980 if (isSide) { 981 var rawW = (h.indexOf('w') > -1 || h === 'ml') ? b.r - local.x : local.x - b.l; 982 m.w = Math.max(0.04, Math.min(1.5, rawW / d.w)); 972 983 } else { 973 m.fs = Math.max(8, Math.min(240, fontSize(o) * s)); 984 var rawW = h.indexOf('w') > -1 ? b.r - local.x : local.x - b.l; 985 var rawH = h.indexOf('n') > -1 ? b.b - local.y : local.y - b.t; 986 var s = Math.max(rawW / ow, rawH / oh, min / Math.min(ow, oh)); 987 m.fs = Math.max(8, Math.min(240, Math.round(fontSize(o) * s))); 974 988 if (o.w) { 975 m.w = Math.min(1 , o.w * s);989 m.w = Math.min(1.5, o.w * s); 976 990 } 977 var lay = textLayout(m, d.w);978 m.x1 = (cw.x - lay.width / 2) / d.w;979 m.y1 = (cw.y - lay.height / 2) / d.h;980 991 } 981 992 return; -
stillframe/trunk/assets/js/stillframe-capture-panel.js
r3727291 r3736872 14 14 var returnFocus = null; 15 15 var captured = null; 16 var resultDevices = null; 16 17 var statusNode = null; 17 18 var propsNode = null; … … 56 57 }; 57 58 58 // Above this many elements, or after a render slower than SLOW_RENDER_MS, 59 // the page is rendered only after the selection is released. A background 60 // render freezes the main thread and would make the drag stutter. 61 var HEAVY_PAGE_NODES = 1500; 59 // A background render freezes the main thread, so it waits until the pointer 60 // has been idle. Full screen starts it immediately. 62 61 var SLOW_RENDER_MS = 700; 63 62 var IDLE_BEFORE_RENDER_MS = 1500; 64 63 65 64 function canPrerender() { 66 // Do not prerender in the background during active capture — it freezes the main thread67 // on pages with many DOM nodes, causing dropped clicks and sluggish interaction.68 return false;65 // The redraw is heavy, so it starts only after the pointer has been idle. 66 // That way a full-screen shot can already be underway before Capture is clicked. 67 return true; 69 68 } 70 69 … … 305 304 } 306 305 306 // Read-only actions such as "edit" stay. These values change data if replayed. 307 var MUTATING_ACTIONS = [ 308 'trash', 'untrash', 'delete', 'delete-all', 'editpost', 'postajaxpost', 'post-quickdraft-save', 309 'deletecomment', 'trashcomment', 'untrashcomment', 'spamcomment', 'unspamcomment', 'approvecomment', 'unapprovecomment', 310 'approve', 'unapprove', 'spam', 'unspam', 'dim-comment', 311 'activate', 'deactivate', 'activate-selected', 'deactivate-selected', 'update-selected', 'delete-selected', 312 'enable-auto-update', 'disable-auto-update', 'error_scrape', 'resume', 313 'upgrade-plugin', 'upgrade-theme', 'do-plugin-upgrade', 'do-theme-upgrade', 'do-core-upgrade', 'do-translation-upgrade', 314 'install-plugin', 'install-theme', 'upload-plugin', 'upload-theme', 315 'logout', 'remove', 'deleteblog', 'spamblog' 316 ]; 317 318 function mutatingActions() { 319 return Array.isArray(config.mutatingActions) && config.mutatingActions.length 320 ? config.mutatingActions 321 : MUTATING_ACTIONS; 322 } 323 324 function actionIsMutating(value) { 325 var list = mutatingActions(); 326 if (Array.isArray(value)) { 327 return value.some(actionIsMutating); 328 } 329 var name = String(value || '').toLowerCase(); 330 if (!name) { 331 return false; 332 } 333 return list.indexOf(name) !== -1; 334 } 335 336 // Drop nonces and state-changing actions from any URL the capture iframe will open. 337 // Read-only actions such as edit stay, so a pasted editor link still opens the editor. 338 function scrubCaptureUrl(urlString) { 339 var current = new URL(window.location.href); 340 var target; 341 try { 342 target = new URL(String(urlString || current.href), current.href); 343 } catch (error) { 344 target = new URL(current.href); 345 } 346 var strip = Array.isArray(config.stripArgs) && config.stripArgs.length 347 ? config.stripArgs 348 : ['_wpnonce', 'nonce', '_wp_http_referer', 'wp_customize']; 349 strip.forEach(function (key) { 350 if (String(key) === 'action') { 351 return; 352 } 353 target.searchParams.delete(String(key)); 354 }); 355 var actions = target.searchParams.getAll('action'); 356 if (actions.some(actionIsMutating)) { 357 target.searchParams.delete('action'); 358 } 359 target.hash = ''; 360 return target.toString(); 361 } 362 307 363 function captureTargetUrl() { 308 364 var current = new URL(window.location.href); … … 316 372 target = new URL(current.href); 317 373 } 318 var strip = Array.isArray(config.stripArgs) && config.stripArgs.length 319 ? config.stripArgs 320 : ['action', '_wpnonce', 'nonce', '_wp_http_referer', 'wp_customize']; 321 strip.forEach(function (key) { 322 target.searchParams.delete(String(key)); 323 }); 324 target.hash = ''; 325 return target.toString(); 374 return scrubCaptureUrl(target.toString()); 326 375 } 327 376 … … 1478 1527 } 1479 1528 1529 // The redraw keeps each box at its on-screen width but can draw text a 1530 // fraction wider, so a row that fit on one line (pagination, the admin bar 1531 // avatar) wraps in the shot. Rows that sit on one line now are marked so the 1532 // copy cannot wrap them; multi-line boxes inside them keep wrapping. 1533 var ATOMIC_TAGS = { IMG: true, INPUT: true, SELECT: true, TEXTAREA: true, BUTTON: true, SVG: true, svg: true, CANVAS: true, VIDEO: true, IFRAME: true, OBJECT: true, EMBED: true }; 1534 1535 // Boxes that sit on the lines of el. Text and inline boxes count; block 1536 // children count as one box each. Out-of-flow content such as WordPress's 1537 // screen-reader-text labels (absolute, clipped to 1px) is ignored. 1538 function lineRects(el, win, range) { 1539 var rects = []; 1540 var stack = [el]; 1541 while (stack.length) { 1542 var node = stack.pop(); 1543 var child; 1544 for (child = node.lastChild; child; child = child.previousSibling) { 1545 if (child.nodeType === 3) { 1546 if (/\S/.test(child.data)) { 1547 range.selectNodeContents(child); 1548 Array.prototype.push.apply(rects, range.getClientRects()); 1549 } 1550 continue; 1551 } 1552 if (child.nodeType !== 1) { 1553 continue; 1554 } 1555 var cs = win.getComputedStyle(child); 1556 if (cs.display === 'none' || cs.position === 'absolute' || cs.position === 'fixed') { 1557 continue; 1558 } 1559 if (cs.display === 'contents' || (cs.display === 'inline' && !ATOMIC_TAGS[child.tagName])) { 1560 stack.push(child); 1561 continue; 1562 } 1563 rects.push(child.getBoundingClientRect()); 1564 } 1565 } 1566 return rects; 1567 } 1568 1569 function markSingleLines(doc, crop) { 1570 var marked = []; 1571 var win = doc.defaultView; 1572 var all = doc.body ? doc.body.getElementsByTagName('*') : []; 1573 var range = doc.createRange(); 1574 var i; 1575 for (i = 0; i < all.length; i++) { 1576 var el = all[i]; 1577 var box = el.getBoundingClientRect(); 1578 if (!box.width || !box.height || box.height > 160) { 1579 continue; 1580 } 1581 if (crop && (box.bottom < crop.y || box.top > crop.y + crop.height || box.right < crop.x || box.left > crop.x + crop.width)) { 1582 continue; 1583 } 1584 var style; 1585 try { 1586 style = win.getComputedStyle(el); 1587 } catch (error) { 1588 continue; 1589 } 1590 if (style.whiteSpace !== 'normal' || style.display === 'inline' || style.display === 'contents' || style.display === 'none') { 1591 continue; 1592 } 1593 var rects = lineRects(el, win, range); 1594 var top = -Infinity; 1595 var bottom = Infinity; 1596 var count = 0; 1597 var r; 1598 for (r = 0; r < rects.length; r++) { 1599 if (!rects[r].width || !rects[r].height) { 1600 continue; 1601 } 1602 top = Math.max(top, rects[r].top); 1603 bottom = Math.min(bottom, rects[r].bottom); 1604 count++; 1605 } 1606 if (!count) { 1607 continue; 1608 } 1609 var name = top < bottom ? 'data-stillframe-line' : 'data-stillframe-wrap'; 1610 if (name === 'data-stillframe-wrap' && !(el.parentElement && el.parentElement.closest('[data-stillframe-line]'))) { 1611 continue; 1612 } 1613 el.setAttribute(name, '1'); 1614 marked.push([el, name]); 1615 } 1616 if (range.detach) { 1617 range.detach(); 1618 } 1619 return function () { 1620 marked.forEach(function (item) { 1621 item[0].removeAttribute(item[1]); 1622 }); 1623 }; 1624 } 1625 1480 1626 function prepareClone(root, view, crop) { 1481 1627 if (!root || root.nodeType !== 1) { … … 1483 1629 } 1484 1630 var doc = view.document; 1631 // The copy writes text-wrap onto every element, so a one-line row is 1632 // held on one line by setting it on each element inside it, except 1633 // inside boxes that wrap on screen. text-wrap-mode leaves spacing alone. 1634 var wrapProp = view.CSS && view.CSS.supports && view.CSS.supports('text-wrap-mode', 'nowrap') ? 'text-wrap-mode' : 'white-space'; 1635 Array.prototype.forEach.call(root.querySelectorAll('[data-stillframe-line], [data-stillframe-line] *'), function (node) { 1636 var owner = node.closest('[data-stillframe-line], [data-stillframe-wrap]'); 1637 if (owner && owner.hasAttribute('data-stillframe-line') && node.style) { 1638 node.style.setProperty(wrapProp, 'nowrap', 'important'); 1639 } 1640 }); 1485 1641 Array.prototype.forEach.call(root.querySelectorAll('[data-stillframe-off]'), function (node) { 1486 1642 if (node.tagName === 'IMG') { … … 1550 1706 } 1551 1707 1708 function fontNames(value) { 1709 return String(value || '').toLowerCase().split(',').map(function (part) { 1710 return part.trim().replace(/^["']|["']$/g, ''); 1711 }).filter(Boolean); 1712 } 1713 1714 // Drop the CSS alt-text separator in values such as "\f148" / "". 1715 function stripContentAlt(text) { 1716 var quote = ''; 1717 var i; 1718 for (i = 0; i < text.length; i++) { 1719 var ch = text.charAt(i); 1720 if (quote) { 1721 if (ch === '\\') { 1722 i++; 1723 continue; 1724 } 1725 if (ch === quote) { 1726 quote = ''; 1727 } 1728 continue; 1729 } 1730 if (ch === '"' || ch === "'") { 1731 quote = ch; 1732 continue; 1733 } 1734 if (ch === '/' && i > 0 && text.charAt(i - 1) === ' ' && text.charAt(i + 1) === ' ') { 1735 return text.slice(0, i - 1).trim(); 1736 } 1737 } 1738 return text; 1739 } 1740 1741 function quotedStrings(text) { 1742 var out = []; 1743 var i = 0; 1744 while (i < text.length) { 1745 var ch = text.charAt(i); 1746 if (ch !== '"' && ch !== "'") { 1747 i++; 1748 continue; 1749 } 1750 var quote = ch; 1751 var buf = ''; 1752 i++; 1753 while (i < text.length) { 1754 var c = text.charAt(i); 1755 if (c === '\\') { 1756 i++; 1757 if (i < text.length) { 1758 buf += text.charAt(i); 1759 } 1760 i++; 1761 continue; 1762 } 1763 if (c === quote) { 1764 i++; 1765 break; 1766 } 1767 buf += c; 1768 i++; 1769 } 1770 out.push(buf); 1771 } 1772 return out; 1773 } 1774 1775 // getComputedStyle returns a quoted string for content, or the keyword none. 1776 function pseudoText(content) { 1777 if (!content || content === 'none' || content === 'normal') { 1778 return ''; 1779 } 1780 var text = stripContentAlt(String(content).trim()); 1781 if (/^(?:url|attr|counter|counters|image-set|-webkit-image-set|[a-z-]*gradient)\(/i.test(text)) { 1782 return ''; 1783 } 1784 var parts = quotedStrings(text); 1785 if (parts.length) { 1786 text = parts.join(''); 1787 } 1788 if (!text || text === 'none' || text === 'normal') { 1789 return ''; 1790 } 1791 return text; 1792 } 1793 1794 function usesIconFont(fontFamily) { 1795 return /dashicons|fontawesome|font-awesome|font awesome|material-icons|material icons|glyphicons|icomoon|ionicons|bootstrap-icons|feather|remixicon|tabler/i.test(fontFamily || ''); 1796 } 1797 1798 // A slash in the shot is a private-use icon character written into a CSS ::before. 1799 // Real text in that same font draws; the pseudo's character does not. 1800 function needsRealGlyph(text, fontFamily) { 1801 var i; 1802 if (!text || /^\s+$/.test(text)) { 1803 return false; 1804 } 1805 if (usesIconFont(fontFamily)) { 1806 return true; 1807 } 1808 for (i = 0; i < text.length; i++) { 1809 if (text.charCodeAt(i) > 126) { 1810 return true; 1811 } 1812 } 1813 return false; 1814 } 1815 1816 function iconContentSelectors(doc) { 1817 var list = []; 1818 var seen = {}; 1819 function add(selectorText) { 1820 if (!selectorText) { 1821 return; 1822 } 1823 selectorText.split(',').forEach(function (raw) { 1824 var sel = raw.replace(/::(?:before|after)/gi, '').replace(/:(?:before|after)/gi, '').trim(); 1825 if (!sel || seen[sel]) { 1826 return; 1827 } 1828 seen[sel] = true; 1829 list.push(sel); 1830 }); 1831 } 1832 function walk(rules) { 1833 var i; 1834 for (i = 0; i < rules.length; i++) { 1835 var rule = rules[i]; 1836 if (rule.cssRules && rule.cssRules.length) { 1837 try { 1838 walk(rule.cssRules); 1839 } catch (error) {} 1840 } 1841 if (!rule.style || !rule.selectorText) { 1842 continue; 1843 } 1844 if (rule.selectorText.indexOf('before') === -1 && rule.selectorText.indexOf('after') === -1) { 1845 continue; 1846 } 1847 var content = rule.style.getPropertyValue('content'); 1848 if (!content || content === 'none' || content === 'normal' || content === '""' || content === "''") { 1849 continue; 1850 } 1851 if (/counter\(|url\(|attr\(|gradient|image-set|open-quote|close-quote/i.test(content)) { 1852 continue; 1853 } 1854 if (!needsRealGlyph(pseudoText(content), rule.style.getPropertyValue('font-family'))) { 1855 continue; 1856 } 1857 add(rule.selectorText); 1858 } 1859 } 1860 var sheets = doc.styleSheets; 1861 var s; 1862 for (s = 0; s < sheets.length; s++) { 1863 var rules; 1864 try { 1865 rules = sheets[s].cssRules; 1866 } catch (error2) { 1867 continue; 1868 } 1869 if (rules) { 1870 walk(rules); 1871 } 1872 } 1873 return list; 1874 } 1875 1876 function iconCandidates(root) { 1877 var doc = root.ownerDocument || document; 1878 var found = []; 1879 var seen = typeof WeakSet === 'function' ? new WeakSet() : null; 1880 function add(el) { 1881 if (!el || el.nodeType !== 1) { 1882 return; 1883 } 1884 if (seen) { 1885 if (seen.has(el)) { 1886 return; 1887 } 1888 seen.add(el); 1889 } 1890 found.push(el); 1891 } 1892 function take(list) { 1893 var i; 1894 for (i = 0; i < list.length; i++) { 1895 add(list[i]); 1896 } 1897 } 1898 try { 1899 take(root.querySelectorAll('[class*="dashicons"], .wp-menu-image, .ab-icon, .view-switch a, .collapse-button-icon')); 1900 } catch (error) { 1901 return found; 1902 } 1903 var selectors = iconContentSelectors(doc); 1904 var batch = []; 1905 function flush() { 1906 if (!batch.length) { 1907 return; 1908 } 1909 var query = batch.join(','); 1910 batch = []; 1911 try { 1912 take(root.querySelectorAll(query)); 1913 } catch (error2) { 1914 query.split(',').forEach(function (sel) { 1915 try { 1916 take(root.querySelectorAll(sel)); 1917 } catch (error3) { 1918 return; 1919 } 1920 }); 1921 } 1922 } 1923 var i; 1924 for (i = 0; i < selectors.length; i++) { 1925 batch.push(selectors[i]); 1926 if (batch.length === 20) { 1927 flush(); 1928 } 1929 } 1930 flush(); 1931 return found; 1932 } 1933 1934 // Icon fonts such as dashicons live on ::before / ::after. The redraw turns 1935 // those characters into slashes, so the glyph is painted as real text instead. 1936 // The live page is only marked. The glyphs go into the redraw's copy, where 1937 // no page stylesheet applies, so nothing on screen moves while the shot is 1938 // taken and no theme rule (span, :first-child, :empty) can restyle them. 1939 var GLYPH_ATTR = 'data-stillframe-glyph'; 1940 1941 function pinIconFonts(root) { 1942 var marked = []; 1943 var glyphs = {}; 1944 var families = []; 1945 var seenFamily = {}; 1946 var doc = root && root.ownerDocument; 1947 var none = { 1948 undo: function () {}, 1949 apply: function () {}, 1950 families: families 1951 }; 1952 if (!root || root.nodeType !== 1 || !root.querySelectorAll || !doc) { 1953 return none; 1954 } 1955 var view = doc.defaultView || window; 1956 var skip = { 1957 IMG: true, 1958 INPUT: true, 1959 TEXTAREA: true, 1960 SELECT: true, 1961 OPTION: true, 1962 BR: true, 1963 HR: true, 1964 SVG: true, 1965 CANVAS: true, 1966 VIDEO: true, 1967 IFRAME: true, 1968 SCRIPT: true, 1969 STYLE: true 1970 }; 1971 var nodes = iconCandidates(root); 1972 var i; 1973 for (i = 0; i < nodes.length; i++) { 1974 var el = nodes[i]; 1975 if (!el || skip[el.tagName]) { 1976 continue; 1977 } 1978 if (el.namespaceURI && el.namespaceURI !== 'http://www.w3.org/1999/xhtml') { 1979 continue; 1980 } 1981 if (el.closest && el.closest('.stillframe-snip, .stillframe-result, .stillframe-capture-panel, .stillframe-capture-frame')) { 1982 continue; 1983 } 1984 var elStyle; 1985 try { 1986 elStyle = view.getComputedStyle(el); 1987 } catch (error) { 1988 continue; 1989 } 1990 if (elStyle.display === 'none' || elStyle.visibility === 'hidden') { 1991 continue; 1992 } 1993 var pseudos = ['::before', '::after']; 1994 var p; 1995 for (p = 0; p < pseudos.length; p++) { 1996 var pseudo = pseudos[p]; 1997 var cs; 1998 try { 1999 cs = view.getComputedStyle(el, pseudo); 2000 } catch (error2) { 2001 continue; 2002 } 2003 var text = pseudoText(cs.content); 2004 if (!needsRealGlyph(text, cs.fontFamily)) { 2005 continue; 2006 } 2007 // Every computed property, so borders, min-width, flex and float 2008 // match the pseudo-element exactly. 2009 var style = []; 2010 var c; 2011 for (c = 0; c < cs.length; c++) { 2012 var prop = cs.item(c); 2013 if (prop !== 'content') { 2014 style.push([prop, cs.getPropertyValue(prop)]); 2015 } 2016 } 2017 var id = el.getAttribute(GLYPH_ATTR); 2018 if (!id) { 2019 id = String(marked.length); 2020 el.setAttribute(GLYPH_ATTR, id); 2021 marked.push(el); 2022 glyphs[id] = {}; 2023 } 2024 glyphs[id][pseudo] = { text: text, style: style }; 2025 var fam = cs.fontFamily; 2026 if (fam && !seenFamily[fam]) { 2027 seenFamily[fam] = true; 2028 families.push(fam); 2029 } 2030 } 2031 } 2032 if (!marked.length) { 2033 return none; 2034 } 2035 2036 function makeGlyph(cloneDoc, entry) { 2037 var span = cloneDoc.createElement('span'); 2038 span.setAttribute('aria-hidden', 'true'); 2039 entry.style.forEach(function (pair) { 2040 span.style.setProperty(pair[0], pair[1]); 2041 }); 2042 span.textContent = entry.text; 2043 return span; 2044 } 2045 2046 return { 2047 // Swap the pseudo-elements for real glyphs inside the cloned tree. 2048 apply: function (cloned) { 2049 if (!cloned || cloned.nodeType !== 1 || !cloned.querySelectorAll) { 2050 return; 2051 } 2052 var cloneDoc = cloned.ownerDocument || doc; 2053 var nodes = Array.prototype.slice.call(cloned.querySelectorAll('[' + GLYPH_ATTR + ']')); 2054 if (cloned.hasAttribute(GLYPH_ATTR)) { 2055 nodes.unshift(cloned); 2056 } 2057 if (!nodes.length) { 2058 return; 2059 } 2060 nodes.forEach(function (host) { 2061 var entry = glyphs[host.getAttribute(GLYPH_ATTR)]; 2062 host.removeAttribute(GLYPH_ATTR); 2063 if (!entry) { 2064 return; 2065 } 2066 if (entry['::before']) { 2067 host.insertBefore(makeGlyph(cloneDoc, entry['::before']), host.firstChild); 2068 host.classList.add('stillframe-icon-before'); 2069 } 2070 if (entry['::after']) { 2071 host.appendChild(makeGlyph(cloneDoc, entry['::after'])); 2072 host.classList.add('stillframe-icon-after'); 2073 } 2074 }); 2075 var styleTag = cloneDoc.createElement('style'); 2076 styleTag.textContent = '.stillframe-icon-before::before{content:none!important;}.stillframe-icon-after::after{content:none!important;}'; 2077 (cloned.querySelector('body') || cloned).appendChild(styleTag); 2078 }, 2079 undo: function () { 2080 marked.forEach(function (el) { 2081 el.removeAttribute(GLYPH_ATTR); 2082 }); 2083 }, 2084 families: families 2085 }; 2086 } 2087 2088 function loadIconFonts(view, families) { 2089 var fonts = view && view.document && view.document.fonts; 2090 if (!fonts || typeof fonts.load !== 'function' || !families || !families.length) { 2091 return Promise.resolve(); 2092 } 2093 return Promise.all(families.map(function (family) { 2094 return fonts.load('20px ' + family).catch(function () { 2095 return null; 2096 }); 2097 })).then(function () { 2098 return fonts.ready; 2099 }).catch(function () { 2100 return null; 2101 }); 2102 } 2103 1552 2104 function renderCanvas(node, scale, bounds, view) { 1553 2105 view = view || window; … … 1558 2110 } 1559 2111 var crop = bounds && bounds.crop ? bounds.crop : null; 1560 var unmark = crop ? markElements(view.document, crop) : function () {}; 2112 var unmarkCrop = crop ? markElements(view.document, crop) : function () {}; 2113 var unmarkLines = function () {}; 2114 try { 2115 unmarkLines = markSingleLines(view.document, crop); 2116 } catch (lineError) { 2117 unmarkLines = function () {}; 2118 } 2119 var unmark = function () { 2120 unmarkCrop(); 2121 unmarkLines(); 2122 }; 2123 var pinned = { undo: function () {}, apply: function () {}, families: [] }; 2124 try { 2125 pinned = pinIconFonts(node); 2126 } catch (pinError) { 2127 pinned = { undo: function () {}, apply: function () {}, families: [] }; 2128 } 1561 2129 try { 1562 2130 var options = { … … 1573 2141 onCloneNode: function (cloned) { 1574 2142 try { 2143 pinned.apply(cloned); 2144 } catch (glyphError) { 2145 // The shot still renders; icons may show as slashes. 2146 } 2147 try { 1575 2148 prepareClone(cloned, view, crop); 1576 2149 } catch (cloneError) { … … 1583 2156 options.height = bounds.height; 1584 2157 } 1585 return window.modernScreenshot.domToCanvas(node, options).then(function (canvas) { 2158 var nodeView = (node.ownerDocument && node.ownerDocument.defaultView) || view; 2159 return loadIconFonts(nodeView, pinned.families).then(function () { 2160 return window.modernScreenshot.domToCanvas(node, options); 2161 }).then(function (canvas) { 2162 pinned.undo(); 1586 2163 unmark(); 1587 2164 return canvas; 1588 2165 }, function (error) { 2166 pinned.undo(); 1589 2167 unmark(); 1590 2168 throw error; 1591 2169 }); 1592 2170 } catch (error) { 2171 pinned.undo(); 1593 2172 unmark(); 1594 2173 if (error && !error.code) { … … 1749 2328 var token = session; 1750 2329 1751 function compatCapture() { 2330 function compatNote(error) { 2331 var name = error && error.name ? String(error.name) : ''; 2332 var message = error && error.message ? String(error.message) : ''; 2333 if (name === 'NotAllowedError' || name === 'AbortError') { 2334 return tr('exactCancelled', 'Sharing was cancelled, so this shot was redrawn from the page. The next capture will ask to share the tab again.'); 2335 } 2336 if (message === 'surface') { 2337 return tr('exactSurface', 'That share was not this browser tab, so this shot was redrawn from the page. The next capture will ask again.'); 2338 } 2339 if (message === 'mismatch') { 2340 return tr('exactMismatch', 'The tab capture did not match this window, so this shot was redrawn from the page. The next capture will ask again.'); 2341 } 2342 return tr('exactFallback', 'Pixel-perfect capture did not finish, so this shot was redrawn from the page. The next capture will ask again.'); 2343 } 2344 2345 function compatCapture(note) { 1752 2346 var work = getViewCanvas().then(function (canvas) { 1753 2347 return cropCanvasToBlob(canvas, rect, viewShot.scale); 1754 2348 }); 2349 if (note) { 2350 rect.compatNote = note; 2351 } 1755 2352 openResult(rect, work, token); 1756 2353 destroySnip(); … … 1768 2365 openResult(rect, Promise.resolve(result.blob), token); 1769 2366 destroySnip(); 1770 }).catch(function ( ) {2367 }).catch(function (error) { 1771 2368 if (hideEl) { 1772 2369 hideEl.style.visibility = ''; … … 1776 2373 return; 1777 2374 } 1778 // Denied or unsupported: use the compatible DOM render for the rest of this page view. 1779 exact.failed = true; 2375 // One failure does not turn pixel-perfect off. The next capture asks again. 1780 2376 stopExact(); 1781 compatCapture(); 1782 }); 2377 compatCapture(compatNote(error)); 2378 }); 2379 return; 2380 } 2381 if (exactPref()) { 2382 compatCapture(tr('exactUnsupported', 'This browser cannot share the tab, so this shot was redrawn from the page.')); 1783 2383 return; 1784 2384 } … … 2116 2716 } 2117 2717 2118 // Drop originals that are no longer in the list, and the oldest ones past the size cap. 2119 function recentPrune(list) { 2718 // A blob can land in IndexedDB before its card is in the list. Don't delete it in that window. 2719 var RECENT_GRACE_MS = 120000; 2720 2721 function commitRecentList(build) { 2722 var next = []; 2723 var pass; 2724 for (pass = 0; pass < 3; pass++) { 2725 next = build(getRecentCaptures().slice()); 2726 if (!Array.isArray(next)) { 2727 next = []; 2728 } 2729 if (next.length > RECENT_MAX) { 2730 next = next.slice(0, RECENT_MAX); 2731 } 2732 while (next.length) { 2733 try { 2734 window.localStorage.setItem(RECENT_KEY, JSON.stringify(next)); 2735 break; 2736 } catch (error) { 2737 next.pop(); 2738 } 2739 } 2740 if (!next.length) { 2741 try { 2742 window.localStorage.removeItem(RECENT_KEY); 2743 } catch (removeError) { 2744 return []; 2745 } 2746 } 2747 } 2748 return getRecentCaptures(); 2749 } 2750 2751 function bitmapScale(cssWidth, pixelWidth, fallback) { 2752 var css = Number(cssWidth); 2753 var pixels = Number(pixelWidth); 2754 if (pixels > 0 && css > 0) { 2755 var ratio = pixels / css; 2756 if (ratio >= 0.5 && ratio <= 6) { 2757 return Math.round(ratio * 100) / 100; 2758 } 2759 } 2760 var scale = Number(fallback); 2761 if (isFinite(scale) && scale > 0) { 2762 return Math.round(scale * 100) / 100; 2763 } 2764 return 1; 2765 } 2766 2767 function recentScaleOf(item) { 2768 return bitmapScale(item && item.width, item && item.pixelWidth, item && item.scale); 2769 } 2770 2771 function cleanRecentTitle(title, url) { 2772 var cleanTitle = title || ''; 2773 if (!cleanTitle || cleanTitle.indexOf('http') === 0) { 2774 try { 2775 cleanTitle = new URL(url || window.location.href).pathname || 'Screenshot'; 2776 if (cleanTitle === '/') { 2777 cleanTitle = 'Home Page'; 2778 } 2779 } catch (error) { 2780 cleanTitle = 'Screenshot'; 2781 } 2782 } 2783 return cleanTitle; 2784 } 2785 2786 // Drop originals that fell off the list, and the oldest ones past the size cap. 2787 // Re-reads the list inside IndexedDB so a second capture is not deleted first. 2788 function recentPrune() { 2120 2789 return idbRun('readwrite', function (store) { 2121 2790 var all = store.getAll(); … … 2123 2792 var byId = {}; 2124 2793 (all.result || []).forEach(function (row) { 2125 byId[row.id] = row; 2126 }); 2127 var total = 0; 2128 var keep = {}; 2129 list.forEach(function (item) { 2130 var row = byId[item.id]; 2131 if (row && total + (row.size || 0) <= RECENT_BYTES) { 2132 total += row.size || 0; 2133 keep[item.id] = true; 2794 if (row && row.id) { 2795 byId[row.id] = row; 2134 2796 } 2135 2797 }); 2798 var list = getRecentCaptures(); 2799 var now = Date.now(); 2800 var total = 0; 2801 var over = false; 2802 var keep = {}; 2803 var evict = {}; 2804 list.forEach(function (item) { 2805 if (!item || !item.id) { 2806 return; 2807 } 2808 var row = byId[item.id]; 2809 var size = row && row.size ? row.size : (item.size || 0); 2810 if (!over && total + size <= RECENT_BYTES) { 2811 total += size; 2812 keep[item.id] = true; 2813 return; 2814 } 2815 over = true; 2816 evict[item.id] = true; 2817 }); 2136 2818 Object.keys(byId).forEach(function (id) { 2137 if (!keep[id]) { 2819 if (keep[id]) { 2820 return; 2821 } 2822 if (evict[id]) { 2138 2823 store.delete(id); 2824 return; 2139 2825 } 2826 var row = byId[id]; 2827 if (row && row.timestamp && (now - row.timestamp) < RECENT_GRACE_MS) { 2828 return; 2829 } 2830 store.delete(id); 2140 2831 }); 2832 if (over) { 2833 commitRecentList(function (latest) { 2834 return latest.filter(function (item) { 2835 return item && !evict[item.id]; 2836 }); 2837 }); 2838 } 2141 2839 }; 2142 2840 return all; … … 2200 2898 } 2201 2899 2202 function saveCaptureToRecent(blob, width, height, title, url ) {2900 function saveCaptureToRecent(blob, width, height, title, url, scale) { 2203 2901 if (!blob || !width || !height) return; 2204 2902 var id = 'sf_' + Date.now() + '_' + Math.random().toString(36).substr(2, 5); 2903 var storedScale = Number(scale); 2904 if (!isFinite(storedScale) || storedScale <= 0) { 2905 storedScale = 1; 2906 } 2907 storedScale = Math.round(storedScale * 100) / 100; 2908 var item = { 2909 id: id, 2910 title: cleanRecentTitle(title, url), 2911 url: url || window.location.href, 2912 width: width, 2913 height: height, 2914 scale: storedScale, 2915 timestamp: Date.now(), 2916 size: blob.size, 2917 thumb: '' 2918 }; 2205 2919 recentBlobs[id] = blob; 2920 // The card is in the list before the thumbnail or the prune, so a second capture cannot be deleted first. 2921 commitRecentList(function (list) { 2922 var next = list.filter(function (entry) { 2923 return entry && entry.id !== id; 2924 }); 2925 next.unshift(item); 2926 return next; 2927 }); 2206 2928 idbRun('readwrite', function (store) { 2207 return store.put({ id: id, blob: blob, size: blob.size, timestamp: Date.now()});2929 return store.put({ id: id, blob: blob, size: blob.size, timestamp: item.timestamp }); 2208 2930 }).catch(function () { 2209 2931 return null; … … 2222 2944 ctx.drawImage(img, 0, 0, thumbCanvas.width, thumbCanvas.height); 2223 2945 var thumbData = thumbCanvas.toDataURL('image/jpeg', 0.85); 2224 2225 var cleanTitle = title || ''; 2226 if (!cleanTitle || cleanTitle.indexOf('http') === 0) { 2227 try { 2228 cleanTitle = new URL(url || window.location.href).pathname || 'Screenshot'; 2229 if (cleanTitle === '/') cleanTitle = 'Home Page'; 2230 } catch (e) { 2231 cleanTitle = 'Screenshot'; 2232 } 2233 } 2234 2235 var item = { 2236 id: id, 2237 title: cleanTitle, 2238 url: url || window.location.href, 2239 width: width, 2240 height: height, 2241 timestamp: Date.now(), 2242 size: blob.size, 2243 pixelWidth: img.naturalWidth || width, 2244 thumb: thumbData 2245 }; 2246 2247 var list = getRecentCaptures(); 2248 list.unshift(item); 2249 if (list.length > RECENT_MAX) list = list.slice(0, RECENT_MAX); 2250 saveRecentList(list); 2251 recentPrune(list); 2946 var pixelWidth = img.naturalWidth || width; 2947 var pixelHeight = img.naturalHeight || height; 2948 var usedScale = bitmapScale(width, pixelWidth, storedScale); 2949 commitRecentList(function (list) { 2950 list.forEach(function (entry) { 2951 if (!entry || entry.id !== id) { 2952 return; 2953 } 2954 entry.thumb = thumbData; 2955 entry.pixelWidth = pixelWidth; 2956 entry.pixelHeight = pixelHeight; 2957 entry.scale = usedScale; 2958 entry.height = Math.max(1, Math.round(pixelHeight / usedScale)); 2959 entry.size = blob.size; 2960 }); 2961 // A missing card was removed or pruned. Do not put it back onto a deleted file. 2962 return list; 2963 }); 2964 showRecentThumb(id, thumbData); 2965 recentPrune(); 2252 2966 } catch (err) { 2253 2967 console.warn('Stillframe recent save error', err); … … 2258 2972 img.onerror = function () { 2259 2973 URL.revokeObjectURL(objUrl); 2974 recentPrune(); 2260 2975 }; 2261 2976 img.src = objUrl; 2262 2977 } 2263 2978 2979 function unsavedMarks() { 2980 if (editor && typeof editor.hasMarks === 'function' && editor.hasMarks()) { 2981 return true; 2982 } 2983 if (!resultDevices) { 2984 return false; 2985 } 2986 var i; 2987 for (i = 0; i < resultDevices.length; i++) { 2988 var marks = resultDevices[i] && resultDevices[i].marks; 2989 if (marks && marks.length) { 2990 return true; 2991 } 2992 } 2993 return false; 2994 } 2995 2264 2996 function openRecentCaptureInEditor(item) { 2997 if (!item) { 2998 return; 2999 } 3000 if (unsavedMarks()) { 3001 var proceed = window.confirm(tr('discardMarks', 'This capture has unsaved marks. Opening another screenshot will discard them. Continue?')); 3002 if (!proceed) { 3003 return; 3004 } 3005 } 2265 3006 var token = ++session; 2266 3007 var rect = { … … 2269 3010 width: item.width || 1440, 2270 3011 height: item.height || 900, 2271 scale: 1,3012 scale: recentScaleOf(item), 2272 3013 url: item.url || window.location.href, 2273 3014 fromRecent: true … … 2325 3066 } 2326 3067 3068 function showRecentThumb(id, thumbData) { 3069 if (!id || !thumbData) { 3070 return; 3071 } 3072 var cards = document.querySelectorAll('.stillframe-recent-card[data-id="' + id + '"]'); 3073 Array.prototype.forEach.call(cards, function (card) { 3074 var wrap = card.querySelector('.stillframe-recent-thumbwrap'); 3075 if (!wrap) { 3076 return; 3077 } 3078 var img = wrap.querySelector('img'); 3079 if (img) { 3080 img.src = thumbData; 3081 return; 3082 } 3083 wrap.appendChild(element('img', { 3084 className: 'stillframe-recent-thumb', 3085 alt: '', 3086 src: thumbData 3087 })); 3088 }); 3089 } 3090 2327 3091 function renderRecentList(container, onSelect, onAction) { 2328 3092 while (container.firstChild) { … … 2340 3104 var listWrap = element('div', { className: 'stillframe-recent-list' }); 2341 3105 list.forEach(function (item) { 2342 var card = element('div', { className: 'stillframe-recent-card' });3106 var card = element('div', { className: 'stillframe-recent-card', 'data-id': item.id || '' }); 2343 3107 var thumbWrap = element('div', { className: 'stillframe-recent-thumbwrap' }); 2344 3108 if (item.thumb) { … … 2588 3352 sizeGroup.appendChild(sizeSlider); 2589 3353 2590 var textOptGroup = element('div', {2591 className: 'stillframe-result__text-opts',2592 role: 'group',2593 'aria-label': 'Text options'2594 });2595 textOptGroup.style.display = 'none';2596 2597 var fontSelect = element('select', { className: 'stillframe-prop__select', 'aria-label': 'Font family', title: 'Font family' });2598 FONT_CHOICES.forEach(function (choice) {2599 fontSelect.appendChild(element('option', { value: choice[0], text: choice[1] }));2600 });2601 fontSelect.value = currentFont || 'sans';2602 fontSelect.addEventListener('change', function () {2603 currentFont = fontSelect.value;2604 if (editor && editor.setFont) {2605 editor.setFont(currentFont);2606 }2607 });2608 textOptGroup.appendChild(fontSelect);2609 2610 var boldBtn = element('button', {2611 type: 'button',2612 className: 'stillframe-prop__toggle',2613 'aria-pressed': currentBold ? 'true' : 'false',2614 text: 'B',2615 title: 'Bold (toggle)'2616 });2617 boldBtn.addEventListener('click', function () {2618 currentBold = boldBtn.getAttribute('aria-pressed') !== 'true';2619 boldBtn.setAttribute('aria-pressed', currentBold ? 'true' : 'false');2620 if (editor && editor.setBold) {2621 editor.setBold(currentBold);2622 }2623 });2624 textOptGroup.appendChild(boldBtn);2625 2626 3354 var shapeOptGroup = element('div', { 2627 3355 className: 'stillframe-result__shape-opts', … … 2687 3415 if (isText) { 2688 3416 currentSliderMode = 'text'; 2689 textOptGroup.style.display = 'inline-flex';2690 3417 shapeOptGroup.style.display = 'none'; 2691 3418 sizeSlider.min = '12'; … … 2698 3425 sizeSlider.title = 'Text size (' + sizeSlider.value + 'px)'; 2699 3426 sizeSlider.setAttribute('aria-label', 'Text size'); 2700 if (info && info.font) {2701 currentFont = info.font;2702 fontSelect.value = currentFont;2703 }2704 if (info && typeof info.bold === 'boolean') {2705 currentBold = info.bold;2706 boldBtn.setAttribute('aria-pressed', currentBold ? 'true' : 'false');2707 }2708 3427 } else if (isShape) { 2709 3428 currentSliderMode = 'stroke'; 2710 textOptGroup.style.display = 'none';2711 3429 shapeOptGroup.style.display = 'inline-flex'; 2712 3430 sizeSlider.min = '1'; … … 2726 3444 } else { 2727 3445 currentSliderMode = 'stroke'; 2728 textOptGroup.style.display = 'none';2729 3446 shapeOptGroup.style.display = 'none'; 2730 3447 sizeSlider.min = '1'; … … 2768 3485 var toolsWrap = element('div', { className: 'stillframe-result__editing' }); 2769 3486 toolsWrap.appendChild(markGroup); 3487 toolsWrap.appendChild(historyGroup); 3488 toolsWrap.appendChild(shapeOptGroup); 2770 3489 toolsWrap.appendChild(colorGroup); 2771 3490 toolsWrap.appendChild(sizeGroup); 2772 toolsWrap.appendChild(textOptGroup);2773 toolsWrap.appendChild(shapeOptGroup);2774 toolsWrap.appendChild(historyGroup);2775 3491 2776 3492 var saveGroup = element('div', { className: 'stillframe-result__actions' }); … … 2859 3575 var hasMulti = multiOptions && Array.isArray(multiOptions.devices) && multiOptions.devices.length > 1; 2860 3576 var devicesList = (multiOptions && multiOptions.devices) || []; 3577 resultDevices = devicesList.length ? devicesList : null; 2861 3578 var currentDeviceId = multiOptions && multiOptions.activeDeviceId ? multiOptions.activeDeviceId : (devicesList[0] ? devicesList[0].id : ''); 2862 3579 var currentDevice = devicesList.find(function (d) { return d.id === currentDeviceId; }) || devicesList[0] || null; … … 3256 3973 setStatus(''); 3257 3974 flushPending(); 3975 if (rect && rect.compatNote) { 3976 setStatus(rect.compatNote); 3977 } 3258 3978 if (captured.blob && !(rect && rect.fromRecent)) { 3259 saveCaptureToRecent(captured.blob, captured.width, captured.height, document.title, captured.url );3979 saveCaptureToRecent(captured.blob, captured.width, captured.height, document.title, captured.url, captured.scale); 3260 3980 } 3261 3981 }).catch(function (error) { … … 3271 3991 text: error && error.code === 'library' ? text('libraryMissing') : text('captureFailed') 3272 3992 })); 3273 setStatus( '');3993 setStatus(rect && rect.compatNote ? rect.compatNote : ''); 3274 3994 if (dialog) { 3275 3995 dialog.setAttribute('aria-busy', 'false'); … … 3286 4006 if (dev.id === currentDeviceId) { 3287 4007 captured.blob = blob; 4008 captured.scale = dev.scale || captured.scale || 1; 3288 4009 captured.width = dev.width; 3289 4010 captured.height = dev.height; … … 3342 4063 3343 4064 function openMultiDirectCapture(sourceUrl, selectedDevices, scale, hideAdminBar, isFullPage) { 4065 sourceUrl = scrubCaptureUrl(sourceUrl); 3344 4066 var token = ++session; 3345 4067 var scaleVal = scale || 1; … … 3406 4128 dev.image = img; 3407 4129 dev.status = 'ready'; 3408 dev.height = Math.round(img.naturalHeight / scaleVal); 4130 dev.scale = bitmapScale(dev.width, img.naturalWidth, scaleVal); 4131 dev.height = Math.max(1, Math.round((img.naturalHeight || 1) / dev.scale)); 3409 4132 controller.onDeviceReady(dev, blob, img); 3410 saveCaptureToRecent(blob, dev.width, dev.height, document.title + ' (' + dev.label + ')', sourceUrl );4133 saveCaptureToRecent(blob, dev.width, dev.height, document.title + ' (' + dev.label + ')', sourceUrl, dev.scale); 3411 4134 }); 3412 4135 }).catch(function (err) { … … 3563 4286 sel = { x: 0, y: 0, width: sizeNow.width, height: sizeNow.height }; 3564 4287 showSelection(sel); 3565 scheduleShot(IDLE_BEFORE_RENDER_MS); 4288 // Start the redraw now, while the selection is on screen, so Capture 4289 // does not wait for the whole page to be built from scratch. 4290 if (!viewShot.canvas && !viewShot.work) { 4291 startViewShot().catch(function () { 4292 return null; 4293 }); 4294 } 3566 4295 }); 3567 4296 … … 3710 4439 var engineText = element('span', { className: 'sfd-engine__text' }); 3711 4440 engineText.appendChild(element('b', { text: tr('engineTitle', 'Pixel-perfect capture') })); 3712 engineText.appendChild(element('small', { text: tr('engineHelp', ' Your browser asks to share this tab, so the screenshot matches the screen exactly. Nothing is uploaded.') }));4441 engineText.appendChild(element('small', { text: tr('engineHelp', 'Area, Window, and Full screen on this tab in Chrome or Edge. The browser asks to share the tab, and nothing is uploaded. Other browsers, Direct Page, and Tools redraw the page.') })); 3713 4442 engineLabel.appendChild(engineCb); 3714 4443 engineLabel.appendChild(element('span', { className: 'stillframe-hub-toggle__track' })); … … 4015 4744 bar.appendChild(windowModeButton); 4016 4745 bar.appendChild(fullButton); 4746 bar.appendChild(devWrap); 4017 4747 bar.appendChild(directWrap); 4018 4748 bar.appendChild(recentWrap); … … 4434 5164 4435 5165 function captureInFrame(url, width, scale, isCancelled, options) { 5166 url = scrubCaptureUrl(url); 4436 5167 options = options || {}; 4437 5168 return new Promise(function (resolve, reject) { … … 5032 5763 } 5033 5764 captured = null; 5765 resultDevices = null; 5034 5766 compactPanel = false; 5035 5767 resultReady = false; -
stillframe/trunk/assets/js/stillframe-tools.js
r3724658 r3736872 2 2 'use strict'; 3 3 4 var p ageSelect = document.getElementById('stillframe-target-page');5 var customUrlWrap = document.getElementById('stillframe-custom-url-wrap');6 var customUrlInput = document.getElementById('stillframe-custom-url');4 var pickerMount = document.getElementById('stillframe-target-page'); 5 var toolsData = window.StillframeToolsData || {}; 6 var pagePicker = null; 7 7 var startBtn = document.getElementById('stillframe-start-multicapture'); 8 8 … … 41 41 }); 42 42 43 if (pageSelect) { 44 pageSelect.addEventListener('change', function () { 45 var isCustom = pageSelect.value === '__custom__'; 46 if (customUrlWrap) { 47 customUrlWrap.hidden = !isCustom; 48 customUrlWrap.style.display = isCustom ? 'block' : 'none'; 49 } 50 if (isCustom && customUrlInput) { 51 customUrlInput.focus(); 52 } 43 if (pickerMount && window.StillframeCapture && typeof window.StillframeCapture.createPagePicker === 'function') { 44 pagePicker = window.StillframeCapture.createPagePicker({ 45 pages: toolsData.pages, 46 homeUrl: toolsData.homeUrl || '' 53 47 }); 48 pickerMount.appendChild(pagePicker.root); 54 49 } 55 50 56 51 function getTargetUrl() { 57 if (!pageSelect) { 58 return ''; 59 } 60 if (pageSelect.value === '__custom__') { 61 return customUrlInput ? customUrlInput.value.trim() : ''; 62 } 63 return pageSelect.value.trim(); 52 return pagePicker ? pagePicker.getValue() : ''; 64 53 } 65 54 … … 86 75 // The capture opens the same pop-up editor as the top-bar capture, 87 76 // with every chosen device ready to switch between. 77 // Tools always redraws the page in an iframe. Pixel-perfect tab capture 78 // is the admin-bar Area, Window, and Full screen control, not this screen. 79 var errorNode = null; 80 81 function showError(message) { 82 if (!errorNode) { 83 errorNode = document.createElement('p'); 84 errorNode.className = 'stillframe-tools-error'; 85 errorNode.setAttribute('role', 'alert'); 86 errorNode.style.cssText = 'margin:8px 0 0;color:#b42318;font-size:13px;font-weight:600'; 87 if (startBtn && startBtn.parentNode) { 88 startBtn.parentNode.insertBefore(errorNode, startBtn); 89 } 90 } 91 errorNode.textContent = message; 92 errorNode.hidden = !message; 93 } 94 88 95 function startCapture() { 96 showError(''); 89 97 var url = getTargetUrl(); 90 98 if (!url) { 91 window.alert('Please select a target page or enter a URL to capture.');92 if ( customUrlInput && pageSelect && pageSelect.value === '__custom__') {93 customUrlInput.focus();99 showError('Please select a target page or enter a URL to capture.'); 100 if (pagePicker) { 101 pagePicker.focusCustom(); 94 102 } 95 103 return; … … 98 106 var devices = getSelectedDevices(); 99 107 if (!devices.length) { 100 window.alert('Please select at least one device to capture.');108 showError('Please select at least one device to capture.'); 101 109 return; 102 110 } … … 104 112 var engine = window.StillframeCapture || {}; 105 113 if (typeof engine.openMultiCapture !== 'function') { 106 window.alert('Stillframe is still loading. Please try again in a moment.');114 showError('Stillframe is still loading. Please try again in a moment.'); 107 115 return; 108 116 } -
stillframe/trunk/includes/class-stillframe-asset-loader.php
r3724658 r3736872 20 20 * @var string 21 21 */ 22 public const PAGES_TRANSIENT = 'stillframe_site_pages ';22 public const PAGES_TRANSIENT = 'stillframe_site_pages_v2'; 23 23 24 24 /** 25 25 * Query arguments that must not be replayed inside the capture frame. 26 26 * 27 * `action` is not in this list. Screens such as post.php?action=edit need it 28 * to stay on the same screen. Mutating values are removed separately. 29 * 27 30 * @var string[] 28 31 */ 29 32 public const STRIPPED_QUERY_ARGS = array( 30 'action',31 33 '_wpnonce', 32 34 'nonce', … … 36 38 37 39 /** 40 * Admin `action` values that change data when loaded with GET. 41 * 42 * Read-only values such as `edit` are kept so the capture iframe opens the 43 * screen the user was on. 44 * 45 * @var string[] 46 */ 47 public const MUTATING_ACTIONS = array( 48 'trash', 49 'untrash', 50 'delete', 51 'delete-all', 52 'editpost', 53 'postajaxpost', 54 'post-quickdraft-save', 55 'deletecomment', 56 'trashcomment', 57 'untrashcomment', 58 'spamcomment', 59 'unspamcomment', 60 'approvecomment', 61 'unapprovecomment', 62 'approve', 63 'unapprove', 64 'spam', 65 'unspam', 66 'dim-comment', 67 'activate', 68 'deactivate', 69 'activate-selected', 70 'deactivate-selected', 71 'update-selected', 72 'delete-selected', 73 'enable-auto-update', 74 'disable-auto-update', 75 'error_scrape', 76 'resume', 77 'upgrade-plugin', 78 'upgrade-theme', 79 'do-plugin-upgrade', 80 'do-theme-upgrade', 81 'do-core-upgrade', 82 'do-translation-upgrade', 83 'install-plugin', 84 'install-theme', 85 'upload-plugin', 86 'upload-theme', 87 'logout', 88 'remove', 89 'deleteblog', 90 'spamblog', 91 ); 92 93 /** 38 94 * Enqueue the admin bar script and its small style, for administrators only. 39 95 * … … 46 102 47 103 $style_url = false; 48 wp_register_style( 'stillframe-admin-bar', $style_url, array(), STILLFRAME_ VERSION);104 wp_register_style( 'stillframe-admin-bar', $style_url, array(), STILLFRAME_ASSET_VER ); 49 105 wp_enqueue_style( 'stillframe-admin-bar' ); 50 106 wp_add_inline_style( 'stillframe-admin-bar', $this->admin_bar_css() ); … … 56 112 $base . 'assets/css/stillframe-capture-panel.css', 57 113 array(), 58 STILLFRAME_VERSION 59 ); 60 61 wp_register_script( 62 'stillframe-annotation-editor', 63 $base . 'assets/js/stillframe-annotation-editor.js', 64 array(), 65 STILLFRAME_VERSION, 66 true 67 ); 114 STILLFRAME_ASSET_VER 115 ); 116 117 /** 118 * Filters whether the Fabric.js annotation editor is used. Return false for the legacy editor. 119 * 120 * @param bool $use_fabric Whether to use the Fabric.js editor. 121 */ 122 if ( apply_filters( 'stillframe_use_fabric_editor', true ) ) { 123 wp_register_script( 124 'stillframe-fabric', 125 $base . 'assets/js/vendor/fabric.min.js', 126 array(), 127 '6.7.1', 128 true 129 ); 130 wp_register_script( 131 'stillframe-annotation-editor', 132 $base . 'assets/js/stillframe-annotation-editor-v2.js', 133 array( 'stillframe-fabric' ), 134 STILLFRAME_ASSET_VER, 135 true 136 ); 137 } else { 138 wp_register_script( 139 'stillframe-annotation-editor', 140 $base . 'assets/js/stillframe-annotation-editor.js', 141 array(), 142 STILLFRAME_ASSET_VER, 143 true 144 ); 145 } 68 146 69 147 wp_register_script( … … 71 149 $base . 'assets/js/stillframe-capture-panel.js', 72 150 array( 'stillframe-annotation-editor' ), 73 STILLFRAME_ VERSION,151 STILLFRAME_ASSET_VER, 74 152 true 75 153 ); … … 79 157 $base . 'assets/js/stillframe-admin-bar.js', 80 158 array(), 81 STILLFRAME_ VERSION,159 STILLFRAME_ASSET_VER, 82 160 true 83 161 ); … … 86 164 'pluginUrl' => $base, 87 165 'version' => STILLFRAME_VERSION, 166 'assetVer' => STILLFRAME_ASSET_VER, 167 'useFabric' => apply_filters( 'stillframe_use_fabric_editor', true ), 88 168 'currentUrl' => $this->current_screen_url(), 89 169 'presets' => $this->device_width_presets(), … … 97 177 'maxWidth' => 2560, 98 178 'homeUrl' => home_url( '/' ), 99 'sitePages' => $this->site_pages(),179 'sitePages' => self::site_pages(), 100 180 'toolsUrl' => admin_url( 'tools.php?page=stillframe' ), 101 'stripArgs' => self::STRIPPED_QUERY_ARGS, 102 'i18n' => $this->script_strings(), 181 'stripArgs' => self::STRIPPED_QUERY_ARGS, 182 'mutatingActions' => self::MUTATING_ACTIONS, 183 'i18n' => $this->script_strings(), 103 184 ); 104 185 … … 115 196 $base . 'assets/css/stillframe-tools.css', 116 197 array(), 117 STILLFRAME_ VERSION198 STILLFRAME_ASSET_VER 118 199 ); 119 200 wp_enqueue_style( 'stillframe-tools' ); … … 123 204 $base . 'assets/js/vendor/modern-screenshot.js', 124 205 array(), 125 STILLFRAME_ VERSION,206 STILLFRAME_ASSET_VER, 126 207 true 127 208 ); … … 134 215 $base . 'assets/js/stillframe-tools.js', 135 216 array( 'stillframe-capture-panel', 'stillframe-modern-screenshot' ), 136 STILLFRAME_ VERSION,217 STILLFRAME_ASSET_VER, 137 218 true 138 219 ); … … 145 226 'mediaNonce' => wp_create_nonce( 'stillframe_save_media' ), 146 227 'canUpload' => current_user_can( 'upload_files' ), 147 'pages' => $this->site_pages(),228 'pages' => self::site_pages(), 148 229 ); 149 230 wp_add_inline_script( … … 164 245 165 246 /** 166 * Retrieve a list of published pages and posts for quick selection. 167 * 168 * @return array<int, array{id: int, title: string, type: string, url: string}> 169 */ 170 private function site_pages() { 247 * Retrieve published content from every public post type, for the page picker. 248 * 249 * Shared by the capture panel and the Tools screen. 250 * 251 * @return array<int, array{id: int, title: string, type: string, url: string, path: string, parent: string, status: string, home: bool}> 252 */ 253 public static function site_pages() { 171 254 $cached = get_transient( self::PAGES_TRANSIENT ); 172 255 if ( is_array( $cached ) ) { … … 174 257 } 175 258 176 $items = array(); 259 $types = array_values( array_diff( get_post_types( array( 'public' => true ) ), array( 'attachment' ) ) ); 260 261 /** 262 * Filters the maximum number of items listed in the page picker. 263 * 264 * @param int $max Maximum number of items. 265 */ 266 $max = (int) apply_filters( 'stillframe_max_pages', 500 ); 267 if ( $max < 1 ) { 268 $max = 500; 269 } 270 177 271 $pages = get_posts( 178 272 array( 179 'post_type' => array( 'page', 'post' ),273 'post_type' => $types, 180 274 'post_status' => 'publish', 181 'posts_per_page' => 25, 182 'orderby' => 'title', 183 'order' => 'ASC', 275 'posts_per_page' => $max, 276 'no_found_rows' => true, 277 'orderby' => array( 278 'type' => 'ASC', 279 'menu_order' => 'ASC', 280 'title' => 'ASC', 281 ), 184 282 ) 185 283 ); 284 285 $home_ids = array_values( 286 array_filter( 287 array( 288 (int) get_option( 'page_on_front' ), 289 (int) get_option( 'page_for_posts' ), 290 ) 291 ) 292 ); 293 294 foreach ( array_reverse( $home_ids ) as $home_id ) { 295 $home_post = get_post( $home_id ); 296 if ( $home_post && 'publish' === $home_post->post_status ) { 297 array_unshift( $pages, $home_post ); 298 } 299 } 300 301 $items = array(); 302 $seen = array(); 186 303 foreach ( $pages as $p ) { 304 if ( isset( $seen[ $p->ID ] ) ) { 305 continue; 306 } 307 $seen[ $p->ID ] = true; 308 187 309 $permalink = get_permalink( $p ); 188 if ( $permalink ) { 189 $items[] = array( 190 'id' => (int) $p->ID, 191 'title' => $p->post_title ? $p->post_title : __( '(No title)', 'stillframe' ), 192 'type' => $p->post_type, 193 'url' => $permalink, 194 ); 195 } 196 } 310 if ( ! $permalink ) { 311 continue; 312 } 313 314 $path = wp_parse_url( $permalink, PHP_URL_PATH ); 315 $items[] = array( 316 'id' => (int) $p->ID, 317 'title' => $p->post_title ? $p->post_title : __( '(No title)', 'stillframe' ), 318 'type' => $p->post_type, 319 'url' => $permalink, 320 'path' => is_string( $path ) && '' !== $path ? $path : '/', 321 'parent' => $p->post_parent ? get_the_title( $p->post_parent ) : '', 322 'status' => $p->post_status, 323 'home' => in_array( (int) $p->ID, $home_ids, true ), 324 ); 325 } 326 197 327 set_transient( self::PAGES_TRANSIENT, $items, 12 * HOUR_IN_SECONDS ); 198 328 return $items; … … 411 541 'recent' => __( 'Recent', 'stillframe' ), 412 542 'devices' => __( 'Devices', 'stillframe' ), 543 'pickCurrent' => __( 'Current screen', 'stillframe' ), 544 'pickHome' => __( 'Home', 'stillframe' ), 545 'pickPages' => __( 'Pages', 'stillframe' ), 546 'pickPosts' => __( 'Posts', 'stillframe' ), 547 'pickCustomPlaceholder' => __( 'Custom URL: paste any address...', 'stillframe' ), 548 'pickCustom' => __( 'Custom URL', 'stillframe' ), 549 'pickUse' => __( 'Use', 'stillframe' ), 550 'pickSearchPlaceholder' => __( 'Search pages or paste a URL from this site', 'stillframe' ), 551 'pickSearch' => __( 'Search pages', 'stillframe' ), 552 'pickEnterUrl' => __( 'Enter a URL below', 'stillframe' ), 553 'pickNoMatch' => __( 'No matching pages. Paste a full URL to use it.', 'stillframe' ), 554 'directTitle' => __( 'Capture a page', 'stillframe' ), 555 'targetPage' => __( 'Page', 'stillframe' ), 556 'currentAdmin' => __( 'Current Admin Screen', 'stillframe' ), 557 'currentScreen' => __( 'Current Screen', 'stillframe' ), 558 'selectDevices' => __( 'Devices', 'stillframe' ), 559 'desktopOnly' => __( 'Desktop Only', 'stillframe' ), 560 'selectAll' => __( 'Select All (3)', 'stillframe' ), 561 'hideAdminBar' => __( 'Hide the admin bar in the screenshot', 'stillframe' ), 562 'resQuality' => __( 'Quality', 'stillframe' ), 563 'captureHeight' => __( 'Height', 'stillframe' ), 564 'res1' => __( '1x', 'stillframe' ), 565 'res2' => __( '2x', 'stillframe' ), 566 'res3' => __( '3x', 'stillframe' ), 567 'res1Help' => __( 'Same pixels as the screen. Smallest files.', 'stillframe' ), 568 'res2Help' => __( 'Sharp on high-density displays. Recommended.', 'stillframe' ), 569 'res3Help' => __( 'Maximum detail. Large files; very long pages may be reduced.', 'stillframe' ), 570 'heightViewport' => __( 'First screen', 'stillframe' ), 571 'heightFull' => __( 'Full Page', 'stillframe' ), 572 'heightViewportHelp' => __( 'Only the first screen (above the fold).', 'stillframe' ), 573 'heightFullHelp' => __( 'The entire page, top to bottom.', 'stillframe' ), 574 'captureAll' => __( 'Capture 3 devices', 'stillframe' ), 575 'dev_desktop' => __( 'Desktop', 'stillframe' ), 576 'dev_tablet' => __( 'iPad', 'stillframe' ), 577 'dev_mobile' => __( 'Mobile', 'stillframe' ), 578 /* translators: 1: rows shown, 2: rows matched. */ 579 'pickShowing' => __( 'Showing %1$d of %2$d. Keep typing to narrow the list.', 'stillframe' ), 580 'pickChoose' => __( 'Choose a page', 'stillframe' ), 581 'captureNone' => __( 'Capture', 'stillframe' ), 582 'chooseDevice' => __( 'Choose at least one device to capture.', 'stillframe' ), 583 'pickUseUrl' => __( 'Use this URL', 'stillframe' ), 584 'engineTitle' => __( 'Pixel-perfect capture', 'stillframe' ), 585 'engineHelp' => __( 'Area, Window, and Full screen on this tab in Chrome or Edge. The browser asks to share the tab, and nothing is uploaded. Other browsers, Direct Page, and Tools redraw the page.', 'stillframe' ), 586 'exactUnsupported' => __( 'This browser cannot share the tab, so this shot was redrawn from the page.', 'stillframe' ), 587 'exactCancelled' => __( 'Sharing was cancelled, so this shot was redrawn from the page. The next capture will ask to share the tab again.', 'stillframe' ), 588 'exactSurface' => __( 'That share was not this browser tab, so this shot was redrawn from the page. The next capture will ask again.', 'stillframe' ), 589 'exactMismatch' => __( 'The tab capture did not match this window, so this shot was redrawn from the page. The next capture will ask again.', 'stillframe' ), 590 'exactFallback' => __( 'Pixel-perfect capture did not finish, so this shot was redrawn from the page. The next capture will ask again.', 'stillframe' ), 591 'discardMarks' => __( 'This capture has unsaved marks. Opening another screenshot will discard them. Continue?', 'stillframe' ), 592 /* translators: %d: number of devices. */ 593 'captureN' => __( 'Capture %d devices', 'stillframe' ), 594 /* translators: %s: device name. */ 595 'captureOne' => __( 'Capture %s', 'stillframe' ), 413 596 ); 414 597 } … … 424 607 425 608 /** 609 * Whether an admin `action` value changes data when the URL is opened. 610 * 611 * Empty and read-only values, including `edit`, are safe to keep. 612 * 613 * @param mixed $action Action value from the query string. 614 * @return bool 615 */ 616 public static function action_is_mutating( $action ) { 617 if ( is_array( $action ) ) { 618 foreach ( $action as $item ) { 619 if ( self::action_is_mutating( $item ) ) { 620 return true; 621 } 622 } 623 return false; 624 } 625 626 if ( ! is_string( $action ) || '' === $action ) { 627 return false; 628 } 629 630 return in_array( strtolower( $action ), self::MUTATING_ACTIONS, true ); 631 } 632 633 /** 634 * Remove nonces and state-changing actions from a capture URL. 635 * 636 * @param string $url Absolute or root-relative URL. 637 * @return string 638 */ 639 public static function scrub_capture_url( $url ) { 640 $clean = remove_query_arg( self::STRIPPED_QUERY_ARGS, $url ); 641 $action = null; 642 $parts = wp_parse_url( $clean ); 643 644 if ( is_array( $parts ) && isset( $parts['query'] ) && is_string( $parts['query'] ) ) { 645 $query = array(); 646 parse_str( $parts['query'], $query ); 647 if ( isset( $query['action'] ) ) { 648 $action = $query['action']; 649 } 650 } 651 652 if ( self::action_is_mutating( $action ) ) { 653 $clean = remove_query_arg( 'action', $clean ); 654 } 655 656 return $clean; 657 } 658 659 /** 426 660 * URL of the screen being viewed, safe to load with a same-origin GET. 427 661 * 428 * The host comes from home_url(). Query arguments that can replay a 429 * state-changing request are removed before the value ever reaches JavaScript. 662 * The host comes from home_url(). Nonces and state-changing actions are 663 * removed before the value ever reaches JavaScript. Read-only actions such 664 * as edit stay, so post.php?action=edit reloads the editor. 430 665 * 431 666 * @return string … … 434 669 $target = $this->request_path_and_query(); 435 670 $origin = $this->home_origin(); 436 $url = remove_query_arg( self::STRIPPED_QUERY_ARGS,$origin . $target );671 $url = self::scrub_capture_url( $origin . $target ); 437 672 $clean = esc_url_raw( $url ); 438 673 -
stillframe/trunk/includes/class-stillframe-media.php
r3724658 r3736872 11 11 12 12 /** 13 * Accepts one PNG from an administrator and stores it as an attachment.13 * Accepts one PNG, JPEG or WebP image from an administrator and stores it as an attachment. 14 14 */ 15 15 class Stillframe_Media { … … 25 25 26 26 /** 27 * Store the uploaded PNGand return its attachment URL.27 * Store the uploaded image and return its attachment URL. 28 28 * 29 29 * @return void … … 66 66 } 67 67 68 $name = $this-> png_filename( wp_unslash( $file['name'] ) );68 $name = $this->image_filename( wp_unslash( $file['name'] ) ); 69 69 70 70 require_once ABSPATH . 'wp-admin/includes/file.php'; … … 76 76 $name, 77 77 array( 78 'png' => 'image/png', 78 'png' => 'image/png', 79 'jpg|jpeg' => 'image/jpeg', 80 'webp' => 'image/webp', 79 81 ) 80 82 ); 81 83 82 if ( empty( $checked['ext'] ) || 'png' !== $checked['ext'] || 'image/png' !== $checked['type']) {84 if ( empty( $checked['ext'] ) || empty( $checked['type'] ) || ! in_array( $checked['type'], array( 'image/png', 'image/jpeg', 'image/webp' ), true ) ) { 83 85 $this->fail(); 84 86 } … … 91 93 'test_form' => false, 92 94 'mimes' => array( 93 'png' => 'image/png', 95 'png' => 'image/png', 96 'jpg|jpeg' => 'image/jpeg', 97 'webp' => 'image/webp', 94 98 ), 95 99 ) … … 104 108 $attachment_id = wp_insert_attachment( 105 109 array( 106 'post_mime_type' => 'image/png',110 'post_mime_type' => $checked['type'], 107 111 'post_title' => sanitize_text_field( pathinfo( $name, PATHINFO_FILENAME ) ), 108 112 'post_content' => '', … … 141 145 142 146 /** 143 * Keep a safe PNGfilename, or fall back to a generated one.147 * Keep a safe image filename, or fall back to a generated one. 144 148 * 145 149 * @param mixed $raw Original upload name. 146 150 * @return string 147 151 */ 148 private function png_filename( $raw ) {152 private function image_filename( $raw ) { 149 153 $name = is_string( $raw ) ? sanitize_file_name( $raw ) : ''; 150 154 $name = strtolower( $name ); 151 155 152 if ( ! preg_match( '/^[a-z0-9][a-z0-9._-]*\. png$/', $name ) ) {156 if ( ! preg_match( '/^[a-z0-9][a-z0-9._-]*\.(png|jpe?g|webp)$/', $name ) ) { 153 157 $name = 'stillframe-' . gmdate( 'Ymd-His' ) . '.png'; 154 158 } -
stillframe/trunk/includes/class-stillframe-plugin.php
r3724658 r3736872 66 66 add_action( 'save_post', array( 'Stillframe_Asset_Loader', 'flush_pages_cache' ) ); 67 67 add_action( 'deleted_post', array( 'Stillframe_Asset_Loader', 'flush_pages_cache' ) ); 68 add_action( 'trashed_post', array( 'Stillframe_Asset_Loader', 'flush_pages_cache' ) ); 69 add_action( 'transition_post_status', array( 'Stillframe_Asset_Loader', 'flush_pages_cache' ) ); 68 70 } 69 71 } -
stillframe/trunk/includes/class-stillframe-tools-page.php
r3724658 r3736872 49 49 } 50 50 51 $home_url = home_url( '/' );52 $site_pages = get_posts(53 array(54 'post_type' => array( 'page', 'post' ),55 'post_status' => 'publish',56 'posts_per_page' => 30,57 'orderby' => 'title',58 'order' => 'ASC',59 )60 );61 51 ?> 62 52 <div class="wrap stillframe-tools-wrap"> … … 72 62 <div class="stillframe-field-group"> 73 63 <div class="stillframe-field-group__header"> 74 < label class="stillframe-field-group__title" for="stillframe-target-page"><?php esc_html_e( 'Target Page', 'stillframe' ); ?></label>64 <span class="stillframe-field-group__title"><?php esc_html_e( 'Target Page', 'stillframe' ); ?></span> 75 65 <span class="stillframe-field-group__hint"><?php esc_html_e( 'Select a site page or enter a custom URL to capture.', 'stillframe' ); ?></span> 76 66 </div> 77 67 <div class="stillframe-page-picker"> 78 <select id="stillframe-target-page" class="stillframe-select"> 79 <option value="<?php echo esc_url( $home_url ); ?>"> 80 <?php echo esc_html__( 'Home Page', 'stillframe' ) . ' (' . esc_html( $home_url ) . ')'; ?> 81 </option> 82 <?php foreach ( $site_pages as $page ) : ?> 83 <?php 84 $page_url = get_permalink( $page ); 85 if ( ! $page_url || $page_url === $home_url ) { 86 continue; 87 } 88 $title = $page->post_title ? $page->post_title : __( '(No title)', 'stillframe' ); 89 ?> 90 <option value="<?php echo esc_url( $page_url ); ?>"> 91 <?php echo esc_html( $title . ' (' . ucfirst( $page->post_type ) . ')' ); ?> 92 </option> 93 <?php endforeach; ?> 94 <option value="__custom__"><?php esc_html_e( 'Custom URL...', 'stillframe' ); ?></option> 95 </select> 96 <div id="stillframe-custom-url-wrap" class="stillframe-custom-url-wrap" hidden style="display: none;"> 97 <input type="url" id="stillframe-custom-url" class="stillframe-input" placeholder="https://example.com/" /> 98 </div> 68 <div id="stillframe-target-page"></div> 99 69 </div> 100 70 </div> … … 174 144 <div class="stillframe-field-group__header"> 175 145 <span class="stillframe-field-group__title"><?php esc_html_e( 'Capture Preferences', 'stillframe' ); ?></span> 176 <span class="stillframe-field-group__hint"><?php esc_html_e( ' Configure resolution quality and display settings.', 'stillframe' ); ?></span>146 <span class="stillframe-field-group__hint"><?php esc_html_e( 'These shots redraw the page. Pixel-perfect capture is Area, Window, and Full screen from the admin bar.', 'stillframe' ); ?></span> 177 147 </div> 178 148 <div class="stillframe-settings-grid"> -
stillframe/trunk/languages/stillframe.pot
r3724658 r3736872 1 # Copyright (C) 2026 Stillframe1 # Copyright (C) 2026 Grafucci 2 2 # This file is distributed under the GPL-2.0-or-later. 3 3 msgid "" 4 4 msgstr "" 5 "Project-Id-Version: Stillframe 0.2.0\n"6 "Report-Msgid-Bugs-To: \n"5 "Project-Id-Version: Stillframe – Screenshot Capture & Markup 0.2.5\n" 6 "Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/stillframe\n" 7 7 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" 8 8 "Language-Team: LANGUAGE <LL@li.org>\n" … … 10 10 "Content-Type: text/plain; charset=UTF-8\n" 11 11 "Content-Transfer-Encoding: 8bit\n" 12 "POT-Creation-Date: 2026- 09-29T08:26:04+00:00\n"12 "POT-Creation-Date: 2026-10-09T14:33:09+00:00\n" 13 13 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 14 14 "X-Generator: WP-CLI 2.12.0\n" 15 15 "X-Domain: stillframe\n" 16 16 17 17 #. Plugin Name of the plugin 18 18 #: stillframe.php 19 #: includes/class-stillframe-admin-bar-menu.php:32 20 msgid "Stillframe" 19 msgid "Stillframe – Screenshot Capture & Markup" 20 msgstr "" 21 22 #. Plugin URI of the plugin 23 #: stillframe.php 24 msgid "https://grafucci.com/stillframe/" 21 25 msgstr "" 22 26 23 27 #. Description of the plugin 24 28 #: stillframe.php 25 msgid "Capture a plugin screen, a theme, or any page at phone, tablet, or a custom width, mark it up, and download a PNG."29 msgid "Capture multi-device screenshots (Desktop, Tablet, Mobile) or custom regions, mark them up with rich annotation tools, and export to PNG or Media Library." 26 30 msgstr "" 27 31 … … 31 35 msgstr "" 32 36 37 #. Author URI of the plugin 38 #: stillframe.php 39 msgid "https://grafucci.com/" 40 msgstr "" 41 42 #: includes/class-stillframe-admin-bar-menu.php:32 33 43 #: includes/class-stillframe-admin-bar-menu.php:36 34 #: includes/class-stillframe-asset-loader.php: 23344 #: includes/class-stillframe-asset-loader.php:487 35 45 msgid "Capture this screen" 36 46 msgstr "" 37 47 38 #: includes/class-stillframe-asset-loader.php:125 39 #: includes/class-stillframe-asset-loader.php:185 48 #: includes/class-stillframe-asset-loader.php:317 49 msgid "(No title)" 50 msgstr "" 51 52 #: includes/class-stillframe-asset-loader.php:379 53 #: includes/class-stillframe-asset-loader.php:439 40 54 msgid "Phone" 41 55 msgstr "" 42 56 43 #: includes/class-stillframe-asset-loader.php:130 44 #: includes/class-stillframe-asset-loader.php:190 57 #: includes/class-stillframe-asset-loader.php:384 58 #: includes/class-stillframe-asset-loader.php:444 59 #: includes/class-stillframe-asset-loader.php:576 45 60 msgid "iPad" 46 61 msgstr "" 47 62 48 #: includes/class-stillframe-asset-loader.php:135 49 #: includes/class-stillframe-asset-loader.php:195 63 #: includes/class-stillframe-asset-loader.php:389 64 #: includes/class-stillframe-asset-loader.php:449 65 #: includes/class-stillframe-asset-loader.php:575 66 #: includes/class-stillframe-tools-page.php:95 50 67 msgid "Desktop" 51 68 msgstr "" 52 69 53 #: includes/class-stillframe-asset-loader.php:213 70 #: includes/class-stillframe-asset-loader.php:467 71 #: includes/class-stillframe-asset-loader.php:564 54 72 msgid "1x" 55 73 msgstr "" 56 74 57 #: includes/class-stillframe-asset-loader.php:217 75 #: includes/class-stillframe-asset-loader.php:471 76 #: includes/class-stillframe-asset-loader.php:565 58 77 msgid "2x" 59 78 msgstr "" 60 79 61 #: includes/class-stillframe-asset-loader.php:221 80 #: includes/class-stillframe-asset-loader.php:475 81 #: includes/class-stillframe-asset-loader.php:566 62 82 msgid "3x" 63 83 msgstr "" 64 84 65 #: includes/class-stillframe-asset-loader.php: 23485 #: includes/class-stillframe-asset-loader.php:488 66 86 msgid "Close" 67 87 msgstr "" 68 88 69 #: includes/class-stillframe-asset-loader.php:235 89 #: includes/class-stillframe-asset-loader.php:489 90 msgid "This screen" 91 msgstr "" 92 93 #: includes/class-stillframe-asset-loader.php:490 70 94 msgid "Width" 71 95 msgstr "" 72 96 73 #: includes/class-stillframe-asset-loader.php: 23697 #: includes/class-stillframe-asset-loader.php:491 74 98 msgid "Custom" 75 99 msgstr "" 76 100 77 #: includes/class-stillframe-asset-loader.php: 237101 #: includes/class-stillframe-asset-loader.php:492 78 102 msgid "Custom width" 79 103 msgstr "" 80 104 81 #: includes/class-stillframe-asset-loader.php: 238105 #: includes/class-stillframe-asset-loader.php:493 82 106 msgid "Export scale" 83 107 msgstr "" 84 108 85 #: includes/class-stillframe-asset-loader.php:239 109 #: includes/class-stillframe-asset-loader.php:494 110 #: includes/class-stillframe-asset-loader.php:581 86 111 msgid "Capture" 87 112 msgstr "" 88 113 89 #: includes/class-stillframe-asset-loader.php: 240114 #: includes/class-stillframe-asset-loader.php:495 90 115 msgid "Capture current window" 91 116 msgstr "" 92 117 93 #: includes/class-stillframe-asset-loader.php:241 94 msgid "Download PNG" 95 msgstr "" 96 97 #: includes/class-stillframe-asset-loader.php:242 118 #: includes/class-stillframe-asset-loader.php:496 119 msgid "Reloading this page at the chosen width." 120 msgstr "" 121 122 #: includes/class-stillframe-asset-loader.php:497 123 msgid "Download" 124 msgstr "" 125 126 #: includes/class-stillframe-asset-loader.php:498 127 msgid "Save to Media" 128 msgstr "" 129 130 #: includes/class-stillframe-asset-loader.php:499 131 msgid "Saving to the Media Library." 132 msgstr "" 133 134 #: includes/class-stillframe-asset-loader.php:500 135 msgid "Saved to Media" 136 msgstr "" 137 138 #: includes/class-stillframe-asset-loader.php:501 139 msgid "View in Media" 140 msgstr "" 141 142 #: includes/class-stillframe-asset-loader.php:502 143 #: includes/class-stillframe-media.php:171 144 msgid "The media save failed. The page was not changed." 145 msgstr "" 146 147 #: includes/class-stillframe-asset-loader.php:503 98 148 msgid "Pen" 99 149 msgstr "" 100 150 101 #: includes/class-stillframe-asset-loader.php: 243151 #: includes/class-stillframe-asset-loader.php:504 102 152 msgid "Circle" 103 153 msgstr "" 104 154 105 #: includes/class-stillframe-asset-loader.php: 244155 #: includes/class-stillframe-asset-loader.php:505 106 156 msgid "Arrow" 107 157 msgstr "" 108 158 109 #: includes/class-stillframe-asset-loader.php: 245159 #: includes/class-stillframe-asset-loader.php:506 110 160 msgid "Undo" 111 161 msgstr "" 112 162 113 #: includes/class-stillframe-asset-loader.php: 246163 #: includes/class-stillframe-asset-loader.php:507 114 164 msgid "Clear" 115 165 msgstr "" 116 166 117 #: includes/class-stillframe-asset-loader.php: 247167 #: includes/class-stillframe-asset-loader.php:508 118 168 msgid "Annotation tools" 119 169 msgstr "" 120 170 121 #: includes/class-stillframe-asset-loader.php: 248171 #: includes/class-stillframe-asset-loader.php:509 122 172 msgid "Captured screen" 123 173 msgstr "" 124 174 125 #: includes/class-stillframe-asset-loader.php: 249175 #: includes/class-stillframe-asset-loader.php:510 126 176 msgid "Stillframe capture frame" 127 177 msgstr "" 128 178 129 #: includes/class-stillframe-asset-loader.php: 250179 #: includes/class-stillframe-asset-loader.php:511 130 180 msgid "Capturing this screen." 131 181 msgstr "" 132 182 133 183 #. translators: 1: width in CSS pixels, 2: export scale. 134 #: includes/class-stillframe-asset-loader.php: 252184 #: includes/class-stillframe-asset-loader.php:513 135 185 #, php-format 136 186 msgid "Captured at %1$d pixels wide and %2$dx. Draw on the picture, then download the PNG." 137 187 msgstr "" 138 188 139 #: includes/class-stillframe-asset-loader.php: 253189 #: includes/class-stillframe-asset-loader.php:514 140 190 msgid "Preparing the PNG." 141 191 msgstr "" 142 192 143 #: includes/class-stillframe-asset-loader.php: 254193 #: includes/class-stillframe-asset-loader.php:515 144 194 msgid "Stillframe could not load the capture tools. The page was not changed." 145 195 msgstr "" 146 196 147 #: includes/class-stillframe-asset-loader.php: 255197 #: includes/class-stillframe-asset-loader.php:516 148 198 msgid "The capture failed. The page was not changed." 149 199 msgstr "" 150 200 151 #: includes/class-stillframe-asset-loader.php: 256201 #: includes/class-stillframe-asset-loader.php:517 152 202 msgid "The capture timed out. The page was not changed." 153 203 msgstr "" 154 204 155 #: includes/class-stillframe-asset-loader.php: 257205 #: includes/class-stillframe-asset-loader.php:518 156 206 msgid "The frame was blocked, so only the current window can be captured. The page was not changed." 157 207 msgstr "" 158 208 159 #: includes/class-stillframe-asset-loader.php: 258209 #: includes/class-stillframe-asset-loader.php:519 160 210 msgid "Enter a whole number from 320 to 2560. The page was not changed." 161 211 msgstr "" 162 212 163 #: includes/class-stillframe-asset-loader.php: 259213 #: includes/class-stillframe-asset-loader.php:520 164 214 msgid "The download failed. The page was not changed." 165 215 msgstr "" 166 216 167 #: includes/class-stillframe-asset-loader.php: 260217 #: includes/class-stillframe-asset-loader.php:521 168 218 msgid "The capture tool did not load. The page was not changed." 169 219 msgstr "" 220 221 #: includes/class-stillframe-asset-loader.php:522 222 msgid "Drag to select an area" 223 msgstr "" 224 225 #: includes/class-stillframe-asset-loader.php:523 226 msgid "Click a section of the page to capture it" 227 msgstr "" 228 229 #: includes/class-stillframe-asset-loader.php:524 230 msgid "Area" 231 msgstr "" 232 233 #: includes/class-stillframe-asset-loader.php:525 234 msgid "Window" 235 msgstr "" 236 237 #: includes/class-stillframe-asset-loader.php:526 238 msgid "Full screen" 239 msgstr "" 240 241 #: includes/class-stillframe-asset-loader.php:527 242 msgid "Capturing…" 243 msgstr "" 244 245 #: includes/class-stillframe-asset-loader.php:528 246 msgid "Almost there. This will finish as soon as the capture is ready." 247 msgstr "" 248 249 #: includes/class-stillframe-asset-loader.php:529 250 msgid "Downloaded." 251 msgstr "" 252 253 #: includes/class-stillframe-asset-loader.php:530 254 msgid "Color" 255 msgstr "" 256 257 #: includes/class-stillframe-asset-loader.php:531 258 msgid "Line thickness" 259 msgstr "" 260 261 #: includes/class-stillframe-asset-loader.php:532 262 msgid "Top bar" 263 msgstr "" 264 265 #: includes/class-stillframe-asset-loader.php:533 266 msgid "Side menu" 267 msgstr "" 268 269 #: includes/class-stillframe-asset-loader.php:534 270 msgid "Hide other plugins" 271 msgstr "" 272 273 #: includes/class-stillframe-asset-loader.php:535 274 msgid "Admin notices" 275 msgstr "" 276 277 #: includes/class-stillframe-asset-loader.php:536 278 msgid "Cancel" 279 msgstr "" 280 281 #: includes/class-stillframe-asset-loader.php:537 282 msgid "More" 283 msgstr "" 284 285 #: includes/class-stillframe-asset-loader.php:538 286 msgid "Include the top bar" 287 msgstr "" 288 289 #: includes/class-stillframe-asset-loader.php:539 290 msgid "Include the side menu" 291 msgstr "" 292 293 #: includes/class-stillframe-asset-loader.php:540 294 msgid "Direct Page" 295 msgstr "" 296 297 #: includes/class-stillframe-asset-loader.php:541 298 msgid "Recent" 299 msgstr "" 300 301 #: includes/class-stillframe-asset-loader.php:542 302 #: includes/class-stillframe-asset-loader.php:558 303 msgid "Devices" 304 msgstr "" 305 306 #: includes/class-stillframe-asset-loader.php:543 307 msgid "Current screen" 308 msgstr "" 309 310 #: includes/class-stillframe-asset-loader.php:544 311 msgid "Home" 312 msgstr "" 313 314 #: includes/class-stillframe-asset-loader.php:545 315 msgid "Pages" 316 msgstr "" 317 318 #: includes/class-stillframe-asset-loader.php:546 319 msgid "Posts" 320 msgstr "" 321 322 #: includes/class-stillframe-asset-loader.php:547 323 msgid "Custom URL: paste any address..." 324 msgstr "" 325 326 #: includes/class-stillframe-asset-loader.php:548 327 msgid "Custom URL" 328 msgstr "" 329 330 #: includes/class-stillframe-asset-loader.php:549 331 msgid "Use" 332 msgstr "" 333 334 #: includes/class-stillframe-asset-loader.php:550 335 msgid "Search pages or paste a URL from this site" 336 msgstr "" 337 338 #: includes/class-stillframe-asset-loader.php:551 339 msgid "Search pages" 340 msgstr "" 341 342 #: includes/class-stillframe-asset-loader.php:552 343 msgid "Enter a URL below" 344 msgstr "" 345 346 #: includes/class-stillframe-asset-loader.php:553 347 msgid "No matching pages. Paste a full URL to use it." 348 msgstr "" 349 350 #: includes/class-stillframe-asset-loader.php:554 351 msgid "Capture a page" 352 msgstr "" 353 354 #: includes/class-stillframe-asset-loader.php:555 355 msgid "Page" 356 msgstr "" 357 358 #: includes/class-stillframe-asset-loader.php:556 359 msgid "Current Admin Screen" 360 msgstr "" 361 362 #: includes/class-stillframe-asset-loader.php:557 363 msgid "Current Screen" 364 msgstr "" 365 366 #: includes/class-stillframe-asset-loader.php:559 367 msgid "Desktop Only" 368 msgstr "" 369 370 #: includes/class-stillframe-asset-loader.php:560 371 msgid "Select All (3)" 372 msgstr "" 373 374 #: includes/class-stillframe-asset-loader.php:561 375 msgid "Hide the admin bar in the screenshot" 376 msgstr "" 377 378 #: includes/class-stillframe-asset-loader.php:562 379 msgid "Quality" 380 msgstr "" 381 382 #: includes/class-stillframe-asset-loader.php:563 383 msgid "Height" 384 msgstr "" 385 386 #: includes/class-stillframe-asset-loader.php:567 387 msgid "Same pixels as the screen. Smallest files." 388 msgstr "" 389 390 #: includes/class-stillframe-asset-loader.php:568 391 msgid "Sharp on high-density displays. Recommended." 392 msgstr "" 393 394 #: includes/class-stillframe-asset-loader.php:569 395 msgid "Maximum detail. Large files; very long pages may be reduced." 396 msgstr "" 397 398 #: includes/class-stillframe-asset-loader.php:570 399 msgid "First screen" 400 msgstr "" 401 402 #: includes/class-stillframe-asset-loader.php:571 403 msgid "Full Page" 404 msgstr "" 405 406 #: includes/class-stillframe-asset-loader.php:572 407 msgid "Only the first screen (above the fold)." 408 msgstr "" 409 410 #: includes/class-stillframe-asset-loader.php:573 411 msgid "The entire page, top to bottom." 412 msgstr "" 413 414 #: includes/class-stillframe-asset-loader.php:574 415 msgid "Capture 3 devices" 416 msgstr "" 417 418 #: includes/class-stillframe-asset-loader.php:577 419 msgid "Mobile" 420 msgstr "" 421 422 #. translators: 1: rows shown, 2: rows matched. 423 #: includes/class-stillframe-asset-loader.php:579 424 #, php-format 425 msgid "Showing %1$d of %2$d. Keep typing to narrow the list." 426 msgstr "" 427 428 #: includes/class-stillframe-asset-loader.php:580 429 msgid "Choose a page" 430 msgstr "" 431 432 #: includes/class-stillframe-asset-loader.php:582 433 msgid "Choose at least one device to capture." 434 msgstr "" 435 436 #: includes/class-stillframe-asset-loader.php:583 437 msgid "Use this URL" 438 msgstr "" 439 440 #: includes/class-stillframe-asset-loader.php:584 441 msgid "Pixel-perfect capture" 442 msgstr "" 443 444 #: includes/class-stillframe-asset-loader.php:585 445 msgid "Area, Window, and Full screen on this tab in Chrome or Edge. The browser asks to share the tab, and nothing is uploaded. Other browsers, Direct Page, and Tools redraw the page." 446 msgstr "" 447 448 #: includes/class-stillframe-asset-loader.php:586 449 msgid "This browser cannot share the tab, so this shot was redrawn from the page." 450 msgstr "" 451 452 #: includes/class-stillframe-asset-loader.php:587 453 msgid "Sharing was cancelled, so this shot was redrawn from the page. The next capture will ask to share the tab again." 454 msgstr "" 455 456 #: includes/class-stillframe-asset-loader.php:588 457 msgid "That share was not this browser tab, so this shot was redrawn from the page. The next capture will ask again." 458 msgstr "" 459 460 #: includes/class-stillframe-asset-loader.php:589 461 msgid "The tab capture did not match this window, so this shot was redrawn from the page. The next capture will ask again." 462 msgstr "" 463 464 #: includes/class-stillframe-asset-loader.php:590 465 msgid "Pixel-perfect capture did not finish, so this shot was redrawn from the page. The next capture will ask again." 466 msgstr "" 467 468 #: includes/class-stillframe-asset-loader.php:591 469 msgid "This capture has unsaved marks. Opening another screenshot will discard them. Continue?" 470 msgstr "" 471 472 #. translators: %d: number of devices. 473 #: includes/class-stillframe-asset-loader.php:593 474 #, php-format 475 msgid "Capture %d devices" 476 msgstr "" 477 478 #. translators: %s: device name. 479 #: includes/class-stillframe-asset-loader.php:595 480 #, php-format 481 msgid "Capture %s" 482 msgstr "" 483 484 #: includes/class-stillframe-media.php:35 485 msgid "You cannot save this capture." 486 msgstr "" 487 488 #: includes/class-stillframe-tools-page.php:33 489 #: includes/class-stillframe-tools-page.php:34 490 #: includes/class-stillframe-tools-page.php:53 491 #: includes/class-stillframe-tools-page.php:57 492 msgid "Stillframe" 493 msgstr "" 494 495 #: includes/class-stillframe-tools-page.php:48 496 msgid "You cannot capture this screen." 497 msgstr "" 498 499 #: includes/class-stillframe-tools-page.php:64 500 msgid "Target Page" 501 msgstr "" 502 503 #: includes/class-stillframe-tools-page.php:65 504 msgid "Select a site page or enter a custom URL to capture." 505 msgstr "" 506 507 #: includes/class-stillframe-tools-page.php:75 508 msgid "Device Viewports" 509 msgstr "" 510 511 #: includes/class-stillframe-tools-page.php:76 512 msgid "Select viewports to capture simultaneously." 513 msgstr "" 514 515 #: includes/class-stillframe-tools-page.php:115 516 msgid "iPad / Tablet" 517 msgstr "" 518 519 #: includes/class-stillframe-tools-page.php:135 520 msgid "Mobile Phone" 521 msgstr "" 522 523 #: includes/class-stillframe-tools-page.php:145 524 msgid "Capture Preferences" 525 msgstr "" 526 527 #: includes/class-stillframe-tools-page.php:146 528 msgid "These shots redraw the page. Pixel-perfect capture is Area, Window, and Full screen from the admin bar." 529 msgstr "" 530 531 #: includes/class-stillframe-tools-page.php:151 532 msgid "Resolution Quality" 533 msgstr "" 534 535 #: includes/class-stillframe-tools-page.php:170 536 msgid "Capture Height" 537 msgstr "" 538 539 #: includes/class-stillframe-tools-page.php:185 540 msgid "Clean Presentation" 541 msgstr "" 542 543 #: includes/class-stillframe-tools-page.php:189 544 msgid "Hide WordPress Admin Bar" 545 msgstr "" 546 547 #: includes/class-stillframe-tools-page.php:202 548 msgid "Capture Selected Devices" 549 msgstr "" -
stillframe/trunk/readme.txt
r3724668 r3736872 4 4 Requires at least: 6.4 5 5 Tested up to: 7.1 6 Stable tag: 0.2. 06 Stable tag: 0.2.5 7 7 Requires PHP: 7.4 8 8 License: GPL-2.0-or-later … … 58 58 == Screenshots == 59 59 60 1. Clean floating capture toolbar with Area selection mode and tooltipon the WordPress dashboard.61 2. Window & element capture mode with live pixel dimensions and instant capture button.62 3. Devices dropdown: frame and capture standard Desktop (1440px), iPad (834px), and Mobile (390px) viewportson screen.63 4. Direct Page capture dropdown: choose target pages, select viewports, toggle clean presentation mode, and select Retina resolution.64 5. Annotation Editor Suite: full canvas markup tools including shapes, arrows, text, numbered steps, blur redaction, and instant export.60 1. Clean floating capture toolbar with Area selection and the drag hint on the WordPress dashboard. 61 2. Window capture: hover any section, see its size, then capture it. 62 3. Window mode device bar: frame Desktop (1440px), iPad (834px), or Mobile (390px) on screen. 63 4. Direct Page: pick a page, choose devices, set quality and height, hide the admin bar, and capture. 64 5. Annotation editor: shapes, arrows, text, numbered steps, blur, and download or save to the Media Library. 65 65 66 66 == Frequently Asked Questions == … … 86 86 Stillframe bundles modern-screenshot 4.7.0 (MIT license) as assets/js/vendor/modern-screenshot.js. Source: https://github.com/qq15725/modern-screenshot. The license text is in assets/js/vendor/LICENSE-modern-screenshot.txt. 87 87 88 Stillframe also bundles Fabric.js 6.7.1 (MIT license) as assets/js/vendor/fabric.min.js. Source: https://github.com/fabricjs/fabric.js (npm package "fabric"). The license text is in assets/js/vendor/LICENSE-fabric.txt. 89 88 90 == Changelog == 91 92 = 0.2.5 = 93 * fix the editor failing to open a capture when no text size was set 94 * keep the current admin screen when capturing it, including the post editor, and still drop links that would delete or change data 95 * ask before another screenshot replaces one that still has unsaved marks 96 * keep one-line rows, such as pagination and the admin bar, on a single line in redrawn screenshots 97 * load dashboard icons before a redraw so they stay visible in the screenshot 98 * change text boxes so the side handles set the line width, the corners scale the letters, and the height follows the text 99 * shrink selection handles on small marks so they no longer cover the shape 100 * let marks and their handles sit in the grey area around the picture without being cut off 101 * show Desktop, iPad, and Mobile on a solid bar under the capture toolbar in Window mode 102 * wait until the pointer is idle before redrawing the page, so selecting and drawing stay responsive 103 * start a full-screen redraw as soon as the area is selected, so Capture does not wait for the page to be built 104 * load the annotation editor from the admin bar using the current file version, so an update is not served from an old cached copy 105 * explain that pixel-perfect capture is Area, Window, and Full screen in Chrome or Edge, and that Direct Page and Tools redraw the page 106 * say when tab sharing is cancelled or does not match this window, then redraw the page instead 107 108 = 0.2.1 = 109 * Text marks stretch freely: the corner handles scale the text evenly, the side handles stretch it wider or narrower, and the top and bottom handles make it taller or shorter. The Size control keeps showing the real letter height. 110 * Marks can now be dragged and resized out over the grey area around the picture. The part outside the picture is shown faded, and only the picture itself is saved. 111 * Every handle of a mark stays visible and reachable: a mark can no longer be pushed or resized past the edge of the working area. 112 * Smaller, lighter transform handles and a closer rotate knob on every mark; a thinner white outline on large text. 113 * Device frame: Desktop, iPad and Mobile frames keep their proportions on small screens, no longer sit under the top bar, and the device buttons stay available so the size can be switched after framing. 114 * The device buttons in Window mode now hang directly under the top bar, centred on it. 115 * Scripts and styles are versioned by file date, so an updated plugin never serves a stale cached copy. 89 116 90 117 = 0.2.0 = -
stillframe/trunk/stillframe.php
r3724658 r3736872 4 4 * Plugin URI: https://grafucci.com/stillframe/ 5 5 * Description: Capture multi-device screenshots (Desktop, Tablet, Mobile) or custom regions, mark them up with rich annotation tools, and export to PNG or Media Library. 6 * Version: 0.2. 06 * Version: 0.2.5 7 7 * Requires at least: 6.4 8 8 * Requires PHP: 7.4 … … 21 21 } 22 22 23 define( 'STILLFRAME_VERSION', '0.2. 0' );23 define( 'STILLFRAME_VERSION', '0.2.5' ); 24 24 define( 'STILLFRAME_FILE', __FILE__ ); 25 25 define( 'STILLFRAME_DIR', plugin_dir_path( __FILE__ ) ); 26 // Asset query string follows the newest file, so updated scripts and styles are never served from a stale browser cache. 27 define( 'STILLFRAME_ASSET_VER', STILLFRAME_VERSION . '.' . max( (int) filemtime( STILLFRAME_DIR . 'assets/js/stillframe-capture-panel.js' ), (int) filemtime( STILLFRAME_DIR . 'assets/js/stillframe-annotation-editor-v2.js' ), (int) filemtime( STILLFRAME_DIR . 'assets/js/stillframe-annotation-editor.js' ), (int) filemtime( STILLFRAME_DIR . 'assets/js/stillframe-admin-bar.js' ), (int) filemtime( STILLFRAME_DIR . 'assets/css/stillframe-capture-panel.css' ) ) ); 26 28 27 29 require_once STILLFRAME_DIR . 'includes/class-stillframe-capability-check.php'; -
stillframe/trunk/uninstall.php
r3724658 r3736872 19 19 20 20 delete_transient( 'stillframe_site_pages' ); 21 delete_transient( 'stillframe_site_pages_v2' );
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)