deepseek-harness-background 0.5.5 → 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
@@ -62,8 +62,9 @@ window.__ModuleLoader__.load({
62
62
  * button, scroll-to-bottom), the load-earlier history button, the composer
63
63
  * dock family (agent task strips: TodoPanel / GoalBar / QueueDock) and its
64
64
  * takeover panels (approval, question, plan review), the subagent lineage
65
- * popover, the home hero "preview" badge, the sidebar build badge, and the
66
- * timeline rail (the official-rail rules below).
65
+ * popover, the home hero "preview" badge, the sidebar build badge, the turn
66
+ * deliverables (the changed-files card and the diff hover preview it opens),
67
+ * and the timeline rail (the official-rail rules below).
67
68
  * Every glassed surface shares ONE recipe:
68
69
  * fill from the painter's --dsw-specific-input-major token (same alpha
69
70
  * curve + theme dimming), the full shared blur/saturate/brightness chain
@@ -71,10 +72,14 @@ window.__ModuleLoader__.load({
71
72
  * surface reads heavier than the composer card. Dialogs, the settings UI,
72
73
  * menus, tooltips, toasts and every hover/accent fill keep the OFFICIAL
73
74
  * opaque paints — they are reading surfaces and must stay legible.
75
+ * The same rule excludes the HoverCard primitive's compact pod (a fixed
76
+ * #2C2C2E chip): it is hover FEEDBACK, not a floating card, and the official
77
+ * opaque paint is what keeps its label legible over any wallpaper.
74
78
  *
75
79
  * Selectors target authored attributes (`data-dsh-bg`, `data-composer-card`,
76
- * `data-terminal`…), the `:global` `.md-code-block` anchor, and CSS-module
77
- * SUFFIX conventions (`_bubble`, `_newSession`…) audited for collisions.
80
+ * `data-terminal`, `data-changes-hover-preview`…), the `:global`
81
+ * `.md-code-block` anchor, and CSS-module SUFFIX conventions (`_bubble`,
82
+ * `_newSession`…) audited for collisions.
78
83
  *
79
84
  * MAINTENANCE NOTE: the suffix anchors are the host's only stable-enough
80
85
  * integration surface today, but a host restyle that renames them silently
@@ -83,11 +88,14 @@ window.__ModuleLoader__.load({
83
88
  * suffix entries.
84
89
  * `_bubble` excludes `role="tooltip"` (Tooltip.module.css shares the suffix),
85
90
  * `_add` is scoped under `[data-composer-card]` (DiffBlock line markers share
86
- * it), `_newSession` excludes `Label` (the button's inner label span) and
87
- * `_toBottom` excludes `Slot` (the zero-height sticky wrapper) — both share
88
- * the substring with the chrome control but must keep their own paint, and
89
- * the subagent popover is `role="tree"` + `_menu` so the generic menu
90
- * surface stays untouched.
91
+ * it), `_newSession` is scoped to the `button` element — an element test, not
92
+ * another suffix exclusion, because 0.1.7-rc.2 added `newSessionLabelMask` /
93
+ * `newSessionContent` / `newSessionShortcut` INSIDE the button and each of
94
+ * those spans would otherwise take the chrome paint (the composer-sized
95
+ * second box the user sees inside the control) — and `_toBottom` excludes
96
+ * `Slot` (the zero-height sticky wrapper) — both share the substring with the
97
+ * chrome control but must keep their own paint, and the subagent popover is
98
+ * `role="tree"` + `_menu` so the generic menu surface stays untouched.
91
99
  */
92
100
  const BACKGROUND_CSS = `
93
101
  .dsh-bg-layer {
@@ -96,6 +104,11 @@ window.__ModuleLoader__.load({
96
104
  .dsh-bg-layer .dsh-bg-image {
97
105
  width: 100%; height: 100%; display: block; border: 0; background: transparent;
98
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%);
99
112
  opacity: var(--bg-opacity, 1);
100
113
  filter: blur(var(--bg-wallpaper-blur, 0px));
101
114
  /* Blur thins the edges; scale the image up to hide the fringe. */
@@ -112,6 +125,21 @@ window.__ModuleLoader__.load({
112
125
  body[data-ds-dark-theme] .dsh-bg-scrim {
113
126
  background: rgba(0, 0, 0, var(--bg-scrim, 0.25));
114
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
+ }
115
143
 
116
144
  /* Active: make the app frame + sidebar background transparent so all columns
117
145
  share the same wallpaper + scrim backdrop. */
@@ -128,7 +156,6 @@ window.__ModuleLoader__.load({
128
156
  light glass slightly DIMS, dark glass keeps a slight lift. Gated on the
129
157
  GLASS ATTR (wallpaper active AND panelOpacity < 1): maxing the panel
130
158
  slider returns these surfaces to the exact official paints. */
131
- body[data-dsh-bg-glass] [data-composer-card],
132
159
  body[data-dsh-bg-glass] [class*="_bubble"]:not([role="tooltip"]),
133
160
  /* The read_image toolview (dsh >= 0.1.2-rc.1 ToolRow) collapses its result
134
161
  into an image body card; glass it like the other content blocks. */
@@ -141,7 +168,7 @@ window.__ModuleLoader__.load({
141
168
  body[data-dsh-bg-glass] [data-web],
142
169
  body[data-dsh-bg-glass] [class*="_ioCard"],
143
170
  body[data-dsh-bg-glass] [class*="_instructionsCard"],
144
- body[data-dsh-bg-glass] [class*="_markdown"] :not(pre) > code {
171
+ body[data-dsh-bg-glass] [class*="_markdown"]:not(pre) > code {
145
172
  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));
146
173
  -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);
147
174
  backdrop-filter: blur(var(--bg-glass-blur, 16px)) saturate(var(--bg-glass-saturate, 1.42)) brightness(var(--bg-glass-brightness, 1)) contrast(1.01);
@@ -152,6 +179,37 @@ window.__ModuleLoader__.load({
152
179
  0 12px 40px rgba(0, 0, 0, 0.12);
153
180
  }
154
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
+
155
213
  /* The sticky code-block header officially occludes scrolled code with
156
214
  bg-base (transparent under a wallpaper): refill it with the banner token
157
215
  (itself translucent glass) so code stays hidden beneath the sticky bar. */
@@ -178,7 +236,7 @@ window.__ModuleLoader__.load({
178
236
  caps), sheen = the shared sheen vars. Hover = the same white glass at the
179
237
  boosted alpha var. Gated on data-dsh-bg-glass (off when the panel is
180
238
  fully opaque). */
181
- body[data-dsh-bg-glass] [class*="_newSession"]:not([class*="Label"]),
239
+ body[data-dsh-bg-glass] button[class*="_newSession"],
182
240
  body[data-dsh-bg-glass] [data-composer-card] [class*="_add"],
183
241
  body[data-dsh-bg-glass] [class*="_toBottom"]:not([class*="Slot"]) {
184
242
  background-color: var(--dsw-specific-input-major);
@@ -190,15 +248,15 @@ window.__ModuleLoader__.load({
190
248
  inset 0 0 0 0.5px rgba(255, 255, 255, 0.12),
191
249
  0 4px 14px rgba(0, 0, 0, 0.08);
192
250
  }
193
- body[data-dsh-bg-glass] [class*="_newSession"]:not([class*="Label"]):hover,
251
+ body[data-dsh-bg-glass] button[class*="_newSession"]:hover,
194
252
  body[data-dsh-bg-glass] [data-composer-card] [class*="_add"]:hover:not(:disabled),
195
253
  body[data-dsh-bg-glass] [class*="_toBottom"]:not([class*="Slot"]):hover {
196
254
  background-color: color-mix(in srgb, rgb(255 255 255) calc(var(--bg-glass-alpha-strong, 0.8) * 100%), transparent);
197
255
  }
198
256
  /* Collapsed rail renders the new-session control as a bare icon — keep the
199
257
  official transparent paint (and its official translucent hover). */
200
- body[data-dsh-bg-glass] [class*="_collapsed"] [class*="_newSession"],
201
- body[data-dsh-bg-glass] [class*="_collapsed"] [class*="_newSession"]:hover {
258
+ body[data-dsh-bg-glass] [data-sidebar-collapsed="true"] [class*="_newSession"],
259
+ body[data-dsh-bg-glass] [data-sidebar-collapsed="true"] [class*="_newSession"]:hover {
202
260
  background-color: transparent;
203
261
  background-image: none;
204
262
  box-shadow: none;
@@ -278,19 +336,74 @@ window.__ModuleLoader__.load({
278
336
  backdrop-filter: blur(var(--bg-glass-blur, 16px)) saturate(var(--bg-glass-saturate, 1.42)) brightness(var(--bg-glass-brightness, 1)) contrast(1.01);
279
337
  }
280
338
 
339
+ /* ---- Turn deliverables: the changed-files card + its hover preview -----
340
+ Both paint with the official opaque --dsw-alias-bg-layer-1 (the token the
341
+ painter deliberately keeps opaque, because it backs the settings UI,
342
+ dialogs and menus), so each needs an EXPLICIT fill on top of the shared
343
+ sheen/blur chain — the token-mediated rule above cannot reach them.
344
+ [data-changed-files] is the card a turn tail renders; its header and
345
+ rows carry their own fills, so the card surface alone is glassed and the
346
+ contents keep their official paints. [data-changes-hover-preview] is
347
+ the fixed HoverCard the changed-files rows open on hover and the shared
348
+ component behind every diff preview; its width/height are set by the
349
+ hover-card primitive, so this only supplies the material. The preview's
350
+ official elevation shadow rides its own token untouched — only the fill
351
+ and the filter chain are taken over here. */
352
+ body[data-dsh-bg-glass] [data-changed-files],
353
+ body[data-dsh-bg-glass] [data-changes-hover-preview] {
354
+ background-color: var(--dsw-specific-input-major);
355
+ 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));
356
+ -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);
357
+ backdrop-filter: blur(var(--bg-glass-blur, 16px)) saturate(var(--bg-glass-saturate, 1.42)) brightness(var(--bg-glass-brightness, 1)) contrast(1.01);
358
+ box-shadow: var(--dsw-elevation-panel, 0 4px 14px rgba(0, 0, 0, 0.08));
359
+ }
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
+
281
390
  /* ---- Official turn rail (dsh >= 0.1.2, TurnNavigator) -----------------
282
391
  The enhance mode borrows the kernel's rail untouched, so its hover
283
392
  preview card keeps the OFFICIAL opaque fill (bg-layer-1, not mediated
284
393
  by any overridden token) — the "悬浮反馈形态不透明" complaint. Glass it
285
394
  explicitly with the same explicit-fill recipe as the chrome buttons
286
- (fill = the painter's composer token + shared blur/sheen). Anchors are
287
- structural, never class names: the kernel's module-CSS class hashes are
288
- not a stable contract, but nav[style*="--turn-natural-height"] (its
289
- inline rail metrics) and the preview's role="tooltip" are. No edge
395
+ (fill = the painter's composer token + shared blur/sheen). The anchor is
396
+ the rail's SEAT, the same one timeline/official-enhance.tsx identifies it
397
+ by: the only nav inside the conversation scrollport. 0.1.7 virtualized
398
+ the rail (commit f89cd786aa), which deleted the turn-natural-height
399
+ inline metric that used to sit on that nav — the nav carries no style
400
+ attribute at all now — so the selector that keyed on it matched nothing
401
+ and the preview went bare. The scrollport attribute and the preview's
402
+ role="tooltip" are the structural hooks; class hashes never are. No edge
290
403
  dissolve on the marks column: the first and last tick dashes straddle
291
404
  the marks box edges by 1px, so a fade zone sized off the box height
292
405
  swallows them instead of the band. */
293
- body[data-dsh-bg-glass] nav[style*="--turn-natural-height"] [role="tooltip"] {
406
+ body[data-dsh-bg-glass] [data-conversation-scroll] nav [role="tooltip"] {
294
407
  background-color: var(--dsw-specific-input-major);
295
408
  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));
296
409
  -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);
@@ -321,6 +434,8 @@ window.__ModuleLoader__.load({
321
434
  const DEFAULT_FIT = "cover";
322
435
  /** Image positioning modes. `cover` fills the frame (cropping as needed); `contain` fits the whole image inside the frame (letterboxing). */
323
436
  const FIT_MODES = ["cover", "contain"];
437
+ /** Default framing offset — the image sits centered on both axes. */
438
+ const DEFAULT_OFFSET = .5;
324
439
  //#endregion
325
440
  //#region src/client/backdrop.ts
326
441
  /**
@@ -525,6 +640,8 @@ window.__ModuleLoader__.load({
525
640
  this.hasSource = true;
526
641
  document.body.style;
527
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));
528
645
  this.setVar("--bg-opacity", String(settings.opacity));
529
646
  this.setVar("--bg-wallpaper-blur", `${settings.wallpaperBlur}px`);
530
647
  this.setVar("--bg-wallpaper-scale", (1 + settings.wallpaperBlur * .006).toFixed(4));
@@ -592,7 +709,13 @@ window.__ModuleLoader__.load({
592
709
  this.setVar("--bg-wallpaper-blur", `${value}px`);
593
710
  this.setVar("--bg-wallpaper-scale", (1 + Number(value) * .006).toFixed(4));
594
711
  break;
595
- 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));
596
719
  }
597
720
  }
598
721
  /**
@@ -689,6 +812,8 @@ window.__ModuleLoader__.load({
689
812
  "--bg-opacity",
690
813
  "--bg-scrim",
691
814
  "--bg-object-fit",
815
+ "--bg-offset-x",
816
+ "--bg-offset-y",
692
817
  "--bg-wallpaper-blur",
693
818
  "--bg-wallpaper-scale",
694
819
  "--bg-glass-blur",
@@ -706,6 +831,8 @@ window.__ModuleLoader__.load({
706
831
  function paintPreviewSurface(el, settings) {
707
832
  const s = el.style;
708
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));
709
836
  s.setProperty("--bg-opacity", String(settings.opacity));
710
837
  s.setProperty("--bg-scrim", String(settings.scrim));
711
838
  s.setProperty("--bg-wallpaper-blur", `${settings.wallpaperBlur}px`);
@@ -951,6 +1078,8 @@ window.__ModuleLoader__.load({
951
1078
  "background.fit": "填充方式",
952
1079
  "background.cover": "铺满",
953
1080
  "background.contain": "完整",
1081
+ "background.previewPanHint": "拖动预览可调整图片位置,双击居中",
1082
+ "background.repositionReset": "重置位置",
954
1083
  "background.clear": "清除背景",
955
1084
  "background.uploadFailed": "上传失败,请重试",
956
1085
  "background.saveFailed": "保存失败,请重试",
@@ -982,6 +1111,8 @@ window.__ModuleLoader__.load({
982
1111
  "background.fit": "Fit",
983
1112
  "background.cover": "Cover",
984
1113
  "background.contain": "Contain",
1114
+ "background.previewPanHint": "Drag the preview to reposition the image; double-click to recenter",
1115
+ "background.repositionReset": "Reset position",
985
1116
  "background.clear": "Clear background",
986
1117
  "background.uploadFailed": "Upload failed, try again",
987
1118
  "background.saveFailed": "Save failed, try again",
@@ -1115,7 +1246,7 @@ window.__ModuleLoader__.load({
1115
1246
  const settingsClient = new SettingsClient();
1116
1247
  //#endregion
1117
1248
  //#region \0dsh-css:D:\Project\deepseek-harness-background\src\client\SettingsRow.module.css.mjs
1118
- 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}}";
1119
1250
  const tagId = "deepseek-harness-background/SettingsRow.module.css";
1120
1251
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
1121
1252
  const tag = document.createElement("style");
@@ -1125,44 +1256,46 @@ window.__ModuleLoader__.load({
1125
1256
  document.head.appendChild(tag);
1126
1257
  }
1127
1258
  var SettingsRow_module_css_default = {
1128
- "previewGlassText": "-ozqdq_previewGlassText",
1129
- "title": "-ozqdq_title",
1130
- "switchKnob": "-ozqdq_switchKnob",
1131
- "sliderValue": "-ozqdq_sliderValue",
1132
- "error": "-ozqdq_error",
1133
- "urlRow": "-ozqdq_urlRow",
1134
- "btn": "-ozqdq_btn",
1135
- "sliderLabel": "-ozqdq_sliderLabel",
1136
- "slider": "-ozqdq_slider",
1137
- "clearBtn": "-ozqdq_clearBtn",
1259
+ "segBtn": "-ozqdq_segBtn",
1260
+ "noticeBtn": "-ozqdq_noticeBtn",
1138
1261
  "preview": "-ozqdq_preview",
1262
+ "previewEmpty": "-ozqdq_previewEmpty",
1263
+ "clearRow": "-ozqdq_clearRow",
1264
+ "previewGlass": "-ozqdq_previewGlass",
1265
+ "segGroup": "-ozqdq_segGroup",
1266
+ "noticeText": "-ozqdq_noticeText",
1267
+ "segActive": "-ozqdq_segActive",
1268
+ "switch": "-ozqdq_switch",
1269
+ "btn": "-ozqdq_btn",
1270
+ "desc": "-ozqdq_desc",
1271
+ "row": "-ozqdq_row",
1272
+ "ratioFrame": "-ozqdq_ratioFrame",
1273
+ "urlRow": "-ozqdq_urlRow",
1139
1274
  "urlInput": "-ozqdq_urlInput",
1140
- "segRow": "-ozqdq_segRow",
1141
- "enabledBadge": "-ozqdq_enabledBadge",
1142
1275
  "titleRow": "-ozqdq_titleRow",
1276
+ "btnPrimary": "-ozqdq_btnPrimary",
1143
1277
  "sectionLabel": "-ozqdq_sectionLabel",
1278
+ "section": "-ozqdq_section",
1279
+ "previewGlassText": "-ozqdq_previewGlassText",
1280
+ "segRow": "-ozqdq_segRow",
1281
+ "notice": "-ozqdq_notice",
1144
1282
  "sourceRow": "-ozqdq_sourceRow",
1145
- "segBtn": "-ozqdq_segBtn",
1146
- "row": "-ozqdq_row",
1147
- "noticeBtn": "-ozqdq_noticeBtn",
1148
- "previewImg": "-ozqdq_previewImg",
1149
- "switch": "-ozqdq_switch",
1283
+ "slider": "-ozqdq_slider",
1284
+ "sliderValue": "-ozqdq_sliderValue",
1285
+ "error": "-ozqdq_error",
1286
+ "hint": "-ozqdq_hint",
1287
+ "enabledBadge": "-ozqdq_enabledBadge",
1150
1288
  "btnDanger": "-ozqdq_btnDanger",
1151
- "segLabel": "-ozqdq_segLabel",
1152
- "previewGlass": "-ozqdq_previewGlass",
1153
- "previewEmpty": "-ozqdq_previewEmpty",
1154
- "noticeText": "-ozqdq_noticeText",
1289
+ "sliderRow": "-ozqdq_sliderRow",
1290
+ "previewImg": "-ozqdq_previewImg",
1291
+ "switchKnob": "-ozqdq_switchKnob",
1292
+ "previewReset": "-ozqdq_previewReset",
1293
+ "title": "-ozqdq_title",
1294
+ "clearBtn": "-ozqdq_clearBtn",
1155
1295
  "header": "-ozqdq_header",
1156
- "section": "-ozqdq_section",
1157
1296
  "previewScrim": "-ozqdq_previewScrim",
1158
- "desc": "-ozqdq_desc",
1159
- "notice": "-ozqdq_notice",
1160
- "sliderRow": "-ozqdq_sliderRow",
1161
- "segGroup": "-ozqdq_segGroup",
1162
- "hint": "-ozqdq_hint",
1163
- "btnPrimary": "-ozqdq_btnPrimary",
1164
- "segActive": "-ozqdq_segActive",
1165
- "clearRow": "-ozqdq_clearRow"
1297
+ "sliderLabel": "-ozqdq_sliderLabel",
1298
+ "segLabel": "-ozqdq_segLabel"
1166
1299
  };
1167
1300
  //#endregion
1168
1301
  //#region src/client/SettingsRow.tsx
@@ -1195,6 +1328,8 @@ window.__ModuleLoader__.load({
1195
1328
  blur: 16,
1196
1329
  wallpaperBlur: 0,
1197
1330
  fit: DEFAULT_FIT,
1331
+ offsetX: DEFAULT_OFFSET,
1332
+ offsetY: DEFAULT_OFFSET,
1198
1333
  timeline: true
1199
1334
  };
1200
1335
  /** Per-field slider geometry: 5% steps for ratios, fine px steps for radii so
@@ -1237,13 +1372,26 @@ window.__ModuleLoader__.load({
1237
1372
  const prefix = `${BACKGROUND_API_PREFIX}/image/`;
1238
1373
  return url.startsWith(prefix) ? url.slice(prefix.length) : "";
1239
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
+ }
1240
1388
  /** Read the settings store (uSES-friendly). */
1241
1389
  function readSnapshot() {
1242
1390
  return settingsClient.getSnapshot();
1243
1391
  }
1244
1392
  /** Field-wise equality for distinguishing our own saves from external changes. */
1245
1393
  function sameSettings(a, b) {
1246
- 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;
1247
1395
  }
1248
1396
  /**
1249
1397
  * A stepped range control. While dragging it re-renders only itself: the value
@@ -1337,6 +1485,14 @@ window.__ModuleLoader__.load({
1337
1485
  const [noticeDismissed, setNoticeDismissed] = (0, react.useState)(false);
1338
1486
  const fileInput = (0, react.useRef)(null);
1339
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);
1340
1496
  const draftRef = (0, react.useRef)(DEFAULTS);
1341
1497
  const editingRef = (0, react.useRef)(false);
1342
1498
  const lastSavedRef = (0, react.useRef)(null);
@@ -1394,6 +1550,36 @@ window.__ModuleLoader__.load({
1394
1550
  saveNowRef.current(draftRef.current);
1395
1551
  }, 120);
1396
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
+ }, []);
1397
1583
  (0, react.useEffect)(() => {
1398
1584
  if (snapshot.status !== "ready" || snapshot.value === void 0) return;
1399
1585
  const saved = lastSavedRef.current;
@@ -1494,8 +1680,109 @@ window.__ModuleLoader__.load({
1494
1680
  setDraft(next);
1495
1681
  saveNow(next);
1496
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
+ };
1497
1781
  const sourceUrl = draft.uploadId ? `${BACKGROUND_API_PREFIX}/image/${draft.uploadId}` : draft.url;
1498
1782
  const hasImage = sourceUrl !== "";
1783
+ (0, react.useEffect)(() => {
1784
+ setImgMeta(null);
1785
+ }, [sourceUrl]);
1499
1786
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1500
1787
  className: SettingsRow_module_css_default.row,
1501
1788
  children: [
@@ -1531,27 +1818,55 @@ window.__ModuleLoader__.load({
1531
1818
  })]
1532
1819
  }),
1533
1820
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1534
- className: SettingsRow_module_css_default.preview,
1535
- ref: previewRef,
1536
- children: hasImage ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
1537
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
1538
- className: SettingsRow_module_css_default.previewImg,
1539
- src: sourceUrl,
1540
- alt: "",
1541
- draggable: false,
1542
- referrerPolicy: "no-referrer"
1543
- }),
1544
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: SettingsRow_module_css_default.previewScrim }),
1545
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1546
- className: SettingsRow_module_css_default.previewGlass,
1547
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1548
- className: SettingsRow_module_css_default.previewGlassText,
1549
- 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
+ })
1550
1859
  })
1551
- })
1552
- ] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1553
- className: SettingsRow_module_css_default.previewEmpty,
1554
- 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
+ })]
1555
1870
  })
1556
1871
  }),
1557
1872
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {