@dickpy/dsh-imagegen 1.5.1 → 1.5.3
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 +13 -9
- package/docs/images/ecommerce-mode.png +0 -0
- package/docs/images/gallery-workspace.png +0 -0
- package/docs/images/image-generation-studio-three-column.png +0 -0
- package/docs/images/imagegen-overview.png +0 -0
- package/docs/images/plugin-settings.png +0 -0
- package/docs/images/prompt-template-library.png +0 -0
- package/lib/client.js +1304 -447
- package/lib/client.js.map +1 -1
- package/lib/index.js +632 -76
- package/package.json +1 -1
- package/src/client/ImageGenPanel.tsx +15 -9
- package/src/client/InspirationGallery.tsx +106 -0
- package/src/client/SettingsCard.tsx +9 -3
- package/src/client/TemplateLibrary.tsx +159 -48
- package/src/client/api.ts +41 -8
- package/src/client/helpers.ts +71 -33
- package/src/client/index.ts +59 -7
- package/src/client/inspiration.module.css +148 -0
- package/src/client/locales.ts +417 -8
- package/src/client/templates.module.css +95 -0
- package/src/client/use-language.ts +14 -0
- package/src/engine.ts +148 -0
- package/src/index.ts +23 -3
- package/src/model-catalog.ts +10 -1
- package/src/presets.ts +15 -0
- package/src/protocol.ts +81 -7
- package/src/routes.ts +124 -10
- package/src/template-favorites.ts +108 -0
- package/src/templates/canghe-cases.json +11126 -0
- package/src/templates-store.ts +179 -68
- package/docs/images/image-generation-studio-single.png +0 -0
- package/docs/images/image-generation-studio.png +0 -0
- package/docs/images/poster-features-16x9.png +0 -0
package/src/client/api.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* data access path the panel uses — plain fetch, same origin.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { CONVERSATION_IMAGE_API, GALLERY_API, GENERATE_API, HISTORY_API, PROMPT_ENHANCE_API, TASK_API, TEMPLATES_API, UPDATE_API, type GenerateRequest, type GenerateResult, type GenerationTask, type HistoryEntry, type HistoryEntryInput, type TemplateListResult, type TemplateRefreshResult, type UpdateInfo } from '../protocol.ts'
|
|
6
|
+
import { CONVERSATION_IMAGE_API, GALLERY_API, GENERATE_API, HISTORY_API, PROMPT_ENHANCE_API, TASK_API, TEMPLATE_FAVORITES_API, TEMPLATES_API, UPDATE_API, type GenerateRequest, type GenerateResult, type GenerationTask, type HistoryEntry, type HistoryEntryInput, type TemplateCase, type TemplateFavorite, type TemplateListResult, type TemplateRefreshResult, type TemplateSample, type UpdateInfo } from '../protocol.ts'
|
|
7
7
|
|
|
8
8
|
/** Error carrying the route's JSON error message. */
|
|
9
9
|
export class ImageGenApiError extends Error {
|
|
@@ -181,11 +181,12 @@ export class ImageGenApi {
|
|
|
181
181
|
return (await readEnvelope<{ ok: true; entries: HistoryEntry[] }>(response)).entries
|
|
182
182
|
}
|
|
183
183
|
|
|
184
|
-
/** Fetch
|
|
185
|
-
async templatesList(): Promise<TemplateListResult> {
|
|
186
|
-
const response = await fetch(TEMPLATES_API.list, { method: 'POST' })
|
|
184
|
+
/** Fetch one template source's list (bundled snapshot or refreshed copy). */
|
|
185
|
+
async templatesList(sourceId: string): Promise<TemplateListResult> {
|
|
186
|
+
const response = await fetch(TEMPLATES_API.list, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ source: sourceId }) })
|
|
187
187
|
const body = await readEnvelope<TemplateListResult & { ok: true }>(response)
|
|
188
188
|
return {
|
|
189
|
+
sourceId: body.sourceId,
|
|
189
190
|
cases: body.cases,
|
|
190
191
|
total: body.total,
|
|
191
192
|
origin: body.origin,
|
|
@@ -194,10 +195,42 @@ export class ImageGenApi {
|
|
|
194
195
|
}
|
|
195
196
|
}
|
|
196
197
|
|
|
197
|
-
/** Re-download
|
|
198
|
-
async templatesRefresh(): Promise<TemplateRefreshResult> {
|
|
199
|
-
const response = await fetch(TEMPLATES_API.refresh, { method: 'POST' })
|
|
198
|
+
/** Re-download one template source's list from its upstream mirror (host-side). */
|
|
199
|
+
async templatesRefresh(sourceId: string): Promise<TemplateRefreshResult> {
|
|
200
|
+
const response = await fetch(TEMPLATES_API.refresh, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ source: sourceId }) })
|
|
200
201
|
const body = await readEnvelope<TemplateRefreshResult & { ok: true }>(response)
|
|
201
|
-
return { total: body.total, fetchedAt: body.fetchedAt }
|
|
202
|
+
return { sourceId: body.sourceId, total: body.total, fetchedAt: body.fetchedAt }
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** Draw random cases across every source (studio inspiration wall). */
|
|
206
|
+
async templatesSample(count: number): Promise<TemplateSample[]> {
|
|
207
|
+
const response = await fetch(TEMPLATES_API.sample, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ count }) })
|
|
208
|
+
return (await readEnvelope<{ ok: true; samples: TemplateSample[] }>(response)).samples
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** List the host-persisted template favorites. */
|
|
212
|
+
async favoritesList(): Promise<TemplateFavorite[]> {
|
|
213
|
+
const response = await fetch(TEMPLATE_FAVORITES_API.list, { method: 'POST' })
|
|
214
|
+
return (await readEnvelope<{ ok: true; favorites: TemplateFavorite[] }>(response)).favorites
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** Star one template (the host keeps a full case snapshot). */
|
|
218
|
+
async favoritesAdd(sourceId: string, item: TemplateCase): Promise<TemplateFavorite[]> {
|
|
219
|
+
const response = await fetch(TEMPLATE_FAVORITES_API.add, {
|
|
220
|
+
method: 'POST',
|
|
221
|
+
headers: { 'content-type': 'application/json' },
|
|
222
|
+
body: JSON.stringify({ source: sourceId, case: item }),
|
|
223
|
+
})
|
|
224
|
+
return (await readEnvelope<{ ok: true; favorites: TemplateFavorite[] }>(response)).favorites
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** Unstar one template by its favorites key. */
|
|
228
|
+
async favoritesRemove(key: string): Promise<TemplateFavorite[]> {
|
|
229
|
+
const response = await fetch(TEMPLATE_FAVORITES_API.remove, {
|
|
230
|
+
method: 'POST',
|
|
231
|
+
headers: { 'content-type': 'application/json' },
|
|
232
|
+
body: JSON.stringify({ key }),
|
|
233
|
+
})
|
|
234
|
+
return (await readEnvelope<{ ok: true; favorites: TemplateFavorite[] }>(response)).favorites
|
|
202
235
|
}
|
|
203
236
|
}
|
package/src/client/helpers.ts
CHANGED
|
@@ -1,33 +1,71 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared panel helpers: the active-dictionary pick
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Shared panel helpers: the active-dictionary pick bound to the dsh-imagegen
|
|
3
|
+
* interpolator, the plugin locale that follows the DSH interface language
|
|
4
|
+
* (bridged in client/index.ts from ctx.locale — the plugin ships zh / en / ru
|
|
5
|
+
* and registers itself as a DSH language pack for Русский), plus a small
|
|
6
|
+
* error-message extractor. All copy stays in the locale dictionaries.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { en, ru, zh, type ImageGenKey } from './locales.ts'
|
|
10
|
+
|
|
11
|
+
/** Template values accepted by the interpolator. */
|
|
12
|
+
export type TranslateValues = Record<string, string | number>
|
|
13
|
+
|
|
14
|
+
/** Languages with a shipped dictionary. */
|
|
15
|
+
export type ImageGenLanguage = 'zh' | 'en' | 'ru'
|
|
16
|
+
|
|
17
|
+
const DICTIONARIES: Record<ImageGenLanguage, Record<string, string>> = { zh, en, ru }
|
|
18
|
+
|
|
19
|
+
/** The active DSH locale mapped onto our dictionary (module-level, one value per app). */
|
|
20
|
+
let activeLocale: ImageGenLanguage = 'zh'
|
|
21
|
+
|
|
22
|
+
/** Bumped on every locale change; useSyncExternalStore version. */
|
|
23
|
+
let languageVersion = 0
|
|
24
|
+
|
|
25
|
+
const languageListeners = new Set<() => void>()
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Adopt the DSH interface language. Unknown ids (future language packs)
|
|
29
|
+
* resolve to English — the same per-key fallback convention the host locale
|
|
30
|
+
* chain uses.
|
|
31
|
+
*/
|
|
32
|
+
export function applyHostLocale(id: unknown): void {
|
|
33
|
+
const next: ImageGenLanguage = id === 'zh' || id === 'ru' ? id : 'en'
|
|
34
|
+
if (next === activeLocale) return
|
|
35
|
+
activeLocale = next
|
|
36
|
+
languageVersion += 1
|
|
37
|
+
for (const listener of [...languageListeners]) listener()
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Monotonic version of the active locale (external-store snapshot). */
|
|
41
|
+
export function getImageGenLanguageVersion(): number {
|
|
42
|
+
return languageVersion
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Observe locale changes; returns the unsubscriber. */
|
|
46
|
+
export function subscribeImageGenLanguage(listener: () => void): () => void {
|
|
47
|
+
languageListeners.add(listener)
|
|
48
|
+
return () => { languageListeners.delete(listener) }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Active dictionary for the current DSH language. */
|
|
52
|
+
export function dictionary(): Record<string, string> {
|
|
53
|
+
return DICTIONARIES[activeLocale]
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Translate a key with optional {name} template params (current language). */
|
|
57
|
+
export function tt(key: ImageGenKey, values?: TranslateValues): string {
|
|
58
|
+
const text = dictionary()[key] ?? key
|
|
59
|
+
if (values === undefined) return text
|
|
60
|
+
let rendered = text
|
|
61
|
+
for (const [name, value] of Object.entries(values)) {
|
|
62
|
+
rendered = rendered.replaceAll(`{${name}}`, String(value))
|
|
63
|
+
}
|
|
64
|
+
return rendered
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Human-readable error text from an unknown thrown value. */
|
|
68
|
+
export function errorMessage(error: unknown): string {
|
|
69
|
+
if (error instanceof Error) return error.message
|
|
70
|
+
return String(error)
|
|
71
|
+
}
|
package/src/client/index.ts
CHANGED
|
@@ -11,18 +11,18 @@
|
|
|
11
11
|
* whole boot when a plugin apply throws, and an external plugin must not take
|
|
12
12
|
* the GUI down.
|
|
13
13
|
*/
|
|
14
|
-
import type { Context as ClientContext } from '@deepseek-ai/cordis'
|
|
15
|
-
import type { ISessions } from '@deepseek-ai/dsh-api-session-controller/client'
|
|
14
|
+
import type { Context as ClientContext } from '@deepseek-ai/cordis'
|
|
15
|
+
import type { ISessions } from '@deepseek-ai/dsh-api-session-controller/client'
|
|
16
16
|
import type { ConnectionHandle } from '@deepseek-ai/dsh-client-connection/client'
|
|
17
17
|
// Type-only: pulls the locale plugin's Context merge (ctx.locale).
|
|
18
|
-
import type {} from '@deepseek-ai/dsh-client-locale/client'
|
|
19
|
-
import type {} from '@deepseek-ai/dsh-client-ui-renderer/client'
|
|
20
|
-
// Type-only: pulls the LocaleNamespaceMap merge table.
|
|
18
|
+
import type {} from '@deepseek-ai/dsh-client-locale/client'
|
|
19
|
+
import type {} from '@deepseek-ai/dsh-client-ui-renderer/client'
|
|
20
|
+
// Type-only: pulls the LocaleNamespaceMap merge table.
|
|
21
21
|
import type {} from '@deepseek-ai/dsh-client-ui-slots'
|
|
22
22
|
import { ImageGenApi } from './api.ts'
|
|
23
23
|
import { ImageGenController } from './controller.ts'
|
|
24
|
-
import { tt } from './helpers.ts'
|
|
25
|
-
import { en, zh, type ImageGenKey } from './locales.ts'
|
|
24
|
+
import { tt, applyHostLocale } from './helpers.ts'
|
|
25
|
+
import { en, ru, zh, type ImageGenKey } from './locales.ts'
|
|
26
26
|
import { mountPanel } from './mount.tsx'
|
|
27
27
|
import { mountSidebarEntry } from './sidebar-entry.ts'
|
|
28
28
|
import { ImageGenSettingsCard, ImageGenSettingsCardController } from './SettingsCard.tsx'
|
|
@@ -68,7 +68,39 @@ export const inject = ['slots', 'locale', 'connection', 'sessions', 'conversatio
|
|
|
68
68
|
* @param ctx - client root context (services: slots, locale, connection).
|
|
69
69
|
*/
|
|
70
70
|
export function apply(ctx: ClientContext): void {
|
|
71
|
+
// The host locale service only knows zh/en dictionaries (its type is
|
|
72
|
+
// fixed); ru rides the untyped single-locale registration instead.
|
|
71
73
|
ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'dsh-imagegen: dictionaries')
|
|
74
|
+
// Russian ships as a language pack: the dictionary lands in this plugin's
|
|
75
|
+
// namespace, and Русский joins the shared DSH language catalog (Settings →
|
|
76
|
+
// General → Language) with per-key fallback to English for host copy.
|
|
77
|
+
// Registration order/aggregation between register(NS, 'ru', …) and
|
|
78
|
+
// addLanguage differs across host builds and a duplicate throws — these
|
|
79
|
+
// surfaces must degrade silently, never fail the GUI boot.
|
|
80
|
+
ctx.effect(() => {
|
|
81
|
+
try {
|
|
82
|
+
return ctx.locale.register(NS, 'ru', ru)
|
|
83
|
+
} catch (error) {
|
|
84
|
+
console.warn('[dsh-imagegen] ru dictionary not registered:', error)
|
|
85
|
+
return () => {}
|
|
86
|
+
}
|
|
87
|
+
}, 'dsh-imagegen: ru dictionary')
|
|
88
|
+
ctx.effect(() => {
|
|
89
|
+
try {
|
|
90
|
+
if (ctx.locale.getLocale().locales.some(locale => locale.id === 'ru')) return () => {}
|
|
91
|
+
return ctx.locale.addLanguage({ id: 'ru', label: 'Русский', fallback: 'en' })
|
|
92
|
+
} catch (error) {
|
|
93
|
+
console.warn('[dsh-imagegen] ru language not added to the catalog:', error)
|
|
94
|
+
return () => {}
|
|
95
|
+
}
|
|
96
|
+
}, 'dsh-imagegen: ru language pack')
|
|
97
|
+
// Every plugin surface renders through tt(); bridge DSH locale switches
|
|
98
|
+
// into it so the whole plugin follows the interface language.
|
|
99
|
+
ctx.effect(() => {
|
|
100
|
+
const applyLocale = (): void => { applyHostLocale(ctx.locale.getLocale().active) }
|
|
101
|
+
applyLocale()
|
|
102
|
+
return ctx.locale.subscribe(applyLocale)
|
|
103
|
+
}, 'dsh-imagegen: follow host locale')
|
|
72
104
|
registerImageToolviews(ctx)
|
|
73
105
|
|
|
74
106
|
const connection = ctx.get('connection') as ConnectionHandle | undefined
|
|
@@ -119,6 +151,26 @@ export function apply(ctx: ClientContext): void {
|
|
|
119
151
|
tt('entry.tooltip'),
|
|
120
152
|
))
|
|
121
153
|
disposers.push(mountPanel(controller, api, scope, { sessions, conversation }))
|
|
154
|
+
// The imperative sidebar tabs render their labels once; relabel them on
|
|
155
|
+
// every DSH language switch so the entry follows the interface too.
|
|
156
|
+
disposers.push(ctx.locale.subscribe(() => {
|
|
157
|
+
const root = document.querySelector('[data-dsh-imagegen-sidebar-root]')
|
|
158
|
+
if (root === null) return
|
|
159
|
+
const labels: Array<[string, string, string]> = [
|
|
160
|
+
['new-session', tt('entry.newSession'), tt('entry.newSessionTooltip')],
|
|
161
|
+
['image', tt('entry.image'), tt('entry.tooltip')],
|
|
162
|
+
]
|
|
163
|
+
for (const [tab, label, tooltip] of labels) {
|
|
164
|
+
const button = root.querySelector<HTMLButtonElement>(`[data-dsh-imagegen-tab="${tab}"]`)
|
|
165
|
+
if (button === null) continue
|
|
166
|
+
button.setAttribute('aria-label', label)
|
|
167
|
+
button.setAttribute('title', tooltip)
|
|
168
|
+
const labelSpan = button.querySelector('span:nth-child(2)')
|
|
169
|
+
if (labelSpan !== null) labelSpan.textContent = label
|
|
170
|
+
}
|
|
171
|
+
const tablist = root.querySelector<HTMLDivElement>('[role="tablist"][data-dsh-imagegen-session-tabs]')
|
|
172
|
+
tablist?.setAttribute('aria-label', tt('entry.tooltip'))
|
|
173
|
+
}))
|
|
122
174
|
} catch (error) {
|
|
123
175
|
// DOM failures degrade the studio, never the GUI.
|
|
124
176
|
console.warn('[dsh-imagegen] mount failed:', error)
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inspiration-wall styles for the studio's empty canvas. Same conventions as
|
|
3
|
+
* panel.module.css: dsh --dsw-* tokens, scoped to this component's subtree.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
.wrap {
|
|
7
|
+
display: flex;
|
|
8
|
+
flex-direction: column;
|
|
9
|
+
align-items: center;
|
|
10
|
+
gap: 16px;
|
|
11
|
+
width: min(880px, 100%);
|
|
12
|
+
margin: auto;
|
|
13
|
+
padding: 24px;
|
|
14
|
+
font-family: var(--dsw-font-family);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.title {
|
|
18
|
+
font-size: 15px;
|
|
19
|
+
font-weight: 650;
|
|
20
|
+
color: var(--dsw-alias-label-primary);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/* Fallback (sample failed / libraries empty): mirrors the panel's plain
|
|
24
|
+
canvas empty state so the swap is invisible. */
|
|
25
|
+
.emptyIcon {
|
|
26
|
+
display: inline-flex;
|
|
27
|
+
color: var(--dsw-alias-label-dimmed);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.emptyTitle {
|
|
31
|
+
margin-top: -6px;
|
|
32
|
+
font-size: 15px;
|
|
33
|
+
font-weight: 650;
|
|
34
|
+
color: var(--dsw-alias-label-primary);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.emptyHint {
|
|
38
|
+
font-size: 12.5px;
|
|
39
|
+
color: var(--dsw-alias-label-tertiary);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.grid {
|
|
43
|
+
display: grid;
|
|
44
|
+
grid-template-columns: repeat(4, 1fr);
|
|
45
|
+
gap: 12px;
|
|
46
|
+
width: 100%;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/* Narrow canvases: fold the wall down instead of shrinking tiles into slivers. */
|
|
50
|
+
@media (max-width: 900px) {
|
|
51
|
+
.grid {
|
|
52
|
+
grid-template-columns: repeat(3, 1fr);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
@media (max-width: 560px) {
|
|
57
|
+
.grid {
|
|
58
|
+
grid-template-columns: repeat(2, 1fr);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.tile {
|
|
63
|
+
position: relative;
|
|
64
|
+
display: block;
|
|
65
|
+
width: 100%;
|
|
66
|
+
aspect-ratio: 1 / 1;
|
|
67
|
+
padding: 0;
|
|
68
|
+
overflow: hidden;
|
|
69
|
+
border: 1px solid var(--dsw-alias-border-l1);
|
|
70
|
+
border-radius: 12px;
|
|
71
|
+
background: var(--dsw-alias-bg-layer-1);
|
|
72
|
+
cursor: pointer;
|
|
73
|
+
transition: transform 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.tile:hover {
|
|
77
|
+
border-color: var(--dsw-alias-brand-primary);
|
|
78
|
+
transform: translateY(-2px);
|
|
79
|
+
box-shadow: 0 6px 18px rgba(0, 0, 0, 0.12);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.thumbWrap {
|
|
83
|
+
position: absolute;
|
|
84
|
+
inset: 0;
|
|
85
|
+
display: block;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.thumb {
|
|
89
|
+
display: block;
|
|
90
|
+
width: 100%;
|
|
91
|
+
height: 100%;
|
|
92
|
+
object-fit: cover;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.thumbFallback {
|
|
96
|
+
display: flex;
|
|
97
|
+
align-items: center;
|
|
98
|
+
justify-content: center;
|
|
99
|
+
width: 100%;
|
|
100
|
+
height: 100%;
|
|
101
|
+
padding: 6px;
|
|
102
|
+
overflow: hidden;
|
|
103
|
+
background: var(--dsw-alias-bg-layer-2);
|
|
104
|
+
color: var(--dsw-alias-label-tertiary);
|
|
105
|
+
font-size: 11px;
|
|
106
|
+
line-height: 1.4;
|
|
107
|
+
text-align: center;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.thumbTitle {
|
|
111
|
+
position: absolute;
|
|
112
|
+
inset: auto 0 0 0;
|
|
113
|
+
padding: 14px 8px 6px;
|
|
114
|
+
overflow: hidden;
|
|
115
|
+
text-overflow: ellipsis;
|
|
116
|
+
white-space: nowrap;
|
|
117
|
+
background: linear-gradient(transparent, rgba(0, 0, 0, 0.55));
|
|
118
|
+
color: #ffffff;
|
|
119
|
+
font-size: 10.5px;
|
|
120
|
+
text-align: center;
|
|
121
|
+
opacity: 0;
|
|
122
|
+
transition: opacity 0.15s ease;
|
|
123
|
+
pointer-events: none;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.tile:hover .thumbTitle {
|
|
127
|
+
opacity: 1;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.spinner {
|
|
131
|
+
width: 20px;
|
|
132
|
+
height: 20px;
|
|
133
|
+
border: 2px solid var(--dsw-alias-border-l2);
|
|
134
|
+
border-top-color: var(--dsw-alias-brand-primary);
|
|
135
|
+
border-radius: 50%;
|
|
136
|
+
animation: dsh-imagegen-inspiration-spin 0.9s linear infinite;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
@keyframes dsh-imagegen-inspiration-spin {
|
|
140
|
+
to { transform: rotate(360deg); }
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/* Touch shells have no hover: keep titles visible. */
|
|
144
|
+
@media (hover: none) {
|
|
145
|
+
.thumbTitle {
|
|
146
|
+
opacity: 1;
|
|
147
|
+
}
|
|
148
|
+
}
|