deepseek-harness-background 0.5.6 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -104,6 +104,11 @@ window.__ModuleLoader__.load({
104
104
  .dsh-bg-layer .dsh-bg-image {
105
105
  width: 100%; height: 100%; display: block; border: 0; background: transparent;
106
106
  object-fit: var(--bg-object-fit, cover);
107
+ /* Normalized crop framing: percent object-position is the position of the
108
+ image inside its own overflow, so one stored 0..1 value holds for every
109
+ window size and every source. Both axes are always written; the axis
110
+ with no overflow simply ignores its value. */
111
+ object-position: calc(var(--bg-offset-x, 0.5) * 100%) calc(var(--bg-offset-y, 0.5) * 100%);
107
112
  opacity: var(--bg-opacity, 1);
108
113
  filter: blur(var(--bg-wallpaper-blur, 0px));
109
114
  /* Blur thins the edges; scale the image up to hide the fringe. */
@@ -120,6 +125,21 @@ window.__ModuleLoader__.load({
120
125
  body[data-ds-dark-theme] .dsh-bg-scrim {
121
126
  background: rgba(0, 0, 0, var(--bg-scrim, 0.25));
122
127
  }
128
+ /* Desktop window drag (Electron on macOS): the shell marks a chrome row
129
+ \`data-window-drag\` and one rule subtracts every other body-level box
130
+ (\`html[data-platform='darwin'] body > :not(#root) { -webkit-app-region:
131
+ no-drag; }\`, ui-web base.css). Chromium collects app-region boxes by
132
+ geometry in DOM order and the LAST box containing a point decides, so
133
+ these two decorative layers — body children that follow #root — would
134
+ swallow the drag surface of the whole window (traffic-light strip,
135
+ conversation header, dock strip). They never take the pointer, so they
136
+ must not join the composition at all: a computed \`none\` is the one value
137
+ Chromium does not collect. The shell rule carries #root's id specificity,
138
+ so a plain declaration cannot outrank it — this needs \`!important\`. */
139
+ body > .dsh-bg-layer,
140
+ body > .dsh-bg-scrim {
141
+ -webkit-app-region: none !important;
142
+ }
123
143
 
124
144
  /* Active: make the app frame + sidebar background transparent so all columns
125
145
  share the same wallpaper + scrim backdrop. */
@@ -136,7 +156,6 @@ window.__ModuleLoader__.load({
136
156
  light glass slightly DIMS, dark glass keeps a slight lift. Gated on the
137
157
  GLASS ATTR (wallpaper active AND panelOpacity < 1): maxing the panel
138
158
  slider returns these surfaces to the exact official paints. */
139
- body[data-dsh-bg-glass] [data-composer-card],
140
159
  body[data-dsh-bg-glass] [class*="_bubble"]:not([role="tooltip"]),
141
160
  /* The read_image toolview (dsh >= 0.1.2-rc.1 ToolRow) collapses its result
142
161
  into an image body card; glass it like the other content blocks. */
@@ -160,6 +179,37 @@ window.__ModuleLoader__.load({
160
179
  0 12px 40px rgba(0, 0, 0, 0.12);
161
180
  }
162
181
 
182
+ /* ---- Composer card: the filter rides a pseudo-element ------------------
183
+ Chromium makes every element that carries a backdrop-filter a Backdrop
184
+ Root, and a filter inside one can only sample what that element itself
185
+ paints. The composer hosts the picker menu as an absolutely positioned
186
+ child ([data-composer-card] > … conversation.input.overlay), so with the
187
+ filter on the card itself that menu sampled the card's own paint only and
188
+ read as flat transparency over the wallpaper. Moving the filter to a
189
+ positioned ::before breaks the root — the pseudo-element is not an
190
+ ancestor of the menu, so the menu blurs the wallpaper again. The card
191
+ keeps its own frost (the ::before supplies it) and needs no layout change:
192
+ ui-conversation already declares it position: relative as the overlay
193
+ anchor context. */
194
+ body[data-dsh-bg-glass] [data-composer-card] {
195
+ background-color: var(--dsw-specific-input-major);
196
+ background-image: linear-gradient(180deg, rgba(255, 255, 255, var(--bg-glass-sheen, 0.07)), rgba(255, 255, 255, var(--bg-glass-sheen-mid, 0.02)) 38%, rgba(255, 255, 255, 0.01));
197
+ box-shadow:
198
+ inset 0 1px 0 rgba(255, 255, 255, 0.32),
199
+ inset 0 -1px 0 rgba(255, 255, 255, 0.08),
200
+ inset 0 0 0 0.5px rgba(255, 255, 255, 0.08),
201
+ 0 12px 40px rgba(0, 0, 0, 0.12);
202
+ }
203
+ body[data-dsh-bg-glass] [data-composer-card]::before {
204
+ content: '';
205
+ position: absolute;
206
+ inset: 0;
207
+ z-index: -1;
208
+ border-radius: inherit;
209
+ -webkit-backdrop-filter: blur(var(--bg-glass-blur, 16px)) saturate(var(--bg-glass-saturate, 1.42)) brightness(var(--bg-glass-brightness, 1)) contrast(1.01);
210
+ backdrop-filter: blur(var(--bg-glass-blur, 16px)) saturate(var(--bg-glass-saturate, 1.42)) brightness(var(--bg-glass-brightness, 1)) contrast(1.01);
211
+ }
212
+
163
213
  /* The sticky code-block header officially occludes scrolled code with
164
214
  bg-base (transparent under a wallpaper): refill it with the banner token
165
215
  (itself translucent glass) so code stays hidden beneath the sticky bar. */
@@ -308,6 +358,35 @@ window.__ModuleLoader__.load({
308
358
  box-shadow: var(--dsw-elevation-panel, 0 4px 14px rgba(0, 0, 0, 0.08));
309
359
  }
310
360
 
361
+ /* The card's header paints the official opaque fill (--changes-fill is a
362
+ static neutral), so the glass card underneath never shows through: hand
363
+ that one surface back to the card by clearing its fill, and give the
364
+ header's own hover/focus feedback the translucent interactive token the
365
+ card's rows already use. */
366
+ body[data-dsh-bg-glass] [data-changed-files] > :first-child {
367
+ background-color: transparent;
368
+ }
369
+ body[data-dsh-bg-glass] [data-changed-files] > :first-child:hover:not(:disabled),
370
+ body[data-dsh-bg-glass] [data-changed-files] > :first-child:focus-visible {
371
+ background-color: var(--dsw-alias-interactive-bg-hover);
372
+ }
373
+
374
+ /* ---- Docked stat dialogs (turn time, session stats, token usage) -------
375
+ These portal to document.body, so unlike the picker menu above they do
376
+ reach the wallpaper; their official menu skin is a 40px blur under a flat
377
+ 58% fill, which on a soft wallpaper still reads as plain transparency. The
378
+ official fill and its readable contrast stay, and the shared sheen +
379
+ exposure chain joins them to the same glass family as every other panel —
380
+ including the user's blur slider (--bg-glass-blur). Anchors are the
381
+ panels' own detail lists: class hashes are never structural hooks. */
382
+ body[data-dsh-bg-glass] [role="dialog"]:has([data-turn-usage-details]),
383
+ body[data-dsh-bg-glass] [role="dialog"]:has([data-session-stats-details]),
384
+ body[data-dsh-bg-glass] [role="dialog"]:has([data-session-stats-usage]) {
385
+ background-image: linear-gradient(180deg, rgba(255, 255, 255, var(--bg-glass-sheen, 0.07)), rgba(255, 255, 255, var(--bg-glass-sheen-mid, 0.02)) 38%, rgba(255, 255, 255, 0.01));
386
+ -webkit-backdrop-filter: blur(var(--bg-glass-blur, 16px)) saturate(var(--bg-glass-saturate, 1.42)) brightness(var(--bg-glass-brightness, 1)) contrast(1.01);
387
+ backdrop-filter: blur(var(--bg-glass-blur, 16px)) saturate(var(--bg-glass-saturate, 1.42)) brightness(var(--bg-glass-brightness, 1)) contrast(1.01);
388
+ }
389
+
311
390
  /* ---- Official turn rail (dsh >= 0.1.2, TurnNavigator) -----------------
312
391
  The enhance mode borrows the kernel's rail untouched, so its hover
313
392
  preview card keeps the OFFICIAL opaque fill (bg-layer-1, not mediated
@@ -355,6 +434,8 @@ window.__ModuleLoader__.load({
355
434
  const DEFAULT_FIT = "cover";
356
435
  /** Image positioning modes. `cover` fills the frame (cropping as needed); `contain` fits the whole image inside the frame (letterboxing). */
357
436
  const FIT_MODES = ["cover", "contain"];
437
+ /** Default framing offset — the image sits centered on both axes. */
438
+ const DEFAULT_OFFSET = .5;
358
439
  //#endregion
359
440
  //#region src/client/backdrop.ts
360
441
  /**
@@ -559,6 +640,8 @@ window.__ModuleLoader__.load({
559
640
  this.hasSource = true;
560
641
  document.body.style;
561
642
  this.setVar("--bg-object-fit", settings.fit);
643
+ this.setVar("--bg-offset-x", String(settings.offsetX));
644
+ this.setVar("--bg-offset-y", String(settings.offsetY));
562
645
  this.setVar("--bg-opacity", String(settings.opacity));
563
646
  this.setVar("--bg-wallpaper-blur", `${settings.wallpaperBlur}px`);
564
647
  this.setVar("--bg-wallpaper-scale", (1 + settings.wallpaperBlur * .006).toFixed(4));
@@ -626,7 +709,13 @@ window.__ModuleLoader__.load({
626
709
  this.setVar("--bg-wallpaper-blur", `${value}px`);
627
710
  this.setVar("--bg-wallpaper-scale", (1 + Number(value) * .006).toFixed(4));
628
711
  break;
629
- case "fit": this.setVar("--bg-object-fit", String(value));
712
+ case "fit":
713
+ this.setVar("--bg-object-fit", String(value));
714
+ break;
715
+ case "offsetX":
716
+ this.setVar("--bg-offset-x", String(value));
717
+ break;
718
+ case "offsetY": this.setVar("--bg-offset-y", String(value));
630
719
  }
631
720
  }
632
721
  /**
@@ -723,6 +812,8 @@ window.__ModuleLoader__.load({
723
812
  "--bg-opacity",
724
813
  "--bg-scrim",
725
814
  "--bg-object-fit",
815
+ "--bg-offset-x",
816
+ "--bg-offset-y",
726
817
  "--bg-wallpaper-blur",
727
818
  "--bg-wallpaper-scale",
728
819
  "--bg-glass-blur",
@@ -740,6 +831,8 @@ window.__ModuleLoader__.load({
740
831
  function paintPreviewSurface(el, settings) {
741
832
  const s = el.style;
742
833
  s.setProperty("--bg-object-fit", settings.fit);
834
+ s.setProperty("--bg-offset-x", String(settings.offsetX));
835
+ s.setProperty("--bg-offset-y", String(settings.offsetY));
743
836
  s.setProperty("--bg-opacity", String(settings.opacity));
744
837
  s.setProperty("--bg-scrim", String(settings.scrim));
745
838
  s.setProperty("--bg-wallpaper-blur", `${settings.wallpaperBlur}px`);
@@ -985,6 +1078,8 @@ window.__ModuleLoader__.load({
985
1078
  "background.fit": "填充方式",
986
1079
  "background.cover": "铺满",
987
1080
  "background.contain": "完整",
1081
+ "background.previewPanHint": "拖动预览可调整图片位置,双击居中",
1082
+ "background.repositionReset": "重置位置",
988
1083
  "background.clear": "清除背景",
989
1084
  "background.uploadFailed": "上传失败,请重试",
990
1085
  "background.saveFailed": "保存失败,请重试",
@@ -1016,6 +1111,8 @@ window.__ModuleLoader__.load({
1016
1111
  "background.fit": "Fit",
1017
1112
  "background.cover": "Cover",
1018
1113
  "background.contain": "Contain",
1114
+ "background.previewPanHint": "Drag the preview to reposition the image; double-click to recenter",
1115
+ "background.repositionReset": "Reset position",
1019
1116
  "background.clear": "Clear background",
1020
1117
  "background.uploadFailed": "Upload failed, try again",
1021
1118
  "background.saveFailed": "Save failed, try again",
@@ -1149,7 +1246,7 @@ window.__ModuleLoader__.load({
1149
1246
  const settingsClient = new SettingsClient();
1150
1247
  //#endregion
1151
1248
  //#region \0dsh-css:D:\Project\deepseek-harness-background\src\client\SettingsRow.module.css.mjs
1152
- const css = ".-ozqdq_row{border-bottom:1px solid var(--dsw-alias-border-l2);flex-direction:column;gap:14px;padding:16px 0;display:flex}.-ozqdq_header{flex-direction:column;gap:4px;display:flex}.-ozqdq_titleRow{justify-content:space-between;align-items:center;gap:12px;min-height:22px;display:flex}.-ozqdq_clearBtn{border-radius:12px;flex:none;height:24px;padding:0 10px;font-size:12px;line-height:24px}.-ozqdq_title{color:var(--dsw-alias-label-primary);font-size:14px;font-weight:400;line-height:22px}.-ozqdq_desc{color:var(--dsw-alias-label-tertiary);font-size:12px;font-weight:400;line-height:18px}.-ozqdq_notice{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);border-radius:8px;justify-content:space-between;align-items:center;gap:12px;padding:8px 12px;display:flex}.-ozqdq_noticeText{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px}.-ozqdq_noticeBtn{color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;flex:none;padding:0;font-size:12px;line-height:18px;text-decoration:underline}.-ozqdq_noticeBtn:hover{color:var(--dsw-alias-label-primary)}.-ozqdq_preview{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);border-radius:12px;height:132px;position:relative;overflow:hidden}.-ozqdq_previewImg{object-fit:var(--bg-object-fit,cover);width:100%;height:100%;opacity:var(--bg-opacity,1);filter:blur(var(--bg-wallpaper-blur,0px));transform:scale(var(--bg-wallpaper-scale,1));display:block;position:absolute;inset:0}.-ozqdq_previewScrim{background:rgba(255, 255, 255, var(--bg-scrim,.25));position:absolute;inset:0}body[data-ds-dark-theme] .-ozqdq_previewScrim{background:rgba(0, 0, 0, var(--bg-scrim,.25))}.-ozqdq_previewGlass{background:var(--bg-preview-glass,#ffffff24);width:44%;height:46px;backdrop-filter:blur(var(--bg-glass-blur,16px)) saturate(var(--bg-glass-saturate,1.42)) brightness(var(--bg-glass-brightness,1));border:1px solid #ffffff38;border-radius:10px;justify-content:center;align-items:center;display:flex;position:absolute;bottom:16px;left:16px;box-shadow:inset 0 1px #ffffff4d}.-ozqdq_previewGlassText{color:#0f172ac7;text-shadow:0 1px 2px #fff6;white-space:nowrap;font-size:11px;line-height:1}body[data-ds-dark-theme] .-ozqdq_previewGlassText{color:#ffffffd9;text-shadow:0 1px 2px #00000059}.-ozqdq_previewEmpty{color:var(--dsw-alias-label-tertiary);justify-content:center;align-items:center;font-size:12px;line-height:1.5;display:flex;position:absolute;inset:0}.-ozqdq_section{flex-direction:column;gap:8px;display:flex}.-ozqdq_sectionLabel{color:var(--dsw-alias-label-tertiary);letter-spacing:.01em;font-size:12px;font-weight:500;line-height:18px}.-ozqdq_sourceRow{flex-wrap:wrap;align-items:center;gap:10px;display:flex}.-ozqdq_urlRow{align-items:center;gap:8px;display:flex}.-ozqdq_urlInput{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);min-width:0;height:32px;font:inherit;color:var(--dsw-alias-label-primary);border-radius:8px;flex:1;padding:0 10px;font-size:13px;line-height:1.5}.-ozqdq_urlInput::placeholder{color:var(--dsw-alias-label-dimmed)}.-ozqdq_urlInput:focus-visible{border-color:var(--dsw-alias-brand-primary);outline:none}.-ozqdq_hint{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:1.5}.-ozqdq_enabledBadge{white-space:nowrap;background:var(--dsw-alias-state-success-tertiary);color:var(--dsw-alias-state-success-primary);border-radius:999px;align-items:center;gap:4px;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px;display:inline-flex}.-ozqdq_btn{border:1px solid var(--dsw-alias-border-l2);height:28px;font:inherit;color:var(--dsw-alias-label-primary);cursor:pointer;white-space:nowrap;background:0 0;border-radius:14px;justify-content:center;align-items:center;padding:0 12px;font-size:12px;line-height:18px;transition:background-color .15s,border-color .15s,color .15s;display:inline-flex}.-ozqdq_btn:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}.-ozqdq_btn:active:not(:disabled){background:var(--dsw-alias-interactive-bg-active)}.-ozqdq_btn:disabled{opacity:.45;cursor:default}.-ozqdq_btnPrimary{background:var(--dsw-alias-button-primary-fill);color:var(--dsw-alias-label-primary-foreground);border-color:#0000}.-ozqdq_btnPrimary:hover:not(:disabled){background:var(--dsw-alias-button-primary-hover)}.-ozqdq_btnDanger{color:var(--dsw-alias-label-error)}.-ozqdq_btnDanger:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover-danger)}.-ozqdq_sliderRow{align-items:center;gap:10px;display:flex}.-ozqdq_sliderLabel{color:var(--dsw-alias-label-primary);flex:0 0 96px;font-size:13px;line-height:1.5}.-ozqdq_slider{-webkit-appearance:none;appearance:none;cursor:pointer;background:0 0;flex:1;min-width:0;height:18px;margin:0}.-ozqdq_slider::-webkit-slider-runnable-track{background:linear-gradient(90deg, var(--dsw-alias-brand-primary) var(--bg-fill,0%), var(--dsw-alias-border-l3) var(--bg-fill,0%));border-radius:2px;height:4px}.-ozqdq_slider::-moz-range-track{background:var(--dsw-alias-border-l3);border-radius:2px;height:4px}.-ozqdq_slider::-moz-range-progress{background:var(--dsw-alias-brand-primary);border-radius:2px;height:4px}.-ozqdq_slider::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;border:2px solid var(--dsw-alias-brand-primary);width:14px;height:14px;box-shadow:0 0 0 3px color-mix(in srgb, var(--dsw-alias-brand-primary) 14%, transparent), 0 1px 3px #00000047;background:#fff;border-radius:50%;margin-top:-5px}.-ozqdq_slider::-moz-range-thumb{border:2px solid var(--dsw-alias-brand-primary);width:14px;height:14px;box-shadow:0 0 0 3px color-mix(in srgb, var(--dsw-alias-brand-primary) 14%, transparent), 0 1px 3px #00000047;background:#fff;border-radius:50%}.-ozqdq_slider:hover::-webkit-slider-thumb{box-shadow:0 0 0 5px color-mix(in srgb, var(--dsw-alias-brand-primary) 18%, transparent), 0 1px 4px #0000004d}.-ozqdq_slider:hover::-moz-range-thumb{box-shadow:0 0 0 5px color-mix(in srgb, var(--dsw-alias-brand-primary) 18%, transparent), 0 1px 4px #0000004d}.-ozqdq_slider:active::-webkit-slider-thumb{box-shadow:0 0 0 7px color-mix(in srgb, var(--dsw-alias-brand-primary) 22%, transparent), 0 1px 5px #00000052;transform:scale(1.12)}.-ozqdq_slider:active::-moz-range-thumb{box-shadow:0 0 0 7px color-mix(in srgb, var(--dsw-alias-brand-primary) 22%, transparent), 0 1px 5px #00000052;transform:scale(1.12)}.-ozqdq_slider:focus-visible{outline:none}.-ozqdq_slider:focus-visible::-webkit-slider-thumb{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}.-ozqdq_slider:focus-visible::-moz-range-thumb{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}.-ozqdq_sliderValue{text-align:right;font-variant-numeric:tabular-nums;color:var(--dsw-alias-label-secondary);flex:0 0 52px;font-size:13px;line-height:1.5}.-ozqdq_segRow{align-items:center;gap:10px;display:flex}.-ozqdq_segLabel{color:var(--dsw-alias-label-primary);flex:0 0 96px;font-size:13px;line-height:1.5}.-ozqdq_segGroup{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);border-radius:9px;flex:1;gap:2px;min-width:0;padding:2px;display:inline-flex}.-ozqdq_segBtn{height:24px;font:inherit;color:var(--dsw-alias-label-secondary);cursor:pointer;white-space:nowrap;background:0 0;border:none;border-radius:7px;flex:1;padding:0 16px;font-size:12px;line-height:24px}.-ozqdq_segBtn:hover:not(.-ozqdq_segActive){color:var(--dsw-alias-label-primary)}.-ozqdq_segActive{background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-primary);font-weight:500}.-ozqdq_error{color:var(--dsw-alias-label-error);font-size:12px;line-height:1.5}.-ozqdq_clearRow{align-items:center;gap:10px;display:flex}.-ozqdq_switch{background:var(--dsw-alias-border-l3);cursor:pointer;border:none;border-radius:11px;flex:none;width:40px;height:22px;padding:0;transition:background-color .15s;position:relative}.-ozqdq_switch[aria-checked=true]{background:var(--dsw-alias-brand-primary)}.-ozqdq_switch:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}.-ozqdq_switchKnob{background:#fff;border-radius:50%;width:16px;height:16px;transition:transform .15s;position:absolute;top:3px;left:3px;box-shadow:0 1px 3px #00000047}.-ozqdq_switch[aria-checked=true] .-ozqdq_switchKnob{transform:translate(18px)}@media (prefers-reduced-motion:reduce){.-ozqdq_btn,.-ozqdq_switch,.-ozqdq_switchKnob{transition:none}}";
1249
+ const css = ".-ozqdq_row{border-bottom:1px solid var(--dsw-alias-border-l2);flex-direction:column;gap:14px;padding:16px 0;display:flex}.-ozqdq_header{flex-direction:column;gap:4px;display:flex}.-ozqdq_titleRow{justify-content:space-between;align-items:center;gap:12px;min-height:22px;display:flex}.-ozqdq_clearBtn{border-radius:12px;flex:none;height:24px;padding:0 10px;font-size:12px;line-height:24px}.-ozqdq_title{color:var(--dsw-alias-label-primary);font-size:14px;font-weight:400;line-height:22px}.-ozqdq_desc{color:var(--dsw-alias-label-tertiary);font-size:12px;font-weight:400;line-height:18px}.-ozqdq_notice{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);border-radius:8px;justify-content:space-between;align-items:center;gap:12px;padding:8px 12px;display:flex}.-ozqdq_noticeText{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px}.-ozqdq_noticeBtn{color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;flex:none;padding:0;font-size:12px;line-height:18px;text-decoration:underline}.-ozqdq_noticeBtn:hover{color:var(--dsw-alias-label-primary)}.-ozqdq_ratioFrame{justify-content:center;display:flex}.-ozqdq_preview{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);border-radius:12px;flex:none;width:100%;height:132px;position:relative;overflow:hidden}.-ozqdq_preview[data-pan]{cursor:move;touch-action:none}.-ozqdq_preview:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}.-ozqdq_previewImg{object-fit:var(--bg-object-fit,cover);object-position:calc(var(--bg-offset-x,.5) * 100%) calc(var(--bg-offset-y,.5) * 100%);width:100%;height:100%;opacity:var(--bg-opacity,1);filter:blur(var(--bg-wallpaper-blur,0px));transform:scale(var(--bg-wallpaper-scale,1));display:block;position:absolute;inset:0}.-ozqdq_previewScrim{background:rgba(255, 255, 255, var(--bg-scrim,.25));position:absolute;inset:0}body[data-ds-dark-theme] .-ozqdq_previewScrim{background:rgba(0, 0, 0, var(--bg-scrim,.25))}.-ozqdq_previewGlass{background:var(--bg-preview-glass,#ffffff24);width:44%;height:46px;backdrop-filter:blur(var(--bg-glass-blur,16px)) saturate(var(--bg-glass-saturate,1.42)) brightness(var(--bg-glass-brightness,1));border:1px solid #ffffff38;border-radius:10px;justify-content:center;align-items:center;display:flex;position:absolute;bottom:16px;left:16px;box-shadow:inset 0 1px #ffffff4d}.-ozqdq_previewGlassText{color:#0f172ac7;text-shadow:0 1px 2px #fff6;white-space:nowrap;font-size:11px;line-height:1}body[data-ds-dark-theme] .-ozqdq_previewGlassText{color:#ffffffd9;text-shadow:0 1px 2px #00000059}.-ozqdq_previewEmpty{color:var(--dsw-alias-label-tertiary);justify-content:center;align-items:center;font-size:12px;line-height:1.5;display:flex;position:absolute;inset:0}.-ozqdq_previewReset{z-index:2;height:22px;font:inherit;color:#fff;cursor:pointer;backdrop-filter:blur(4px);background:#0f172a73;border:none;border-radius:11px;padding:0 9px;font-size:11px;line-height:22px;transition:background-color .15s;position:absolute;top:8px;right:8px}.-ozqdq_previewReset:hover{background:#0f172aa6}.-ozqdq_section{flex-direction:column;gap:8px;display:flex}.-ozqdq_sectionLabel{color:var(--dsw-alias-label-tertiary);letter-spacing:.01em;font-size:12px;font-weight:500;line-height:18px}.-ozqdq_sourceRow{flex-wrap:wrap;align-items:center;gap:10px;display:flex}.-ozqdq_urlRow{align-items:center;gap:8px;display:flex}.-ozqdq_urlInput{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);min-width:0;height:32px;font:inherit;color:var(--dsw-alias-label-primary);border-radius:8px;flex:1;padding:0 10px;font-size:13px;line-height:1.5}.-ozqdq_urlInput::placeholder{color:var(--dsw-alias-label-dimmed)}.-ozqdq_urlInput:focus-visible{border-color:var(--dsw-alias-brand-primary);outline:none}.-ozqdq_hint{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:1.5}.-ozqdq_enabledBadge{white-space:nowrap;background:var(--dsw-alias-state-success-tertiary);color:var(--dsw-alias-state-success-primary);border-radius:999px;align-items:center;gap:4px;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px;display:inline-flex}.-ozqdq_btn{border:1px solid var(--dsw-alias-border-l2);height:28px;font:inherit;color:var(--dsw-alias-label-primary);cursor:pointer;white-space:nowrap;background:0 0;border-radius:14px;justify-content:center;align-items:center;padding:0 12px;font-size:12px;line-height:18px;transition:background-color .15s,border-color .15s,color .15s;display:inline-flex}.-ozqdq_btn:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}.-ozqdq_btn:active:not(:disabled){background:var(--dsw-alias-interactive-bg-active)}.-ozqdq_btn:disabled{opacity:.45;cursor:default}.-ozqdq_btnPrimary{background:var(--dsw-alias-button-primary-fill);color:var(--dsw-alias-label-primary-foreground);border-color:#0000}.-ozqdq_btnPrimary:hover:not(:disabled){background:var(--dsw-alias-button-primary-hover)}.-ozqdq_btnDanger{color:var(--dsw-alias-label-error)}.-ozqdq_btnDanger:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover-danger)}.-ozqdq_sliderRow{align-items:center;gap:10px;display:flex}.-ozqdq_sliderLabel{color:var(--dsw-alias-label-primary);flex:0 0 96px;font-size:13px;line-height:1.5}.-ozqdq_slider{-webkit-appearance:none;appearance:none;cursor:pointer;background:0 0;flex:1;min-width:0;height:18px;margin:0}.-ozqdq_slider::-webkit-slider-runnable-track{background:linear-gradient(90deg, var(--dsw-alias-brand-primary) var(--bg-fill,0%), var(--dsw-alias-border-l3) var(--bg-fill,0%));border-radius:2px;height:4px}.-ozqdq_slider::-moz-range-track{background:var(--dsw-alias-border-l3);border-radius:2px;height:4px}.-ozqdq_slider::-moz-range-progress{background:var(--dsw-alias-brand-primary);border-radius:2px;height:4px}.-ozqdq_slider::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;border:2px solid var(--dsw-alias-brand-primary);width:14px;height:14px;box-shadow:0 0 0 3px color-mix(in srgb, var(--dsw-alias-brand-primary) 14%, transparent), 0 1px 3px #00000047;background:#fff;border-radius:50%;margin-top:-5px}.-ozqdq_slider::-moz-range-thumb{border:2px solid var(--dsw-alias-brand-primary);width:14px;height:14px;box-shadow:0 0 0 3px color-mix(in srgb, var(--dsw-alias-brand-primary) 14%, transparent), 0 1px 3px #00000047;background:#fff;border-radius:50%}.-ozqdq_slider:hover::-webkit-slider-thumb{box-shadow:0 0 0 5px color-mix(in srgb, var(--dsw-alias-brand-primary) 18%, transparent), 0 1px 4px #0000004d}.-ozqdq_slider:hover::-moz-range-thumb{box-shadow:0 0 0 5px color-mix(in srgb, var(--dsw-alias-brand-primary) 18%, transparent), 0 1px 4px #0000004d}.-ozqdq_slider:active::-webkit-slider-thumb{box-shadow:0 0 0 7px color-mix(in srgb, var(--dsw-alias-brand-primary) 22%, transparent), 0 1px 5px #00000052;transform:scale(1.12)}.-ozqdq_slider:active::-moz-range-thumb{box-shadow:0 0 0 7px color-mix(in srgb, var(--dsw-alias-brand-primary) 22%, transparent), 0 1px 5px #00000052;transform:scale(1.12)}.-ozqdq_slider:focus-visible{outline:none}.-ozqdq_slider:focus-visible::-webkit-slider-thumb{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}.-ozqdq_slider:focus-visible::-moz-range-thumb{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}.-ozqdq_sliderValue{text-align:right;font-variant-numeric:tabular-nums;color:var(--dsw-alias-label-secondary);flex:0 0 52px;font-size:13px;line-height:1.5}.-ozqdq_segRow{align-items:center;gap:10px;display:flex}.-ozqdq_segLabel{color:var(--dsw-alias-label-primary);flex:0 0 96px;font-size:13px;line-height:1.5}.-ozqdq_segGroup{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);border-radius:9px;flex:1;gap:2px;min-width:0;padding:2px;display:inline-flex}.-ozqdq_segBtn{height:24px;font:inherit;color:var(--dsw-alias-label-secondary);cursor:pointer;white-space:nowrap;background:0 0;border:none;border-radius:7px;flex:1;padding:0 16px;font-size:12px;line-height:24px}.-ozqdq_segBtn:hover:not(.-ozqdq_segActive){color:var(--dsw-alias-label-primary)}.-ozqdq_segActive{background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-primary);font-weight:500}.-ozqdq_error{color:var(--dsw-alias-label-error);font-size:12px;line-height:1.5}.-ozqdq_clearRow{align-items:center;gap:10px;display:flex}.-ozqdq_switch{background:var(--dsw-alias-border-l3);cursor:pointer;border:none;border-radius:11px;flex:none;width:40px;height:22px;padding:0;transition:background-color .15s;position:relative}.-ozqdq_switch[aria-checked=true]{background:var(--dsw-alias-brand-primary)}.-ozqdq_switch:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}.-ozqdq_switchKnob{background:#fff;border-radius:50%;width:16px;height:16px;transition:transform .15s;position:absolute;top:3px;left:3px;box-shadow:0 1px 3px #00000047}.-ozqdq_switch[aria-checked=true] .-ozqdq_switchKnob{transform:translate(18px)}@media (prefers-reduced-motion:reduce){.-ozqdq_btn,.-ozqdq_switch,.-ozqdq_switchKnob{transition:none}}";
1153
1250
  const tagId = "deepseek-harness-background/SettingsRow.module.css";
1154
1251
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
1155
1252
  const tag = document.createElement("style");
@@ -1159,44 +1256,46 @@ window.__ModuleLoader__.load({
1159
1256
  document.head.appendChild(tag);
1160
1257
  }
1161
1258
  var SettingsRow_module_css_default = {
1162
- "sectionLabel": "-ozqdq_sectionLabel",
1163
- "row": "-ozqdq_row",
1259
+ "segBtn": "-ozqdq_segBtn",
1260
+ "noticeBtn": "-ozqdq_noticeBtn",
1261
+ "preview": "-ozqdq_preview",
1262
+ "previewEmpty": "-ozqdq_previewEmpty",
1263
+ "clearRow": "-ozqdq_clearRow",
1164
1264
  "previewGlass": "-ozqdq_previewGlass",
1165
- "switchKnob": "-ozqdq_switchKnob",
1166
- "clearBtn": "-ozqdq_clearBtn",
1167
- "segRow": "-ozqdq_segRow",
1265
+ "segGroup": "-ozqdq_segGroup",
1266
+ "noticeText": "-ozqdq_noticeText",
1267
+ "segActive": "-ozqdq_segActive",
1268
+ "switch": "-ozqdq_switch",
1269
+ "btn": "-ozqdq_btn",
1168
1270
  "desc": "-ozqdq_desc",
1169
- "sourceRow": "-ozqdq_sourceRow",
1170
- "btnPrimary": "-ozqdq_btnPrimary",
1271
+ "row": "-ozqdq_row",
1272
+ "ratioFrame": "-ozqdq_ratioFrame",
1273
+ "urlRow": "-ozqdq_urlRow",
1274
+ "urlInput": "-ozqdq_urlInput",
1171
1275
  "titleRow": "-ozqdq_titleRow",
1172
- "noticeBtn": "-ozqdq_noticeBtn",
1276
+ "btnPrimary": "-ozqdq_btnPrimary",
1277
+ "sectionLabel": "-ozqdq_sectionLabel",
1173
1278
  "section": "-ozqdq_section",
1174
- "sliderRow": "-ozqdq_sliderRow",
1175
- "enabledBadge": "-ozqdq_enabledBadge",
1176
- "noticeText": "-ozqdq_noticeText",
1177
- "urlRow": "-ozqdq_urlRow",
1178
- "previewScrim": "-ozqdq_previewScrim",
1179
- "sliderLabel": "-ozqdq_sliderLabel",
1279
+ "previewGlassText": "-ozqdq_previewGlassText",
1280
+ "segRow": "-ozqdq_segRow",
1281
+ "notice": "-ozqdq_notice",
1282
+ "sourceRow": "-ozqdq_sourceRow",
1283
+ "slider": "-ozqdq_slider",
1180
1284
  "sliderValue": "-ozqdq_sliderValue",
1181
- "header": "-ozqdq_header",
1182
1285
  "error": "-ozqdq_error",
1183
- "urlInput": "-ozqdq_urlInput",
1184
1286
  "hint": "-ozqdq_hint",
1287
+ "enabledBadge": "-ozqdq_enabledBadge",
1185
1288
  "btnDanger": "-ozqdq_btnDanger",
1186
- "title": "-ozqdq_title",
1289
+ "sliderRow": "-ozqdq_sliderRow",
1187
1290
  "previewImg": "-ozqdq_previewImg",
1188
- "segLabel": "-ozqdq_segLabel",
1189
- "switch": "-ozqdq_switch",
1190
- "preview": "-ozqdq_preview",
1191
- "segGroup": "-ozqdq_segGroup",
1192
- "previewGlassText": "-ozqdq_previewGlassText",
1193
- "segActive": "-ozqdq_segActive",
1194
- "btn": "-ozqdq_btn",
1195
- "clearRow": "-ozqdq_clearRow",
1196
- "notice": "-ozqdq_notice",
1197
- "segBtn": "-ozqdq_segBtn",
1198
- "previewEmpty": "-ozqdq_previewEmpty",
1199
- "slider": "-ozqdq_slider"
1291
+ "switchKnob": "-ozqdq_switchKnob",
1292
+ "previewReset": "-ozqdq_previewReset",
1293
+ "title": "-ozqdq_title",
1294
+ "clearBtn": "-ozqdq_clearBtn",
1295
+ "header": "-ozqdq_header",
1296
+ "previewScrim": "-ozqdq_previewScrim",
1297
+ "sliderLabel": "-ozqdq_sliderLabel",
1298
+ "segLabel": "-ozqdq_segLabel"
1200
1299
  };
1201
1300
  //#endregion
1202
1301
  //#region src/client/SettingsRow.tsx
@@ -1229,6 +1328,8 @@ window.__ModuleLoader__.load({
1229
1328
  blur: 16,
1230
1329
  wallpaperBlur: 0,
1231
1330
  fit: DEFAULT_FIT,
1331
+ offsetX: DEFAULT_OFFSET,
1332
+ offsetY: DEFAULT_OFFSET,
1232
1333
  timeline: true
1233
1334
  };
1234
1335
  /** Per-field slider geometry: 5% steps for ratios, fine px steps for radii so
@@ -1271,13 +1372,26 @@ window.__ModuleLoader__.load({
1271
1372
  const prefix = `${BACKGROUND_API_PREFIX}/image/`;
1272
1373
  return url.startsWith(prefix) ? url.slice(prefix.length) : "";
1273
1374
  }
1375
+ /** Cap on the preview card's height. Past it the card shrinks as a whole —
1376
+ * width follows the ratio — and centers, so a very tall (or very wide) window
1377
+ * keeps the exact viewport ratio instead of squashing the crop. */
1378
+ const PREVIEW_MAX_HEIGHT = 360;
1379
+ /** Clamp a framing offset into its schema domain, trimming the float dust the
1380
+ * pointer-delta division leaves behind (0.30000000000000004 → 0.3). */
1381
+ function clampOffset(value) {
1382
+ return Math.round((value < 0 ? 0 : value > 1 ? 1 : value) * 1e4) / 1e4;
1383
+ }
1384
+ /** Round ratio dust out of an inline px box (400 / (4/3) is 300.00000000000006). */
1385
+ function trimPx(value) {
1386
+ return Math.round(value * 100) / 100;
1387
+ }
1274
1388
  /** Read the settings store (uSES-friendly). */
1275
1389
  function readSnapshot() {
1276
1390
  return settingsClient.getSnapshot();
1277
1391
  }
1278
1392
  /** Field-wise equality for distinguishing our own saves from external changes. */
1279
1393
  function sameSettings(a, b) {
1280
- return a.enabled === b.enabled && a.uploadId === b.uploadId && a.url === b.url && a.opacity === b.opacity && a.scrim === b.scrim && a.panelOpacity === b.panelOpacity && a.blur === b.blur && a.wallpaperBlur === b.wallpaperBlur && a.fit === b.fit && a.timeline === b.timeline;
1394
+ return a.enabled === b.enabled && a.uploadId === b.uploadId && a.url === b.url && a.opacity === b.opacity && a.scrim === b.scrim && a.panelOpacity === b.panelOpacity && a.blur === b.blur && a.wallpaperBlur === b.wallpaperBlur && a.fit === b.fit && a.offsetX === b.offsetX && a.offsetY === b.offsetY && a.timeline === b.timeline;
1281
1395
  }
1282
1396
  /**
1283
1397
  * A stepped range control. While dragging it re-renders only itself: the value
@@ -1371,6 +1485,14 @@ window.__ModuleLoader__.load({
1371
1485
  const [noticeDismissed, setNoticeDismissed] = (0, react.useState)(false);
1372
1486
  const fileInput = (0, react.useRef)(null);
1373
1487
  const previewRef = (0, react.useRef)(null);
1488
+ const frameRef = (0, react.useRef)(null);
1489
+ const [viewport, setViewport] = (0, react.useState)(() => ({
1490
+ w: window.innerWidth,
1491
+ h: window.innerHeight
1492
+ }));
1493
+ const [frameWidth, setFrameWidth] = (0, react.useState)(0);
1494
+ const [imgMeta, setImgMeta] = (0, react.useState)(null);
1495
+ const panRef = (0, react.useRef)(null);
1374
1496
  const draftRef = (0, react.useRef)(DEFAULTS);
1375
1497
  const editingRef = (0, react.useRef)(false);
1376
1498
  const lastSavedRef = (0, react.useRef)(null);
@@ -1428,6 +1550,36 @@ window.__ModuleLoader__.load({
1428
1550
  saveNowRef.current(draftRef.current);
1429
1551
  }, 120);
1430
1552
  }, []);
1553
+ (0, react.useEffect)(() => {
1554
+ let raf = 0;
1555
+ const sync = () => {
1556
+ raf = 0;
1557
+ setViewport({
1558
+ w: window.innerWidth,
1559
+ h: window.innerHeight
1560
+ });
1561
+ };
1562
+ const onResize = () => {
1563
+ if (raf !== 0) return;
1564
+ raf = window.requestAnimationFrame(sync);
1565
+ };
1566
+ window.addEventListener("resize", onResize);
1567
+ return () => {
1568
+ window.removeEventListener("resize", onResize);
1569
+ if (raf !== 0) window.cancelAnimationFrame(raf);
1570
+ };
1571
+ }, []);
1572
+ (0, react.useEffect)(() => {
1573
+ const el = frameRef.current;
1574
+ if (el === null) return;
1575
+ if (typeof ResizeObserver === "undefined") {
1576
+ setFrameWidth(el.clientWidth);
1577
+ return;
1578
+ }
1579
+ const observer = new ResizeObserver(() => setFrameWidth(el.clientWidth));
1580
+ observer.observe(el);
1581
+ return () => observer.disconnect();
1582
+ }, []);
1431
1583
  (0, react.useEffect)(() => {
1432
1584
  if (snapshot.status !== "ready" || snapshot.value === void 0) return;
1433
1585
  const saved = lastSavedRef.current;
@@ -1528,8 +1680,109 @@ window.__ModuleLoader__.load({
1528
1680
  setDraft(next);
1529
1681
  saveNow(next);
1530
1682
  };
1683
+ /** Natural aspect ratio of the loaded source (null until loaded / on error). */
1684
+ const imageAspect = imgMeta !== null && imgMeta.w > 0 && imgMeta.h > 0 ? imgMeta.w / imgMeta.h : null;
1685
+ /** Preview card box at the live viewport ratio, capped at PREVIEW_MAX_HEIGHT
1686
+ * (past the cap the card shrinks as a whole and the frame centers it); a
1687
+ * width of 0 means "not measured yet" (CSS fallbacks apply). */
1688
+ const cardRatio = viewport.w > 0 && viewport.h > 0 ? viewport.w / viewport.h : 16 / 9;
1689
+ const cardWidth = frameWidth > 0 ? trimPx(Math.min(frameWidth, PREVIEW_MAX_HEIGHT * cardRatio)) : 0;
1690
+ const cardHeight = cardWidth > 0 ? trimPx(cardWidth / cardRatio) : 0;
1691
+ /** The axis the cover crop has slack on — and therefore the only one the
1692
+ * user may pan. A landscape source in a portrait frame overflows sideways
1693
+ * (pan x); a portrait source in a landscape frame overflows vertically
1694
+ * (pan y). null for `contain`, an unloaded source, an unmeasured card or an
1695
+ * exact ratio match (nothing to pan, and no division by zero). */
1696
+ const panAxis = draft.fit === "cover" && imageAspect !== null && cardWidth > 0 && Math.abs(imageAspect - cardRatio) >= 1e-4 ? imageAspect > cardRatio ? "x" : "y" : null;
1697
+ /** Whether a pan has moved the crop off center (drives the recenter chip). */
1698
+ const offsetMoved = draft.offsetX !== .5 || draft.offsetY !== .5;
1699
+ /** Overflow (px) of the rendered image past the card on one axis, measured
1700
+ * from the live DOM (the same frame the wallpaper layer uses), or null when
1701
+ * that axis has nothing to pan. */
1702
+ const overflowFor = (axis) => {
1703
+ const el = previewRef.current;
1704
+ if (el === null || imgMeta === null) return null;
1705
+ const fw = el.clientWidth;
1706
+ const fh = el.clientHeight;
1707
+ if (fw <= 0 || fh <= 0 || imgMeta.w <= 0 || imgMeta.h <= 0) return null;
1708
+ const scale = Math.max(fw / imgMeta.w, fh / imgMeta.h);
1709
+ const overflow = axis === "x" ? imgMeta.w * scale - fw : imgMeta.h * scale - fh;
1710
+ return overflow > 0 ? overflow : null;
1711
+ };
1712
+ /** Write one framing offset live: CSS variables only, no React state — the
1713
+ * pan hot path mirrors ControlSlider's onLive. */
1714
+ const setOffsetLive = (key, value) => {
1715
+ const next = {
1716
+ ...draftRef.current,
1717
+ [key]: value
1718
+ };
1719
+ draftRef.current = next;
1720
+ paintBackgroundKnob(key, value);
1721
+ if (previewRef.current) paintPreviewSurface(previewRef.current, next);
1722
+ };
1723
+ const handlePanDown = (e) => {
1724
+ if (panAxis === null) return;
1725
+ const overflow = overflowFor(panAxis);
1726
+ if (overflow === null) return;
1727
+ e.currentTarget.setPointerCapture(e.pointerId);
1728
+ e.preventDefault();
1729
+ markEditing();
1730
+ panRef.current = {
1731
+ axis: panAxis,
1732
+ startPos: panAxis === "x" ? e.clientX : e.clientY,
1733
+ startOffset: panAxis === "x" ? draftRef.current.offsetX : draftRef.current.offsetY,
1734
+ overflow
1735
+ };
1736
+ };
1737
+ const handlePanMove = (e) => {
1738
+ const pan = panRef.current;
1739
+ if (pan === null) return;
1740
+ const delta = (pan.axis === "x" ? e.clientX : e.clientY) - pan.startPos;
1741
+ setOffsetLive(pan.axis === "x" ? "offsetX" : "offsetY", clampOffset(pan.startOffset - delta / pan.overflow));
1742
+ };
1743
+ const handlePanUp = (e) => {
1744
+ if (panRef.current === null) return;
1745
+ panRef.current = null;
1746
+ if (e.currentTarget.hasPointerCapture(e.pointerId)) e.currentTarget.releasePointerCapture(e.pointerId);
1747
+ setDraft({ ...draftRef.current });
1748
+ onCommit();
1749
+ };
1750
+ /** Arrow-key pan on the movable axis (Shift = coarse step). */
1751
+ const nudgeOffset = (axis, direction, coarse) => {
1752
+ const key = axis === "x" ? "offsetX" : "offsetY";
1753
+ setOffsetLive(key, clampOffset(draftRef.current[key] + direction * (coarse ? .1 : .02)));
1754
+ setDraft({ ...draftRef.current });
1755
+ onCommit();
1756
+ };
1757
+ const handlePanKeyDown = (e) => {
1758
+ if (panAxis === null) return;
1759
+ if (panAxis === "x" && (e.key === "ArrowLeft" || e.key === "ArrowRight")) {
1760
+ e.preventDefault();
1761
+ nudgeOffset("x", e.key === "ArrowLeft" ? -1 : 1, e.shiftKey);
1762
+ return;
1763
+ }
1764
+ if (panAxis === "y" && (e.key === "ArrowUp" || e.key === "ArrowDown")) {
1765
+ e.preventDefault();
1766
+ nudgeOffset("y", e.key === "ArrowUp" ? -1 : 1, e.shiftKey);
1767
+ }
1768
+ };
1769
+ /** Recenter the crop on both axes (recenter chip / double-click). */
1770
+ const handleResetOffset = () => {
1771
+ const next = {
1772
+ ...draftRef.current,
1773
+ offsetX: DEFAULT_OFFSET,
1774
+ offsetY: DEFAULT_OFFSET
1775
+ };
1776
+ draftRef.current = next;
1777
+ setDraft(next);
1778
+ paintLive(next);
1779
+ saveNow(next);
1780
+ };
1531
1781
  const sourceUrl = draft.uploadId ? `${BACKGROUND_API_PREFIX}/image/${draft.uploadId}` : draft.url;
1532
1782
  const hasImage = sourceUrl !== "";
1783
+ (0, react.useEffect)(() => {
1784
+ setImgMeta(null);
1785
+ }, [sourceUrl]);
1533
1786
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1534
1787
  className: SettingsRow_module_css_default.row,
1535
1788
  children: [
@@ -1565,27 +1818,55 @@ window.__ModuleLoader__.load({
1565
1818
  })]
1566
1819
  }),
1567
1820
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1568
- className: SettingsRow_module_css_default.preview,
1569
- ref: previewRef,
1570
- children: hasImage ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
1571
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
1572
- className: SettingsRow_module_css_default.previewImg,
1573
- src: sourceUrl,
1574
- alt: "",
1575
- draggable: false,
1576
- referrerPolicy: "no-referrer"
1577
- }),
1578
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: SettingsRow_module_css_default.previewScrim }),
1579
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1580
- className: SettingsRow_module_css_default.previewGlass,
1581
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1582
- className: SettingsRow_module_css_default.previewGlassText,
1583
- children: t("background.previewGlass")
1821
+ className: SettingsRow_module_css_default.ratioFrame,
1822
+ ref: frameRef,
1823
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1824
+ className: SettingsRow_module_css_default.preview,
1825
+ ref: previewRef,
1826
+ "data-pan": panAxis ?? void 0,
1827
+ style: cardHeight > 0 ? {
1828
+ width: `${cardWidth}px`,
1829
+ height: `${cardHeight}px`
1830
+ } : void 0,
1831
+ tabIndex: panAxis === null ? void 0 : 0,
1832
+ "aria-label": panAxis === null ? void 0 : t("background.previewPanHint"),
1833
+ onPointerDown: handlePanDown,
1834
+ onPointerMove: handlePanMove,
1835
+ onPointerUp: handlePanUp,
1836
+ onPointerCancel: handlePanUp,
1837
+ onKeyDown: handlePanKeyDown,
1838
+ onDoubleClick: panAxis === null ? void 0 : handleResetOffset,
1839
+ children: [hasImage ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
1840
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
1841
+ className: SettingsRow_module_css_default.previewImg,
1842
+ src: sourceUrl,
1843
+ alt: "",
1844
+ draggable: false,
1845
+ referrerPolicy: "no-referrer",
1846
+ onLoad: (e) => setImgMeta({
1847
+ w: e.currentTarget.naturalWidth,
1848
+ h: e.currentTarget.naturalHeight
1849
+ }),
1850
+ onError: () => setImgMeta(null)
1851
+ }),
1852
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: SettingsRow_module_css_default.previewScrim }),
1853
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1854
+ className: SettingsRow_module_css_default.previewGlass,
1855
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1856
+ className: SettingsRow_module_css_default.previewGlassText,
1857
+ children: t("background.previewGlass")
1858
+ })
1584
1859
  })
1585
- })
1586
- ] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1587
- className: SettingsRow_module_css_default.previewEmpty,
1588
- children: t("background.noPreview")
1860
+ ] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1861
+ className: SettingsRow_module_css_default.previewEmpty,
1862
+ children: t("background.noPreview")
1863
+ }), panAxis !== null && offsetMoved && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1864
+ type: "button",
1865
+ className: SettingsRow_module_css_default.previewReset,
1866
+ onPointerDown: (e) => e.stopPropagation(),
1867
+ onClick: handleResetOffset,
1868
+ children: t("background.repositionReset")
1869
+ })]
1589
1870
  })
1590
1871
  }),
1591
1872
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {