@dickpy/dsh-imagegen 1.0.3 → 1.0.5
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/README.md +7 -7
- package/docs/images/image-generation-studio.png +0 -0
- package/docs/images/plugin-settings.png +0 -0
- package/lib/client.js +333 -112
- package/lib/client.js.map +1 -1
- package/lib/index.js +1 -1
- package/package.json +1 -1
- package/src/client/ImageGenPanel.tsx +132 -19
- package/src/client/locales.ts +16 -0
- package/src/client/panel.module.css +148 -10
- package/src/protocol.ts +1 -1
package/lib/index.js
CHANGED
|
@@ -14,7 +14,7 @@ import { spawn } from "node:child_process";
|
|
|
14
14
|
/** Settings namespace this plugin owns (host settings seam + bridge). */
|
|
15
15
|
const IMAGEGEN_SETTINGS_NAMESPACE = "dsh-imagegen";
|
|
16
16
|
/** Published package version shared by the host updater and the client UI. */
|
|
17
|
-
const PLUGIN_VERSION = "1.0.
|
|
17
|
+
const PLUGIN_VERSION = "1.0.5";
|
|
18
18
|
/** Same-origin route family (loopback-only, mirroring the dsh-ssh fence). */
|
|
19
19
|
const SETTINGS_API = {
|
|
20
20
|
describe: "/api/dsh-imagegen/settings/describe",
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dickpy/dsh-imagegen",
|
|
3
3
|
"description": "AI 生图 (image generation) plugin for the dsh web GUI: text-to-image and image-to-image through a configurable OpenAI-compatible endpoint (gpt-image-2 / gpt-image-1 / dall-e-3), with a settings card for api_url / api_key and a sidebar entry opening a split-pane generation studio.",
|
|
4
|
-
"version": "1.0.
|
|
4
|
+
"version": "1.0.5",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"exports": {
|
|
@@ -42,6 +42,13 @@ const DETAILS = ['', 'standard', 'high'] as const
|
|
|
42
42
|
|
|
43
43
|
const PROMPT_MAX = 2000
|
|
44
44
|
const REF_IMAGE_MAX_BYTES = 10 * 1024 * 1024
|
|
45
|
+
const PREVIEW_SCALE_MIN = 0.5
|
|
46
|
+
const PREVIEW_SCALE_MAX = 3
|
|
47
|
+
const PREVIEW_SCALE_STEP = 0.25
|
|
48
|
+
|
|
49
|
+
function clampPreviewScale(scale: number): number {
|
|
50
|
+
return Math.min(PREVIEW_SCALE_MAX, Math.max(PREVIEW_SCALE_MIN, scale))
|
|
51
|
+
}
|
|
45
52
|
|
|
46
53
|
/** Read the current config from the settings scope snapshot. */
|
|
47
54
|
function useConfig(scope: ImageGenScope): ImageGenConfig | undefined {
|
|
@@ -133,11 +140,14 @@ export function ImageGenPanel(props: {
|
|
|
133
140
|
const [history, setHistory] = useState<HistoryEntry[]>([])
|
|
134
141
|
const [viewingHistoryId, setViewingHistoryId] = useState<string | null>(null)
|
|
135
142
|
const [preview, setPreview] = useState<{ images: GeneratedImage[]; index: number } | null>(null)
|
|
143
|
+
const [previewScale, setPreviewScale] = useState(1)
|
|
144
|
+
const [promptCopied, setPromptCopied] = useState(false)
|
|
136
145
|
const [update, setUpdate] = useState<UpdateInfo | null>(null)
|
|
137
146
|
const [updating, setUpdating] = useState(false)
|
|
138
147
|
const [updateMessage, setUpdateMessage] = useState<string | null>(null)
|
|
139
148
|
const [updateResult, setUpdateResult] = useState<'success' | 'failed' | null>(null)
|
|
140
149
|
const fileInput = useRef<HTMLInputElement>(null)
|
|
150
|
+
const previewStage = useRef<HTMLDivElement>(null)
|
|
141
151
|
const elapsed = useElapsed(generating, startedAt)
|
|
142
152
|
|
|
143
153
|
// Load the host-persisted history once on mount (it lives in ~/.dsh on the
|
|
@@ -242,10 +252,20 @@ export function ImageGenPanel(props: {
|
|
|
242
252
|
/** Open the full-screen image preview at a given index. */
|
|
243
253
|
const openPreview = (previewImages: GeneratedImage[], index: number): void => {
|
|
244
254
|
setPreview({ images: previewImages, index })
|
|
255
|
+
setPreviewScale(1)
|
|
256
|
+
setPromptCopied(false)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const closePreview = (): void => {
|
|
260
|
+
setPreview(null)
|
|
261
|
+
setPreviewScale(1)
|
|
262
|
+
setPromptCopied(false)
|
|
245
263
|
}
|
|
246
264
|
|
|
247
265
|
/** Step the preview by ±1, wrapping around. */
|
|
248
266
|
const stepPreview = (delta: number): void => {
|
|
267
|
+
setPreviewScale(1)
|
|
268
|
+
setPromptCopied(false)
|
|
249
269
|
setPreview(current => {
|
|
250
270
|
if (current === null) return null
|
|
251
271
|
const total = current.images.length
|
|
@@ -257,14 +277,30 @@ export function ImageGenPanel(props: {
|
|
|
257
277
|
useEffect(() => {
|
|
258
278
|
if (preview === null) return
|
|
259
279
|
const onKey = (event: KeyboardEvent): void => {
|
|
260
|
-
if (event.key === 'Escape')
|
|
280
|
+
if (event.key === 'Escape') closePreview()
|
|
261
281
|
else if (event.key === 'ArrowLeft') stepPreview(-1)
|
|
262
282
|
else if (event.key === 'ArrowRight') stepPreview(1)
|
|
283
|
+
else if (event.key === '+' || event.key === '=') setPreviewScale(current => clampPreviewScale(current + PREVIEW_SCALE_STEP))
|
|
284
|
+
else if (event.key === '-') setPreviewScale(current => clampPreviewScale(current - PREVIEW_SCALE_STEP))
|
|
285
|
+
else if (event.key === '0') setPreviewScale(1)
|
|
263
286
|
}
|
|
264
287
|
window.addEventListener('keydown', onKey)
|
|
265
288
|
return () => window.removeEventListener('keydown', onKey)
|
|
266
289
|
}, [preview])
|
|
267
290
|
|
|
291
|
+
// A scaled image owns real scrollable space, rather than being visually
|
|
292
|
+
// transformed and clipped. Recenter the viewport after every zoom or slide.
|
|
293
|
+
useEffect(() => {
|
|
294
|
+
if (preview === null) return
|
|
295
|
+
const frame = window.requestAnimationFrame(() => {
|
|
296
|
+
const stage = previewStage.current
|
|
297
|
+
if (stage === null) return
|
|
298
|
+
stage.scrollLeft = Math.max(0, (stage.scrollWidth - stage.clientWidth) / 2)
|
|
299
|
+
stage.scrollTop = Math.max(0, (stage.scrollHeight - stage.clientHeight) / 2)
|
|
300
|
+
})
|
|
301
|
+
return () => window.cancelAnimationFrame(frame)
|
|
302
|
+
}, [preview, previewScale])
|
|
303
|
+
|
|
268
304
|
/** Load a past generation's images into the canvas. */
|
|
269
305
|
const viewHistoryEntry = async (entry: HistoryEntry): Promise<void> => {
|
|
270
306
|
try {
|
|
@@ -321,6 +357,42 @@ export function ImageGenPanel(props: {
|
|
|
321
357
|
const generateDisabled = generating || !enabled || !configured
|
|
322
358
|
const viewingEntry = viewingHistoryId === null ? null : history.find(entry => entry.id === viewingHistoryId) ?? null
|
|
323
359
|
const previewImage = preview === null ? null : preview.images[preview.index] ?? null
|
|
360
|
+
const previewFrameScale = Math.max(1, previewScale)
|
|
361
|
+
const previewImageScale = previewScale / previewFrameScale
|
|
362
|
+
|
|
363
|
+
const copyPreviewPrompt = async (text: string): Promise<void> => {
|
|
364
|
+
try {
|
|
365
|
+
if (navigator.clipboard?.writeText !== undefined) {
|
|
366
|
+
await navigator.clipboard.writeText(text)
|
|
367
|
+
} else {
|
|
368
|
+
const textarea = document.createElement('textarea')
|
|
369
|
+
textarea.value = text
|
|
370
|
+
textarea.style.position = 'fixed'
|
|
371
|
+
textarea.style.opacity = '0'
|
|
372
|
+
document.body.appendChild(textarea)
|
|
373
|
+
textarea.select()
|
|
374
|
+
const copied = document.execCommand('copy')
|
|
375
|
+
textarea.remove()
|
|
376
|
+
if (!copied) throw new Error('copy failed')
|
|
377
|
+
}
|
|
378
|
+
setPromptCopied(true)
|
|
379
|
+
window.setTimeout(() => { setPromptCopied(false) }, 1800)
|
|
380
|
+
} catch {
|
|
381
|
+
setPromptCopied(false)
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const addPreviewToEdit = (): void => {
|
|
386
|
+
if (previewImage === null || preview === null) return
|
|
387
|
+
setMode('edit')
|
|
388
|
+
setRefImage({
|
|
389
|
+
dataUrl: srcOf(previewImage),
|
|
390
|
+
name: `dsh-image-${preview.index + 1}.${extensionOf(previewImage.mime)}`,
|
|
391
|
+
})
|
|
392
|
+
if (prompt.trim() === '' && previewImage.revisedPrompt !== undefined) setPrompt(previewImage.revisedPrompt)
|
|
393
|
+
setError(null)
|
|
394
|
+
closePreview()
|
|
395
|
+
}
|
|
324
396
|
|
|
325
397
|
return (
|
|
326
398
|
<div className={css.panel}>
|
|
@@ -569,7 +641,7 @@ export function ImageGenPanel(props: {
|
|
|
569
641
|
<span className={css.canvasHistoryTag}>{tt('history.viewing', { time: formatTime(viewingEntry.createdAt) })}</span>
|
|
570
642
|
) : null}
|
|
571
643
|
</div>
|
|
572
|
-
<div className={css.grid}>
|
|
644
|
+
<div className={css.grid} data-count={images.length}>
|
|
573
645
|
{images.map((image, index) => (
|
|
574
646
|
<figure
|
|
575
647
|
key={index}
|
|
@@ -677,9 +749,9 @@ export function ImageGenPanel(props: {
|
|
|
677
749
|
role="dialog"
|
|
678
750
|
aria-modal="true"
|
|
679
751
|
aria-label={tt('preview.title')}
|
|
680
|
-
onClick={
|
|
752
|
+
onClick={closePreview}
|
|
681
753
|
>
|
|
682
|
-
<button type="button" className={css.lightboxClose} aria-label={tt('preview.close')}
|
|
754
|
+
<button type="button" className={css.lightboxClose} aria-label={tt('preview.close')} title={tt('preview.close')} onClick={closePreview}>
|
|
683
755
|
<svg viewBox="0 0 16 16" width="18" height="18" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" aria-hidden="true"><path d="M4 4l8 8M12 4l-8 8"/></svg>
|
|
684
756
|
</button>
|
|
685
757
|
{preview.images.length > 1 ? (
|
|
@@ -693,25 +765,66 @@ export function ImageGenPanel(props: {
|
|
|
693
765
|
</>
|
|
694
766
|
) : null}
|
|
695
767
|
<figure className={css.lightboxFigure} onClick={(event) => { event.stopPropagation() }}>
|
|
696
|
-
<
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
768
|
+
<div
|
|
769
|
+
ref={previewStage}
|
|
770
|
+
className={css.lightboxStage}
|
|
771
|
+
onWheel={(event) => {
|
|
772
|
+
event.preventDefault()
|
|
773
|
+
setPreviewScale(current => clampPreviewScale(current + (event.deltaY < 0 ? PREVIEW_SCALE_STEP : -PREVIEW_SCALE_STEP)))
|
|
774
|
+
}}
|
|
775
|
+
>
|
|
776
|
+
<div
|
|
777
|
+
className={css.lightboxScaleFrame}
|
|
778
|
+
style={{ width: `${previewFrameScale * 100}%`, height: `${previewFrameScale * 100}%` }}
|
|
779
|
+
>
|
|
780
|
+
<img
|
|
781
|
+
className={css.lightboxImage}
|
|
782
|
+
style={{ width: `${previewImageScale * 100}%`, height: `${previewImageScale * 100}%` }}
|
|
783
|
+
src={srcOf(previewImage)}
|
|
784
|
+
alt={previewImage.revisedPrompt ?? tt('preview.title')}
|
|
785
|
+
/>
|
|
786
|
+
</div>
|
|
787
|
+
</div>
|
|
788
|
+
<div className={css.lightboxTools} role="group" aria-label={tt('preview.zoomControls')}>
|
|
789
|
+
<button type="button" className={css.lightboxTool} aria-label={tt('preview.zoomOut')} title={tt('preview.zoomOut')} onClick={() => { setPreviewScale(current => clampPreviewScale(current - PREVIEW_SCALE_STEP)) }}>
|
|
790
|
+
<svg viewBox="0 0 16 16" width="17" height="17" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" aria-hidden="true"><circle cx="7" cy="7" r="4.2"/><path d="M4.8 7h4.4M13 13l-2.8-2.8"/></svg>
|
|
791
|
+
</button>
|
|
792
|
+
<button type="button" className={css.lightboxZoomLevel} aria-label={tt('preview.zoomReset')} title={tt('preview.zoomReset')} onClick={() => { setPreviewScale(1) }}>
|
|
793
|
+
{tt('preview.zoomLevel', { percent: Math.round(previewScale * 100) })}
|
|
794
|
+
</button>
|
|
795
|
+
<button type="button" className={css.lightboxTool} aria-label={tt('preview.zoomIn')} title={tt('preview.zoomIn')} onClick={() => { setPreviewScale(current => clampPreviewScale(current + PREVIEW_SCALE_STEP)) }}>
|
|
796
|
+
<svg viewBox="0 0 16 16" width="17" height="17" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" aria-hidden="true"><circle cx="7" cy="7" r="4.2"/><path d="M7 4.8v4.4M4.8 7h4.4M13 13l-2.8-2.8"/></svg>
|
|
797
|
+
</button>
|
|
798
|
+
</div>
|
|
701
799
|
{previewImage.revisedPrompt !== undefined ? (
|
|
702
|
-
<
|
|
703
|
-
{
|
|
704
|
-
|
|
800
|
+
<div className={css.lightboxCaptionRow}>
|
|
801
|
+
<figcaption className={css.lightboxCaption} title={previewImage.revisedPrompt}>
|
|
802
|
+
{tt('revisedPrompt', { prompt: previewImage.revisedPrompt })}
|
|
803
|
+
</figcaption>
|
|
804
|
+
<button type="button" className={css.lightboxCopy} aria-label={tt(promptCopied ? 'preview.copied' : 'preview.copyPrompt')} title={tt(promptCopied ? 'preview.copied' : 'preview.copyPrompt')} onClick={() => { void copyPreviewPrompt(previewImage.revisedPrompt!) }}>
|
|
805
|
+
{promptCopied ? (
|
|
806
|
+
<svg viewBox="0 0 16 16" width="16" height="16" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><path d="M3 8l3 3 7-7"/></svg>
|
|
807
|
+
) : (
|
|
808
|
+
<svg viewBox="0 0 16 16" width="16" height="16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><rect x="5" y="5" width="7" height="8" rx="1"/><path d="M3 10V3.8c0-.44.36-.8.8-.8H9"/></svg>
|
|
809
|
+
)}
|
|
810
|
+
<span>{tt(promptCopied ? 'preview.copied' : 'preview.copyPrompt')}</span>
|
|
811
|
+
</button>
|
|
812
|
+
</div>
|
|
705
813
|
) : null}
|
|
706
814
|
<div className={css.lightboxMeta}>
|
|
707
815
|
<span className={css.lightboxIndex}>{tt('preview.index', { index: preview.index + 1, total: preview.images.length })}</span>
|
|
708
|
-
<
|
|
709
|
-
className={css.
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
816
|
+
<span className={css.lightboxActions}>
|
|
817
|
+
<button type="button" className={css.lightboxEdit} onClick={addPreviewToEdit}>
|
|
818
|
+
{tt('preview.addToEdit')}
|
|
819
|
+
</button>
|
|
820
|
+
<a
|
|
821
|
+
className={css.lightboxDownload}
|
|
822
|
+
href={srcOf(previewImage)}
|
|
823
|
+
download={`dsh-image-${preview.index + 1}.${extensionOf(previewImage.mime)}`}
|
|
824
|
+
>
|
|
825
|
+
{tt('download')}
|
|
826
|
+
</a>
|
|
827
|
+
</span>
|
|
715
828
|
</div>
|
|
716
829
|
</figure>
|
|
717
830
|
</div>,
|
package/src/client/locales.ts
CHANGED
|
@@ -72,6 +72,14 @@ export const zh = {
|
|
|
72
72
|
'preview.prev': '上一张',
|
|
73
73
|
'preview.next': '下一张',
|
|
74
74
|
'preview.index': '{index} / {total}',
|
|
75
|
+
'preview.zoomControls': '图片缩放控制',
|
|
76
|
+
'preview.zoomIn': '放大',
|
|
77
|
+
'preview.zoomOut': '缩小',
|
|
78
|
+
'preview.zoomReset': '重置缩放',
|
|
79
|
+
'preview.zoomLevel': '{percent}%',
|
|
80
|
+
'preview.copyPrompt': '复制提示词',
|
|
81
|
+
'preview.copied': '已复制',
|
|
82
|
+
'preview.addToEdit': '添加到图生图',
|
|
75
83
|
// config banner
|
|
76
84
|
'config.missing': '尚未配置 API:请前往「设置 → 插件 → 可配置」为 AI 生图填写 api_url 与 api_key。',
|
|
77
85
|
'config.configured': '已连接 {url}',
|
|
@@ -178,6 +186,14 @@ export const en: Record<keyof typeof zh, string> = {
|
|
|
178
186
|
'preview.prev': 'Previous',
|
|
179
187
|
'preview.next': 'Next',
|
|
180
188
|
'preview.index': '{index} / {total}',
|
|
189
|
+
'preview.zoomControls': 'Image zoom controls',
|
|
190
|
+
'preview.zoomIn': 'Zoom in',
|
|
191
|
+
'preview.zoomOut': 'Zoom out',
|
|
192
|
+
'preview.zoomReset': 'Reset zoom',
|
|
193
|
+
'preview.zoomLevel': '{percent}%',
|
|
194
|
+
'preview.copyPrompt': 'Copy prompt',
|
|
195
|
+
'preview.copied': 'Copied',
|
|
196
|
+
'preview.addToEdit': 'Add to image to image',
|
|
181
197
|
'config.missing': 'API not configured: open "Settings → Plugins → Configurable" and fill in api_url and api_key for AI Image.',
|
|
182
198
|
'config.configured': 'Connected to {url}',
|
|
183
199
|
'config.disabled': 'The plugin is disabled — re-enable it in Settings.',
|
|
@@ -798,9 +798,15 @@ html[data-dsh-imagegen-active]:not([data-dsh-taskboard-active]):not([data-dsh-ss
|
|
|
798
798
|
|
|
799
799
|
.grid {
|
|
800
800
|
display: grid;
|
|
801
|
-
|
|
801
|
+
flex: 1;
|
|
802
|
+
min-height: 0;
|
|
802
803
|
gap: 14px;
|
|
803
|
-
|
|
804
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
805
|
+
grid-template-rows: repeat(2, minmax(0, 1fr));
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
.grid[data-count='1'] .imageCard {
|
|
809
|
+
grid-area: 1 / 1 / 3 / 3;
|
|
804
810
|
}
|
|
805
811
|
|
|
806
812
|
.imageCard {
|
|
@@ -813,6 +819,7 @@ html[data-dsh-imagegen-active]:not([data-dsh-taskboard-active]):not([data-dsh-ss
|
|
|
813
819
|
overflow: hidden;
|
|
814
820
|
background: var(--dsw-alias-bg-layer-2);
|
|
815
821
|
cursor: zoom-in;
|
|
822
|
+
min-height: 0;
|
|
816
823
|
}
|
|
817
824
|
|
|
818
825
|
.imageCard:focus-visible {
|
|
@@ -822,8 +829,9 @@ html[data-dsh-imagegen-active]:not([data-dsh-taskboard-active]):not([data-dsh-ss
|
|
|
822
829
|
|
|
823
830
|
.image {
|
|
824
831
|
display: block;
|
|
832
|
+
flex: 1;
|
|
833
|
+
min-height: 0;
|
|
825
834
|
width: 100%;
|
|
826
|
-
aspect-ratio: 1 / 1;
|
|
827
835
|
object-fit: cover;
|
|
828
836
|
background: var(--dsw-alias-bg-base);
|
|
829
837
|
}
|
|
@@ -971,11 +979,11 @@ html[data-dsh-imagegen-active]:not([data-dsh-taskboard-active]):not([data-dsh-ss
|
|
|
971
979
|
}
|
|
972
980
|
|
|
973
981
|
.lightboxNav[data-dir='prev'] {
|
|
974
|
-
left:
|
|
982
|
+
left: max(20px, calc(50% - 640px));
|
|
975
983
|
}
|
|
976
984
|
|
|
977
985
|
.lightboxNav[data-dir='next'] {
|
|
978
|
-
right:
|
|
986
|
+
right: max(20px, calc(50% - 640px));
|
|
979
987
|
}
|
|
980
988
|
|
|
981
989
|
.lightboxFigure {
|
|
@@ -984,19 +992,91 @@ html[data-dsh-imagegen-active]:not([data-dsh-taskboard-active]):not([data-dsh-ss
|
|
|
984
992
|
gap: 10px;
|
|
985
993
|
margin: 0;
|
|
986
994
|
max-width: min(1100px, calc(100vw - 160px));
|
|
987
|
-
|
|
995
|
+
width: min(1100px, calc(100vw - 160px));
|
|
996
|
+
height: min(820px, calc(100vh - 48px));
|
|
997
|
+
min-height: 0;
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
.lightboxStage {
|
|
1001
|
+
flex: 1;
|
|
1002
|
+
min-height: 0;
|
|
1003
|
+
position: relative;
|
|
1004
|
+
overflow: auto;
|
|
1005
|
+
border-radius: 10px;
|
|
1006
|
+
background: rgba(255, 255, 255, 0.04);
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
.lightboxScaleFrame {
|
|
1010
|
+
display: flex;
|
|
1011
|
+
align-items: center;
|
|
1012
|
+
justify-content: center;
|
|
1013
|
+
min-width: 100%;
|
|
1014
|
+
min-height: 100%;
|
|
988
1015
|
}
|
|
989
1016
|
|
|
990
1017
|
.lightboxImage {
|
|
1018
|
+
display: block;
|
|
991
1019
|
max-width: 100%;
|
|
992
|
-
max-height:
|
|
1020
|
+
max-height: 100%;
|
|
993
1021
|
object-fit: contain;
|
|
994
1022
|
border-radius: 10px;
|
|
995
1023
|
box-shadow: 0 24px 80px rgba(0, 0, 0, 0.5);
|
|
996
|
-
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
.lightboxTools {
|
|
1027
|
+
display: flex;
|
|
1028
|
+
align-items: center;
|
|
1029
|
+
justify-content: center;
|
|
1030
|
+
gap: 6px;
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
.lightboxTool,
|
|
1034
|
+
.lightboxZoomLevel,
|
|
1035
|
+
.lightboxCopy {
|
|
1036
|
+
display: inline-flex;
|
|
1037
|
+
align-items: center;
|
|
1038
|
+
justify-content: center;
|
|
1039
|
+
color: #fff;
|
|
1040
|
+
background: rgba(255, 255, 255, 0.14);
|
|
1041
|
+
border: 1px solid rgba(255, 255, 255, 0.28);
|
|
1042
|
+
cursor: pointer;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
.lightboxTool,
|
|
1046
|
+
.lightboxZoomLevel {
|
|
1047
|
+
height: 32px;
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
.lightboxTool {
|
|
1051
|
+
width: 32px;
|
|
1052
|
+
border-radius: 50%;
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
.lightboxZoomLevel {
|
|
1056
|
+
min-width: 58px;
|
|
1057
|
+
padding: 0 9px;
|
|
1058
|
+
border-radius: 999px;
|
|
1059
|
+
font: inherit;
|
|
1060
|
+
font-size: 12px;
|
|
1061
|
+
font-variant-numeric: tabular-nums;
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
.lightboxTool:hover,
|
|
1065
|
+
.lightboxZoomLevel:hover,
|
|
1066
|
+
.lightboxCopy:hover {
|
|
1067
|
+
background: rgba(255, 255, 255, 0.26);
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
.lightboxCaptionRow {
|
|
1071
|
+
display: flex;
|
|
1072
|
+
align-items: flex-start;
|
|
1073
|
+
gap: 8px;
|
|
1074
|
+
min-width: 0;
|
|
997
1075
|
}
|
|
998
1076
|
|
|
999
1077
|
.lightboxCaption {
|
|
1078
|
+
flex: 1;
|
|
1079
|
+
min-width: 0;
|
|
1000
1080
|
font-size: 12px;
|
|
1001
1081
|
line-height: 1.6;
|
|
1002
1082
|
color: rgba(255, 255, 255, 0.9);
|
|
@@ -1006,6 +1086,17 @@ html[data-dsh-imagegen-active]:not([data-dsh-taskboard-active]):not([data-dsh-ss
|
|
|
1006
1086
|
overflow: hidden;
|
|
1007
1087
|
}
|
|
1008
1088
|
|
|
1089
|
+
.lightboxCopy {
|
|
1090
|
+
flex: none;
|
|
1091
|
+
gap: 5px;
|
|
1092
|
+
min-height: 28px;
|
|
1093
|
+
padding: 4px 9px;
|
|
1094
|
+
border-radius: 999px;
|
|
1095
|
+
font: inherit;
|
|
1096
|
+
font-size: 12px;
|
|
1097
|
+
white-space: nowrap;
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1009
1100
|
.lightboxMeta {
|
|
1010
1101
|
display: flex;
|
|
1011
1102
|
align-items: center;
|
|
@@ -1019,8 +1110,16 @@ html[data-dsh-imagegen-active]:not([data-dsh-taskboard-active]):not([data-dsh-ss
|
|
|
1019
1110
|
font-variant-numeric: tabular-nums;
|
|
1020
1111
|
}
|
|
1021
1112
|
|
|
1022
|
-
.
|
|
1113
|
+
.lightboxActions {
|
|
1114
|
+
display: inline-flex;
|
|
1115
|
+
align-items: center;
|
|
1116
|
+
gap: 8px;
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
.lightboxDownload,
|
|
1120
|
+
.lightboxEdit {
|
|
1023
1121
|
padding: 4px 14px;
|
|
1122
|
+
font: inherit;
|
|
1024
1123
|
font-size: 12.5px;
|
|
1025
1124
|
font-weight: 500;
|
|
1026
1125
|
color: #fff;
|
|
@@ -1028,12 +1127,51 @@ html[data-dsh-imagegen-active]:not([data-dsh-taskboard-active]):not([data-dsh-ss
|
|
|
1028
1127
|
border: 1px solid rgba(255, 255, 255, 0.28);
|
|
1029
1128
|
border-radius: 999px;
|
|
1030
1129
|
text-decoration: none;
|
|
1130
|
+
cursor: pointer;
|
|
1031
1131
|
}
|
|
1032
1132
|
|
|
1033
|
-
.lightboxDownload:hover
|
|
1133
|
+
.lightboxDownload:hover,
|
|
1134
|
+
.lightboxEdit:hover {
|
|
1034
1135
|
background: rgba(255, 255, 255, 0.26);
|
|
1035
1136
|
}
|
|
1036
1137
|
|
|
1138
|
+
.lightboxEdit {
|
|
1139
|
+
color: #fff;
|
|
1140
|
+
background: rgba(255, 255, 255, 0.14);
|
|
1141
|
+
border: 1px solid rgba(255, 255, 255, 0.28);
|
|
1142
|
+
border-radius: 999px;
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
@media (max-width: 720px) {
|
|
1146
|
+
.lightbox {
|
|
1147
|
+
padding: 16px;
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
.lightboxFigure {
|
|
1151
|
+
width: calc(100vw - 32px);
|
|
1152
|
+
max-width: none;
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
.lightboxNav[data-dir='prev'] {
|
|
1156
|
+
left: 20px;
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
.lightboxNav[data-dir='next'] {
|
|
1160
|
+
right: 20px;
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
.lightboxCaptionRow,
|
|
1164
|
+
.lightboxMeta {
|
|
1165
|
+
align-items: stretch;
|
|
1166
|
+
flex-direction: column;
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
.lightboxCopy,
|
|
1170
|
+
.lightboxActions {
|
|
1171
|
+
align-self: flex-end;
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1037
1175
|
@keyframes dshImageGenSpin {
|
|
1038
1176
|
to { transform: rotate(360deg); }
|
|
1039
1177
|
}
|
package/src/protocol.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
export const IMAGEGEN_SETTINGS_NAMESPACE = 'dsh-imagegen'
|
|
9
9
|
|
|
10
10
|
/** Published package version shared by the host updater and the client UI. */
|
|
11
|
-
export const PLUGIN_VERSION = '1.0.
|
|
11
|
+
export const PLUGIN_VERSION = '1.0.5'
|
|
12
12
|
|
|
13
13
|
/** Same-origin route family (loopback-only, mirroring the dsh-ssh fence). */
|
|
14
14
|
export const SETTINGS_API = {
|