@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/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.3";
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.3",
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') setPreview(null)
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={() => { setPreview(null) }}
752
+ onClick={closePreview}
681
753
  >
682
- <button type="button" className={css.lightboxClose} aria-label={tt('preview.close')} onClick={() => { setPreview(null) }}>
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
- <img
697
- className={css.lightboxImage}
698
- src={srcOf(previewImage)}
699
- alt={previewImage.revisedPrompt ?? tt('preview.title')}
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
- <figcaption className={css.lightboxCaption} title={previewImage.revisedPrompt}>
703
- {tt('revisedPrompt', { prompt: previewImage.revisedPrompt })}
704
- </figcaption>
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
- <a
709
- className={css.lightboxDownload}
710
- href={srcOf(previewImage)}
711
- download={`dsh-image-${preview.index + 1}.${extensionOf(previewImage.mime)}`}
712
- >
713
- {tt('download')}
714
- </a>
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>,
@@ -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
- grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
801
+ flex: 1;
802
+ min-height: 0;
802
803
  gap: 14px;
803
- align-content: start;
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: 16px;
982
+ left: max(20px, calc(50% - 640px));
975
983
  }
976
984
 
977
985
  .lightboxNav[data-dir='next'] {
978
- right: 16px;
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
- max-height: calc(100vh - 48px);
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: calc(100vh - 160px);
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
- background: rgba(255, 255, 255, 0.04);
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
- .lightboxDownload {
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.3'
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 = {