@ash-qw/dsh-theme-prts 2.3.0 → 2.3.1

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.
@@ -2,6 +2,7 @@ import { mkdir, readFile, writeFile, rename, rm } from 'node:fs/promises'
2
2
  import { join } from 'node:path'
3
3
  import { randomUUID } from 'node:crypto'
4
4
  import sharp from 'sharp'
5
+ import { validBackgroundRegion } from '../shared/background-region.js'
5
6
  import { body, json, isBase64 } from './api.js'
6
7
  import { fault } from './emblems.js'
7
8
 
@@ -26,8 +27,11 @@ export async function createBackgroundService(directory) {
26
27
  const task = queue.then(async () => {
27
28
  if (!BACKGROUND_SLOTS.includes(data?.slot)) throw fault(400, '背景区域无效')
28
29
  if (data.revision !== state.revision) throw fault(409, '背景已在其他页面更新,请重新操作')
29
- let entry = null, output
30
- if (data.image !== null) {
30
+ const regionOnly = Object.hasOwn(data, 'region') && !Object.hasOwn(data, 'image')
31
+ if (Object.hasOwn(data, 'region') && !validBackgroundRegion(data.region)) throw fault(400, '背景保护区域无效')
32
+ if (regionOnly && !state[data.slot]) throw fault(400, '请先上传背景图片')
33
+ let entry = regionOnly ? { ...state[data.slot], region: data.region } : null, output
34
+ if (!regionOnly && data.image !== null) {
31
35
  if (!isBase64(data.image) || data.image.length > 14 * 1024 * 1024) throw fault(400, '请上传不超过 10 MB 的图片')
32
36
  const bytes = Buffer.from(data.image, 'base64')
33
37
  if (!bytes.length || bytes.length > 10 * 1024 * 1024) throw fault(400, '请上传不超过 10 MB 的图片')
@@ -37,22 +41,22 @@ export async function createBackgroundService(directory) {
37
41
  if (!['png', 'jpeg', 'webp', 'avif'].includes(metadata.format)) throw Error('format')
38
42
  const result = await input.rotate().resize({ width: 3840, height: 2160, fit: 'inside', withoutEnlargement: true }).webp({ quality: 86 }).toBuffer({ resolveWithObject: true })
39
43
  output = result.data
40
- entry = { id: randomUUID(), width: result.info.width, height: result.info.height }
44
+ entry = { id: randomUUID(), width: result.info.width, height: result.info.height, region: data.region ?? null }
41
45
  } catch { throw fault(400, '无法读取图片,请使用 PNG、JPEG、WebP 或 AVIF(最多 4000 万像素)') }
42
46
  }
43
47
  const next = { ...state, revision: state.revision + 1, [data.slot]: entry }
44
48
  const temporary = configurationPath + '.' + randomUUID() + '.tmp'
45
49
  try {
46
- if (entry) await writeFile(join(directory, entry.id + '.webp'), output, { mode: 0o600 })
50
+ if (output) await writeFile(join(directory, entry.id + '.webp'), output, { mode: 0o600 })
47
51
  await writeFile(temporary, JSON.stringify(next), { mode: 0o600 })
48
52
  await rename(temporary, configurationPath)
49
53
  } catch (error) {
50
- if (entry) await rm(join(directory, entry.id + '.webp'), { force: true }).catch(() => {})
54
+ if (output) await rm(join(directory, entry.id + '.webp'), { force: true }).catch(() => {})
51
55
  throw error
52
56
  } finally { await rm(temporary, { force: true }).catch(() => {}) }
53
57
  const previous = state[data.slot]
54
58
  state = next
55
- if (previous) await rm(join(directory, previous.id + '.webp'), { force: true }).catch(() => {})
59
+ if (previous && previous.id !== entry?.id) await rm(join(directory, previous.id + '.webp'), { force: true }).catch(() => {})
56
60
  for (const listener of listeners) { try { listener(state.revision) } catch {} }
57
61
  return snapshot()
58
62
  })
@@ -0,0 +1,21 @@
1
+ // Coordinates are fractions of the oriented, stored image.
2
+ export function validBackgroundRegion(region) {
3
+ return region === null || (region && typeof region === 'object' && !Array.isArray(region)
4
+ && ['x','y','width','height'].every(key => Number.isFinite(region[key]))
5
+ && region.x >= 0 && region.y >= 0 && region.width > 0 && region.height > 0
6
+ && region.x + region.width <= 1.000001 && region.y + region.height <= 1.000001)
7
+ }
8
+ export function backgroundPlacement(imageWidth, imageHeight, width, height, region = null) {
9
+ if (![imageWidth,imageHeight,width,height].every(v => Number.isFinite(v) && v > 0)) return {x:0,y:0,width:0,height:0,clipped:false}
10
+ const scale = Math.max(width / imageWidth, height / imageHeight)
11
+ const w = imageWidth * scale, h = imageHeight * scale
12
+ const clamp = (v,a,b) => Math.max(a,Math.min(b,v))
13
+ const axis = (size, viewport, start, length, vertical = false) => {
14
+ const centered = (viewport-size)/2
15
+ if (!region) return centered
16
+ if (length*size > viewport) return clamp(vertical ? -start*size : viewport/2-(start+length/2)*size,viewport-size,0)
17
+ return clamp(centered, Math.max(viewport-size,-start*size), Math.min(0,viewport-(start+length)*size))
18
+ }
19
+ return {x:axis(w,width,region?.x,region?.width),y:axis(h,height,region?.y,region?.height,true),width:w,height:h,
20
+ clipped:Boolean(region && (region.width*w > width+.01 || region.height*h > height+.01))}
21
+ }
@@ -33,3 +33,11 @@
33
33
  html[data-dsh-prts] :is([data-prts-emblem-sort-active],[data-prts-emblem-sort-active] *) {
34
34
  cursor: var(--prts-sort-cursor, grabbing) !important;
35
35
  }
36
+
37
+ /* The crop surface uses one native cursor family, including during pointer capture. */
38
+ html[data-dsh-prts] [data-prts-region-stage] { cursor:default; }
39
+ html[data-dsh-prts] [data-prts-region-stage] [data-prts-region-box] { cursor:move; }
40
+ html[data-dsh-prts] [data-prts-region-stage] [data-prts-region-handle] { cursor:ns-resize!important; }
41
+ html[data-dsh-prts] [data-prts-region-stage] :is([data-prts-region-handle="e"],[data-prts-region-handle="w"]) { cursor:ew-resize!important; }
42
+ html[data-dsh-prts] [data-prts-region-stage] :is([data-prts-region-handle="nw"],[data-prts-region-handle="se"]) { cursor:nwse-resize!important; }
43
+ html[data-dsh-prts] [data-prts-region-stage] :is([data-prts-region-handle="ne"],[data-prts-region-handle="sw"]) { cursor:nesw-resize!important; }
@@ -1,18 +1,18 @@
1
1
  /* Viewport-aligned wallpaper. */
2
2
  html[data-dsh-prts] { --prts-background-wash: color-mix(in srgb, var(--prts-canvas) var(--prts-background-shade, 45%), transparent); }
3
3
  html[data-dsh-prts][data-prts-background-mode="full"][data-prts-background-full] :is([data-prts-region="frame"], [data-prts-nav-rail]) {
4
- background: linear-gradient(var(--prts-background-wash), var(--prts-background-wash)), var(--prts-background-full) center / cover fixed;
4
+ background: linear-gradient(var(--prts-background-wash), var(--prts-background-wash)), var(--prts-background-full) var(--prts-protected-full-x, 50%) var(--prts-protected-full-y, 50%) / cover fixed;
5
5
  }
6
6
  html[data-dsh-prts][data-prts-background-mode="full"][data-prts-background-full] :is([data-prts-region="sessions"], [data-prts-region="operation"]) { background: transparent; }
7
7
  html[data-dsh-prts][data-prts-background-mode="split"][data-prts-background-sidebar] {
8
- --prts-wall-width:max(calc(var(--prts-sidebar-width,280px) + var(--prts-nav-width)),calc((100dvh - var(--prts-host-top-inset)) * var(--prts-sidebar-image-ratio,1)));
9
- --prts-wall-x:calc((var(--prts-sidebar-width,280px) + var(--prts-nav-width) - var(--prts-wall-width)) / 2);
10
- & :is([data-prts-region="sessions"],[data-prts-nav-rail]){background:linear-gradient(var(--prts-background-wash),var(--prts-background-wash)),var(--prts-background-sidebar) var(--prts-wall-x) center / var(--prts-wall-width) auto no-repeat}
11
- & [data-prts-region="sessions"]{background-position:center,calc(var(--prts-wall-x) - var(--prts-nav-width)) center}
8
+ --prts-wall-width:var(--prts-protected-sidebar-width,max(calc(var(--prts-sidebar-width,280px) + var(--prts-nav-width)),calc((100dvh - var(--prts-host-top-inset)) * var(--prts-sidebar-image-ratio,1))));
9
+ --prts-wall-x:var(--prts-protected-sidebar-x,calc((var(--prts-sidebar-width,280px) + var(--prts-nav-width) - var(--prts-wall-width)) / 2));
10
+ & :is([data-prts-region="sessions"],[data-prts-nav-rail]){background:linear-gradient(var(--prts-background-wash),var(--prts-background-wash)),var(--prts-background-sidebar) var(--prts-wall-x) var(--prts-protected-sidebar-y, 50%) / var(--prts-wall-width) auto no-repeat}
11
+ & [data-prts-region="sessions"]{background-position:center,calc(var(--prts-wall-x) - var(--prts-nav-width)) var(--prts-protected-sidebar-y, 50%)}
12
12
  }
13
13
  html[data-dsh-prts][data-prts-rail-mode="overlay"]:is([data-prts-background-mode="split"][data-prts-background-sidebar],[data-prts-background-mode="full"][data-prts-background-full]) [data-prts-nav-rail] { background: transparent; }
14
14
  html[data-dsh-prts][data-prts-background-mode="split"][data-prts-background-conversation] [data-prts-region="operation"] {
15
- background: linear-gradient(var(--prts-background-wash), var(--prts-background-wash)), var(--prts-background-conversation) center / cover no-repeat;
15
+ background: linear-gradient(var(--prts-background-wash), var(--prts-background-wash)), var(--prts-background-conversation) var(--prts-protected-conversation-x, 50%) var(--prts-protected-conversation-y, 50%) / cover no-repeat;
16
16
  }
17
17
  html[data-dsh-prts][data-prts-background-mode="full"][data-prts-background-full] [data-prts-ambient-layer],
18
18
  html[data-dsh-prts][data-prts-background-mode="split"][data-prts-background-conversation] [data-prts-ambient-layer] { display: none !important; }
@@ -34,13 +34,23 @@ html[data-dsh-prts][data-prts-background-mode="split"][data-prts-background-conv
34
34
  [data-prts-background-images] { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px; }
35
35
  [data-prts-background-card="full"] { grid-column: 1 / -1; }
36
36
  [data-prts-background-card-heading] { display: flex; justify-content: space-between; align-items: center; min-height: 30px; margin-bottom: 6px; font-size: 13px; }
37
- [data-prts-background-upload] { position: relative; display: grid; place-items: center; width: 100%; height: 120px; padding: 0; border: 1px dashed var(--prts-line); border-radius: 8px; color: var(--prts-muted); background: var(--prts-panel); overflow: hidden; cursor: pointer; }
38
- [data-prts-background-card="full"] [data-prts-background-upload] { height: 155px; }
39
- [data-prts-background-upload][data-has-image] { border-style: solid; }
40
- [data-prts-background-upload]:hover { border-color: var(--prts-cyan); color: var(--prts-ink); }
41
- [data-prts-background-thumbnail] { position: absolute; inset: 0; background: center / cover no-repeat; }
42
- [data-prts-background-caption] { position: relative; padding: 6px 12px; border-radius: 5px; font-size: 12px; }
43
- [data-has-image] [data-prts-background-caption] { align-self: end; justify-self: end; margin: 8px; background: var(--prts-panel); color: var(--prts-ink); box-shadow: 0 1px 5px #0003; }
37
+ [data-prts-background-image-controls] { position:relative;display:grid;place-items:center;min-height:140px;border:1px dashed var(--prts-line);border-radius:8px;background:var(--prts-panel);overflow:hidden; }
38
+ [data-prts-background-card="full"] [data-prts-background-image-controls] { min-height:180px; }
39
+ [data-prts-background-image-controls][data-has-image] { border-style:solid; }
40
+ [data-prts-background-thumbnail] { position:absolute;inset:0;background:center / cover no-repeat;pointer-events:none; }
41
+ [data-prts-background-empty] {font-size:12px;color:var(--prts-muted);padding-bottom:40px}
42
+ [data-has-image] [data-prts-background-empty] {visibility:hidden}
43
+ /* One compact toolbar stays legible over either bright or dark photography. */
44
+ [data-prts-background-image-actions] {position:absolute;bottom:10px;right:10px;display:flex;align-items:center;gap:2px;padding:2px;border:1px solid #ffffff26;border-radius:7px;background:rgb(20 26 30 / 82%);backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);box-shadow:0 2px 8px #0002;color:#edf2f3}
45
+ [data-prts-background-image-actions] button {display:inline-flex;align-items:center;justify-content:center;gap:5px;min-height:26px;padding:4px 7px;border:0;border-radius:5px;background:transparent;color:inherit;box-shadow:none;font:500 11px/1.2 var(--prts-body);white-space:nowrap;cursor:pointer;transition:background-color 120ms ease,color 120ms ease}
46
+ [data-prts-background-image-actions] button + button {position:relative}
47
+ [data-prts-background-image-actions] button + button::before {content:"";position:absolute;left:-1px;top:7px;bottom:7px;width:1px;background:#ffffff24}
48
+ [data-prts-background-image-actions] button[hidden] + button::before {display:none}
49
+ [data-prts-background-image-actions] svg {width:13px;height:13px;flex:none;fill:none;stroke:currentColor;stroke-width:1.4;stroke-linecap:round;stroke-linejoin:round;opacity:.8}
50
+ [data-prts-background-image-actions] button:hover {background:#ffffff18;color:#fff}
51
+ [data-prts-background-image-actions] button:active {background:#ffffff28}
52
+ [data-prts-background-image-actions] button:disabled {opacity:.45;cursor:wait}
53
+ [data-prts-background-settings] [data-prts-background-image-actions] button:focus-visible {outline:2px solid #b9e6ee;outline-offset:-2px}
44
54
  [data-prts-background-remove] { padding: 4px 8px; background: transparent; color: var(--prts-muted); border: 0; border-radius: 4px; font-size: 12px; cursor: pointer; }
45
55
  [data-prts-background-remove]:hover { color: var(--prts-red); background: color-mix(in srgb, var(--prts-red) 8%, transparent); }
46
56
  [data-prts-background-shade] { display: grid; gap: 8px; font-size: 13px; padding-top: 14px; border-top: 1px solid var(--prts-line); }
@@ -343,3 +353,49 @@ html[data-dsh-prts] [data-prts-background-hero-setting][hidden] { display:none;
343
353
 
344
354
  /* Windows chrome must not clip a different canvas color into the conversation. */
345
355
  html[data-dsh-prts][data-windows-titlebar] [data-prts-region="operation"] { border-radius: 0; }
356
+
357
+ [data-prts-region-editor] { width:min(1000px,calc(100vw - 32px));max-width:none;max-height:calc(100dvh - 32px);box-sizing:border-box;margin:auto;padding:24px;border:1px solid var(--prts-line);border-radius:14px;background:var(--prts-panel);color:var(--prts-ink);overflow:auto; }
358
+ [data-prts-region-editor]:not([open]) {display:none}
359
+ [data-prts-region-editor]::backdrop {background:#0009;backdrop-filter:blur(5px)}
360
+ [data-prts-region-editor] > header {display:flex;justify-content:space-between;align-items:start;gap:20px;margin-bottom:24px}
361
+ [data-prts-region-editor] > header strong {font-size:18px}
362
+ [data-prts-region-workspace] {display:grid;grid-template-columns:minmax(0,1.5fr) minmax(290px,1fr);gap:28px}
363
+ [data-prts-region-original],[data-prts-region-samples] {display:grid;align-content:start;gap:12px;min-width:0}
364
+ [data-prts-region-original] {padding:0 8px 0 24px}
365
+ [data-prts-region-stage] {margin:24px 0 12px}
366
+ [data-prts-region-preview-grid] {display:grid;grid-template-columns:minmax(0,1.5fr) minmax(0,1fr);gap:14px;align-items:start;margin-top:12px}
367
+ [data-prts-region-preview-grid] figure {display:grid;justify-items:center;align-content:start;gap:12px;margin:0;min-width:0}
368
+ [data-prts-region-preview-grid] figcaption {font-size:11px;color:var(--prts-muted)}
369
+ [data-prts-region-editor] > footer {display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-top:24px;padding-top:16px;border-top:1px solid var(--prts-line)}
370
+ [data-prts-region-editor] > footer > span {flex:1}
371
+ [data-prts-region-ruler] {position:absolute;pointer-events:none;color:var(--prts-muted);font:10px/1 var(--prts-mono)}
372
+ [data-prts-region-ruler="x"] {left:0;right:0;top:-20px;height:10px;border-bottom:1px solid var(--prts-line)}
373
+ [data-prts-region-ruler="y"] {top:0;bottom:0;left:-24px;width:16px;border-right:1px solid var(--prts-line)}
374
+ [data-prts-region-ruler] span,[data-prts-region-ruler] b {position:absolute;font-weight:400}
375
+ [data-prts-region-ruler="x"] span,[data-prts-region-ruler="x"] b {transform:translateX(-50%)}
376
+ [data-prts-region-ruler="y"] span,[data-prts-region-ruler="y"] b {transform:translateY(-50%)}
377
+ [data-prts-region-ruler] b {background:var(--prts-panel);color:var(--prts-cyan);z-index:1}
378
+ @media(max-width:760px){[data-prts-region-editor]{padding:18px;width:calc(100vw - 16px);max-height:calc(100dvh - 16px)}[data-prts-region-workspace]{grid-template-columns:1fr}[data-prts-region-preview-grid]{grid-template-columns:1fr 1fr}[data-prts-region-editor] > footer > span{display:none}}
379
+ [data-prts-region-editor] p { margin:0;font-size:12px;color:var(--prts-muted); }
380
+ [data-prts-region-stage] { position:relative;max-width:100%;justify-self:center;touch-action:none;user-select:none;cursor:default; }
381
+ [data-prts-region-stage] img { display:block;width:100%;height:auto;pointer-events:none; }
382
+ [data-prts-region-box] { position:absolute;box-sizing:border-box;border:2px solid #fff;outline:1px solid #111;box-shadow:0 0 0 1px #111;background:#ffffff08;cursor:move;touch-action:none; }
383
+ [data-prts-region-box] > span { position:absolute;top:4px;left:4px;background:#111b;color:#fff;font-size:10px;white-space:nowrap;pointer-events:none; }
384
+ [data-prts-region-handle] { position:absolute;width:16px!important;height:16px!important;min-width:0!important;padding:0!important;border:1px solid #111!important;background:#fff!important;border-radius:3px!important;touch-action:none; }
385
+ [data-prts-region-handle="nw"] {left:-8px;top:-8px;cursor:nwse-resize}
386
+ [data-prts-region-handle="ne"] {right:-8px;top:-8px;cursor:nesw-resize}
387
+ [data-prts-region-handle="sw"] {left:-8px;bottom:-8px;cursor:nesw-resize}
388
+ [data-prts-region-handle="se"] {right:-8px;bottom:-8px;cursor:nwse-resize}
389
+ [data-prts-region-preview] { position:relative;justify-self:center;max-width:100%;overflow:hidden;background-repeat:no-repeat;border:1px solid var(--prts-line);box-sizing:content-box; }
390
+ [data-prts-region-preview] i { position:absolute;border:1px solid #fff;box-shadow:0 0 0 1px #111;box-sizing:border-box;pointer-events:none; }
391
+ [data-prts-region-actions] { display:flex;gap:8px;flex-wrap:wrap; }
392
+ [data-prts-region-editor] button { padding:6px 10px;border:1px solid var(--prts-line);border-radius:5px;background:var(--prts-panel);color:var(--prts-ink);cursor:pointer;font-size:12px; }
393
+ [data-prts-region-save] { border-color:var(--prts-cyan)!important; }
394
+ [data-prts-region-status]:empty {display:none}
395
+
396
+ [data-prts-region-editor="sidebar"] [data-prts-region-preview-grid] {grid-template-columns:minmax(0,1fr) minmax(0,1.45fr)}
397
+
398
+ [data-prts-region-handle="n"] {left:50%;top:-8px;transform:translateX(-50%);cursor:ns-resize}
399
+ [data-prts-region-handle="s"] {left:50%;bottom:-8px;transform:translateX(-50%);cursor:ns-resize}
400
+ [data-prts-region-handle="w"] {left:-8px;top:50%;transform:translateY(-50%);cursor:ew-resize}
401
+ [data-prts-region-handle="e"] {right:-8px;top:50%;transform:translateY(-50%);cursor:ew-resize}
@@ -1907,12 +1907,12 @@ html[data-dsh-prts][data-prts-conversation-style="deck-chat"] {
1907
1907
  & [data-message-role="assistant"]::after,
1908
1908
  & :is([data-chat-flow-kind="user"], [data-submission-echo]:not([data-pending-steering])) [class$="_bubble"]::before,
1909
1909
  & [data-message-role="user"]::before {
1910
- content: ""; position: absolute; top: 0; left: -9px; width: 10px; height: 18px; pointer-events: none;
1910
+ content: ""; position: absolute; top: 0; left: -10px; width: 10px; height: 18px; pointer-events: none;
1911
1911
  background: var(--prts-bubble-surface, var(--prts-chat-user-surface));
1912
1912
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 18'%3E%3Cpath d='M0 0H10V18Q10 6 0 0Z'/%3E%3C/svg%3E") center / 100% 100% no-repeat;
1913
1913
  }
1914
1914
  & :is([data-chat-flow-kind="user"], [data-submission-echo]:not([data-pending-steering])) [class$="_bubble"]::before,
1915
- & [data-message-role="user"]::before { left: auto; right: -9px; transform: scaleX(-1); }
1915
+ & [data-message-role="user"]::before { left: auto; right: -10px; transform: scaleX(-1); }
1916
1916
  }
1917
1917
 
1918
1918
  /* The upstream component replaces only the composer's two background plates. */
@@ -393,6 +393,12 @@ html[data-dsh-prts] [data-prts-conversation-control="danger"] {
393
393
  }
394
394
 
395
395
  /* Empty session: emblem owns the upper field, composer anchors in the lower third. */
396
+ /* Reserve the native blank header before session restoration adds its controls.
397
+ Otherwise its height grows from 0 to 40px after the emblem has been positioned. */
398
+ html[data-dsh-prts] [data-phase="hero"] > [data-slot="conversation.header"] > header {
399
+ min-height: 40px;
400
+ }
401
+
396
402
  html[data-dsh-prts] [data-prts-hero-active] {
397
403
  --prts-hero-narrow-spacer: clamp(24px, 8dvh, 72px);
398
404
  --prts-hero-wide-spacer: clamp(96px, 22dvh, 340px);