@dbx-tools/cli 0.9.139 → 0.9.140

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.
@@ -0,0 +1,344 @@
1
+ /**
2
+ * Self-contained HTML presentation for an exported brand package.
3
+ *
4
+ * @module
5
+ */
6
+
7
+ import { stringUtils } from "@dbx-tools/shared-core";
8
+ import type { BrandPackage } from "@dbx-tools/shared-generator";
9
+
10
+ function safeAsset(value: string | undefined): string | undefined {
11
+ if (!value) return undefined;
12
+ if (/^(?:data:image\/|https:\/\/)/i.test(value)) return value;
13
+ return /^\.{0,2}\//.test(value) ? value : undefined;
14
+ }
15
+
16
+ function safeWebsite(value: string | undefined): string | undefined {
17
+ return value && /^https:\/\//i.test(value) ? value : undefined;
18
+ }
19
+
20
+ function safeFontFamily(value: string): string {
21
+ return /^[\w\s'",.-]+$/.test(value) ? value : "ui-sans-serif, system-ui, sans-serif";
22
+ }
23
+
24
+ function listItems(values: readonly string[]): string {
25
+ return values.map((value) => `<li>${stringUtils.escapeHtml(value)}</li>`).join("");
26
+ }
27
+
28
+ /** Render one responsive brand presentation with desktop and mobile mockups. */
29
+ export function renderBrandShowcase(brandPackage: BrandPackage): string {
30
+ const { brand, visualReferences } = brandPackage;
31
+ const html = stringUtils.escapeHtml;
32
+ const logo = safeAsset(brand.assets.logo.light) ?? safeAsset(brand.assets.icon.light);
33
+ const icon = safeAsset(brand.assets.icon.light);
34
+ const website = safeWebsite(brand.links.website);
35
+ const screenshots = visualReferences
36
+ .flatMap((reference) =>
37
+ reference.images.flatMap((image) =>
38
+ image.localPath
39
+ ? [{ appName: reference.appName, platform: reference.platform, path: image.localPath }]
40
+ : [],
41
+ ),
42
+ )
43
+ .slice(0, 4);
44
+ const identityMark = logo
45
+ ? `<img src="${html(logo)}" alt="${html(brand.name)} logo">`
46
+ : `<span class="wordmark">${html(brand.shortName)}</span>`;
47
+ const compactMark = icon
48
+ ? `<img src="${html(icon)}" alt="" aria-hidden="true">`
49
+ : `<span class="compact-wordmark">${html(brand.shortName.slice(0, 2))}</span>`;
50
+ const action = website
51
+ ? `<a class="button" href="${html(website)}">Visit website</a>`
52
+ : `<span class="button">Explore the brand</span>`;
53
+ const gallery = screenshots.length
54
+ ? `
55
+ <section class="references" aria-labelledby="references-title">
56
+ <div class="section-heading">
57
+ <h2 id="references-title">Visual references</h2>
58
+ <p>Supplemental mobile references used during research.</p>
59
+ </div>
60
+ <div class="reference-grid">
61
+ ${screenshots
62
+ .map(
63
+ ({ appName, path, platform }) => `
64
+ <figure>
65
+ <img src="${html(path)}" alt="${html(appName)} ${html(platform)} reference">
66
+ <figcaption>${html(appName)} / ${html(platform)}</figcaption>
67
+ </figure>
68
+ `,
69
+ )
70
+ .join("")}
71
+ </div>
72
+ </section>
73
+ `
74
+ : "";
75
+ const headline = brand.tagline ?? brand.name;
76
+
77
+ // prettier-ignore
78
+ return stringUtils.dedent(
79
+ // ============================================================================
80
+ /*html*/`
81
+ <!doctype html>
82
+ <html lang="en">
83
+ <head>
84
+ <meta charset="utf-8">
85
+ <meta name="viewport" content="width=device-width, initial-scale=1">
86
+ <title>${html(brand.name)} brand presentation</title>
87
+ <style>
88
+ :root {
89
+ color-scheme: light;
90
+ --primary: ${brand.colors.primary};
91
+ --primary-hover: ${brand.colors.primaryHover};
92
+ --accent: ${brand.colors.accent};
93
+ --foreground: ${brand.colors.foreground};
94
+ --background: ${brand.colors.background};
95
+ --surface: ${brand.colors.surface};
96
+ --muted: ${brand.colors.muted};
97
+ --border: ${brand.colors.border};
98
+ --sans: ${safeFontFamily(brand.typography.sans)};
99
+ --mono: ${safeFontFamily(brand.typography.mono)};
100
+ }
101
+ * { box-sizing: border-box; }
102
+ body {
103
+ margin: 0;
104
+ background: var(--background);
105
+ color: var(--foreground);
106
+ font-family: var(--sans);
107
+ }
108
+ a { color: inherit; }
109
+ img { display: block; max-width: 100%; }
110
+ main { margin: 0 auto; max-width: 1240px; padding: 0 32px 72px; }
111
+ .masthead {
112
+ align-items: center;
113
+ border-bottom: 1px solid var(--border);
114
+ display: flex;
115
+ justify-content: space-between;
116
+ min-height: 88px;
117
+ }
118
+ .masthead img { max-height: 42px; max-width: 210px; }
119
+ .wordmark { font-size: 24px; font-weight: 750; letter-spacing: -0.04em; }
120
+ .meta { color: var(--muted); font-family: var(--mono); font-size: 12px; }
121
+ .identity {
122
+ display: grid;
123
+ gap: 48px;
124
+ grid-template-columns: minmax(0, 1.4fr) minmax(260px, 0.6fr);
125
+ padding: 72px 0 64px;
126
+ }
127
+ .identity h1 {
128
+ font-size: clamp(48px, 8vw, 104px);
129
+ letter-spacing: -0.065em;
130
+ line-height: 0.92;
131
+ margin: 0;
132
+ max-width: 850px;
133
+ }
134
+ .identity-copy { align-self: end; }
135
+ .identity-copy p { color: var(--muted); font-size: 17px; line-height: 1.65; margin: 0 0 28px; }
136
+ .button {
137
+ background: var(--primary);
138
+ border-radius: 999px;
139
+ color: var(--background);
140
+ display: inline-flex;
141
+ font-weight: 700;
142
+ padding: 12px 20px;
143
+ text-decoration: none;
144
+ }
145
+ .showcase {
146
+ background: var(--surface);
147
+ border: 1px solid var(--border);
148
+ border-radius: 28px;
149
+ overflow: hidden;
150
+ padding: 38px;
151
+ }
152
+ .section-heading { align-items: end; display: flex; gap: 24px; justify-content: space-between; margin-bottom: 28px; }
153
+ .section-heading h2 { font-size: 30px; letter-spacing: -0.035em; margin: 0; }
154
+ .section-heading p { color: var(--muted); margin: 0; max-width: 460px; text-align: right; }
155
+ .devices { align-items: end; display: grid; gap: 28px; grid-template-columns: minmax(0, 1fr) 280px; }
156
+ .browser, .phone {
157
+ background: var(--background);
158
+ border: 1px solid color-mix(in srgb, var(--foreground) 18%, transparent);
159
+ box-shadow: 0 24px 70px color-mix(in srgb, var(--foreground) 13%, transparent);
160
+ overflow: hidden;
161
+ }
162
+ .browser { border-radius: 18px; }
163
+ .browser-bar { align-items: center; background: var(--foreground); display: flex; gap: 7px; height: 38px; padding: 0 16px; }
164
+ .browser-bar i { background: var(--accent); border-radius: 50%; display: block; height: 7px; opacity: 0.8; width: 7px; }
165
+ .browser-nav, .phone-nav { align-items: center; display: flex; justify-content: space-between; padding: 20px 24px; }
166
+ .browser-nav img { max-height: 30px; max-width: 150px; }
167
+ .nav-links { color: var(--muted); display: flex; font-size: 12px; gap: 22px; }
168
+ .mock-hero { display: grid; gap: 28px; grid-template-columns: 1.1fr 0.9fr; min-height: 350px; padding: 44px; }
169
+ .mock-copy { align-self: center; }
170
+ .mock-copy h3 { font-size: clamp(34px, 5vw, 64px); letter-spacing: -0.055em; line-height: 0.98; margin: 0 0 22px; }
171
+ .mock-copy p { color: var(--muted); line-height: 1.55; margin: 0 0 24px; max-width: 520px; }
172
+ .mock-visual {
173
+ align-content: end;
174
+ background: var(--primary);
175
+ border-radius: 20px;
176
+ color: var(--background);
177
+ display: grid;
178
+ min-height: 250px;
179
+ overflow: hidden;
180
+ padding: 28px;
181
+ position: relative;
182
+ }
183
+ .mock-visual::before {
184
+ background: var(--accent);
185
+ border-radius: 50%;
186
+ content: "";
187
+ height: 210px;
188
+ position: absolute;
189
+ right: -70px;
190
+ top: -80px;
191
+ width: 210px;
192
+ }
193
+ .mock-visual strong { font-size: 22px; line-height: 1.15; max-width: 220px; position: relative; }
194
+ .phone { border: 8px solid var(--foreground); border-radius: 34px; }
195
+ .phone-screen { min-height: 540px; }
196
+ .phone-nav { padding: 18px; }
197
+ .phone-nav img { height: 30px; max-width: 112px; object-fit: contain; object-position: left center; }
198
+ .phone-mark img { height: 30px; width: 30px; }
199
+ .compact-wordmark { font-weight: 800; }
200
+ .phone-copy { padding: 34px 22px 24px; }
201
+ .phone-copy h3 { font-size: 38px; letter-spacing: -0.05em; line-height: 0.98; margin: 0 0 18px; }
202
+ .phone-copy p { color: var(--muted); font-size: 14px; line-height: 1.55; margin: 0; }
203
+ .phone-accent { background: var(--accent); height: 120px; margin: 18px 22px 22px; }
204
+ .system {
205
+ display: grid;
206
+ gap: 48px;
207
+ grid-template-columns: 1fr 1fr;
208
+ padding: 72px 0;
209
+ }
210
+ .palette { display: grid; grid-template-columns: repeat(4, 1fr); min-height: 250px; }
211
+ .swatch { align-items: end; display: flex; padding: 16px; }
212
+ .swatch span { background: var(--background); color: var(--foreground); font-family: var(--mono); font-size: 11px; padding: 5px 7px; }
213
+ .type-specimen { border-top: 1px solid var(--border); padding-top: 24px; }
214
+ .type-specimen strong { display: block; font-size: clamp(44px, 6vw, 76px); letter-spacing: -0.05em; line-height: 0.95; }
215
+ .type-specimen code { color: var(--muted); display: block; font-family: var(--mono); margin-top: 20px; }
216
+ .voice { border-top: 1px solid var(--border); display: grid; gap: 32px; grid-template-columns: 1fr 1fr; padding: 36px 0; }
217
+ .voice h2 { font-size: 30px; letter-spacing: -0.035em; margin: 0; }
218
+ .voice h3 { font-size: 14px; margin: 0 0 12px; }
219
+ .voice ul { color: var(--muted); line-height: 1.6; margin: 0; padding-left: 18px; }
220
+ .references { border-top: 1px solid var(--border); padding-top: 56px; }
221
+ .reference-grid { display: grid; gap: 18px; grid-template-columns: repeat(4, 1fr); }
222
+ figure { margin: 0; }
223
+ figure img { aspect-ratio: 9 / 16; background: var(--surface); border: 1px solid var(--border); object-fit: contain; width: 100%; }
224
+ figcaption { color: var(--muted); font-size: 11px; margin-top: 8px; }
225
+ @media (max-width: 860px) {
226
+ main { padding: 0 18px 48px; }
227
+ .identity, .system { grid-template-columns: 1fr; }
228
+ .identity { gap: 28px; padding: 52px 0 44px; }
229
+ .showcase { border-radius: 20px; padding: 20px; }
230
+ .devices { grid-template-columns: 1fr; }
231
+ .phone { margin: 0 auto; max-width: 320px; width: 100%; }
232
+ .reference-grid { grid-template-columns: repeat(2, 1fr); }
233
+ }
234
+ @media (max-width: 560px) {
235
+ .masthead { min-height: 72px; }
236
+ .identity h1 { font-size: 52px; }
237
+ .section-heading { align-items: start; flex-direction: column; }
238
+ .section-heading p { text-align: left; }
239
+ .mock-hero { grid-template-columns: 1fr; padding: 24px; }
240
+ .mock-visual { min-height: 190px; }
241
+ .nav-links { display: none; }
242
+ .palette { grid-template-columns: 1fr 1fr; }
243
+ .voice { grid-template-columns: 1fr; }
244
+ }
245
+ @media (prefers-reduced-motion: no-preference) {
246
+ .button { transition: background-color 160ms ease, transform 160ms ease; }
247
+ .button:hover { background: var(--primary-hover); transform: translateY(-1px); }
248
+ }
249
+ .button:focus-visible { outline: 3px solid var(--accent); outline-offset: 3px; }
250
+ </style>
251
+ </head>
252
+ <body>
253
+ <main>
254
+ <header class="masthead">
255
+ ${identityMark}
256
+ <span class="meta">Brand system / ${html(brand.schemaVersion)}</span>
257
+ </header>
258
+
259
+ <section class="identity" aria-labelledby="brand-name">
260
+ <h1 id="brand-name">${html(brand.name)}</h1>
261
+ <div class="identity-copy">
262
+ <p>${html(brand.description)}</p>
263
+ ${action}
264
+ </div>
265
+ </section>
266
+
267
+ <section class="showcase" aria-labelledby="mockup-title">
268
+ <div class="section-heading">
269
+ <h2 id="mockup-title">Website mockups</h2>
270
+ <p>One responsive direction shown at desktop and mobile sizes.</p>
271
+ </div>
272
+ <div class="devices">
273
+ <article class="browser" aria-label="Desktop website mockup">
274
+ <div class="browser-bar"><i></i><i></i><i></i></div>
275
+ <div class="browser-nav">
276
+ ${identityMark}
277
+ <div class="nav-links"><span>Overview</span><span>Product</span><span>Resources</span></div>
278
+ </div>
279
+ <div class="mock-hero">
280
+ <div class="mock-copy">
281
+ <h3>${html(headline)}</h3>
282
+ <p>${html(brand.description)}</p>
283
+ ${action}
284
+ </div>
285
+ <div class="mock-visual">
286
+ <strong>${html(brand.voice.principles[0] ?? brand.name)}</strong>
287
+ </div>
288
+ </div>
289
+ </article>
290
+
291
+ <article class="phone" aria-label="Mobile website mockup">
292
+ <div class="phone-screen">
293
+ <div class="phone-nav">
294
+ <span class="phone-mark">${compactMark}</span>
295
+ <span class="meta">Menu</span>
296
+ </div>
297
+ <div class="phone-copy">
298
+ <h3>${html(headline)}</h3>
299
+ <p>${html(brand.description)}</p>
300
+ </div>
301
+ <div class="phone-accent"></div>
302
+ </div>
303
+ </article>
304
+ </div>
305
+ </section>
306
+
307
+ <section class="system" aria-label="Brand system">
308
+ <div>
309
+ <div class="section-heading"><h2>Core palette</h2></div>
310
+ <div class="palette">
311
+ <div class="swatch" style="background: var(--primary)"><span>${brand.colors.primary}</span></div>
312
+ <div class="swatch" style="background: var(--accent)"><span>${brand.colors.accent}</span></div>
313
+ <div class="swatch" style="background: var(--surface)"><span>${brand.colors.surface}</span></div>
314
+ <div class="swatch" style="background: var(--foreground)"><span>${brand.colors.foreground}</span></div>
315
+ </div>
316
+ </div>
317
+ <div>
318
+ <div class="section-heading"><h2>Typography</h2></div>
319
+ <div class="type-specimen">
320
+ <strong>Aa 01</strong>
321
+ <code>${html(brand.typography.sans)}</code>
322
+ </div>
323
+ </div>
324
+ </section>
325
+
326
+ <section class="voice" aria-labelledby="voice-title">
327
+ <div>
328
+ <h2 id="voice-title">Voice</h2>
329
+ <p class="meta">${html(brand.voice.tone.join(", "))}</p>
330
+ </div>
331
+ <div>
332
+ <h3>Principles</h3>
333
+ <ul>${listItems(brand.voice.principles)}</ul>
334
+ </div>
335
+ </section>
336
+
337
+ ${gallery}
338
+ </main>
339
+ </body>
340
+ </html>
341
+ `
342
+ // ============================================================================
343
+ );
344
+ }