@goodandready/dsh-image-gen 0.10.35 → 0.11.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.
package/lib/client.js CHANGED
@@ -100,7 +100,7 @@ window.__ModuleLoader__.load({
100
100
  .ig-btn:hover:not(:disabled){background:var(--dsw-alias-bg-layer-4, var(--dsw-alias-bg-layer-2));border-color:var(--dsw-alias-label-dimmed, var(--dsw-alias-border-l2))}
101
101
  .ig-btn-primary{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3);border-color:transparent}
102
102
  .ig-btn-primary:hover:not(:disabled){opacity:0.9}
103
- .ig-btn-active{background:var(--dsw-alias-brand-primary, #3b82f6);color:#ffffff;border-color:transparent}
103
+ .ig-btn-active{background:var(--dsw-alias-state-brand-primary);color:var(--dsw-alias-bg-layer-1);border-color:transparent}
104
104
  .ig-btn-danger{color:var(--dsw-alias-state-error-primary);border-color:var(--dsw-alias-state-error-primary)}
105
105
  .ig-btn-danger:hover:not(:disabled){background:var(--dsw-alias-state-error-bg, color-mix(in srgb, var(--dsw-alias-state-error-primary) 12%, transparent));border-color:var(--dsw-alias-state-error-primary)}
106
106
  .ig-btn-disabled{opacity:0.5;cursor:not-allowed}
@@ -119,6 +119,39 @@ window.__ModuleLoader__.load({
119
119
  .ig-matrix-img{width:100%;aspect-ratio:1/1;object-fit:cover;cursor:pointer}
120
120
  .ig-matrix-bar{display:flex;justify-content:space-between;align-items:center;padding:6px 10px;font-size:11px}
121
121
 
122
+ /* Revision History (#148) */
123
+ .ig-rev-bar{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:6px 10px;background:var(--dsw-alias-bg-layer-2);border-radius:8px;border:1px solid var(--dsw-alias-border-l2);margin-bottom:8px;font-size:12px}
124
+ .ig-rev-controls{display:flex;align-items:center;gap:6px}
125
+ .ig-rev-btn{appearance:none;border:1px solid var(--dsw-alias-border-l2);background:transparent;color:var(--dsw-alias-label-primary);border-radius:4px;padding:2px 8px;cursor:pointer;font-size:11px;font-weight:600;transition:all .15s ease}
126
+ .ig-rev-btn:hover:not(:disabled){background:var(--dsw-alias-bg-layer-3);border-color:var(--dsw-alias-label-secondary)}
127
+ .ig-rev-btn:disabled{opacity:0.4;cursor:not-allowed}
128
+ .ig-rev-badge{font-size:11px;color:var(--dsw-alias-label-secondary)}
129
+
130
+ /* Progressive Draft Preview (#147) */
131
+ .ig-progressive-wrap{position:relative;width:100%;overflow:hidden;border-radius:8px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);min-height:160px;display:flex;align-items:center;justify-content:center}
132
+ .ig-draft-img{position:absolute;inset:0;width:100%;height:100%;object-fit:contain;filter:blur(6px);opacity:0.75;transform:scale(1.02);transition:opacity 0.4s ease}
133
+ .ig-final-img{position:relative;width:100%;height:auto;display:block;opacity:1;transition:opacity 0.35s ease}
134
+ .ig-draft-overlay{position:absolute;bottom:8px;left:8px;padding:3px 8px;border-radius:4px;background:color-mix(in srgb, var(--dsw-alias-bg-layer-1) 80%, transparent);font-size:11px;font-weight:600;color:var(--dsw-alias-label-primary);display:flex;align-items:center;gap:6px;backdrop-filter:blur(4px)}
135
+
136
+ @media (prefers-reduced-motion: reduce){
137
+ .ig-draft-img,.ig-final-img{transition:none !important}
138
+ }
139
+
140
+ /* Asset Vault & Studio (#158, #159) */
141
+ .ig-vault-header{display:flex;flex-wrap:wrap;gap:10px;align-items:center;justify-content:space-between;margin-bottom:12px}
142
+ .ig-vault-grid{display:grid;grid-template-columns:repeat(auto-fill, minmax(180px, 1fr));gap:12px}
143
+ .ig-vault-card{border:1px solid var(--dsw-alias-border-l2);border-radius:10px;overflow:hidden;background:var(--dsw-alias-bg-layer-2);display:flex;flex-direction:column;transition:transform .15s ease, border-color .15s ease}
144
+ .ig-vault-card:hover{border-color:var(--dsw-alias-state-brand-primary);transform:translateY(-2px)}
145
+ .ig-vault-thumb{width:100%;aspect-ratio:1/1;object-fit:cover;cursor:pointer;background:var(--dsw-alias-bg-layer-3)}
146
+ .ig-vault-body{padding:8px 10px;display:flex;flex-direction:column;gap:4px;font-size:12px}
147
+ .ig-vault-prompt{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-weight:500;color:var(--dsw-alias-label-primary)}
148
+ .ig-vault-meta{font-size:11px;color:var(--dsw-alias-label-secondary);display:flex;justify-content:space-between}
149
+
150
+ .ig-studio-layout{display:grid;grid-template-columns:320px 1fr;gap:18px;min-height:550px}
151
+ @media (max-width: 800px){.ig-studio-layout{grid-template-columns:1fr}}
152
+ .ig-studio-sidebar{display:flex;flex-direction:column;gap:12px;padding:16px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px}
153
+ .ig-studio-canvas{display:flex;flex-direction:column;gap:12px;padding:16px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);border-radius:12px;align-items:center;justify-content:center;position:relative}
154
+
122
155
  /* Backward-compatibility alias styles for toolview */
123
156
  .fal_head{font-size:13px;font-weight:600;margin-bottom:8px;color:var(--dsw-alias-label-primary)}
124
157
  .ig-prompt{font-size:13px;padding:8px 12px;background:var(--dsw-alias-bg-layer-2);border-radius:6px;border:1px solid var(--dsw-alias-border-l2);margin-bottom:8px;word-break:break-word}
@@ -126,8 +159,7 @@ window.__ModuleLoader__.load({
126
159
  .ig-err{padding:10px;border-radius:6px;background:var(--dsw-alias-state-error-bg, color-mix(in srgb, var(--dsw-alias-state-error-primary) 10%, transparent));color:var(--dsw-alias-state-error-primary);font-size:12px}
127
160
  `
128
161
  document.head.appendChild(style)
129
- }
130
- function createErrorBoundary() {
162
+ } function createErrorBoundary() {
131
163
  if (!React || typeof React.Component !== 'function') {
132
164
  return function NoopBoundary(props) { return props?.children || null }
133
165
  }
@@ -1374,10 +1406,6 @@ window.__ModuleLoader__.load({
1374
1406
  const [responsiveTab, setResponsiveTab] = react.useState(0)
1375
1407
  const [copied, setCopied] = react.useState(false)
1376
1408
 
1377
- react.useEffect(() => {
1378
- ensureCss()
1379
- }, [])
1380
-
1381
1409
  let args = {}
1382
1410
  let prompt = ''
1383
1411
  try {
@@ -1388,6 +1416,24 @@ window.__ModuleLoader__.load({
1388
1416
  }
1389
1417
  } catch (_) { /* malformed tool args */ }
1390
1418
 
1419
+ const initialUrl = parsed.attachment ? attachmentImageUrl(parsed.attachment) : parsed.url
1420
+ const [revisions, setRevisions] = react.useState(() => initialUrl ? [{ url: initialUrl, attachment: parsed.attachment, seed: args.seed, timestamp: Date.now() }] : [])
1421
+ const [activeRev, setActiveRev] = react.useState(0)
1422
+
1423
+ react.useEffect(() => {
1424
+ ensureCss()
1425
+ }, [])
1426
+
1427
+ react.useEffect(() => {
1428
+ if (!initialUrl) return
1429
+ setRevisions((prev) => {
1430
+ if (prev.some((r) => r.url === initialUrl)) return prev
1431
+ const next = [...prev, { url: initialUrl, attachment: parsed.attachment, seed: args.seed, timestamp: Date.now() }]
1432
+ setActiveRev(next.length - 1)
1433
+ return next
1434
+ })
1435
+ }, [initialUrl, args.seed])
1436
+
1391
1437
  const sendActionPrompt = async (text) => {
1392
1438
  try {
1393
1439
  const sessions = props.sessions
@@ -1468,21 +1514,28 @@ window.__ModuleLoader__.load({
1468
1514
  [active.width && active.height ? `${active.width}×${active.height}` : null, active.path ? String(active.path) : null].filter(Boolean).join(' · ')
1469
1515
  ) : null
1470
1516
  )
1471
- } else if (!parsed.attachment && parsed.url) {
1472
- body.push(react.createElement('img', {
1473
- key: 'i',
1474
- src: parsed.url,
1475
- alt: prompt || 'generated image',
1476
- loading: 'lazy',
1477
- style: { maxWidth: '100%', borderRadius: '8px', border: '1px solid var(--dsw-alias-border-l2)' },
1478
- }))
1479
- } else if (parsed.attachment) {
1480
- body.push(react.createElement('img', {
1481
- key: 'i',
1482
- src: attachmentImageUrl(parsed.attachment),
1517
+ } else {
1518
+ // Revision bar (#148)
1519
+ if (revisions.length > 1) {
1520
+ body.push(react.createElement(ImageRevisionBar, {
1521
+ key: 'revs',
1522
+ revisions,
1523
+ activeIndex: activeRev,
1524
+ onSelect: (idx) => setActiveRev(idx),
1525
+ t,
1526
+ }))
1527
+ }
1528
+ // Progressive preview (#147)
1529
+ const currentRev = revisions[activeRev] || {}
1530
+ const displaySrc = currentRev.url || initialUrl
1531
+ const draftSrc = block && (block.draftUrl || block.previewUrl || '')
1532
+ body.push(react.createElement(ProgressiveImagePreview, {
1533
+ key: 'prog-img',
1534
+ src: displaySrc,
1535
+ draftSrc,
1536
+ isRunning: running,
1483
1537
  alt: prompt || 'generated image',
1484
- loading: 'lazy',
1485
- style: { maxWidth: '100%', borderRadius: '8px', border: '1px solid var(--dsw-alias-border-l2)' },
1538
+ t,
1486
1539
  }))
1487
1540
  }
1488
1541
 
@@ -1595,30 +1648,128 @@ window.__ModuleLoader__.load({
1595
1648
  remixDrawer,
1596
1649
  inpaintDrawer
1597
1650
  )
1598
- }
1651
+ } // -------------------------------------------------------------- Image Revision History (#148)
1652
+ function ImageRevisionBar(props) {
1653
+ const revisions = props.revisions
1654
+ const activeIndex = props.activeIndex
1655
+ const onSelect = props.onSelect
1656
+ const t = props.t || ((k) => k)
1657
+
1658
+ if (!Array.isArray(revisions) || revisions.length <= 1) return null
1659
+
1660
+ const current = revisions[activeIndex] || {}
1661
+ const hasPrev = activeIndex > 0
1662
+ const hasNext = activeIndex < revisions.length - 1
1663
+
1664
+ return react.createElement(
1665
+ 'div',
1666
+ { className: 'ig-rev-bar', role: 'navigation', 'aria-label': t('card.revisions') || 'Image revisions' },
1667
+ react.createElement(
1668
+ 'div',
1669
+ { className: 'ig-rev-controls' },
1670
+ react.createElement(
1671
+ 'button',
1672
+ {
1673
+ type: 'button',
1674
+ className: 'ig-rev-btn',
1675
+ disabled: !hasPrev,
1676
+ onClick: () => onSelect(activeIndex - 1),
1677
+ title: t('card.prevRevision') || 'Previous revision',
1678
+ },
1679
+ '◀'
1680
+ ),
1681
+ react.createElement(
1682
+ 'span',
1683
+ { style: { fontWeight: '600', color: 'var(--dsw-alias-label-primary)' } },
1684
+ `v${activeIndex + 1} / ${revisions.length}`
1685
+ ),
1686
+ react.createElement(
1687
+ 'button',
1688
+ {
1689
+ type: 'button',
1690
+ className: 'ig-rev-btn',
1691
+ disabled: !hasNext,
1692
+ onClick: () => onSelect(activeIndex + 1),
1693
+ title: t('card.nextRevision') || 'Next revision',
1694
+ },
1695
+ '▶'
1696
+ )
1697
+ ),
1698
+ react.createElement(
1699
+ 'div',
1700
+ { className: 'ig-rev-badge' },
1701
+ current.seed !== undefined ? `seed: ${current.seed}` : (current.timestamp ? new Date(current.timestamp).toLocaleTimeString() : '')
1702
+ )
1703
+ )
1704
+ } // -------------------------------------------------------------- Progressive Draft Preview (#147)
1705
+ function ProgressiveImagePreview(props) {
1706
+ const src = props.src
1707
+ const draftSrc = props.draftSrc
1708
+ const isRunning = props.isRunning
1709
+ const alt = props.alt || 'Generated image'
1710
+ const t = props.t || ((k) => k)
1711
+
1712
+ const [loaded, setLoaded] = react.useState(false)
1713
+
1714
+ react.useEffect(() => {
1715
+ setLoaded(false)
1716
+ }, [src])
1599
1717
 
1600
- // -------------------------------------------------------------- Dictionaries
1601
- // 105-inpaint-canvas.js — Interactive in-chat canvas drawing overlay for inpainting (#285).
1718
+ if (isRunning && !src && draftSrc) {
1719
+ return react.createElement(
1720
+ 'div',
1721
+ { className: 'ig-progressive-wrap' },
1722
+ react.createElement('img', {
1723
+ src: draftSrc,
1724
+ alt: alt + ' (preview)',
1725
+ className: 'ig-draft-img',
1726
+ }),
1727
+ react.createElement(
1728
+ 'div',
1729
+ { className: 'ig-draft-overlay' },
1730
+ '⚡ ' + (t('card.draftPreview') || 'Rendering draft...')
1731
+ )
1732
+ )
1733
+ }
1734
+
1735
+ if (!src && !draftSrc) return null
1736
+
1737
+ return react.createElement(
1738
+ 'div',
1739
+ { className: 'ig-progressive-wrap', style: { minHeight: 'auto' } },
1740
+ draftSrc && !loaded ? react.createElement('img', {
1741
+ src: draftSrc,
1742
+ alt: alt + ' (draft)',
1743
+ className: 'ig-draft-img',
1744
+ }) : null,
1745
+ src ? react.createElement('img', {
1746
+ src,
1747
+ alt,
1748
+ loading: 'lazy',
1749
+ className: 'ig-final-img',
1750
+ style: { opacity: loaded || !draftSrc ? 1 : 0.8 },
1751
+ onLoad: () => setLoaded(true),
1752
+ }) : null
1753
+ )
1754
+ }// 105-inpaint-canvas.js — Interactive in-chat canvas drawing overlay for inpainting (#285, #301).
1602
1755
 
1603
- function InpaintCanvasOverlay({ react, t, parsed, onClose }) {
1756
+ function InpaintCanvas(props) {
1757
+ const { imgUrl, targetRef, onClose, t } = props
1604
1758
  const canvasRef = react.useRef(null)
1605
1759
  const isDrawingRef = react.useRef(false)
1760
+ const undoStackRef = react.useRef([])
1606
1761
  const [brushSize, setBrushSize] = react.useState(24)
1607
- const [toolMode, setToolMode] = react.useState('brush')
1608
- const [inpaintPrompt, setInpaintPrompt] = react.useState('')
1609
- const [copied, setCopied] = react.useState(false)
1762
+ const [toolMode, setToolMode] = react.useState('brush') // 'brush' | 'eraser'
1610
1763
  const [hasStrokes, setHasStrokes] = react.useState(false)
1611
- const undoStackRef = react.useRef([])
1612
-
1613
- const imgUrl = (parsed && (parsed.url || (parsed.attachment && attachmentImageUrl(parsed.attachment)))) || ''
1614
- const targetRef = (parsed && (parsed.attachment?.attachmentId || parsed.path || parsed.url)) || 'current image'
1764
+ const [copied, setCopied] = react.useState(false)
1765
+ const [inpaintPrompt, setInpaintPrompt] = react.useState('')
1615
1766
 
1616
1767
  const initCanvas = react.useCallback(() => {
1617
1768
  const canvas = canvasRef.current
1618
1769
  if (!canvas) return
1619
1770
  const ctx = canvas.getContext('2d')
1620
1771
  if (!ctx) return
1621
- ctx.fillStyle = '#000000'
1772
+ ctx.fillStyle = 'black'
1622
1773
  ctx.fillRect(0, 0, canvas.width, canvas.height)
1623
1774
  undoStackRef.current = [ctx.getImageData(0, 0, canvas.width, canvas.height)]
1624
1775
  }, [])
@@ -1627,9 +1778,7 @@ window.__ModuleLoader__.load({
1627
1778
  initCanvas()
1628
1779
  }, [initCanvas])
1629
1780
 
1630
- function getPos(e) {
1631
- const canvas = canvasRef.current
1632
- if (!canvas) return { x: 0, y: 0 }
1781
+ function getPos(e, canvas) {
1633
1782
  const rect = canvas.getBoundingClientRect()
1634
1783
  const clientX = e.touches ? e.touches[0].clientX : e.clientX
1635
1784
  const clientY = e.touches ? e.touches[0].clientY : e.clientY
@@ -1643,42 +1792,40 @@ window.__ModuleLoader__.load({
1643
1792
 
1644
1793
  function startDraw(e) {
1645
1794
  if (e.touches && e.touches.length > 1) return
1646
- e.preventDefault()
1647
1795
  const canvas = canvasRef.current
1648
1796
  if (!canvas) return
1649
1797
  const ctx = canvas.getContext('2d')
1650
1798
  if (!ctx) return
1651
-
1652
1799
  isDrawingRef.current = true
1653
- const pos = getPos(e)
1800
+ const pos = getPos(e, canvas)
1801
+ ctx.beginPath()
1802
+ ctx.moveTo(pos.x, pos.y)
1654
1803
  ctx.lineWidth = brushSize
1655
1804
  ctx.lineCap = 'round'
1656
1805
  ctx.lineJoin = 'round'
1657
- const color = toolMode === 'eraser' ? '#000000' : '#ffffff'
1806
+ const color = toolMode === 'eraser' ? 'black' : 'white'
1658
1807
  ctx.strokeStyle = color
1659
1808
  ctx.fillStyle = color
1660
1809
 
1661
- ctx.beginPath()
1662
1810
  ctx.arc(pos.x, pos.y, brushSize / 2, 0, Math.PI * 2)
1663
1811
  ctx.fill()
1664
1812
  ctx.beginPath()
1665
1813
  ctx.moveTo(pos.x, pos.y)
1814
+ setHasStrokes(true)
1666
1815
  }
1667
1816
 
1668
1817
  function draw(e) {
1669
1818
  if (!isDrawingRef.current) return
1670
1819
  if (e.touches && e.touches.length > 1) return
1671
- e.preventDefault()
1672
1820
  const canvas = canvasRef.current
1673
1821
  if (!canvas) return
1674
1822
  const ctx = canvas.getContext('2d')
1675
1823
  if (!ctx) return
1676
-
1677
- const pos = getPos(e)
1824
+ const pos = getPos(e, canvas)
1678
1825
  ctx.lineWidth = brushSize
1679
1826
  ctx.lineCap = 'round'
1680
1827
  ctx.lineJoin = 'round'
1681
- const color = toolMode === 'eraser' ? '#000000' : '#ffffff'
1828
+ const color = toolMode === 'eraser' ? 'black' : 'white'
1682
1829
  ctx.strokeStyle = color
1683
1830
  ctx.lineTo(pos.x, pos.y)
1684
1831
  ctx.stroke()
@@ -1852,7 +1999,7 @@ window.__ModuleLoader__.load({
1852
1999
  borderRadius: '8px',
1853
2000
  overflow: 'hidden',
1854
2001
  border: '1px solid var(--dsw-alias-border-l2)',
1855
- background: '#000000',
2002
+ background: 'black',
1856
2003
  margin: '4px 0',
1857
2004
  userSelect: 'none',
1858
2005
  touchAction: 'none',
@@ -1918,9 +2065,11 @@ window.__ModuleLoader__.load({
1918
2065
  )
1919
2066
  )
1920
2067
  }
1921
- // 106-style-matrix-view.js — 2×2 Style Matrix toolview with Blind A/B Compare (#286).
2068
+ var InpaintCanvasOverlay = InpaintCanvas
2069
+ // 106-style-matrix-view.js — 2×2 Style Matrix toolview with Blind A/B Compare (#286, #297, #301).
1922
2070
 
1923
2071
  function StyleMatrixCard(props) {
2072
+ const t = props.t || ((k) => k)
1924
2073
  const block = props.block || {}
1925
2074
  const parsed = react.useMemo(() => {
1926
2075
  const raw = block.output || block.text || ''
@@ -2013,7 +2162,7 @@ window.__ModuleLoader__.load({
2013
2162
  left: 0,
2014
2163
  right: 0,
2015
2164
  bottom: 0,
2016
- background: 'rgba(0,0,0,0.85)',
2165
+ background: 'var(--dsw-alias-bg-overlay, rgb(0 0 0 / 85%))',
2017
2166
  display: 'flex',
2018
2167
  alignItems: 'center',
2019
2168
  justifyContent: 'center',
@@ -2030,11 +2179,116 @@ window.__ModuleLoader__.load({
2030
2179
  ) : null
2031
2180
  )
2032
2181
  }
2182
+ // -------------------------------------------------------------- Theme Pair Toolview (#189, #296)
2183
+ function ThemePairView(props) {
2184
+ const block = props.block
2185
+ const t = props.t || ((k) => k)
2186
+ const [copiedHtml, setCopiedHtml] = react.useState(false)
2187
+ const [copiedCss, setCopiedCss] = react.useState(false)
2188
+ const [themeTab, setThemeTab] = react.useState('light')
2189
+
2190
+ const raw = (block && (block.call ? block.call.argsRaw : block.argsRaw)) || ''
2191
+ let args = {}
2192
+ try { if (raw) args = JSON.parse(raw) } catch (_) { /* invalid json */ }
2193
+
2194
+ let data = {}
2195
+ try {
2196
+ const out = block && (block.output || block.result || block.text || '')
2197
+ if (typeof out === 'object') data = out
2198
+ else if (typeof out === 'string') data = JSON.parse(out)
2199
+ } catch (_) { /* invalid json */ }
2200
+
2201
+ const light = data.light || {}
2202
+ const dark = data.dark || {}
2203
+ const htmlSnippet = data.html_snippet || ''
2204
+ const cssSnippet = data.css_snippet || ''
2205
+
2206
+ const copyCode = (text, isHtml) => {
2207
+ if (typeof navigator !== 'undefined' && navigator.clipboard) {
2208
+ navigator.clipboard.writeText(text)
2209
+ if (isHtml) {
2210
+ setCopiedHtml(true)
2211
+ setTimeout(() => setCopiedHtml(false), 2000)
2212
+ } else {
2213
+ setCopiedCss(true)
2214
+ setTimeout(() => setCopiedCss(false), 2000)
2215
+ }
2216
+ }
2217
+ }
2218
+
2219
+ const activeImg = themeTab === 'light' ? light : dark
2220
+
2221
+ return react.createElement(
2222
+ 'div',
2223
+ { className: 'ig-page', style: { padding: '4px 0', gap: '10px' } },
2224
+ react.createElement('div', { className: 'fal_head' }, '🌓 ' + (t('themePair.title') || 'Theme Pair Graphic')),
2225
+ args.prompt ? react.createElement('div', { className: 'ig-prompt' }, args.prompt) : null,
2226
+ react.createElement(
2227
+ 'div',
2228
+ { className: 'ig-tabs' },
2229
+ react.createElement(
2230
+ 'button',
2231
+ {
2232
+ type: 'button',
2233
+ className: themeTab === 'light' ? 'ig-tab-btn ig-tab-btn-active' : 'ig-tab-btn',
2234
+ onClick: () => setThemeTab('light'),
2235
+ },
2236
+ '☀️ ' + (t('themePair.light') || 'Light Theme')
2237
+ ),
2238
+ react.createElement(
2239
+ 'button',
2240
+ {
2241
+ type: 'button',
2242
+ className: themeTab === 'dark' ? 'ig-tab-btn ig-tab-btn-active' : 'ig-tab-btn',
2243
+ onClick: () => setThemeTab('dark'),
2244
+ },
2245
+ '🌙 ' + (t('themePair.dark') || 'Dark Theme')
2246
+ )
2247
+ ),
2248
+ activeImg.url || activeImg.path ? react.createElement('img', {
2249
+ src: activeImg.url || activeImg.path,
2250
+ alt: args.prompt || 'theme image',
2251
+ loading: 'lazy',
2252
+ style: {
2253
+ width: '100%',
2254
+ maxHeight: '400px',
2255
+ objectFit: 'contain',
2256
+ borderRadius: '8px',
2257
+ border: '1px solid var(--dsw-alias-border-l2)',
2258
+ background: themeTab === 'light' ? 'var(--dsw-alias-bg-layer-1)' : 'var(--dsw-alias-bg-layer-3)',
2259
+ },
2260
+ }) : null,
2261
+ htmlSnippet ? react.createElement(
2262
+ 'div',
2263
+ { style: { display: 'flex', gap: '8px', marginTop: '4px' } },
2264
+ react.createElement(
2265
+ 'button',
2266
+ {
2267
+ type: 'button',
2268
+ className: 'ig-btn',
2269
+ onClick: () => copyCode(htmlSnippet, true),
2270
+ },
2271
+ copiedHtml ? '✓ ' + (t('themePair.copiedHtml') || 'Copied HTML') : '📋 ' + (t('themePair.copyHtml') || 'Copy <picture> HTML')
2272
+ ),
2273
+ cssSnippet ? react.createElement(
2274
+ 'button',
2275
+ {
2276
+ type: 'button',
2277
+ className: 'ig-btn',
2278
+ onClick: () => copyCode(cssSnippet, false),
2279
+ },
2280
+ copiedCss ? '✓ ' + (t('themePair.copiedCss') || 'Copied CSS') : '🎨 ' + (t('themePair.copyCss') || 'Copy Theme CSS')
2281
+ ) : null
2282
+ ) : null
2283
+ )
2284
+ }
2285
+ var ThemePairToolView = ThemePairView
2033
2286
  const en = {
2034
2287
  'settings.navLabel': 'Image Studio',
2035
2288
  'settings.title': 'Image Studio',
2036
2289
  'settings.description': 'AI Image generation, inpainting, variations, upscale & vectorization.',
2037
- 'settings.unavailable': 'Plugin settings are initializing. They will appear automatically in a few seconds.',
2290
+ 'settings.unavailable': 'Plugin settings are unavailable: the host settings namespace is not ready.',
2291
+ 'settings.loading': 'Loading plugin settings…',
2038
2292
  'settings.unsaved': 'Unsaved changes',
2039
2293
  'settings.saving': 'Saving…',
2040
2294
  'settings.save': 'Save settings',
@@ -2258,6 +2512,22 @@ window.__ModuleLoader__.load({
2258
2512
  'f.cacheTtlDays': 'Cache Retention (Legacy)',
2259
2513
  'f.cacheTtlDaysHint': 'Legacy name for retention period in days.',
2260
2514
  'p.cacheTtlDays': '0',
2515
+
2516
+ 'studio.title': 'Image Studio',
2517
+ 'studio.canvas': 'Studio Canvas',
2518
+ 'studio.vault': 'Asset Vault',
2519
+ 'studio.parameters': 'Generation Parameters',
2520
+ 'studio.generate': 'Generate in Chat',
2521
+ 'studio.recent': 'Recent Generations',
2522
+ 'vault.title': 'Asset Browser & Vault',
2523
+ 'vault.search': 'Search prompt or tags...',
2524
+ 'vault.loadMore': 'Load more assets',
2525
+ 'vault.empty': 'No assets found in vault.',
2526
+ 'vault.inspector': 'Asset Inspector',
2527
+ 'vault.reroll': 'Re-roll with seed',
2528
+ 'vault.insert': 'Insert in Chat',
2529
+ 'vault.download': 'Download',
2530
+ 'vault.delete': 'Delete',
2261
2531
  }
2262
2532
 
2263
2533
  const zh = {
@@ -2331,6 +2601,7 @@ window.__ModuleLoader__.load({
2331
2601
  'settings.reset': '重置',
2332
2602
  'settings.invalidNumber': '请输入有效数值',
2333
2603
  'settings.unavailable': '设置命名空间当前不可用。',
2604
+ 'settings.loading': '正在加载插件设置…',
2334
2605
 
2335
2606
  'f.enabled': '启用图像生成',
2336
2607
  'f.enabledHint': '关闭后将禁用所有图像生成及处理工具。',
@@ -2476,49 +2747,38 @@ window.__ModuleLoader__.load({
2476
2747
  'f.cacheTtlDays': '缓存生命周期 (Legacy)',
2477
2748
  'f.cacheTtlDaysHint': '已废弃的缓存保留时长参数。',
2478
2749
  'p.cacheTtlDays': '0',
2750
+
2751
+ 'studio.title': '图像创作工作台',
2752
+ 'studio.canvas': '创作画布',
2753
+ 'studio.vault': '素材资产库',
2754
+ 'studio.parameters': '生成参数配置',
2755
+ 'studio.generate': '在对话中生成',
2756
+ 'studio.recent': '最近生成历史',
2757
+ 'vault.title': '图像素材库与浏览器',
2758
+ 'vault.search': '搜索提示词或标签...',
2759
+ 'vault.loadMore': '加载更多素材',
2760
+ 'vault.empty': '素材库中暂无图像。',
2761
+ 'vault.inspector': '素材检查器',
2762
+ 'vault.reroll': '使用此种子重新生成',
2763
+ 'vault.insert': '插入到当前对话',
2764
+ 'vault.download': '下载图像',
2765
+ 'vault.delete': '删除',
2479
2766
  }
2480
2767
 
2481
2768
  // -------------------------------------------------------------- Registration & Apply
2482
- // Dual compatibility with DSH 0.1.5-rc.3 (settingsScope) and 0.1.6+ (configForms) (#291)
2769
+ // Dual compatibility with DSH 0.1.5-rc.3 (settingsScope) and 0.1.6+ (configForms) (#291, #295)
2483
2770
  const inject = ['slots', 'locale', 'sessions']
2484
2771
 
2485
2772
  function resolveActiveScope(ctx) {
2486
2773
  if (!ctx) return null
2487
- if (ctx.get && typeof ctx.get === 'function') {
2488
- try {
2489
- const lan = ctx.get('lanSettings')
2490
- if (lan && typeof lan.get === 'function') {
2491
- const s = lan.get(SETTINGS_NS)
2492
- if (s) return s
2493
- }
2494
- } catch (_) { /* scope unavailable */ }
2495
- try {
2496
- const cf = ctx.get('configForms')
2497
- if (cf && typeof cf.get === 'function') {
2498
- const s = cf.get(SETTINGS_NS)
2499
- if (s) return s
2500
- }
2501
- } catch (_) { /* scope unavailable */ }
2502
- try {
2503
- const ss = ctx.get('settingsScope')
2504
- if (ss && typeof ss.bind === 'function') {
2505
- const s = ss.bind({ namespace: SETTINGS_NS })
2506
- if (s) return s
2507
- }
2508
- } catch (_) { /* scope unavailable */ }
2509
- }
2510
- if (ctx.configForms && typeof ctx.configForms.get === 'function') {
2511
- try {
2512
- const s = ctx.configForms.get(SETTINGS_NS)
2513
- if (s) return s
2514
- } catch (_) { /* scope unavailable */ }
2515
- }
2516
- if (ctx.settingsScope && typeof ctx.settingsScope.bind === 'function') {
2517
- try {
2518
- const s = ctx.settingsScope.bind({ namespace: SETTINGS_NS })
2519
- if (s) return s
2520
- } catch (_) { /* scope unavailable */ }
2521
- }
2774
+ try {
2775
+ const lan = (ctx.get && ctx.get('lanSettings')) || ctx.lanSettings
2776
+ if (lan?.get) return lan.get(SETTINGS_NS)
2777
+ const cf = (ctx.get && ctx.get('configForms')) || ctx.configForms
2778
+ if (cf?.get) return cf.get(SETTINGS_NS)
2779
+ const ss = (ctx.get && ctx.get('settingsScope')) || ctx.settingsScope
2780
+ if (ss?.bind) return ss.bind({ namespace: SETTINGS_NS })
2781
+ } catch (_) { /* scope unavailable */ }
2522
2782
  return null
2523
2783
  }
2524
2784
 
@@ -2526,70 +2786,93 @@ window.__ModuleLoader__.load({
2526
2786
  const listeners = new Set()
2527
2787
  let activeTarget = resolveActiveScope(ctx)
2528
2788
  let activeUnsub = null
2789
+ let httpSnapshot = { status: 'loading', writable: true, value: {} }
2790
+
2791
+ const notify = () => { for (const fn of listeners) fn() }
2792
+
2793
+ const syncHttpConfig = () => {
2794
+ if (typeof fetch !== 'function') return
2795
+ fetch('/dsh-image-gen/config', { cache: 'no-store' })
2796
+ .then((r) => r.json())
2797
+ .then((data) => {
2798
+ if (data && data.ok && data.config) {
2799
+ httpSnapshot = { status: 'ready', writable: true, value: data.config }
2800
+ notify()
2801
+ }
2802
+ })
2803
+ .catch(() => {})
2804
+ }
2805
+ syncHttpConfig()
2529
2806
 
2530
2807
  const bindTarget = (target) => {
2531
2808
  if (!target || target === activeTarget) return
2532
2809
  if (typeof activeUnsub === 'function') {
2533
- try { activeUnsub() } catch (_) { /* scope unavailable */ }
2810
+ try { activeUnsub() } catch { /* ignore */ }
2534
2811
  activeUnsub = null
2535
2812
  }
2536
2813
  activeTarget = target
2537
2814
  if (typeof activeTarget.subscribe === 'function') {
2538
2815
  try {
2539
- activeUnsub = activeTarget.subscribe(() => {
2540
- for (const fn of listeners) fn()
2541
- })
2542
- } catch (_) { /* scope unavailable */ }
2816
+ activeUnsub = activeTarget.subscribe(notify)
2817
+ } catch { /* ignore */ }
2543
2818
  }
2544
- for (const fn of listeners) fn()
2819
+ notify()
2545
2820
  }
2546
2821
 
2547
- if (activeTarget && typeof activeTarget.subscribe === 'function') {
2548
- try {
2549
- activeUnsub = activeTarget.subscribe(() => {
2550
- for (const fn of listeners) fn()
2551
- })
2552
- } catch (_) { /* scope unavailable */ }
2822
+ if (activeTarget?.subscribe) {
2823
+ try { activeUnsub = activeTarget.subscribe(notify) } catch { /* ignore */ }
2553
2824
  }
2554
2825
 
2555
2826
  if (typeof ctx?.inject === 'function') {
2556
2827
  try {
2557
2828
  ctx.inject(['configForms'], (sctx) => {
2558
- try {
2559
- const s = sctx?.configForms?.get?.(SETTINGS_NS)
2560
- if (s) bindTarget(s)
2561
- } catch (_) { /* scope unavailable */ }
2829
+ const s = sctx?.configForms?.get?.(SETTINGS_NS)
2830
+ if (s) bindTarget(s)
2562
2831
  })
2563
- } catch (_) { /* scope unavailable */ }
2564
- try {
2565
2832
  ctx.inject(['settingsScope'], (sctx) => {
2566
- try {
2567
- const s = sctx?.settingsScope?.bind?.({ namespace: SETTINGS_NS })
2568
- if (s) bindTarget(s)
2569
- } catch (_) { /* scope unavailable */ }
2833
+ const s = sctx?.settingsScope?.bind?.({ namespace: SETTINGS_NS })
2834
+ if (s) bindTarget(s)
2570
2835
  })
2571
- } catch (_) { /* scope unavailable */ }
2836
+ } catch { /* ignore */ }
2572
2837
  }
2573
2838
 
2574
2839
  return {
2575
2840
  getSnapshot() {
2576
2841
  const target = activeTarget || resolveActiveScope(ctx)
2577
- if (target && typeof target.getSnapshot === 'function') {
2578
- return target.getSnapshot()
2579
- }
2580
- return { status: 'unavailable', writable: false, value: {} }
2842
+ const snap = target?.getSnapshot ? target.getSnapshot() : null
2843
+ if (snap?.status === 'ready' && snap.writable) return snap
2844
+ if (httpSnapshot.status === 'ready') return httpSnapshot
2845
+ return snap || httpSnapshot
2581
2846
  },
2582
2847
  async set(key, val) {
2583
2848
  const target = activeTarget || resolveActiveScope(ctx)
2584
- if (target && typeof target.set === 'function') {
2585
- return target.set(key, val)
2586
- }
2849
+ const snap = target?.getSnapshot ? target.getSnapshot() : null
2850
+ if (snap?.status === 'ready' && snap.writable && target.set) return target.set(key, val)
2851
+ httpSnapshot = { ...httpSnapshot, value: { ...httpSnapshot.value, [key]: val } }
2852
+ notify()
2853
+ try {
2854
+ await fetch('/dsh-image-gen/config', {
2855
+ method: 'PUT',
2856
+ headers: { 'Content-Type': 'application/json' },
2857
+ body: JSON.stringify({ [key]: val }),
2858
+ })
2859
+ } catch { /* ignore */ }
2587
2860
  },
2588
2861
  async delete(key) {
2589
2862
  const target = activeTarget || resolveActiveScope(ctx)
2590
- if (target && typeof target.delete === 'function') {
2591
- return target.delete(key)
2592
- }
2863
+ const snap = target?.getSnapshot ? target.getSnapshot() : null
2864
+ if (snap?.status === 'ready' && snap.writable && target.delete) return target.delete(key)
2865
+ const nextVal = { ...httpSnapshot.value }
2866
+ delete nextVal[key]
2867
+ httpSnapshot = { ...httpSnapshot, value: nextVal }
2868
+ notify()
2869
+ try {
2870
+ await fetch('/dsh-image-gen/config', {
2871
+ method: 'PUT',
2872
+ headers: { 'Content-Type': 'application/json' },
2873
+ body: JSON.stringify({ [key]: undefined }),
2874
+ })
2875
+ } catch { /* ignore */ }
2593
2876
  },
2594
2877
  subscribe(fn) {
2595
2878
  listeners.add(fn)
@@ -2618,27 +2901,17 @@ window.__ModuleLoader__.load({
2618
2901
  if (typeof ctx.slots.inject === 'function') {
2619
2902
  try {
2620
2903
  ctx.slots.inject(slotName, () => {
2621
- try {
2622
- return registerFn()
2623
- } catch (err) {
2624
- console.warn('[dsh-image-gen] Error registering slot ' + slotName + ':', err)
2625
- }
2904
+ try { return registerFn() } catch (err) { console.warn('[dsh-image-gen] Slot err ' + slotName + ':', err) }
2626
2905
  })
2627
2906
  return
2628
- } catch (err) {
2629
- console.warn('[dsh-image-gen] Failed to inject slot ' + slotName + ':', err)
2630
- }
2907
+ } catch (err) { console.warn('[dsh-image-gen] Failed inject ' + slotName + ':', err) }
2631
2908
  }
2632
2909
  if (typeof ctx.slots.register === 'function') {
2633
- try {
2634
- registerFn()
2635
- } catch (err) {
2636
- console.warn('[dsh-image-gen] Failed direct registration for ' + slotName + ':', err)
2637
- }
2910
+ try { registerFn() } catch (err) { console.warn('[dsh-image-gen] Direct reg err ' + slotName + ':', err) }
2638
2911
  }
2639
2912
  }
2640
2913
 
2641
- // Register toolviews for all 8 visual tools
2914
+ // Register toolviews for all visual tools
2642
2915
  const toolviewTools = [
2643
2916
  'generate_image',
2644
2917
  'edit_image',
@@ -2682,6 +2955,17 @@ window.__ModuleLoader__.load({
2682
2955
  )
2683
2956
  )
2684
2957
 
2958
+ registerSlotWhenReady('tool.call.toolview', () =>
2959
+ ctx.slots.register(
2960
+ {
2961
+ name: 'tool.call.toolview',
2962
+ key: 'generate_theme_pair',
2963
+ locale: NS,
2964
+ inject: () => ({ sessions: ctx.sessions }),
2965
+ },
2966
+ (props) => react.createElement(ErrorBoundary, null, react.createElement(ThemePairView, props))
2967
+ )
2968
+ )
2685
2969
 
2686
2970
  // Native Sidebar Right Pane Tab & BetterSidebar
2687
2971
  if (typeof ctx.inject === 'function') {
@@ -2691,16 +2975,16 @@ window.__ModuleLoader__.load({
2691
2975
  if (!tabs || typeof tabs.register !== 'function') return
2692
2976
  try {
2693
2977
  const def = {
2694
- id: '@goodandready/dsh-image-gen:gallery',
2695
- kind: 'image-gallery',
2978
+ id: '@goodandready/dsh-image-gen:studio',
2979
+ kind: 'image-studio',
2696
2980
  priority: 'extension',
2697
- title: () => 'Gallery',
2981
+ title: () => 'Image Studio',
2698
2982
  guide: [
2699
2983
  {
2700
2984
  order: 45,
2701
- title: () => 'Image Studio Gallery',
2702
- description: () => 'Browse and inspect recent image generations',
2703
- icon: () => react.createElement('span', null, '🖼️'),
2985
+ title: () => 'Image Studio & Vault',
2986
+ description: () => 'Interactive visual studio & asset browser',
2987
+ icon: () => react.createElement('span', null, '🎨'),
2704
2988
  },
2705
2989
  ],
2706
2990
  }
@@ -2720,7 +3004,7 @@ window.__ModuleLoader__.load({
2720
3004
  locale: NS,
2721
3005
  inject: () => ({ ctx }),
2722
3006
  },
2723
- (props) => react.createElement(ErrorBoundary, null, react.createElement(GalleryView, { ...props, ctx }))
3007
+ (props) => react.createElement(ErrorBoundary, null, react.createElement(ImageStudioView, { ...props, ctx }))
2724
3008
  )
2725
3009
  } catch (e) {
2726
3010
  console.warn('[dsh-image-gen] native sidebar pane tab register failed', e)
@@ -2736,7 +3020,7 @@ window.__ModuleLoader__.load({
2736
3020
  console.warn('[dsh-image-gen] native sidebar registration failed', e)
2737
3021
  }
2738
3022
  })
2739
- } catch (e) { /* native sidebar unavailable */ }
3023
+ } catch (_) { /* native sidebar unavailable */ }
2740
3024
 
2741
3025
  try {
2742
3026
  ctx.inject(['betterSidebar'], (sctx) => {
@@ -2748,13 +3032,13 @@ window.__ModuleLoader__.load({
2748
3032
  title: () => 'Gallery',
2749
3033
  icon: () => react.createElement('span', null, '🖼️'),
2750
3034
  order: 45,
2751
- component: ({ scope }) => react.createElement(ErrorBoundary, null, react.createElement(GalleryView, { ctx, scope })),
3035
+ component: ({ scope }) => react.createElement(ErrorBoundary, null, react.createElement(ImageStudioView, { ctx, scope })),
2752
3036
  })
2753
3037
  } catch (e) {
2754
3038
  console.warn('[dsh-image-gen] betterSidebar registerTab failed', e)
2755
3039
  }
2756
3040
  })
2757
- } catch (e) { /* betterSidebar unavailable */ }
3041
+ } catch (_) { /* betterSidebar unavailable */ }
2758
3042
  }
2759
3043
 
2760
3044
  // Conversation header utilities chip (quick-access gallery button in chat header)
@@ -2771,25 +3055,7 @@ window.__ModuleLoader__.load({
2771
3055
  )
2772
3056
  )
2773
3057
 
2774
- // List seat (plugins.item): the seat the Plugins page renders as the plugin's own
2775
- // page with its configuration. The label is a static string on purpose — it is
2776
- // resolved while the page renders, and a locale lookup there would take the whole
2777
- // client batch down with it.
2778
- registerSlotWhenReady('plugins.item', () =>
2779
- ctx.slots.register(
2780
- {
2781
- name: 'plugins.item',
2782
- id: 'dsh-image-gen',
2783
- order: 60,
2784
- label: () => 'Image Studio',
2785
- locale: NS,
2786
- inject: () => cardOnce().inject(),
2787
- },
2788
- (props) => react.createElement(ErrorBoundary, null, react.createElement(FalSettingsCard, props))
2789
- )
2790
- )
2791
-
2792
- // Settings card item (legacy seat, kept as a fallback)
3058
+ // Settings card item — sole slot registration for settings card (#208, #300)
2793
3059
  registerSlotWhenReady('settings.plugin.item', () =>
2794
3060
  ctx.slots.register(
2795
3061
  {
@@ -2802,7 +3068,574 @@ window.__ModuleLoader__.load({
2802
3068
  )
2803
3069
  )
2804
3070
  }
3071
+ // -------------------------------------------------------------- Asset Vault View (#159)
3072
+ function AssetVaultView(props) {
3073
+ const { ctx, onSelectAsset } = props
3074
+ const [items, setItems] = react.useState([])
3075
+ const [loading, setLoading] = react.useState(false)
3076
+ const [q, setQ] = react.useState('')
3077
+ const [provider, setProvider] = react.useState('all')
3078
+ const [aspect, setAspect] = react.useState('all')
3079
+ const [sort, setSort] = react.useState('newest')
3080
+ const [offset, setOffset] = react.useState(0)
3081
+ const [hasMore, setHasMore] = react.useState(false)
3082
+ const [selected, setSelected] = react.useState(null)
3083
+ const [feedback, setFeedback] = react.useState('')
3084
+
3085
+ const showFeedback = (msg) => {
3086
+ setFeedback(msg)
3087
+ setTimeout(() => setFeedback(''), 2500)
3088
+ }
3089
+
3090
+ const loadItems = (newOffset = 0, append = false) => {
3091
+ setLoading(true)
3092
+ const params = new URLSearchParams({
3093
+ q,
3094
+ provider: provider === 'all' ? '' : provider,
3095
+ aspect: aspect === 'all' ? '' : aspect,
3096
+ sort,
3097
+ offset: String(newOffset),
3098
+ limit: '24',
3099
+ })
3100
+ fetch('/dsh-image-gen/vault?' + params.toString())
3101
+ .then((r) => r.json())
3102
+ .then((data) => {
3103
+ setLoading(false)
3104
+ if (data && data.ok) {
3105
+ setItems(append ? (prev) => [...prev, ...data.items] : data.items)
3106
+ setOffset(data.offset + data.items.length)
3107
+ setHasMore(data.hasMore)
3108
+ }
3109
+ })
3110
+ .catch(() => setLoading(false))
3111
+ }
3112
+
3113
+ react.useEffect(() => {
3114
+ loadItems(0, false)
3115
+ }, [q, provider, aspect, sort])
3116
+
3117
+ const handleDelete = (id) => {
3118
+ if (!window.confirm('Are you sure you want to delete this asset?')) return
3119
+ fetch('/dsh-image-gen/vault?id=' + encodeURIComponent(id), { method: 'DELETE' })
3120
+ .then((r) => r.json())
3121
+ .then((res) => {
3122
+ if (res.ok) {
3123
+ setItems((prev) => prev.filter((it) => it.id !== id && it.attachmentId !== id))
3124
+ if (selected && (selected.id === id || selected.attachmentId === id)) {
3125
+ setSelected(null)
3126
+ }
3127
+ showFeedback('Asset deleted')
3128
+ }
3129
+ })
3130
+ .catch(() => showFeedback('Failed to delete asset'))
3131
+ }
3132
+
3133
+ const handleInsert = (item) => {
3134
+ const link = '![' + (item.prompt || 'Generated Image') + '](' + (item.url || item.thumbnailUrl) + ')'
3135
+ if (typeof navigator !== 'undefined' && navigator.clipboard) {
3136
+ navigator.clipboard.writeText(link)
3137
+ showFeedback('Markdown snippet copied to clipboard!')
3138
+ }
3139
+ }
3140
+
3141
+ const handleReroll = (item) => {
3142
+ if (typeof onSelectAsset === 'function') {
3143
+ onSelectAsset(item)
3144
+ return
3145
+ }
3146
+ const cmd = '/image ' + (item.prompt || '') + (item.seed !== undefined ? ' --seed ' + item.seed : '')
3147
+ if (typeof navigator !== 'undefined' && navigator.clipboard) {
3148
+ navigator.clipboard.writeText(cmd)
3149
+ showFeedback('Prompt & seed copied to clipboard!')
3150
+ }
3151
+ }
3152
+
3153
+ return react.createElement(
3154
+ 'div',
3155
+ { className: 'ig-vault-container', style: { display: 'flex', flexDirection: 'column', gap: '14px', width: '100%' } },
3156
+ react.createElement(
3157
+ 'div',
3158
+ { className: 'ig-vault-header' },
3159
+ react.createElement('input', {
3160
+ type: 'text',
3161
+ className: 'ig-input',
3162
+ style: { flex: '1 1 200px', padding: '6px 12px', borderRadius: '6px', border: '1px solid var(--dsw-alias-border-l2)', background: 'var(--dsw-alias-bg-layer-2)', color: 'var(--dsw-alias-label-primary)' },
3163
+ placeholder: 'Search prompt or tags...',
3164
+ value: q,
3165
+ onChange: (e) => setQ(e.target.value),
3166
+ }),
3167
+ react.createElement(
3168
+ 'select',
3169
+ {
3170
+ className: 'ig-select',
3171
+ value: provider,
3172
+ onChange: (e) => setProvider(e.target.value),
3173
+ style: { padding: '6px 10px', borderRadius: '6px', border: '1px solid var(--dsw-alias-border-l2)', background: 'var(--dsw-alias-bg-layer-2)', color: 'var(--dsw-alias-label-primary)' },
3174
+ },
3175
+ react.createElement('option', { value: 'all' }, 'All Providers'),
3176
+ react.createElement('option', { value: 'fal' }, 'FAL.ai'),
3177
+ react.createElement('option', { value: 'openai' }, 'OpenAI'),
3178
+ react.createElement('option', { value: 'local' }, 'Local (ComfyUI)')
3179
+ ),
3180
+ react.createElement(
3181
+ 'select',
3182
+ {
3183
+ className: 'ig-select',
3184
+ value: aspect,
3185
+ onChange: (e) => setAspect(e.target.value),
3186
+ style: { padding: '6px 10px', borderRadius: '6px', border: '1px solid var(--dsw-alias-border-l2)', background: 'var(--dsw-alias-bg-layer-2)', color: 'var(--dsw-alias-label-primary)' },
3187
+ },
3188
+ react.createElement('option', { value: 'all' }, 'All Ratios'),
3189
+ react.createElement('option', { value: '1:1' }, '1:1 Square'),
3190
+ react.createElement('option', { value: '16:9' }, '16:9 Landscape'),
3191
+ react.createElement('option', { value: '9:16' }, '9:16 Portrait'),
3192
+ react.createElement('option', { value: '4:3' }, '4:3 Standard'),
3193
+ react.createElement('option', { value: '21:9' }, '21:9 Ultrawide')
3194
+ ),
3195
+ react.createElement(
3196
+ 'button',
3197
+ {
3198
+ type: 'button',
3199
+ className: 'ig-tab-btn',
3200
+ onClick: () => setSort(sort === 'newest' ? 'oldest' : 'newest'),
3201
+ style: { border: '1px solid var(--dsw-alias-border-l2)', padding: '6px 10px', borderRadius: '6px', background: 'var(--dsw-alias-bg-layer-2)', color: 'var(--dsw-alias-label-primary)', cursor: 'pointer' },
3202
+ },
3203
+ sort === 'newest' ? '↓ Newest' : '↑ Oldest'
3204
+ )
3205
+ ),
3206
+ feedback && react.createElement('div', { style: { padding: '6px 12px', borderRadius: '6px', background: 'color-mix(in srgb, var(--dsw-alias-state-brand-primary) 15%, transparent)', color: 'var(--dsw-alias-state-brand-primary)', fontSize: '12px' } }, feedback),
3207
+ items.length === 0 && !loading
3208
+ ? react.createElement('div', { style: { padding: '32px', textAlign: 'center', color: 'var(--dsw-alias-label-secondary)', fontSize: '13px' } }, 'No assets found in vault.')
3209
+ : react.createElement(
3210
+ 'div',
3211
+ { className: 'ig-vault-grid' },
3212
+ items.map((item) =>
3213
+ react.createElement(
3214
+ 'div',
3215
+ { key: item.id || item.attachmentId, className: 'ig-vault-card' },
3216
+ react.createElement('img', {
3217
+ src: item.thumbnailUrl || item.url,
3218
+ className: 'ig-vault-thumb',
3219
+ loading: 'lazy',
3220
+ onClick: () => setSelected(item),
3221
+ }),
3222
+ react.createElement(
3223
+ 'div',
3224
+ { className: 'ig-vault-body' },
3225
+ react.createElement('div', { className: 'ig-vault-prompt', title: item.prompt }, item.prompt || 'Untitled'),
3226
+ react.createElement(
3227
+ 'div',
3228
+ { className: 'ig-vault-meta' },
3229
+ react.createElement('span', null, item.provider || 'local'),
3230
+ item.seed !== undefined && react.createElement('span', null, 'Seed ' + item.seed)
3231
+ ),
3232
+ react.createElement(
3233
+ 'div',
3234
+ { style: { display: 'flex', gap: '4px', marginTop: '6px' } },
3235
+ react.createElement('button', { type: 'button', className: 'ig-rev-btn', style: { flex: 1 }, onClick: () => handleReroll(item), title: 'Re-roll with seed' }, '🎲 Re-roll'),
3236
+ react.createElement('button', { type: 'button', className: 'ig-rev-btn', style: { flex: 1 }, onClick: () => handleInsert(item), title: 'Insert into chat' }, '💬 Insert'),
3237
+ react.createElement('button', { type: 'button', className: 'ig-rev-btn', onClick: () => handleDelete(item.id || item.attachmentId), title: 'Delete' }, '🗑️')
3238
+ )
3239
+ )
3240
+ )
3241
+ )
3242
+ ),
3243
+ hasMore &&
3244
+ react.createElement(
3245
+ 'div',
3246
+ { style: { textAlign: 'center', margin: '14px 0' } },
3247
+ react.createElement(
3248
+ 'button',
3249
+ {
3250
+ type: 'button',
3251
+ className: 'ig-tab-btn',
3252
+ onClick: () => loadItems(offset, true),
3253
+ disabled: loading,
3254
+ style: { padding: '8px 20px', borderRadius: '6px', border: '1px solid var(--dsw-alias-border-l2)', background: 'var(--dsw-alias-bg-layer-2)', color: 'var(--dsw-alias-label-primary)', cursor: 'pointer' },
3255
+ },
3256
+ loading ? 'Loading...' : 'Load more assets'
3257
+ )
3258
+ ),
3259
+ selected &&
3260
+ react.createElement(
3261
+ 'div',
3262
+ {
3263
+ style: { position: 'fixed', inset: 0, background: 'var(--dsw-alias-overlay-backdrop, color-mix(in srgb, var(--dsw-alias-bg-layer-0, black) 70%, transparent))', zIndex: 10000, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '20px' },
3264
+ onClick: () => setSelected(null),
3265
+ },
3266
+ react.createElement(
3267
+ 'div',
3268
+ {
3269
+ style: { background: 'var(--dsw-alias-bg-layer-3)', border: '1px solid var(--dsw-alias-border-l2)', borderRadius: '12px', maxWidth: '580px', width: '100%', maxHeight: '90vh', overflowY: 'auto', padding: '18px', display: 'flex', flexDirection: 'column', gap: '12px' },
3270
+ onClick: (e) => e.stopPropagation(),
3271
+ },
3272
+ react.createElement(
3273
+ 'div',
3274
+ { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' } },
3275
+ react.createElement('span', { style: { fontWeight: '700', fontSize: '14px', color: 'var(--dsw-alias-label-primary)' } }, 'Asset Inspector'),
3276
+ react.createElement('button', { type: 'button', onClick: () => setSelected(null), style: { background: 'none', border: 'none', cursor: 'pointer', fontSize: '16px', color: 'var(--dsw-alias-label-secondary)' } }, '✕')
3277
+ ),
3278
+ react.createElement('img', {
3279
+ src: selected.url || selected.thumbnailUrl,
3280
+ style: { width: '100%', borderRadius: '8px', maxHeight: '360px', objectFit: 'contain', background: 'var(--dsw-alias-bg-layer-2)' },
3281
+ }),
3282
+ react.createElement(
3283
+ 'div',
3284
+ { style: { display: 'flex', flexWrap: 'wrap', gap: '6px', fontSize: '11px' } },
3285
+ selected.seed !== undefined && react.createElement('span', { className: 'ig-badge' }, 'Seed: ' + selected.seed),
3286
+ selected.provider && react.createElement('span', { className: 'ig-badge' }, 'Provider: ' + selected.provider),
3287
+ selected.width && react.createElement('span', { className: 'ig-badge' }, selected.width + '×' + selected.height),
3288
+ selected.cost ? react.createElement('span', { className: 'ig-badge' }, '$' + Number(selected.cost).toFixed(4)) : null
3289
+ ),
3290
+ react.createElement('div', { style: { fontSize: '12px', color: 'var(--dsw-alias-label-primary)', background: 'var(--dsw-alias-bg-layer-2)', padding: '10px', borderRadius: '6px', lineHeight: '1.4' } }, selected.prompt || 'No prompt'),
3291
+ react.createElement(
3292
+ 'div',
3293
+ { style: { display: 'flex', gap: '8px', justifyContent: 'flex-end', marginTop: '6px' } },
3294
+ react.createElement('button', { type: 'button', className: 'ig-tab-btn', onClick: () => handleReroll(selected) }, '🎲 Re-roll'),
3295
+ react.createElement('button', { type: 'button', className: 'ig-tab-btn', onClick: () => handleInsert(selected) }, '💬 Insert in Chat'),
3296
+ react.createElement(
3297
+ 'a',
3298
+ { href: selected.url || selected.thumbnailUrl, download: 'vault-asset-' + (selected.id || 'image') + '.png', className: 'ig-tab-btn', style: { textDecoration: 'none', display: 'inline-flex', alignItems: 'center' } },
3299
+ '⬇ Download'
3300
+ ),
3301
+ react.createElement('button', { type: 'button', className: 'ig-tab-btn', style: { color: 'var(--dsw-alias-state-error-primary)' }, onClick: () => handleDelete(selected.id || selected.attachmentId) }, '🗑️ Delete')
3302
+ )
3303
+ )
3304
+ )
3305
+ )
3306
+ }
3307
+ // -------------------------------------------------------------- Fullscreen Image Studio (#158)
3308
+ function ImageStudioView(props) {
3309
+ const { ctx } = props
3310
+ const [viewMode, setViewMode] = react.useState('studio') // 'studio' | 'vault'
3311
+ const [isFullscreen, setIsFullscreen] = react.useState(false)
3312
+ const [layoutGrid, setLayoutGrid] = react.useState('1x1') // '1x1' | '2x2' | '1x4'
3313
+ const [prompt, setPrompt] = react.useState(() => {
3314
+ try { return window.localStorage.getItem('dsh_studio_prompt') || '' } catch (_) { return '' }
3315
+ })
3316
+ const [stylePreset, setStylePreset] = react.useState('none')
3317
+ const [aspectRatio, setAspectRatio] = react.useState('1:1')
3318
+ const [provider, setProvider] = react.useState('fal')
3319
+ const [seed, setSeed] = react.useState('')
3320
+ const [activeImages, setActiveImages] = react.useState([])
3321
+ const [recentStrip, setRecentStrip] = react.useState([])
3322
+ const [status, setStatus] = react.useState('')
3323
+
3324
+ const presets = [
3325
+ { id: 'none', label: 'Default' },
3326
+ { id: 'photorealistic', label: 'Photo' },
3327
+ { id: 'anime', label: 'Anime' },
3328
+ { id: 'cyberpunk', label: 'Cyberpunk' },
3329
+ { id: 'flat_vector', label: 'Vector' },
3330
+ { id: '3d_render', label: '3D Render' },
3331
+ ]
3332
+
3333
+ const refreshRecent = () => {
3334
+ fetch('/dsh-image-gen/vault?limit=12')
3335
+ .then((r) => r.json())
3336
+ .then((data) => {
3337
+ if (data && data.ok && Array.isArray(data.items)) {
3338
+ setRecentStrip(data.items)
3339
+ if (activeImages.length === 0 && data.items.length > 0) {
3340
+ setActiveImages([data.items[0]])
3341
+ }
3342
+ }
3343
+ })
3344
+ .catch(() => {})
3345
+ }
3346
+
3347
+ react.useEffect(() => {
3348
+ refreshRecent()
3349
+ }, [])
3350
+
3351
+ const updatePrompt = (val) => {
3352
+ setPrompt(val)
3353
+ try { window.localStorage.setItem('dsh_studio_prompt', val) } catch (_) { /* storage unavailable */ }
3354
+ }
3355
+
3356
+ const handleSelectFromVault = (asset) => {
3357
+ if (!asset) return
3358
+ setActiveImages([asset])
3359
+ if (asset.prompt) updatePrompt(asset.prompt)
3360
+ if (asset.seed !== undefined) setSeed(String(asset.seed))
3361
+ if (asset.provider) setProvider(asset.provider)
3362
+ setViewMode('studio')
3363
+ }
3364
+
3365
+ const handleCopySnippet = (format) => {
3366
+ const first = activeImages[0]
3367
+ if (!first) return
3368
+ const url = first.url || first.thumbnailUrl
3369
+ let snippet = ''
3370
+ if (format === 'markdown') snippet = '![' + (first.prompt || 'Artwork') + '](' + url + ')'
3371
+ if (format === 'html') snippet = '<img src="' + url + '" alt="' + (first.prompt || 'Artwork') + '" />'
3372
+ if (typeof navigator !== 'undefined' && navigator.clipboard) {
3373
+ navigator.clipboard.writeText(snippet)
3374
+ setStatus('Copied ' + format.toUpperCase() + ' snippet!')
3375
+ setTimeout(() => setStatus(''), 2000)
3376
+ }
3377
+ }
3378
+
3379
+ const containerStyle = isFullscreen
3380
+ ? { position: 'fixed', inset: 0, zIndex: 99999, background: 'var(--dsw-alias-bg-layer-1)', padding: '20px', overflowY: 'auto', display: 'flex', flexDirection: 'column', gap: '14px' }
3381
+ : { display: 'flex', flexDirection: 'column', gap: '14px', width: '100%' }
2805
3382
 
3383
+ return react.createElement(
3384
+ 'div',
3385
+ { className: 'ig-studio-container', style: containerStyle },
3386
+ react.createElement(
3387
+ 'div',
3388
+ { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: '10px', borderBottom: '1px solid var(--dsw-alias-border-l2)', paddingBottom: '10px' } },
3389
+ react.createElement(
3390
+ 'div',
3391
+ { style: { display: 'flex', gap: '8px', alignItems: 'center' } },
3392
+ react.createElement('span', { style: { fontWeight: '700', fontSize: '15px', color: 'var(--dsw-alias-label-primary)' } }, '🎨 Image Studio'),
3393
+ react.createElement(
3394
+ 'button',
3395
+ {
3396
+ type: 'button',
3397
+ className: 'ig-tab-btn ' + (viewMode === 'studio' ? 'active' : ''),
3398
+ onClick: () => setViewMode('studio'),
3399
+ },
3400
+ 'Studio Canvas'
3401
+ ),
3402
+ react.createElement(
3403
+ 'button',
3404
+ {
3405
+ type: 'button',
3406
+ className: 'ig-tab-btn ' + (viewMode === 'vault' ? 'active' : ''),
3407
+ onClick: () => setViewMode('vault'),
3408
+ },
3409
+ '🗃️ Asset Vault'
3410
+ )
3411
+ ),
3412
+ react.createElement(
3413
+ 'div',
3414
+ { style: { display: 'flex', gap: '8px', alignItems: 'center' } },
3415
+ viewMode === 'studio' &&
3416
+ react.createElement(
3417
+ 'div',
3418
+ { style: { display: 'flex', gap: '4px' } },
3419
+ ['1x1', '2x2', '1x4'].map((grid) =>
3420
+ react.createElement(
3421
+ 'button',
3422
+ {
3423
+ key: grid,
3424
+ type: 'button',
3425
+ className: 'ig-tab-btn ' + (layoutGrid === grid ? 'active' : ''),
3426
+ onClick: () => setLayoutGrid(grid),
3427
+ title: 'Grid layout: ' + grid,
3428
+ },
3429
+ grid
3430
+ )
3431
+ )
3432
+ ),
3433
+ react.createElement(
3434
+ 'button',
3435
+ {
3436
+ type: 'button',
3437
+ className: 'ig-tab-btn',
3438
+ onClick: () => setIsFullscreen(!isFullscreen),
3439
+ style: { border: '1px solid var(--dsw-alias-border-l2)' },
3440
+ },
3441
+ isFullscreen ? '✕ Exit Fullscreen' : '⛶ Fullscreen'
3442
+ )
3443
+ )
3444
+ ),
3445
+ viewMode === 'vault'
3446
+ ? react.createElement(AssetVaultView, { ctx, onSelectAsset: handleSelectFromVault })
3447
+ : react.createElement(
3448
+ react.Fragment,
3449
+ null,
3450
+ react.createElement(
3451
+ 'div',
3452
+ { className: 'ig-studio-layout' },
3453
+ react.createElement(
3454
+ 'div',
3455
+ { className: 'ig-studio-sidebar' },
3456
+ react.createElement('span', { style: { fontWeight: '600', fontSize: '13px', color: 'var(--dsw-alias-label-primary)' } }, 'Generation Parameters'),
3457
+ react.createElement(
3458
+ 'div',
3459
+ null,
3460
+ react.createElement('label', { style: { fontSize: '11px', color: 'var(--dsw-alias-label-secondary)', display: 'block', marginBottom: '4px' } }, 'Prompt:'),
3461
+ react.createElement('textarea', {
3462
+ className: 'ig-input',
3463
+ rows: 4,
3464
+ style: { width: '100%', resize: 'vertical', borderRadius: '6px', padding: '8px', fontSize: '12px' },
3465
+ placeholder: 'Describe your vision...',
3466
+ value: prompt,
3467
+ onChange: (e) => updatePrompt(e.target.value),
3468
+ })
3469
+ ),
3470
+ react.createElement(
3471
+ 'div',
3472
+ null,
3473
+ react.createElement('label', { style: { fontSize: '11px', color: 'var(--dsw-alias-label-secondary)', display: 'block', marginBottom: '4px' } }, 'Style Preset:'),
3474
+ react.createElement(
3475
+ 'div',
3476
+ { style: { display: 'flex', flexWrap: 'wrap', gap: '4px' } },
3477
+ presets.map((p) =>
3478
+ react.createElement(
3479
+ 'button',
3480
+ {
3481
+ key: p.id,
3482
+ type: 'button',
3483
+ className: 'ig-tab-btn ' + (stylePreset === p.id ? 'active' : ''),
3484
+ style: { fontSize: '11px', padding: '3px 8px' },
3485
+ onClick: () => setStylePreset(p.id),
3486
+ },
3487
+ p.label
3488
+ )
3489
+ )
3490
+ )
3491
+ ),
3492
+ react.createElement(
3493
+ 'div',
3494
+ { style: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '8px' } },
3495
+ react.createElement(
3496
+ 'div',
3497
+ null,
3498
+ react.createElement('label', { style: { fontSize: '11px', color: 'var(--dsw-alias-label-secondary)', display: 'block', marginBottom: '4px' } }, 'Aspect Ratio:'),
3499
+ react.createElement(
3500
+ 'select',
3501
+ {
3502
+ className: 'ig-select',
3503
+ style: { width: '100%', padding: '6px', fontSize: '12px' },
3504
+ value: aspectRatio,
3505
+ onChange: (e) => setAspectRatio(e.target.value),
3506
+ },
3507
+ react.createElement('option', { value: '1:1' }, '1:1 Square'),
3508
+ react.createElement('option', { value: '16:9' }, '16:9 Landscape'),
3509
+ react.createElement('option', { value: '9:16' }, '9:16 Portrait'),
3510
+ react.createElement('option', { value: '4:3' }, '4:3 Standard'),
3511
+ react.createElement('option', { value: '21:9' }, '21:9 Ultrawide')
3512
+ )
3513
+ ),
3514
+ react.createElement(
3515
+ 'div',
3516
+ null,
3517
+ react.createElement('label', { style: { fontSize: '11px', color: 'var(--dsw-alias-label-secondary)', display: 'block', marginBottom: '4px' } }, 'Provider:'),
3518
+ react.createElement(
3519
+ 'select',
3520
+ {
3521
+ className: 'ig-select',
3522
+ style: { width: '100%', padding: '6px', fontSize: '12px' },
3523
+ value: provider,
3524
+ onChange: (e) => setProvider(e.target.value),
3525
+ },
3526
+ react.createElement('option', { value: 'fal' }, 'FAL.ai'),
3527
+ react.createElement('option', { value: 'openai' }, 'OpenAI'),
3528
+ react.createElement('option', { value: 'local' }, 'Local (ComfyUI)')
3529
+ )
3530
+ )
3531
+ ),
3532
+ react.createElement(
3533
+ 'div',
3534
+ null,
3535
+ react.createElement('label', { style: { fontSize: '11px', color: 'var(--dsw-alias-label-secondary)', display: 'block', marginBottom: '4px' } }, 'Seed (optional):'),
3536
+ react.createElement(
3537
+ 'div',
3538
+ { style: { display: 'flex', gap: '6px' } },
3539
+ react.createElement('input', {
3540
+ type: 'number',
3541
+ className: 'ig-input',
3542
+ style: { flex: 1, padding: '6px', fontSize: '12px' },
3543
+ placeholder: 'Random',
3544
+ value: seed,
3545
+ onChange: (e) => setSeed(e.target.value),
3546
+ }),
3547
+ react.createElement(
3548
+ 'button',
3549
+ {
3550
+ type: 'button',
3551
+ className: 'ig-tab-btn',
3552
+ onClick: () => setSeed(String(Math.floor(Math.random() * 2147483647))),
3553
+ title: 'Randomize seed',
3554
+ },
3555
+ '🎲'
3556
+ )
3557
+ )
3558
+ ),
3559
+ react.createElement(
3560
+ 'button',
3561
+ {
3562
+ type: 'button',
3563
+ className: 'ig-save-btn',
3564
+ style: { marginTop: '8px', padding: '10px' },
3565
+ onClick: () => {
3566
+ const cmd = '/image ' + (prompt || 'Artwork') + (stylePreset !== 'none' ? ' --style ' + stylePreset : '') + ' --aspect ' + aspectRatio + ' --provider ' + provider + (seed ? ' --seed ' + seed : '')
3567
+ if (typeof navigator !== 'undefined' && navigator.clipboard) {
3568
+ navigator.clipboard.writeText(cmd)
3569
+ setStatus('Copied generation command to clipboard!')
3570
+ setTimeout(() => setStatus(''), 2500)
3571
+ }
3572
+ },
3573
+ },
3574
+ '🎨 Generate in Chat'
3575
+ ),
3576
+ status && react.createElement('div', { style: { fontSize: '11px', color: 'var(--dsw-alias-state-brand-primary)', textAlign: 'center' } }, status)
3577
+ ),
3578
+ react.createElement(
3579
+ 'div',
3580
+ { className: 'ig-studio-canvas' },
3581
+ activeImages.length === 0
3582
+ ? react.createElement('div', { style: { color: 'var(--dsw-alias-label-secondary)', fontSize: '13px' } }, 'Select an image from the recent strip below or click Generate.')
3583
+ : react.createElement(
3584
+ 'div',
3585
+ {
3586
+ style: {
3587
+ display: 'grid',
3588
+ gridTemplateColumns: layoutGrid === '2x2' ? 'repeat(2, 1fr)' : layoutGrid === '1x4' ? 'repeat(4, 1fr)' : '1fr',
3589
+ gap: '12px',
3590
+ width: '100%',
3591
+ maxHeight: '440px',
3592
+ overflowY: 'auto',
3593
+ },
3594
+ },
3595
+ (layoutGrid === '1x1' ? activeImages.slice(0, 1) : activeImages.slice(0, layoutGrid === '2x2' ? 4 : 4)).map((img, i) =>
3596
+ react.createElement('img', {
3597
+ key: img.id || i,
3598
+ src: img.url || img.thumbnailUrl,
3599
+ style: { width: '100%', height: 'auto', maxHeight: '400px', objectFit: 'contain', borderRadius: '8px', background: 'var(--dsw-alias-bg-layer-3)' },
3600
+ })
3601
+ )
3602
+ ),
3603
+ activeImages.length > 0 &&
3604
+ react.createElement(
3605
+ 'div',
3606
+ { style: { display: 'flex', gap: '8px', marginTop: '10px' } },
3607
+ react.createElement('button', { type: 'button', className: 'ig-tab-btn', onClick: () => handleCopySnippet('markdown') }, 'Copy Markdown'),
3608
+ react.createElement('button', { type: 'button', className: 'ig-tab-btn', onClick: () => handleCopySnippet('html') }, 'Copy HTML'),
3609
+ react.createElement(
3610
+ 'a',
3611
+ { href: activeImages[0].url || activeImages[0].thumbnailUrl, download: 'studio-image.png', className: 'ig-tab-btn', style: { textDecoration: 'none' } },
3612
+ '⬇ Download'
3613
+ )
3614
+ )
3615
+ )
3616
+ ),
3617
+ react.createElement(
3618
+ 'div',
3619
+ { style: { display: 'flex', flexDirection: 'column', gap: '6px', marginTop: '10px' } },
3620
+ react.createElement('span', { style: { fontSize: '12px', fontWeight: '600', color: 'var(--dsw-alias-label-secondary)' } }, 'Recent Generations:'),
3621
+ react.createElement(
3622
+ 'div',
3623
+ { className: 'ig-studio-strip', style: { display: 'flex', gap: '8px', overflowX: 'auto', padding: '4px 0' } },
3624
+ recentStrip.map((item) =>
3625
+ react.createElement('img', {
3626
+ key: item.id || item.attachmentId,
3627
+ src: item.thumbnailUrl || item.url,
3628
+ className: 'ig-studio-strip-thumb',
3629
+ style: { width: '64px', height: '64px', borderRadius: '6px', objectFit: 'cover', cursor: 'pointer', border: '1px solid var(--dsw-alias-border-l2)', flexShrink: 0 },
3630
+ title: item.prompt || 'Artwork',
3631
+ onClick: () => handleSelectFromVault(item),
3632
+ })
3633
+ )
3634
+ )
3635
+ )
3636
+ )
3637
+ )
3638
+ }
2806
3639
  exports.apply = apply
2807
3640
  exports.inject = inject
2808
3641
  return module.exports