open-brandkit 0.4.7
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/LICENSE +72 -0
- package/README.md +284 -0
- package/dist/adapters/next/brandkit-page.d.ts +15 -0
- package/dist/adapters/next/brandkit-page.d.ts.map +1 -0
- package/dist/adapters/next/brandkit-page.js +1085 -0
- package/dist/adapters/next/brandkit-page.js.map +1 -0
- package/dist/adapters/next/index.d.ts +3 -0
- package/dist/adapters/next/index.d.ts.map +1 -0
- package/dist/adapters/next/index.js +3 -0
- package/dist/adapters/next/index.js.map +1 -0
- package/dist/adapters/next/manifest.d.ts +33 -0
- package/dist/adapters/next/manifest.d.ts.map +1 -0
- package/dist/adapters/next/manifest.js +57 -0
- package/dist/adapters/next/manifest.js.map +1 -0
- package/dist/adapters/next/route-handlers.d.ts +102 -0
- package/dist/adapters/next/route-handlers.d.ts.map +1 -0
- package/dist/adapters/next/route-handlers.js +451 -0
- package/dist/adapters/next/route-handlers.js.map +1 -0
- package/dist/adapters/next/server.d.ts +2 -0
- package/dist/adapters/next/server.d.ts.map +1 -0
- package/dist/adapters/next/server.js +2 -0
- package/dist/adapters/next/server.js.map +1 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +1079 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/core/assets.d.ts +34 -0
- package/dist/core/assets.d.ts.map +1 -0
- package/dist/core/assets.js +200 -0
- package/dist/core/assets.js.map +1 -0
- package/dist/core/banner-renderer.d.ts +21 -0
- package/dist/core/banner-renderer.d.ts.map +1 -0
- package/dist/core/banner-renderer.js +119 -0
- package/dist/core/banner-renderer.js.map +1 -0
- package/dist/core/build.d.ts +13 -0
- package/dist/core/build.d.ts.map +1 -0
- package/dist/core/build.js +345 -0
- package/dist/core/build.js.map +1 -0
- package/dist/core/colors.d.ts +12 -0
- package/dist/core/colors.d.ts.map +1 -0
- package/dist/core/colors.js +335 -0
- package/dist/core/colors.js.map +1 -0
- package/dist/core/config.d.ts +103 -0
- package/dist/core/config.d.ts.map +1 -0
- package/dist/core/config.js +119 -0
- package/dist/core/config.js.map +1 -0
- package/dist/core/index.d.ts +8 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +8 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/static-page.d.ts +3 -0
- package/dist/core/static-page.d.ts.map +1 -0
- package/dist/core/static-page.js +1830 -0
- package/dist/core/static-page.js.map +1 -0
- package/dist/core/types.d.ts +163 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/types.js +2 -0
- package/dist/core/types.js.map +1 -0
- package/docs/STYLE_CONTRACT.md +48 -0
- package/examples/acme-studio-color-system.md +99 -0
- package/package.json +89 -0
|
@@ -0,0 +1,1085 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { AlignCenter, AlignLeft, AlignRight, ArrowDown, Copy, Download, RotateCcw, Upload, X, } from 'lucide-react';
|
|
4
|
+
import { useEffect, useMemo, useRef, useState, } from 'react';
|
|
5
|
+
const transparentPreviewStyle = {
|
|
6
|
+
backgroundColor: '#f7f7f7',
|
|
7
|
+
backgroundImage: 'repeating-conic-gradient(#ececec 0% 25%, #f8f8f8 0% 50%)',
|
|
8
|
+
backgroundPosition: '50%',
|
|
9
|
+
backgroundSize: '20px 20px',
|
|
10
|
+
};
|
|
11
|
+
const avatarShapeOptions = [
|
|
12
|
+
{ label: 'Square', value: 'square' },
|
|
13
|
+
{ label: 'Round', value: 'round' },
|
|
14
|
+
{ label: 'Rounded', value: 'rounded' },
|
|
15
|
+
];
|
|
16
|
+
const avatarBorderThicknessOptions = [
|
|
17
|
+
{ label: 'None', value: 'none', ratio: 0 },
|
|
18
|
+
{ label: 'Thin', value: 'thin', ratio: 0.16 },
|
|
19
|
+
{ label: 'Medium', value: 'medium', ratio: 0.32 },
|
|
20
|
+
{ label: 'Heavy', value: 'heavy', ratio: 0.48 },
|
|
21
|
+
];
|
|
22
|
+
const avatarSizeOptions = [512, 1024];
|
|
23
|
+
const avatarBorderStep = 4;
|
|
24
|
+
const avatarMaxBorderRatio = 96 / 512;
|
|
25
|
+
const bannerPreviewScale = 0.5;
|
|
26
|
+
const deterministicIntro = 'Approved marks, avatar-ready presets, social profile assets, and the current color system.';
|
|
27
|
+
function getAssetDownloadHref(downloadUrl) {
|
|
28
|
+
return downloadUrl;
|
|
29
|
+
}
|
|
30
|
+
function assetFileLabel(asset) {
|
|
31
|
+
return asset.downloads.map((download) => download.fileName).join(' / ');
|
|
32
|
+
}
|
|
33
|
+
function getLightboxDownload(asset) {
|
|
34
|
+
return (asset.downloads.find((download) => download.format === 'PNG') ??
|
|
35
|
+
asset.downloads.find((download) => download.format === 'SVG') ??
|
|
36
|
+
asset.downloads[0]);
|
|
37
|
+
}
|
|
38
|
+
function getAssetSearchText(asset) {
|
|
39
|
+
return [
|
|
40
|
+
asset.id,
|
|
41
|
+
asset.title,
|
|
42
|
+
asset.previewUrl,
|
|
43
|
+
...asset.downloads.map((download) => download.fileName),
|
|
44
|
+
]
|
|
45
|
+
.join(' ')
|
|
46
|
+
.toLowerCase();
|
|
47
|
+
}
|
|
48
|
+
function isWordmarkAsset(asset) {
|
|
49
|
+
const compact = getAssetSearchText(asset).replace(/[^a-z0-9]+/g, '');
|
|
50
|
+
return compact.includes('wordmark');
|
|
51
|
+
}
|
|
52
|
+
function isIconAsset(asset) {
|
|
53
|
+
const text = getAssetSearchText(asset);
|
|
54
|
+
const tokens = text.split(/[^a-z0-9]+/).filter(Boolean);
|
|
55
|
+
const compact = tokens.join('');
|
|
56
|
+
return (!isWordmarkAsset(asset) &&
|
|
57
|
+
(tokens.some((token) => ['icon', 'icons', 'symbol', 'symbols', 'favicon'].includes(token)) ||
|
|
58
|
+
compact.includes('brandmark')));
|
|
59
|
+
}
|
|
60
|
+
function findAvatarAssets(groups) {
|
|
61
|
+
const iconGroup = groups.find((group) => /icon/i.test(group.key)) ??
|
|
62
|
+
groups.find((group) => /icon/i.test(group.label));
|
|
63
|
+
const iconGroupAssets = iconGroup?.items.filter((asset) => !isWordmarkAsset(asset)) ?? [];
|
|
64
|
+
if (iconGroupAssets.length)
|
|
65
|
+
return iconGroupAssets;
|
|
66
|
+
return groups
|
|
67
|
+
.flatMap((group) => group.items)
|
|
68
|
+
.filter((asset) => isIconAsset(asset));
|
|
69
|
+
}
|
|
70
|
+
function findHeroAsset(groups) {
|
|
71
|
+
return findAvatarAssets(groups)[0] ?? groups[0]?.items[0] ?? null;
|
|
72
|
+
}
|
|
73
|
+
function findFooterAsset(groups) {
|
|
74
|
+
const logoGroup = groups.find((group) => /logo|lockup|wordmark/i.test(group.key)) ??
|
|
75
|
+
groups.find((group) => /logo|lockup|wordmark/i.test(group.label));
|
|
76
|
+
return logoGroup?.items[0] ?? groups[0]?.items[0] ?? null;
|
|
77
|
+
}
|
|
78
|
+
function normalizeOptionalHexColor(value) {
|
|
79
|
+
if (!value)
|
|
80
|
+
return null;
|
|
81
|
+
const trimmed = value.trim().replace(/^#/, '');
|
|
82
|
+
if (/^[0-9a-f]{3}$/i.test(trimmed)) {
|
|
83
|
+
return `#${trimmed
|
|
84
|
+
.split('')
|
|
85
|
+
.map((character) => character + character)
|
|
86
|
+
.join('')}`;
|
|
87
|
+
}
|
|
88
|
+
if (/^[0-9a-f]{6}$/i.test(trimmed))
|
|
89
|
+
return `#${trimmed}`;
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
function normalizeHexColor(value) {
|
|
93
|
+
return normalizeOptionalHexColor(value) ?? '#ffffff';
|
|
94
|
+
}
|
|
95
|
+
function tokenize(value) {
|
|
96
|
+
return value
|
|
97
|
+
.toLowerCase()
|
|
98
|
+
.split(/[^a-z0-9]+/)
|
|
99
|
+
.filter(Boolean);
|
|
100
|
+
}
|
|
101
|
+
function findPrimaryColor(colors) {
|
|
102
|
+
return (colors.find((color) => /primary/i.test(color.name)) ??
|
|
103
|
+
colors[0] ?? {
|
|
104
|
+
hex: '#0d2249',
|
|
105
|
+
name: 'Primary',
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
function getBrandGradientStops(colors) {
|
|
109
|
+
return [
|
|
110
|
+
...new Set(colors
|
|
111
|
+
.map((color) => normalizeOptionalHexColor(color.hex))
|
|
112
|
+
.filter((hex) => Boolean(hex))),
|
|
113
|
+
];
|
|
114
|
+
}
|
|
115
|
+
function getCustomColorPreviewStyle(colors) {
|
|
116
|
+
const stops = getBrandGradientStops(colors);
|
|
117
|
+
if (stops.length === 0) {
|
|
118
|
+
return { backgroundColor: '#ffffff' };
|
|
119
|
+
}
|
|
120
|
+
if (stops.length === 1) {
|
|
121
|
+
return { backgroundColor: stops[0] };
|
|
122
|
+
}
|
|
123
|
+
return {
|
|
124
|
+
backgroundColor: stops[0],
|
|
125
|
+
backgroundImage: `linear-gradient(135deg, ${stops
|
|
126
|
+
.map((hex, index) => {
|
|
127
|
+
const stop = (index / (stops.length - 1)) * 100;
|
|
128
|
+
const formattedStop = Number.isInteger(stop)
|
|
129
|
+
? String(stop)
|
|
130
|
+
: stop.toFixed(2).replace(/\.?0+$/, '');
|
|
131
|
+
return `${hex} ${formattedStop}%`;
|
|
132
|
+
})
|
|
133
|
+
.join(', ')})`,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
function findCustomPreviewColors(colors, colorSections) {
|
|
137
|
+
const colorByName = new Map(colors.map((color) => [color.name.toLowerCase(), color]));
|
|
138
|
+
const colorByHex = new Map(colors.flatMap((color) => {
|
|
139
|
+
const hex = normalizeOptionalHexColor(color.hex);
|
|
140
|
+
return hex ? [[hex.toLowerCase(), color]] : [];
|
|
141
|
+
}));
|
|
142
|
+
const selected = [];
|
|
143
|
+
const seen = new Set();
|
|
144
|
+
for (const section of colorSections) {
|
|
145
|
+
if (!/primary|secondary/i.test(section.label))
|
|
146
|
+
continue;
|
|
147
|
+
for (const colorReference of section.rows.flat()) {
|
|
148
|
+
const color = colorByName.get(colorReference.toLowerCase()) ??
|
|
149
|
+
colorByHex.get(normalizeOptionalHexColor(colorReference)?.toLowerCase() ?? '');
|
|
150
|
+
if (!color || seen.has(color.name.toLowerCase()))
|
|
151
|
+
continue;
|
|
152
|
+
seen.add(color.name.toLowerCase());
|
|
153
|
+
selected.push(color);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return selected.length ? selected : colors;
|
|
157
|
+
}
|
|
158
|
+
function getColorPickerFallback(colors) {
|
|
159
|
+
return getBrandGradientStops(colors)[0] ?? '#05070b';
|
|
160
|
+
}
|
|
161
|
+
function makeFixedBackgroundOptions(customPreviewColors, customHex) {
|
|
162
|
+
const customColor = normalizeOptionalHexColor(customHex);
|
|
163
|
+
return [
|
|
164
|
+
{ color: null, key: 'transparent', label: 'Transparent' },
|
|
165
|
+
{ color: '#05070b', key: 'black', label: 'Black' },
|
|
166
|
+
{ color: '#ffffff', key: 'white', label: 'White' },
|
|
167
|
+
{
|
|
168
|
+
color: customColor,
|
|
169
|
+
key: 'custom',
|
|
170
|
+
label: 'Custom',
|
|
171
|
+
previewStyle: customColor
|
|
172
|
+
? undefined
|
|
173
|
+
: getCustomColorPreviewStyle(customPreviewColors),
|
|
174
|
+
},
|
|
175
|
+
];
|
|
176
|
+
}
|
|
177
|
+
function makeFixedBorderOptions(colors, customPreviewColors, customHex) {
|
|
178
|
+
const primary = findPrimaryColor(colors);
|
|
179
|
+
const customColor = normalizeOptionalHexColor(customHex);
|
|
180
|
+
return [
|
|
181
|
+
{ color: primary.hex, key: 'primary', label: 'Primary' },
|
|
182
|
+
{ color: '#05070b', key: 'black', label: 'Black' },
|
|
183
|
+
{ color: '#ffffff', key: 'white', label: 'White' },
|
|
184
|
+
{
|
|
185
|
+
color: customColor,
|
|
186
|
+
key: 'custom',
|
|
187
|
+
label: 'Custom',
|
|
188
|
+
previewStyle: customColor
|
|
189
|
+
? undefined
|
|
190
|
+
: getCustomColorPreviewStyle(customPreviewColors),
|
|
191
|
+
},
|
|
192
|
+
];
|
|
193
|
+
}
|
|
194
|
+
const avatarVariantStopTokens = new Set([
|
|
195
|
+
'brandmark',
|
|
196
|
+
'favicon',
|
|
197
|
+
'favicons',
|
|
198
|
+
'icon',
|
|
199
|
+
'icons',
|
|
200
|
+
'logo',
|
|
201
|
+
'logos',
|
|
202
|
+
'mark',
|
|
203
|
+
'marks',
|
|
204
|
+
'symbol',
|
|
205
|
+
'symbols',
|
|
206
|
+
]);
|
|
207
|
+
function getAvatarAssetTokens(asset) {
|
|
208
|
+
const fileName = asset.downloads[0]?.fileName ??
|
|
209
|
+
asset.previewUrl.split('/').pop() ??
|
|
210
|
+
asset.id.split(':').pop() ??
|
|
211
|
+
asset.title;
|
|
212
|
+
return tokenize(fileName.replace(/\.[^.]+$/, ''));
|
|
213
|
+
}
|
|
214
|
+
function getCommonAvatarAssetTokens(assets) {
|
|
215
|
+
const tokenLists = assets.map((asset) => new Set(getAvatarAssetTokens(asset)));
|
|
216
|
+
if (!tokenLists.length)
|
|
217
|
+
return new Set();
|
|
218
|
+
return new Set(Array.from(tokenLists[0] ?? []).filter((token) => tokenLists.every((tokens) => tokens.has(token))));
|
|
219
|
+
}
|
|
220
|
+
function titleFromAvatarVariantTokens(tokens) {
|
|
221
|
+
return tokens
|
|
222
|
+
.map((token) => token.toUpperCase() === token
|
|
223
|
+
? token
|
|
224
|
+
: token.charAt(0).toUpperCase() + token.slice(1))
|
|
225
|
+
.join(' ');
|
|
226
|
+
}
|
|
227
|
+
function getAvatarIconLabel(asset, commonTokens) {
|
|
228
|
+
const variantTokens = getAvatarAssetTokens(asset).filter((token) => !commonTokens.has(token) && !avatarVariantStopTokens.has(token));
|
|
229
|
+
return variantTokens.length
|
|
230
|
+
? titleFromAvatarVariantTokens(variantTokens)
|
|
231
|
+
: 'Primary';
|
|
232
|
+
}
|
|
233
|
+
function inferAvatarIconOptions(assets, colors) {
|
|
234
|
+
const primary = findPrimaryColor(colors);
|
|
235
|
+
const brandColorTokenCounts = colors
|
|
236
|
+
.map((color) => new Set(tokenize(color.name)))
|
|
237
|
+
.reduce((counts, tokens) => {
|
|
238
|
+
for (const token of tokens) {
|
|
239
|
+
counts.set(token, (counts.get(token) ?? 0) + 1);
|
|
240
|
+
}
|
|
241
|
+
return counts;
|
|
242
|
+
}, new Map());
|
|
243
|
+
const colorCandidates = [
|
|
244
|
+
...colors.map((color, index) => ({
|
|
245
|
+
color: color.hex,
|
|
246
|
+
key: `brand-${index}-${color.hex.toLowerCase()}`,
|
|
247
|
+
label: color.name,
|
|
248
|
+
tokens: tokenize(color.name).filter((token) => token.length > 2 && (brandColorTokenCounts.get(token) ?? 0) === 1),
|
|
249
|
+
})),
|
|
250
|
+
{
|
|
251
|
+
color: '#ffffff',
|
|
252
|
+
key: 'white',
|
|
253
|
+
label: 'White',
|
|
254
|
+
tokens: ['white'],
|
|
255
|
+
},
|
|
256
|
+
{
|
|
257
|
+
color: '#05070b',
|
|
258
|
+
key: 'black',
|
|
259
|
+
label: 'Black',
|
|
260
|
+
tokens: ['black'],
|
|
261
|
+
},
|
|
262
|
+
];
|
|
263
|
+
const options = [];
|
|
264
|
+
const seen = new Set();
|
|
265
|
+
const commonAssetTokens = getCommonAvatarAssetTokens(assets);
|
|
266
|
+
for (const asset of assets) {
|
|
267
|
+
const text = getAssetSearchText(asset);
|
|
268
|
+
const candidate = colorCandidates.find((option) => option.tokens.some((token) => token && text.includes(token))) ??
|
|
269
|
+
(/\bprimary\b/.test(text)
|
|
270
|
+
? {
|
|
271
|
+
color: primary.hex,
|
|
272
|
+
key: 'primary',
|
|
273
|
+
label: primary.name,
|
|
274
|
+
tokens: ['primary'],
|
|
275
|
+
}
|
|
276
|
+
: null) ?? {
|
|
277
|
+
color: primary.hex,
|
|
278
|
+
key: `primary-${primary.hex.toLowerCase()}`,
|
|
279
|
+
label: primary.name,
|
|
280
|
+
tokens: ['primary'],
|
|
281
|
+
};
|
|
282
|
+
const key = `asset:${asset.id}`;
|
|
283
|
+
if (seen.has(key))
|
|
284
|
+
continue;
|
|
285
|
+
options.push({
|
|
286
|
+
asset,
|
|
287
|
+
color: candidate.key.startsWith('primary-') ? '#05070b' : candidate.color,
|
|
288
|
+
key,
|
|
289
|
+
label: getAvatarIconLabel(asset, commonAssetTokens),
|
|
290
|
+
});
|
|
291
|
+
seen.add(key);
|
|
292
|
+
}
|
|
293
|
+
return options.length
|
|
294
|
+
? options
|
|
295
|
+
: assets.map((asset, index) => ({
|
|
296
|
+
asset,
|
|
297
|
+
color: '#05070b',
|
|
298
|
+
key: `icon-${index}:${asset.id}`,
|
|
299
|
+
label: getAvatarIconLabel(asset, commonAssetTokens),
|
|
300
|
+
}));
|
|
301
|
+
}
|
|
302
|
+
function getColorOption(options, key) {
|
|
303
|
+
return options.find((option) => option.key === key) ?? options[0];
|
|
304
|
+
}
|
|
305
|
+
function getMaxBorderThickness(size) {
|
|
306
|
+
return size * avatarMaxBorderRatio;
|
|
307
|
+
}
|
|
308
|
+
function roundBorderThickness(thickness) {
|
|
309
|
+
return Math.round(thickness / avatarBorderStep) * avatarBorderStep;
|
|
310
|
+
}
|
|
311
|
+
function getAvatarBorderThickness(thickness, size) {
|
|
312
|
+
const ratio = avatarBorderThicknessOptions.find((option) => option.value === thickness)
|
|
313
|
+
?.ratio ?? 0;
|
|
314
|
+
return roundBorderThickness(getMaxBorderThickness(size) * ratio);
|
|
315
|
+
}
|
|
316
|
+
function getAvatarShapeClass(shape) {
|
|
317
|
+
if (shape === 'round')
|
|
318
|
+
return 'rounded-full';
|
|
319
|
+
if (shape === 'rounded')
|
|
320
|
+
return 'rounded-[22%]';
|
|
321
|
+
return 'rounded-none';
|
|
322
|
+
}
|
|
323
|
+
function selectionRing(isSelected) {
|
|
324
|
+
return isSelected
|
|
325
|
+
? 'border-[#3a89c0] ring-2 ring-[#3a89c0] ring-offset-2'
|
|
326
|
+
: 'border-neutral-300 hover:border-neutral-500';
|
|
327
|
+
}
|
|
328
|
+
function addAvatarShapePath(context, size, shape, inset = 0) {
|
|
329
|
+
const edge = size - inset * 2;
|
|
330
|
+
const radius = Math.min(edge / 2, Math.max(0, size * 0.22 - inset));
|
|
331
|
+
context.beginPath();
|
|
332
|
+
if (shape === 'round') {
|
|
333
|
+
context.arc(size / 2, size / 2, edge / 2, 0, Math.PI * 2);
|
|
334
|
+
context.closePath();
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
if (shape === 'rounded') {
|
|
338
|
+
if (typeof context.roundRect === 'function') {
|
|
339
|
+
context.roundRect(inset, inset, edge, edge, radius);
|
|
340
|
+
context.closePath();
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
context.moveTo(inset + radius, inset);
|
|
344
|
+
context.lineTo(inset + edge - radius, inset);
|
|
345
|
+
context.arcTo(inset + edge, inset, inset + edge, inset + radius, radius);
|
|
346
|
+
context.lineTo(inset + edge, inset + edge - radius);
|
|
347
|
+
context.arcTo(inset + edge, inset + edge, inset + edge - radius, inset + edge, radius);
|
|
348
|
+
context.lineTo(inset + radius, inset + edge);
|
|
349
|
+
context.arcTo(inset, inset + edge, inset, inset + edge - radius, radius);
|
|
350
|
+
context.lineTo(inset, inset + radius);
|
|
351
|
+
context.arcTo(inset, inset, inset + radius, inset, radius);
|
|
352
|
+
context.closePath();
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
context.rect(inset, inset, edge, edge);
|
|
356
|
+
context.closePath();
|
|
357
|
+
}
|
|
358
|
+
function loadCanvasImage(src) {
|
|
359
|
+
return new Promise((resolve, reject) => {
|
|
360
|
+
const image = document.createElement('img');
|
|
361
|
+
image.onload = () => resolve(image);
|
|
362
|
+
image.onerror = () => reject(new Error(`Could not load ${src}`));
|
|
363
|
+
image.src = src;
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
async function drawAvatarCanvas({ backgroundColor, borderColor, borderThickness, canvas, iconSrc, iconWidthPercent, shape, size, isCurrent = () => true, }) {
|
|
367
|
+
const context = canvas.getContext('2d');
|
|
368
|
+
canvas.width = size;
|
|
369
|
+
canvas.height = size;
|
|
370
|
+
if (!context)
|
|
371
|
+
throw new Error('Canvas is unavailable.');
|
|
372
|
+
context.clearRect(0, 0, size, size);
|
|
373
|
+
if (borderThickness > 0) {
|
|
374
|
+
context.save();
|
|
375
|
+
addAvatarShapePath(context, size, shape);
|
|
376
|
+
context.fillStyle = borderColor;
|
|
377
|
+
context.fill();
|
|
378
|
+
context.restore();
|
|
379
|
+
}
|
|
380
|
+
if (backgroundColor || borderThickness > 0) {
|
|
381
|
+
context.save();
|
|
382
|
+
addAvatarShapePath(context, size, shape, borderThickness);
|
|
383
|
+
context.clip();
|
|
384
|
+
if (backgroundColor) {
|
|
385
|
+
context.fillStyle = backgroundColor;
|
|
386
|
+
context.fill();
|
|
387
|
+
}
|
|
388
|
+
else {
|
|
389
|
+
context.clearRect(0, 0, size, size);
|
|
390
|
+
}
|
|
391
|
+
context.restore();
|
|
392
|
+
}
|
|
393
|
+
const image = await loadCanvasImage(iconSrc);
|
|
394
|
+
if (!isCurrent())
|
|
395
|
+
return;
|
|
396
|
+
const maxIconWidth = size * (iconWidthPercent / 100);
|
|
397
|
+
const maxIconHeight = size * (iconWidthPercent / 100);
|
|
398
|
+
const naturalWidth = image.naturalWidth || image.width;
|
|
399
|
+
const naturalHeight = image.naturalHeight || image.height;
|
|
400
|
+
const scale = Math.min(maxIconWidth / naturalWidth, maxIconHeight / naturalHeight);
|
|
401
|
+
const iconWidth = naturalWidth * scale;
|
|
402
|
+
const iconHeight = naturalHeight * scale;
|
|
403
|
+
context.drawImage(image, (size - iconWidth) / 2, (size - iconHeight) / 2, iconWidth, iconHeight);
|
|
404
|
+
}
|
|
405
|
+
function createColorRows(manifest) {
|
|
406
|
+
const colorMap = new Map(manifest.brandColors.map((color) => [color.name.toLowerCase(), color]));
|
|
407
|
+
const configuredRows = manifest.colorSections
|
|
408
|
+
.map((section) => ({
|
|
409
|
+
columns: section.columns ?? 3,
|
|
410
|
+
label: getDisplayColorSectionLabel(section.label),
|
|
411
|
+
rows: section.rows
|
|
412
|
+
.map((row) => row
|
|
413
|
+
.map((name) => colorMap.get(name.toLowerCase()))
|
|
414
|
+
.filter((color) => Boolean(color)))
|
|
415
|
+
.filter((row) => row.length > 0),
|
|
416
|
+
}))
|
|
417
|
+
.filter((section) => section.rows.length > 0);
|
|
418
|
+
if (configuredRows.length)
|
|
419
|
+
return configuredRows;
|
|
420
|
+
const rows = [];
|
|
421
|
+
for (let index = 0; index < manifest.brandColors.length; index += 3) {
|
|
422
|
+
rows.push(manifest.brandColors.slice(index, index + 3));
|
|
423
|
+
}
|
|
424
|
+
return [{ columns: 3, label: 'Primary', rows }];
|
|
425
|
+
}
|
|
426
|
+
function getDisplayColorSectionLabel(label) {
|
|
427
|
+
const trimmed = label.trim();
|
|
428
|
+
if (/^brand colors?$/i.test(trimmed))
|
|
429
|
+
return 'Primary';
|
|
430
|
+
return trimmed.replace(/\s+colors?$/i, '');
|
|
431
|
+
}
|
|
432
|
+
function formatAssetCount(count) {
|
|
433
|
+
return `${count} ${count === 1 ? 'asset' : 'assets'} available`;
|
|
434
|
+
}
|
|
435
|
+
function slugify(value) {
|
|
436
|
+
return value
|
|
437
|
+
.toLowerCase()
|
|
438
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
439
|
+
.replace(/(^-|-$)/g, '');
|
|
440
|
+
}
|
|
441
|
+
function DownloadIcon() {
|
|
442
|
+
return _jsx(Download, { "aria-hidden": true, className: "h-4 w-4" });
|
|
443
|
+
}
|
|
444
|
+
function UploadIcon() {
|
|
445
|
+
return _jsx(Upload, { "aria-hidden": true, className: "h-4 w-4" });
|
|
446
|
+
}
|
|
447
|
+
function ResetIcon() {
|
|
448
|
+
return _jsx(RotateCcw, { "aria-hidden": true, className: "h-4 w-4" });
|
|
449
|
+
}
|
|
450
|
+
function CloseIcon() {
|
|
451
|
+
return _jsx(X, { "aria-hidden": true, className: "h-4 w-4" });
|
|
452
|
+
}
|
|
453
|
+
function toastToneClasses(tone) {
|
|
454
|
+
if (tone === 'error')
|
|
455
|
+
return 'border-red-200 text-red-950';
|
|
456
|
+
if (tone === 'info')
|
|
457
|
+
return 'border-slate-200 text-slate-950';
|
|
458
|
+
return 'border-neutral-200 text-neutral-950';
|
|
459
|
+
}
|
|
460
|
+
function ToastStack({ toasts }) {
|
|
461
|
+
if (!toasts.length)
|
|
462
|
+
return null;
|
|
463
|
+
return (_jsx("div", { className: "pointer-events-none fixed bottom-4 left-1/2 z-[140] flex w-[min(24rem,calc(100vw-2rem))] -translate-x-1/2 flex-col items-center gap-2", children: toasts.map((toast) => (_jsx("div", { className: `pointer-events-auto w-full rounded-md border bg-white px-4 py-3 text-left text-sm font-medium shadow-lg ${toastToneClasses(toast.tone)}`, role: "status", children: toast.message }, toast.id))) }));
|
|
464
|
+
}
|
|
465
|
+
function AssetDownloadButton({ download, }) {
|
|
466
|
+
return (_jsxs("a", { className: "inline-flex items-center gap-1 rounded-md bg-[#2b333f] px-3 py-2 text-sm font-medium text-white transition-colors hover:bg-[#1d232b]", download: download.fileName, href: getAssetDownloadHref(download.url), children: [_jsx(DownloadIcon, {}), _jsx("span", { children: download.format })] }));
|
|
467
|
+
}
|
|
468
|
+
function DownloadAllButton({ href, label = 'Download all' }) {
|
|
469
|
+
return (_jsxs("a", { className: "inline-flex items-center gap-1 rounded-md border border-neutral-300 bg-white px-3 py-1.5 text-sm font-medium text-neutral-800 transition-colors hover:border-neutral-400 hover:bg-neutral-50", download: true, href: href, children: [_jsx(DownloadIcon, {}), _jsx("span", { children: label })] }));
|
|
470
|
+
}
|
|
471
|
+
function AssetCard({ asset, onPreview, }) {
|
|
472
|
+
return (_jsxs("article", { className: "flex h-full flex-col overflow-hidden rounded-lg border border-neutral-200 bg-white", children: [_jsx("button", { className: "flex min-h-[220px] items-center justify-center border-b border-neutral-200 px-6 py-10 transition-opacity hover:opacity-90", onClick: () => onPreview(asset), style: transparentPreviewStyle, type: "button", children: _jsx("img", { src: asset.previewUrl, alt: asset.title, className: "max-h-28 w-auto max-w-full object-contain", loading: "lazy" }) }), _jsxs("div", { className: "flex flex-1 flex-col justify-between gap-4 p-4", children: [_jsx("div", { children: _jsx("p", { className: "text-sm font-semibold text-neutral-900", children: asset.title }) }), _jsx("div", { className: "flex flex-wrap items-center gap-2", children: asset.downloads.map((download) => (_jsx(AssetDownloadButton, { download: download }, `${asset.id}-${download.fileName}`))) })] })] }));
|
|
473
|
+
}
|
|
474
|
+
function AssetGroup({ downloadHref, group, onPreview, }) {
|
|
475
|
+
return (_jsxs("section", { className: "space-y-5", children: [_jsxs("div", { className: "flex flex-col gap-3 sm:flex-row sm:items-end sm:justify-between", children: [_jsxs("div", { children: [_jsx("h3", { className: "text-lg font-semibold text-slate-950", children: group.label }), group.description ? (_jsx("p", { className: "mt-1 text-sm text-slate-600", children: group.description })) : null] }), _jsxs("div", { className: "flex items-center gap-3", children: [_jsx("span", { className: "text-sm text-slate-500", children: formatAssetCount(group.items.length) }), _jsx(DownloadAllButton, { href: downloadHref })] })] }), _jsx("div", { className: "grid gap-4 sm:grid-cols-2 lg:grid-cols-4", children: group.items.map((asset) => (_jsx(AssetCard, { asset: asset, onPreview: onPreview }, asset.id))) })] }));
|
|
476
|
+
}
|
|
477
|
+
function CopyButton({ label, onToast, value, }) {
|
|
478
|
+
async function copy() {
|
|
479
|
+
try {
|
|
480
|
+
await navigator.clipboard.writeText(value);
|
|
481
|
+
onToast(`${label} copied.`);
|
|
482
|
+
}
|
|
483
|
+
catch {
|
|
484
|
+
onToast('Could not copy that color value.', 'error');
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
return (_jsxs("button", { className: "inline-flex cursor-pointer items-center gap-1 rounded-md border border-neutral-200 bg-white px-2.5 py-1.5 text-xs font-medium text-neutral-700 transition-colors hover:border-neutral-300 hover:bg-neutral-50", onClick: () => void copy(), title: `Copy ${label}`, type: "button", children: [_jsx(Copy, { "aria-hidden": true, className: "h-3.5 w-3.5" }), _jsx("span", { children: value })] }));
|
|
488
|
+
}
|
|
489
|
+
function ColorCard({ color, onToast, }) {
|
|
490
|
+
return (_jsxs("article", { className: "overflow-hidden rounded-lg border border-neutral-200 bg-white", children: [_jsx("div", { className: "h-28 w-full border-b border-neutral-200", style: { backgroundColor: color.hex } }), _jsxs("div", { className: "space-y-4 p-4", children: [_jsx("div", { children: _jsx("p", { className: "text-sm font-semibold text-neutral-900", children: color.name }) }), _jsx("div", { className: "flex flex-wrap items-center gap-2", children: _jsx(CopyButton, { label: `${color.name} hex`, onToast: onToast, value: color.hex }) })] })] }));
|
|
491
|
+
}
|
|
492
|
+
function PrintCopyButton({ label, onToast, value, }) {
|
|
493
|
+
async function copy() {
|
|
494
|
+
try {
|
|
495
|
+
await navigator.clipboard.writeText(value);
|
|
496
|
+
document.activeElement?.blur();
|
|
497
|
+
onToast(`${label} copied.`);
|
|
498
|
+
}
|
|
499
|
+
catch {
|
|
500
|
+
onToast('Could not copy that print color value.', 'error');
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
return (_jsx("button", { "aria-label": `Copy ${label}`, className: "cursor-pointer text-left font-mono text-xs leading-5 text-neutral-800 transition-colors hover:text-[#3a89c0] hover:underline", onClick: () => void copy(), type: "button", children: value }));
|
|
504
|
+
}
|
|
505
|
+
function PrintComponentCopyButton({ channel, label, onToast, value, }) {
|
|
506
|
+
async function copy() {
|
|
507
|
+
try {
|
|
508
|
+
await navigator.clipboard.writeText(value);
|
|
509
|
+
document.activeElement?.blur();
|
|
510
|
+
onToast(`${label} copied.`);
|
|
511
|
+
}
|
|
512
|
+
catch {
|
|
513
|
+
onToast('Could not copy that print color value.', 'error');
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
return (_jsxs("button", { "aria-label": `Copy ${label}`, className: "flex cursor-pointer items-baseline gap-1.5 text-left font-mono text-xs leading-5 text-neutral-800 transition-colors hover:text-[#3a89c0] hover:underline", onClick: () => void copy(), type: "button", children: [_jsx("span", { className: "w-4 font-sans text-xs font-semibold text-neutral-500", children: channel }), _jsx("span", { children: value })] }));
|
|
517
|
+
}
|
|
518
|
+
function PrintValueGroup({ color, label, onToast, values, }) {
|
|
519
|
+
return (_jsxs("div", { children: [_jsx("p", { className: "text-xs font-semibold tracking-[0.12em] text-neutral-500 uppercase", children: label }), _jsxs("div", { className: "mt-1 flex items-center gap-2", children: [_jsx(Copy, { "aria-hidden": true, className: "h-3.5 w-3.5 shrink-0 text-neutral-400" }), _jsx("div", { className: "flex items-center gap-3", children: values.map((item) => item.value ? (_jsx(PrintComponentCopyButton, { channel: item.channel, label: `${color.pantone} ${label} ${item.channel}`, onToast: onToast, value: item.value }, item.channel)) : null) })] })] }));
|
|
520
|
+
}
|
|
521
|
+
function PrintColorCard({ color, onToast, }) {
|
|
522
|
+
const swatchRef = useRef(null);
|
|
523
|
+
const [popoverOpen, setPopoverOpen] = useState(false);
|
|
524
|
+
const [popoverPlacement, setPopoverPlacement] = useState('bottom');
|
|
525
|
+
const [popoverAlignment, setPopoverAlignment] = useState('left');
|
|
526
|
+
const rgbValues = ['R', 'G', 'B'].map((channel, index) => ({
|
|
527
|
+
channel,
|
|
528
|
+
value: color.rgb[index] ?? '',
|
|
529
|
+
}));
|
|
530
|
+
const cmykValues = ['C', 'M', 'Y', 'K'].map((channel, index) => ({
|
|
531
|
+
channel,
|
|
532
|
+
value: color.cmyk[index] ?? '',
|
|
533
|
+
}));
|
|
534
|
+
function preparePopover() {
|
|
535
|
+
const swatch = swatchRef.current;
|
|
536
|
+
if (!swatch) {
|
|
537
|
+
setPopoverOpen(true);
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
const rect = swatch.getBoundingClientRect();
|
|
541
|
+
const estimatedPopoverHeight = 220;
|
|
542
|
+
const estimatedPopoverWidth = 320;
|
|
543
|
+
const viewportPadding = 16;
|
|
544
|
+
const spaceBelow = window.innerHeight - rect.bottom;
|
|
545
|
+
const spaceAbove = rect.top;
|
|
546
|
+
setPopoverPlacement(spaceBelow < estimatedPopoverHeight && spaceAbove > spaceBelow
|
|
547
|
+
? 'top'
|
|
548
|
+
: 'bottom');
|
|
549
|
+
setPopoverAlignment(rect.left + estimatedPopoverWidth > window.innerWidth - viewportPadding
|
|
550
|
+
? 'right'
|
|
551
|
+
: 'left');
|
|
552
|
+
setPopoverOpen(true);
|
|
553
|
+
}
|
|
554
|
+
useEffect(() => {
|
|
555
|
+
if (!popoverOpen)
|
|
556
|
+
return;
|
|
557
|
+
const closeOnOutsidePointer = (event) => {
|
|
558
|
+
if (!swatchRef.current?.contains(event.target)) {
|
|
559
|
+
setPopoverOpen(false);
|
|
560
|
+
}
|
|
561
|
+
};
|
|
562
|
+
document.addEventListener('pointerdown', closeOnOutsidePointer);
|
|
563
|
+
return () => document.removeEventListener('pointerdown', closeOnOutsidePointer);
|
|
564
|
+
}, [popoverOpen]);
|
|
565
|
+
return (_jsxs("article", { className: "relative", onMouseEnter: preparePopover, onMouseLeave: () => setPopoverOpen(false), ref: swatchRef, children: [_jsxs("div", { className: "overflow-hidden rounded-md border border-neutral-200 bg-white shadow-sm", children: [_jsx("button", { "aria-label": `${color.pantone} color values`, className: "aspect-square w-full border-b border-neutral-200 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#3a89c0]", onClick: preparePopover, onFocus: preparePopover, style: { backgroundColor: color.hex }, type: "button" }), _jsxs("div", { className: "bg-white px-2.5 pt-1.5 pb-2.5 text-left", children: [_jsx("p", { className: "text-[10px] font-semibold tracking-[0.12em] text-neutral-500 uppercase", children: "Pantone" }), _jsx("p", { className: "mt-0.5 text-xs font-semibold text-neutral-900", children: color.pantone })] })] }), _jsx("div", { className: `absolute z-40 w-max max-w-[calc(100vw-2rem)] min-w-64 ${popoverAlignment === 'right' ? 'right-0' : 'left-0'} ${popoverPlacement === 'top' ? 'bottom-full pb-2' : 'top-full pt-2'} ${popoverOpen ? 'block' : 'hidden'}`, children: _jsxs("div", { className: "relative rounded-lg border border-neutral-200 bg-white p-4 text-left shadow-xl", children: [_jsxs("div", { className: "absolute top-3 right-3 rounded-md border border-neutral-200 bg-white px-2.5 py-1.5 text-right shadow-sm", children: [_jsx("p", { className: "text-[9px] font-semibold tracking-[0.12em] text-neutral-500 uppercase", children: "Pantone" }), _jsx("p", { className: "mt-0.5 text-xs font-semibold text-neutral-900", children: color.pantone })] }), _jsxs("div", { className: "space-y-3", children: [_jsxs("div", { children: [_jsx("p", { className: "text-xs font-semibold tracking-[0.12em] text-neutral-500 uppercase", children: "Hex" }), _jsxs("div", { className: "mt-1 flex items-center gap-2", children: [_jsx(Copy, { "aria-hidden": true, className: "h-3.5 w-3.5 shrink-0 text-neutral-400" }), _jsx(PrintCopyButton, { label: `${color.pantone} hex`, onToast: onToast, value: color.hex })] })] }), _jsx(PrintValueGroup, { color: color, label: "RGB", onToast: onToast, values: rgbValues }), _jsx(PrintValueGroup, { color: color, label: "CMYK", onToast: onToast, values: cmykValues })] })] }) })] }));
|
|
566
|
+
}
|
|
567
|
+
function PrintColorGroups({ groups, onToast, }) {
|
|
568
|
+
if (!groups.length)
|
|
569
|
+
return null;
|
|
570
|
+
return (_jsxs("section", { className: "space-y-8", children: [_jsxs("div", { children: [_jsx("h3", { className: "text-lg font-semibold text-neutral-900", children: "Print Colors" }), _jsx("p", { className: "text-sm text-neutral-600", children: "Pantone-aligned swatches for merch, packaging, and print production." })] }), groups.map((group) => (_jsxs("section", { className: "space-y-5", children: [_jsx("h4", { className: "text-base font-semibold text-neutral-900", children: group.label }), _jsx("div", { className: "grid grid-cols-3 gap-3 sm:grid-cols-5 lg:grid-cols-7 xl:grid-cols-10", children: group.items.map((color) => (_jsx(PrintColorCard, { color: color, onToast: onToast }, `${group.label}-${color.pantone}`))) })] }, group.label)))] }));
|
|
571
|
+
}
|
|
572
|
+
function AvatarIconColorChip({ option, selected, onSelect, }) {
|
|
573
|
+
return (_jsxs("button", { "aria-pressed": selected, className: "flex w-16 cursor-pointer flex-col items-start gap-2 text-center", onClick: onSelect, type: "button", children: [_jsx("span", { className: `block aspect-square w-16 rounded-md border transition-colors ${selectionRing(selected)}`, style: { backgroundColor: normalizeHexColor(option.color) } }), _jsx("span", { className: "w-16 text-xs leading-4 font-medium text-neutral-700", children: option.label })] }));
|
|
574
|
+
}
|
|
575
|
+
function AvatarColorChip({ option, selected, onSelect, }) {
|
|
576
|
+
return (_jsxs("button", { "aria-pressed": selected, className: "flex w-16 cursor-pointer flex-col items-start gap-2 text-center", onClick: onSelect, type: "button", children: [_jsx("span", { className: `block aspect-square w-16 rounded-md border transition-colors ${selectionRing(selected)}`, style: option.previewStyle ??
|
|
577
|
+
(option.color
|
|
578
|
+
? { backgroundColor: normalizeHexColor(option.color) }
|
|
579
|
+
: transparentPreviewStyle) }), _jsx("span", { className: "w-16 text-xs leading-4 font-medium text-neutral-700", children: option.label })] }));
|
|
580
|
+
}
|
|
581
|
+
function AvatarCustomColorChip({ draftValue, inputLabel, open, option, pickerValue, placeholder, selected, onChange, onClose, onDraftChange, onOpen, }) {
|
|
582
|
+
const containerRef = useRef(null);
|
|
583
|
+
useEffect(() => {
|
|
584
|
+
if (!open)
|
|
585
|
+
return;
|
|
586
|
+
function closeOnOutsidePointer(event) {
|
|
587
|
+
if (event.target instanceof Node &&
|
|
588
|
+
!containerRef.current?.contains(event.target)) {
|
|
589
|
+
onClose();
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
function closeOnEscape(event) {
|
|
593
|
+
if (event.key === 'Escape')
|
|
594
|
+
onClose();
|
|
595
|
+
}
|
|
596
|
+
document.addEventListener('pointerdown', closeOnOutsidePointer);
|
|
597
|
+
document.addEventListener('keydown', closeOnEscape);
|
|
598
|
+
return () => {
|
|
599
|
+
document.removeEventListener('pointerdown', closeOnOutsidePointer);
|
|
600
|
+
document.removeEventListener('keydown', closeOnEscape);
|
|
601
|
+
};
|
|
602
|
+
}, [onClose, open]);
|
|
603
|
+
return (_jsxs("div", { className: "relative flex w-16 flex-col items-start gap-2 text-center", ref: containerRef, children: [_jsxs("button", { "aria-expanded": open, "aria-haspopup": "dialog", "aria-pressed": selected, className: "flex w-16 cursor-pointer flex-col items-start gap-2", onClick: onOpen, type: "button", children: [_jsx("span", { className: `block aspect-square w-16 rounded-md border transition-colors ${selectionRing(selected)}`, style: option.previewStyle ??
|
|
604
|
+
(option.color
|
|
605
|
+
? { backgroundColor: normalizeHexColor(option.color) }
|
|
606
|
+
: transparentPreviewStyle) }), _jsx("span", { className: "w-16 text-xs leading-4 font-medium text-neutral-700", children: option.label })] }), open ? (_jsxs("div", { "aria-label": inputLabel, className: "absolute top-full left-1/2 z-30 mt-3 w-56 -translate-x-1/2 rounded-md border border-slate-200 bg-white p-3 text-left shadow-xl", role: "dialog", children: [_jsxs("div", { className: "flex items-center justify-between gap-3", children: [_jsx("p", { className: "text-xs font-semibold tracking-[0.12em] text-slate-500 uppercase", children: "Custom" }), _jsx("button", { "aria-label": "Close custom color picker", className: "inline-flex h-7 w-7 cursor-pointer items-center justify-center rounded-md text-slate-500 transition hover:bg-slate-100 hover:text-slate-900", onClick: onClose, type: "button", children: _jsx(CloseIcon, {}) })] }), _jsxs("div", { className: "mt-3 grid grid-cols-[3rem_minmax(0,1fr)] items-end gap-3", children: [_jsxs("label", { className: "flex flex-col gap-1 text-xs font-semibold tracking-[0.12em] text-slate-500 uppercase", children: [_jsx("span", { children: "Pick" }), _jsx("input", { "aria-label": `${inputLabel} picker`, className: "h-9 w-12 cursor-pointer rounded-md border border-slate-300 bg-white p-1", onChange: (event) => onChange(event.currentTarget.value), type: "color", value: pickerValue })] }), _jsxs("label", { className: "flex min-w-0 flex-col gap-1 text-xs font-semibold tracking-[0.12em] text-slate-500 uppercase", children: [_jsx("span", { children: "Hex" }), _jsx("input", { "aria-label": `${inputLabel} hex`, className: "h-9 min-w-0 rounded-md border border-slate-300 bg-white px-2.5 font-mono text-sm tracking-normal text-slate-900 uppercase", onChange: (event) => onDraftChange(event.currentTarget.value), maxLength: 7, placeholder: placeholder, type: "text", value: draftValue })] })] })] })) : null] }));
|
|
607
|
+
}
|
|
608
|
+
function AvatarShapeChip({ label, selected, shape, onSelect, }) {
|
|
609
|
+
return (_jsxs("button", { "aria-pressed": selected, className: "flex w-16 cursor-pointer flex-col items-start gap-2 text-center", onClick: onSelect, type: "button", children: [_jsx("span", { className: `flex aspect-square w-16 items-center justify-center rounded-md border bg-white transition-colors ${selectionRing(selected)}`, children: _jsx("span", { className: `block h-8 w-8 bg-[#2b333f] ${getAvatarShapeClass(shape)}` }) }), _jsx("span", { className: "w-16 text-xs leading-4 font-medium text-neutral-700", children: label })] }));
|
|
610
|
+
}
|
|
611
|
+
function AvatarBorderThicknessChip({ label, selected, thickness, onSelect, }) {
|
|
612
|
+
const previewThickness = thickness === 'none'
|
|
613
|
+
? 1
|
|
614
|
+
: Math.max(2, Math.round((avatarBorderThicknessOptions.find((option) => option.value === thickness)?.ratio ?? 0) * 14));
|
|
615
|
+
return (_jsxs("button", { "aria-pressed": selected, className: "flex w-16 cursor-pointer flex-col items-start gap-2 text-center", onClick: onSelect, type: "button", children: [_jsx("span", { className: `flex aspect-square w-16 items-center justify-center rounded-md border bg-white transition-colors ${selectionRing(selected)}`, children: _jsx("span", { className: "block h-8 w-8 rounded-[4px] bg-white", style: {
|
|
616
|
+
boxShadow: thickness === 'none'
|
|
617
|
+
? `inset 0 0 0 ${previewThickness}px #cbd5e1`
|
|
618
|
+
: `inset 0 0 0 ${previewThickness}px #0d2249`,
|
|
619
|
+
} }) }), _jsx("span", { className: "w-16 text-xs leading-4 font-medium text-neutral-700", children: label })] }));
|
|
620
|
+
}
|
|
621
|
+
function AvatarSizeChip({ selected, size, onSelect, }) {
|
|
622
|
+
return (_jsxs("button", { "aria-pressed": selected, className: "flex w-16 cursor-pointer flex-col items-start gap-2 text-center", onClick: onSelect, type: "button", children: [_jsx("span", { className: `flex aspect-square w-16 items-center justify-center rounded-md border bg-white font-mono text-sm font-semibold text-neutral-800 transition-colors ${selectionRing(selected)}`, children: size }), _jsxs("span", { className: "w-16 text-xs leading-4 font-medium text-neutral-700", children: [size, "px"] })] }));
|
|
623
|
+
}
|
|
624
|
+
function AvatarGenerator({ assets, brandLabel, canUseDevActions, colorSections, colors, endpoints, }) {
|
|
625
|
+
const previewCanvasRef = useRef(null);
|
|
626
|
+
const iconOptions = useMemo(() => inferAvatarIconOptions(assets, colors), [assets, colors]);
|
|
627
|
+
const [iconKey, setIconKey] = useState(iconOptions[0]?.key ?? '');
|
|
628
|
+
const [background, setBackground] = useState('transparent');
|
|
629
|
+
const [backgroundCustomDraft, setBackgroundCustomDraft] = useState('');
|
|
630
|
+
const [backgroundCustomHex, setBackgroundCustomHex] = useState('');
|
|
631
|
+
const [backgroundCustomOpen, setBackgroundCustomOpen] = useState(false);
|
|
632
|
+
const [border, setBorder] = useState('primary');
|
|
633
|
+
const [borderCustomDraft, setBorderCustomDraft] = useState('');
|
|
634
|
+
const [borderCustomHex, setBorderCustomHex] = useState('');
|
|
635
|
+
const [borderCustomOpen, setBorderCustomOpen] = useState(false);
|
|
636
|
+
const [borderThickness, setBorderThickness] = useState('none');
|
|
637
|
+
const [shape, setShape] = useState('square');
|
|
638
|
+
const [padding, setPadding] = useState(18);
|
|
639
|
+
const [avatarSize, setAvatarSize] = useState(1024);
|
|
640
|
+
const [status, setStatus] = useState('');
|
|
641
|
+
const [isInstallingFavicon, setInstallingFavicon] = useState(false);
|
|
642
|
+
const customPreviewColors = useMemo(() => findCustomPreviewColors(colors, colorSections), [colorSections, colors]);
|
|
643
|
+
const customPickerFallback = getColorPickerFallback(customPreviewColors);
|
|
644
|
+
const backgroundOptions = useMemo(() => makeFixedBackgroundOptions(customPreviewColors, backgroundCustomHex), [backgroundCustomHex, customPreviewColors]);
|
|
645
|
+
const borderOptions = useMemo(() => makeFixedBorderOptions(colors, customPreviewColors, borderCustomHex), [borderCustomHex, colors, customPreviewColors]);
|
|
646
|
+
const selectedIconOption = iconOptions.find((option) => option.key === iconKey) ?? iconOptions[0] ?? null;
|
|
647
|
+
const selectedAsset = selectedIconOption?.asset ?? null;
|
|
648
|
+
const selectedBackground = getColorOption(backgroundOptions, background);
|
|
649
|
+
const selectedBorder = getColorOption(borderOptions, border);
|
|
650
|
+
const backgroundColor = selectedBackground?.color ?? null;
|
|
651
|
+
const borderColor = selectedBorder?.color ?? '#05070b';
|
|
652
|
+
const avatarFilePrefix = slugify(brandLabel) || 'brand';
|
|
653
|
+
const borderThicknessPixels = getAvatarBorderThickness(borderThickness, avatarSize);
|
|
654
|
+
const iconWidthPercent = 100 - padding * 2;
|
|
655
|
+
const previewSurfaceStyle = !backgroundColor || shape !== 'square' ? transparentPreviewStyle : undefined;
|
|
656
|
+
const canInstallFavicon = canUseDevActions && Boolean(endpoints?.favicon);
|
|
657
|
+
useEffect(() => {
|
|
658
|
+
if (!selectedIconOption && iconOptions[0]) {
|
|
659
|
+
setIconKey(iconOptions[0].key);
|
|
660
|
+
}
|
|
661
|
+
}, [iconOptions, selectedIconOption]);
|
|
662
|
+
useEffect(() => {
|
|
663
|
+
const canvas = previewCanvasRef.current;
|
|
664
|
+
if (!canvas || !selectedAsset)
|
|
665
|
+
return;
|
|
666
|
+
let isCurrent = true;
|
|
667
|
+
drawAvatarCanvas({
|
|
668
|
+
backgroundColor,
|
|
669
|
+
borderColor,
|
|
670
|
+
borderThickness: borderThicknessPixels,
|
|
671
|
+
canvas,
|
|
672
|
+
iconSrc: selectedAsset.previewUrl,
|
|
673
|
+
iconWidthPercent,
|
|
674
|
+
shape,
|
|
675
|
+
size: avatarSize,
|
|
676
|
+
isCurrent: () => isCurrent,
|
|
677
|
+
}).catch(() => {
|
|
678
|
+
if (isCurrent)
|
|
679
|
+
setStatus('Could not render avatar preview.');
|
|
680
|
+
});
|
|
681
|
+
return () => {
|
|
682
|
+
isCurrent = false;
|
|
683
|
+
};
|
|
684
|
+
}, [
|
|
685
|
+
avatarSize,
|
|
686
|
+
backgroundColor,
|
|
687
|
+
borderColor,
|
|
688
|
+
borderThicknessPixels,
|
|
689
|
+
iconWidthPercent,
|
|
690
|
+
selectedAsset,
|
|
691
|
+
shape,
|
|
692
|
+
]);
|
|
693
|
+
function resetAvatarGenerator() {
|
|
694
|
+
setIconKey(iconOptions[0]?.key ?? '');
|
|
695
|
+
setBackground('transparent');
|
|
696
|
+
setBackgroundCustomDraft('');
|
|
697
|
+
setBackgroundCustomHex('');
|
|
698
|
+
setBackgroundCustomOpen(false);
|
|
699
|
+
setBorder('primary');
|
|
700
|
+
setBorderCustomDraft('');
|
|
701
|
+
setBorderCustomHex('');
|
|
702
|
+
setBorderCustomOpen(false);
|
|
703
|
+
setBorderThickness('none');
|
|
704
|
+
setShape('square');
|
|
705
|
+
setPadding(18);
|
|
706
|
+
setAvatarSize(1024);
|
|
707
|
+
setStatus('');
|
|
708
|
+
}
|
|
709
|
+
async function createAvatarCanvas(targetSize = avatarSize) {
|
|
710
|
+
if (!selectedAsset)
|
|
711
|
+
throw new Error('Choose an icon first.');
|
|
712
|
+
const canvas = document.createElement('canvas');
|
|
713
|
+
await drawAvatarCanvas({
|
|
714
|
+
backgroundColor,
|
|
715
|
+
borderColor,
|
|
716
|
+
borderThickness: getAvatarBorderThickness(borderThickness, targetSize),
|
|
717
|
+
canvas,
|
|
718
|
+
iconSrc: selectedAsset.previewUrl,
|
|
719
|
+
iconWidthPercent,
|
|
720
|
+
shape,
|
|
721
|
+
size: targetSize,
|
|
722
|
+
});
|
|
723
|
+
return canvas;
|
|
724
|
+
}
|
|
725
|
+
async function downloadAvatar(targetSize = avatarSize, fileName) {
|
|
726
|
+
const canvas = await createAvatarCanvas(targetSize);
|
|
727
|
+
const blob = await new Promise((resolve, reject) => {
|
|
728
|
+
canvas.toBlob((result) => {
|
|
729
|
+
if (result) {
|
|
730
|
+
resolve(result);
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
reject(new Error('Could not export PNG.'));
|
|
734
|
+
}, 'image/png');
|
|
735
|
+
});
|
|
736
|
+
const objectUrl = URL.createObjectURL(blob);
|
|
737
|
+
const link = document.createElement('a');
|
|
738
|
+
link.href = objectUrl;
|
|
739
|
+
link.download = fileName ?? `${avatarFilePrefix}-avatar-${targetSize}px.png`;
|
|
740
|
+
document.body.appendChild(link);
|
|
741
|
+
link.click();
|
|
742
|
+
link.remove();
|
|
743
|
+
window.setTimeout(() => URL.revokeObjectURL(objectUrl), 1000);
|
|
744
|
+
}
|
|
745
|
+
async function downloadFavicons() {
|
|
746
|
+
for (const faviconSize of [16, 32, 180, 192, 512]) {
|
|
747
|
+
await downloadAvatar(faviconSize, faviconSize === 180
|
|
748
|
+
? 'apple-touch-icon.png'
|
|
749
|
+
: `favicon-${faviconSize}x${faviconSize}.png`);
|
|
750
|
+
}
|
|
751
|
+
setStatus('Downloaded favicon PNGs.');
|
|
752
|
+
}
|
|
753
|
+
async function installFavicons() {
|
|
754
|
+
if (!endpoints?.favicon)
|
|
755
|
+
return;
|
|
756
|
+
setInstallingFavicon(true);
|
|
757
|
+
setStatus('Installing favicon files...');
|
|
758
|
+
try {
|
|
759
|
+
const canvas = await createAvatarCanvas(1024);
|
|
760
|
+
const response = await fetch(endpoints.favicon, {
|
|
761
|
+
method: 'POST',
|
|
762
|
+
headers: { 'Content-Type': 'application/json' },
|
|
763
|
+
body: JSON.stringify({ imageDataUrl: canvas.toDataURL('image/png') }),
|
|
764
|
+
});
|
|
765
|
+
const result = (await response.json());
|
|
766
|
+
if (!response.ok) {
|
|
767
|
+
throw new Error(result.error ?? 'Could not install favicon files.');
|
|
768
|
+
}
|
|
769
|
+
setStatus(`Installed ${result.files?.length ?? 0} favicon files.`);
|
|
770
|
+
}
|
|
771
|
+
catch (error) {
|
|
772
|
+
setStatus(error instanceof Error ? error.message : 'Could not install favicon files.');
|
|
773
|
+
}
|
|
774
|
+
finally {
|
|
775
|
+
setInstallingFavicon(false);
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
function applyBackgroundCustomPickerHex(value) {
|
|
779
|
+
setBackgroundCustomOpen(true);
|
|
780
|
+
setBorderCustomOpen(false);
|
|
781
|
+
const normalized = normalizeOptionalHexColor(value);
|
|
782
|
+
if (!normalized)
|
|
783
|
+
return;
|
|
784
|
+
setBackgroundCustomDraft(normalized);
|
|
785
|
+
setBackgroundCustomHex(normalized);
|
|
786
|
+
setBackground('custom');
|
|
787
|
+
}
|
|
788
|
+
function applyBackgroundCustomTextHex(value) {
|
|
789
|
+
setBackgroundCustomOpen(true);
|
|
790
|
+
setBorderCustomOpen(false);
|
|
791
|
+
setBackgroundCustomDraft(value);
|
|
792
|
+
const normalized = normalizeOptionalHexColor(value);
|
|
793
|
+
if (!normalized)
|
|
794
|
+
return;
|
|
795
|
+
setBackgroundCustomHex(normalized);
|
|
796
|
+
setBackground('custom');
|
|
797
|
+
}
|
|
798
|
+
function ensureBorderVisible() {
|
|
799
|
+
if (borderThickness === 'none')
|
|
800
|
+
setBorderThickness('thin');
|
|
801
|
+
}
|
|
802
|
+
function applyBorderCustomPickerHex(value) {
|
|
803
|
+
setBorderCustomOpen(true);
|
|
804
|
+
setBackgroundCustomOpen(false);
|
|
805
|
+
const normalized = normalizeOptionalHexColor(value);
|
|
806
|
+
if (!normalized)
|
|
807
|
+
return;
|
|
808
|
+
setBorderCustomDraft(normalized);
|
|
809
|
+
setBorderCustomHex(normalized);
|
|
810
|
+
setBorder('custom');
|
|
811
|
+
ensureBorderVisible();
|
|
812
|
+
}
|
|
813
|
+
function applyBorderCustomTextHex(value) {
|
|
814
|
+
setBorderCustomOpen(true);
|
|
815
|
+
setBackgroundCustomOpen(false);
|
|
816
|
+
setBorderCustomDraft(value);
|
|
817
|
+
const normalized = normalizeOptionalHexColor(value);
|
|
818
|
+
if (!normalized)
|
|
819
|
+
return;
|
|
820
|
+
setBorderCustomHex(normalized);
|
|
821
|
+
setBorder('custom');
|
|
822
|
+
ensureBorderVisible();
|
|
823
|
+
}
|
|
824
|
+
function selectBackgroundOption(option) {
|
|
825
|
+
if (option.key === 'custom') {
|
|
826
|
+
setBackgroundCustomOpen(true);
|
|
827
|
+
setBorderCustomOpen(false);
|
|
828
|
+
return;
|
|
829
|
+
}
|
|
830
|
+
setBackground(option.key);
|
|
831
|
+
setBackgroundCustomOpen(false);
|
|
832
|
+
}
|
|
833
|
+
function selectBorderOption(option) {
|
|
834
|
+
if (option.key === 'custom') {
|
|
835
|
+
setBorderCustomOpen(true);
|
|
836
|
+
setBackgroundCustomOpen(false);
|
|
837
|
+
return;
|
|
838
|
+
}
|
|
839
|
+
setBorder(option.key);
|
|
840
|
+
setBorderCustomOpen(false);
|
|
841
|
+
ensureBorderVisible();
|
|
842
|
+
}
|
|
843
|
+
if (!assets.length)
|
|
844
|
+
return null;
|
|
845
|
+
return (_jsxs("section", { className: "space-y-5", children: [_jsxs("div", { children: [_jsx("h3", { className: "text-lg font-semibold text-slate-950", children: "Avatar Generator" }), _jsx("p", { className: "mt-1 text-sm text-slate-600", children: "Make a profile-ready PNG from the approved icon." })] }), _jsxs("div", { className: "grid gap-5 lg:grid-cols-[minmax(0,1fr)_24rem] lg:items-stretch", children: [_jsxs("div", { className: "relative grid gap-y-12 rounded-md border border-slate-200 bg-white p-6 md:grid-cols-[minmax(0,1fr)_1px_minmax(0,1fr)]", children: [_jsx("button", { "aria-label": "Reset avatar generator", className: "absolute top-3 left-3 z-10 inline-flex h-8 w-8 cursor-pointer items-center justify-center text-slate-500 transition hover:text-blue-700", onClick: resetAvatarGenerator, title: "Reset avatar generator", type: "button", children: _jsx(ResetIcon, {}) }), _jsxs("div", { className: "flex w-max max-w-full flex-col gap-4 justify-self-center md:col-start-1 md:row-start-1", children: [_jsx("p", { className: "text-xs font-semibold tracking-[0.12em] text-slate-500 uppercase", children: "Icon" }), _jsx("div", { className: "flex flex-wrap justify-center gap-x-3 gap-y-4", children: iconOptions.map((option) => (_jsx(AvatarIconColorChip, { option: option, selected: selectedIconOption?.key === option.key, onSelect: () => setIconKey(option.key) }, option.key))) })] }), _jsxs("div", { className: "flex w-max max-w-full flex-col gap-4 justify-self-center md:col-start-1 md:row-start-2", children: [_jsx("p", { className: "text-xs font-semibold tracking-[0.12em] text-slate-500 uppercase", children: "Background" }), _jsx("div", { className: "flex flex-wrap justify-center gap-x-3 gap-y-4", children: backgroundOptions.map((option) => option.key === 'custom' ? (_jsx(AvatarCustomColorChip, { draftValue: backgroundCustomDraft, inputLabel: "Custom background color", open: backgroundCustomOpen, option: option, pickerValue: backgroundCustomHex || customPickerFallback, placeholder: customPickerFallback, selected: backgroundCustomOpen || selectedBackground?.key === option.key, onChange: applyBackgroundCustomPickerHex, onClose: () => setBackgroundCustomOpen(false), onDraftChange: applyBackgroundCustomTextHex, onOpen: () => {
|
|
846
|
+
setBackgroundCustomOpen(true);
|
|
847
|
+
setBorderCustomOpen(false);
|
|
848
|
+
} }, option.key)) : (_jsx(AvatarColorChip, { option: option, selected: selectedBackground?.key === option.key, onSelect: () => selectBackgroundOption(option) }, option.key))) })] }), _jsxs("div", { className: "flex w-max max-w-full flex-col gap-4 justify-self-center md:col-start-1 md:row-start-3", children: [_jsx("p", { className: "text-xs font-semibold tracking-[0.12em] text-slate-500 uppercase", children: "Shape" }), _jsx("div", { className: "flex flex-wrap justify-center gap-x-3 gap-y-4", children: avatarShapeOptions.map((option) => (_jsx(AvatarShapeChip, { label: option.label, shape: option.value, selected: shape === option.value, onSelect: () => setShape(option.value) }, option.value))) })] }), _jsx("div", { "aria-hidden": "true", className: "relative hidden self-stretch md:col-start-2 md:row-span-3 md:row-start-1 md:block", children: _jsx("span", { className: "absolute -top-3 -bottom-3 left-1/2 w-px -translate-x-1/2 bg-gradient-to-b from-transparent via-slate-200 to-transparent" }) }), _jsxs("div", { className: "flex w-max max-w-full flex-col gap-4 justify-self-center md:col-start-3 md:row-start-1", children: [_jsx("p", { className: "text-xs font-semibold tracking-[0.12em] text-slate-500 uppercase", children: "Border color" }), _jsx("div", { className: "flex flex-wrap justify-center gap-x-3 gap-y-4", children: borderOptions.map((option) => option.key === 'custom' ? (_jsx(AvatarCustomColorChip, { draftValue: borderCustomDraft, inputLabel: "Custom border color", open: borderCustomOpen, option: option, pickerValue: borderCustomHex || customPickerFallback, placeholder: customPickerFallback, selected: borderCustomOpen || selectedBorder?.key === option.key, onChange: applyBorderCustomPickerHex, onClose: () => setBorderCustomOpen(false), onDraftChange: applyBorderCustomTextHex, onOpen: () => {
|
|
849
|
+
setBorderCustomOpen(true);
|
|
850
|
+
setBackgroundCustomOpen(false);
|
|
851
|
+
} }, option.key)) : (_jsx(AvatarColorChip, { option: option, selected: selectedBorder?.key === option.key, onSelect: () => selectBorderOption(option) }, option.key))) })] }), _jsxs("div", { className: "flex w-max max-w-full flex-col gap-4 justify-self-center md:col-start-3 md:row-start-2", children: [_jsx("p", { className: "text-xs font-semibold tracking-[0.12em] text-slate-500 uppercase", children: "Border thickness" }), _jsx("div", { className: "flex flex-wrap justify-center gap-x-3 gap-y-4", children: avatarBorderThicknessOptions.map((option) => (_jsx(AvatarBorderThicknessChip, { label: option.label, thickness: option.value, selected: borderThickness === option.value, onSelect: () => setBorderThickness(option.value) }, option.value))) })] }), _jsxs("div", { className: "flex w-max max-w-full flex-col gap-4 justify-self-center md:col-start-3 md:row-start-3", children: [_jsx("p", { className: "text-xs font-semibold tracking-[0.12em] text-slate-500 uppercase", children: "Size" }), _jsx("div", { className: "flex flex-wrap justify-center gap-x-3 gap-y-4", children: avatarSizeOptions.map((size) => (_jsx(AvatarSizeChip, { size: size, selected: avatarSize === size, onSelect: () => setAvatarSize(size) }, size))) })] })] }), _jsxs("div", { className: "flex min-h-80 flex-col items-center justify-center gap-5 rounded-md border border-slate-200 bg-white p-5 text-center shadow-sm", children: [_jsx("span", { className: `flex aspect-square w-full max-w-64 items-center justify-center overflow-hidden ${getAvatarShapeClass(shape)}`, style: previewSurfaceStyle, children: _jsx("canvas", { ref: previewCanvasRef, "aria-label": "Avatar preview", className: "block aspect-square w-full" }) }), _jsxs("label", { className: "flex w-full max-w-64 flex-col gap-3", children: [_jsx("span", { className: "flex items-center justify-between gap-3", children: _jsx("span", { className: "text-xs font-semibold tracking-[0.12em] text-slate-500 uppercase", children: "Icon padding" }) }), _jsx("input", { className: "w-full cursor-pointer accent-[#3a89c0]", max: "34", min: "0", onChange: (event) => setPadding(Number(event.currentTarget.value)), type: "range", value: padding })] }), _jsxs("button", { className: "inline-flex cursor-pointer items-center gap-1 rounded-md bg-[#2b333f] px-3 py-2 text-sm font-medium text-white transition-colors hover:bg-[#1d232b]", onClick: () => void downloadAvatar(), type: "button", children: [_jsx(DownloadIcon, {}), _jsxs("span", { children: ["Download PNG (", avatarSize, "px)"] })] }), _jsxs("button", { className: "inline-flex cursor-pointer items-center gap-1 rounded-md border border-slate-300 bg-white px-3 py-2 text-sm font-medium text-slate-800 transition-colors hover:border-slate-400 hover:bg-slate-50", onClick: () => void downloadFavicons(), type: "button", children: [_jsx(DownloadIcon, {}), _jsx("span", { children: "Download favicon PNGs" })] }), canInstallFavicon ? (_jsxs("button", { className: "inline-flex cursor-pointer items-center gap-1 rounded-md border border-slate-300 bg-white px-3 py-2 text-sm font-medium text-slate-800 transition-colors hover:border-slate-400 hover:bg-slate-50 disabled:cursor-not-allowed disabled:opacity-60", disabled: isInstallingFavicon, onClick: () => void installFavicons(), type: "button", children: [_jsx(UploadIcon, {}), _jsx("span", { children: isInstallingFavicon ? 'Installing favicon...' : 'Install as favicon' })] })) : null, status ? (_jsx("p", { className: "max-w-64 text-sm font-medium text-slate-500", children: status })) : null] })] })] }));
|
|
852
|
+
}
|
|
853
|
+
function BannerPresetSelect({ disabled, label, onChange, options, value, }) {
|
|
854
|
+
return (_jsxs("fieldset", { className: "space-y-2", children: [_jsx("legend", { className: "text-xs font-semibold tracking-[0.16em] text-neutral-500 uppercase", children: label }), _jsx("select", { className: "h-8 w-full cursor-pointer rounded-md border border-neutral-300 bg-white px-2.5 text-xs font-medium text-neutral-800 transition-colors hover:border-[#0d2249] hover:bg-slate-50 hover:text-slate-950 focus:border-[#0d2249] focus:ring-[#0d2249] disabled:cursor-not-allowed disabled:opacity-60", disabled: disabled, onChange: (event) => onChange(event.currentTarget.value), value: value, children: options.map((option) => (_jsx("option", { value: option.key, children: option.label }, option.key))) })] }));
|
|
855
|
+
}
|
|
856
|
+
function BannerDotGroup({ disabled, label, onChange, options, value, }) {
|
|
857
|
+
return (_jsxs("fieldset", { className: "space-y-2", children: [_jsx("legend", { className: "text-xs font-semibold tracking-[0.16em] text-neutral-500 uppercase", children: label }), _jsx("div", { className: "inline-flex rounded-md border border-neutral-300 bg-white p-1", children: options.map((option) => {
|
|
858
|
+
const selected = value === option.key;
|
|
859
|
+
return (_jsxs("button", { "aria-label": `${label}: ${option.label}`, className: `flex h-7 w-7 cursor-pointer items-center justify-center rounded-md transition-all hover:bg-slate-100 hover:shadow-[inset_0_0_0_1px_#64748b] disabled:cursor-not-allowed disabled:opacity-40 ${selected
|
|
860
|
+
? 'bg-slate-100 shadow-[inset_0_0_0_1px_#0d2249]'
|
|
861
|
+
: 'bg-white'}`, disabled: disabled, onClick: () => onChange(option.key), title: option.label, type: "button", children: [_jsx("span", { className: "h-4 w-4 rounded-full shadow-[0_0_0_1px_rgba(0,0,0,0.18)]", style: { backgroundColor: option.hex } }), _jsx("span", { className: "sr-only", children: option.label })] }, option.key));
|
|
862
|
+
}) })] }));
|
|
863
|
+
}
|
|
864
|
+
const bannerAlignmentIcons = {
|
|
865
|
+
center: AlignCenter,
|
|
866
|
+
left: AlignLeft,
|
|
867
|
+
right: AlignRight,
|
|
868
|
+
};
|
|
869
|
+
function BannerAlignmentGroup({ disabled, onChange, options, value, }) {
|
|
870
|
+
return (_jsxs("fieldset", { className: "space-y-2", children: [_jsx("legend", { className: "text-xs font-semibold tracking-[0.16em] text-neutral-500 uppercase", children: "Align" }), _jsx("div", { className: "inline-flex rounded-md border border-neutral-300 bg-white p-1", children: options.map((option) => {
|
|
871
|
+
const Icon = bannerAlignmentIcons[option.key] ?? AlignCenter;
|
|
872
|
+
const selected = value === option.key;
|
|
873
|
+
return (_jsxs("button", { "aria-label": `Align ${option.label.toLowerCase()}`, className: `flex h-7 w-8 cursor-pointer items-center justify-center rounded-md transition-all disabled:cursor-not-allowed disabled:opacity-60 ${selected
|
|
874
|
+
? 'bg-[#0d2249] text-white hover:bg-slate-800'
|
|
875
|
+
: 'text-slate-700 hover:bg-slate-100 hover:text-slate-950 hover:shadow-[inset_0_0_0_1px_#64748b]'}`, disabled: disabled, onClick: () => onChange(option.key), title: option.label, type: "button", children: [_jsx(Icon, { "aria-hidden": true, className: "h-4 w-4" }), _jsx("span", { className: "sr-only", children: option.label })] }, option.key));
|
|
876
|
+
}) })] }));
|
|
877
|
+
}
|
|
878
|
+
function BannerPresetControls({ controls, endpoint, onToast, onUpdated, }) {
|
|
879
|
+
function getMarkColorOptions(markVariantKey) {
|
|
880
|
+
const markVariant = controls.markVariants.find((variant) => variant.key === markVariantKey);
|
|
881
|
+
if (markVariant?.colorOptions?.length)
|
|
882
|
+
return markVariant.colorOptions;
|
|
883
|
+
if (!markVariant?.colorKeys?.length)
|
|
884
|
+
return controls.colors;
|
|
885
|
+
const filtered = controls.colors.filter((color) => markVariant.colorKeys?.includes(color.key));
|
|
886
|
+
return filtered.length ? filtered : controls.colors;
|
|
887
|
+
}
|
|
888
|
+
function getDefaultMarkColor(markVariantKey) {
|
|
889
|
+
return (getMarkColorOptions(markVariantKey)[0]?.key ??
|
|
890
|
+
'');
|
|
891
|
+
}
|
|
892
|
+
const defaultState = useMemo(() => {
|
|
893
|
+
const markVariant = controls.markVariants[0]?.key ?? '';
|
|
894
|
+
return {
|
|
895
|
+
alignment: 'center',
|
|
896
|
+
backgroundColor: controls.colors[0]?.key ?? '',
|
|
897
|
+
markColor: getDefaultMarkColor(markVariant),
|
|
898
|
+
markVariant,
|
|
899
|
+
pattern: controls.patterns[0]?.key ?? 'diagonal-sweep',
|
|
900
|
+
secondaryColor: controls.colors[2]?.key ?? controls.colors[0]?.key ?? '',
|
|
901
|
+
};
|
|
902
|
+
}, [controls]);
|
|
903
|
+
const [state, setState] = useState(defaultState);
|
|
904
|
+
const [isApplying, setApplying] = useState(false);
|
|
905
|
+
const markColorOptions = getMarkColorOptions(state.markVariant);
|
|
906
|
+
async function apply(nextState) {
|
|
907
|
+
setApplying(true);
|
|
908
|
+
try {
|
|
909
|
+
const response = await fetch(endpoint, {
|
|
910
|
+
method: 'POST',
|
|
911
|
+
headers: { 'Content-Type': 'application/json' },
|
|
912
|
+
body: JSON.stringify(nextState),
|
|
913
|
+
});
|
|
914
|
+
const result = (await response.json());
|
|
915
|
+
if (!response.ok) {
|
|
916
|
+
throw new Error(result.error ?? 'Could not update banner presets.');
|
|
917
|
+
}
|
|
918
|
+
onUpdated();
|
|
919
|
+
}
|
|
920
|
+
catch (error) {
|
|
921
|
+
onToast(error instanceof Error ? error.message : 'Could not update banner presets.', 'error');
|
|
922
|
+
}
|
|
923
|
+
finally {
|
|
924
|
+
setApplying(false);
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
function update(key, value) {
|
|
928
|
+
const nextState = { ...state, [key]: value };
|
|
929
|
+
if (key === 'markVariant') {
|
|
930
|
+
const nextMarkColorOptions = getMarkColorOptions(String(value));
|
|
931
|
+
if (!nextMarkColorOptions.some((option) => option.key === nextState.markColor)) {
|
|
932
|
+
nextState.markColor = nextMarkColorOptions[0]?.key ?? '';
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
setState(nextState);
|
|
936
|
+
void apply(nextState);
|
|
937
|
+
}
|
|
938
|
+
return (_jsx("div", { className: "mt-6 w-full rounded-md border border-slate-200 bg-white p-3", children: _jsxs("div", { className: "grid gap-3 sm:grid-cols-2 lg:grid-cols-[minmax(9rem,1fr)_auto_auto_auto_minmax(9rem,1fr)] lg:items-start", children: [_jsx(BannerPresetSelect, { disabled: isApplying, label: "Mark", onChange: (value) => update('markVariant', value), options: controls.markVariants, value: state.markVariant }), _jsx(BannerDotGroup, { disabled: isApplying, label: "Color", onChange: (value) => update('markColor', value), options: markColorOptions, value: state.markColor }), _jsx(BannerAlignmentGroup, { disabled: isApplying, onChange: (value) => update('alignment', value), options: controls.alignments, value: state.alignment }), _jsx(BannerDotGroup, { disabled: isApplying, label: "Base", onChange: (value) => update('backgroundColor', value), options: controls.colors, value: state.backgroundColor }), _jsx(BannerPresetSelect, { disabled: isApplying, label: "Pattern", onChange: (value) => update('pattern', value), options: controls.patterns, value: state.pattern })] }) }));
|
|
939
|
+
}
|
|
940
|
+
function BannerCard({ asset, canUpload, endpoint, isCustom, previewVersion, onCustomStateChange, onToast, onUpdated, }) {
|
|
941
|
+
const inputRef = useRef(null);
|
|
942
|
+
const [isReplacing, setReplacing] = useState(false);
|
|
943
|
+
const [isResetting, setResetting] = useState(false);
|
|
944
|
+
const previewUrl = previewVersion
|
|
945
|
+
? `${asset.previewUrl}?v=${previewVersion}`
|
|
946
|
+
: asset.previewUrl;
|
|
947
|
+
const previewWidth = Math.round(asset.width * bannerPreviewScale);
|
|
948
|
+
async function replaceBanner(file) {
|
|
949
|
+
if (!endpoint)
|
|
950
|
+
return;
|
|
951
|
+
const formData = new FormData();
|
|
952
|
+
formData.append('assetId', asset.id);
|
|
953
|
+
formData.append('file', file);
|
|
954
|
+
setReplacing(true);
|
|
955
|
+
onToast('Uploading custom banner...', 'info');
|
|
956
|
+
try {
|
|
957
|
+
const response = await fetch(endpoint, {
|
|
958
|
+
method: 'POST',
|
|
959
|
+
body: formData,
|
|
960
|
+
});
|
|
961
|
+
const result = (await response.json());
|
|
962
|
+
if (!response.ok) {
|
|
963
|
+
throw new Error(result.error ?? 'Could not replace banner image.');
|
|
964
|
+
}
|
|
965
|
+
onCustomStateChange(asset.id, result.isCustom ?? true);
|
|
966
|
+
onToast('Custom banner uploaded.');
|
|
967
|
+
onUpdated();
|
|
968
|
+
}
|
|
969
|
+
catch (error) {
|
|
970
|
+
onToast(error instanceof Error ? error.message : 'Could not replace banner image.', 'error');
|
|
971
|
+
}
|
|
972
|
+
finally {
|
|
973
|
+
setReplacing(false);
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
async function resetBanner() {
|
|
977
|
+
if (!endpoint)
|
|
978
|
+
return;
|
|
979
|
+
setResetting(true);
|
|
980
|
+
onToast('Resetting banner...', 'info');
|
|
981
|
+
try {
|
|
982
|
+
const response = await fetch(endpoint, {
|
|
983
|
+
method: 'POST',
|
|
984
|
+
headers: { 'Content-Type': 'application/json' },
|
|
985
|
+
body: JSON.stringify({ action: 'reset', assetId: asset.id }),
|
|
986
|
+
});
|
|
987
|
+
const result = (await response.json());
|
|
988
|
+
if (!response.ok) {
|
|
989
|
+
throw new Error(result.error ?? 'Could not reset banner image.');
|
|
990
|
+
}
|
|
991
|
+
onCustomStateChange(asset.id, result.isCustom ?? false);
|
|
992
|
+
onToast('Banner reset to default.');
|
|
993
|
+
onUpdated();
|
|
994
|
+
}
|
|
995
|
+
catch (error) {
|
|
996
|
+
onToast(error instanceof Error ? error.message : 'Could not reset banner image.', 'error');
|
|
997
|
+
}
|
|
998
|
+
finally {
|
|
999
|
+
setResetting(false);
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
function handleFileChange(event) {
|
|
1003
|
+
const file = event.currentTarget.files?.[0];
|
|
1004
|
+
event.currentTarget.value = '';
|
|
1005
|
+
if (file)
|
|
1006
|
+
void replaceBanner(file);
|
|
1007
|
+
}
|
|
1008
|
+
return (_jsxs("article", { className: "max-w-full overflow-hidden rounded-lg border border-neutral-200 bg-white", style: { width: previewWidth }, children: [_jsx("div", { className: "relative w-full overflow-hidden border-b border-neutral-200 bg-slate-100", style: { aspectRatio: `${asset.width} / ${asset.height}` }, children: _jsx("img", { src: previewUrl, alt: asset.title, className: "h-full w-full object-cover", loading: "lazy" }) }), _jsxs("div", { className: "space-y-4 p-4", children: [_jsxs("div", { children: [_jsx("p", { className: "text-sm font-semibold text-neutral-900", children: asset.title }), _jsx("p", { className: "mt-1 font-mono text-xs text-neutral-500", children: asset.description })] }), _jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [asset.downloads.map((download) => (_jsx(AssetDownloadButton, { download: download }, `${asset.id}-${download.fileName}`))), canUpload ? (_jsxs(_Fragment, { children: [_jsx("input", { accept: "image/png,image/jpeg,image/webp,image/svg+xml", className: "sr-only", onChange: handleFileChange, ref: inputRef, type: "file" }), _jsxs("button", { className: "inline-flex items-center gap-1 rounded-md border border-neutral-300 bg-white px-3 py-2 text-sm font-medium text-neutral-800 transition-colors hover:border-neutral-400 hover:bg-neutral-50 disabled:cursor-not-allowed disabled:opacity-60", disabled: isReplacing, onClick: () => inputRef.current?.click(), type: "button", children: [_jsx(UploadIcon, {}), _jsx("span", { children: isReplacing ? 'Uploading...' : 'Upload Custom' })] }), isCustom ? (_jsx("button", { "aria-label": "Reset to default", className: "inline-flex h-9 w-9 items-center justify-center rounded-md border border-neutral-300 bg-white text-neutral-800 transition-colors hover:border-neutral-400 hover:bg-neutral-50 disabled:cursor-not-allowed disabled:opacity-60", disabled: isReplacing || isResetting, onClick: () => void resetBanner(), title: "Reset to default", type: "button", children: _jsx(ResetIcon, {}) })) : null] })) : null] })] })] }));
|
|
1009
|
+
}
|
|
1010
|
+
function BannerGroup({ canUseDevActions, customBannerIds, endpoints, group, previewVersion, onCustomStateChange, onToast, onUpdated, }) {
|
|
1011
|
+
return (_jsxs("section", { className: "space-y-5", children: [_jsxs("div", { children: [_jsx("h3", { className: "text-sm font-semibold tracking-[0.12em] text-neutral-500 uppercase", children: group.label }), group.description ? (_jsx("p", { className: "mt-1 text-sm text-slate-600", children: group.description })) : null] }), _jsx("div", { className: "flex flex-col gap-4", children: group.items.map((asset) => (_jsx(BannerCard, { asset: asset, canUpload: canUseDevActions && Boolean(endpoints?.bannerUpload), endpoint: endpoints?.bannerUpload, isCustom: customBannerIds.has(asset.id), onCustomStateChange: onCustomStateChange, onToast: onToast, onUpdated: onUpdated, previewVersion: previewVersion }, asset.id))) })] }));
|
|
1012
|
+
}
|
|
1013
|
+
function Lightbox({ asset, onClose, }) {
|
|
1014
|
+
const lightboxDownload = asset ? getLightboxDownload(asset) : null;
|
|
1015
|
+
useEffect(() => {
|
|
1016
|
+
if (!asset)
|
|
1017
|
+
return;
|
|
1018
|
+
function closeOnEscape(event) {
|
|
1019
|
+
if (event.key === 'Escape')
|
|
1020
|
+
onClose();
|
|
1021
|
+
}
|
|
1022
|
+
window.addEventListener('keydown', closeOnEscape);
|
|
1023
|
+
return () => window.removeEventListener('keydown', closeOnEscape);
|
|
1024
|
+
}, [asset, onClose]);
|
|
1025
|
+
if (!asset)
|
|
1026
|
+
return null;
|
|
1027
|
+
return (_jsx("div", { "aria-modal": "true", className: "fixed inset-0 z-[120] bg-black/70 p-4 sm:p-8", onMouseDown: (event) => {
|
|
1028
|
+
if (event.target === event.currentTarget)
|
|
1029
|
+
onClose();
|
|
1030
|
+
}, role: "dialog", children: _jsx("div", { className: "flex min-h-full items-center justify-center", children: _jsxs("div", { className: "w-full max-w-5xl overflow-hidden rounded-lg bg-white shadow-2xl", children: [_jsx("div", { className: "flex min-h-[60vh] items-center justify-center bg-[#f7f7f7] px-8 py-10 sm:px-12", children: _jsx("div", { className: "max-w-full overflow-auto rounded-md border border-neutral-300 p-4 shadow-sm", style: transparentPreviewStyle, children: _jsx("img", { src: lightboxDownload?.url ?? asset.previewUrl, alt: asset.title, className: "block h-auto max-h-[70vh] w-auto max-w-full" }) }) }), _jsxs("div", { className: "flex flex-col gap-4 border-t border-neutral-200 p-5 sm:flex-row sm:items-center sm:justify-between", children: [_jsxs("div", { children: [_jsx("h2", { className: "text-lg font-semibold text-neutral-900", children: asset.title }), _jsx("p", { className: "mt-1 font-mono text-xs text-neutral-500", children: lightboxDownload?.fileName ?? assetFileLabel(asset) })] }), _jsxs("div", { className: "flex flex-wrap items-center gap-3", children: [_jsx("button", { className: "rounded-md border border-neutral-300 px-4 py-2 text-sm font-medium text-neutral-800 transition-colors hover:border-neutral-400 hover:bg-neutral-50", onClick: onClose, type: "button", children: "Close" }), asset.downloads.map((download) => (_jsx(AssetDownloadButton, { download: download }, `${asset.id}-${download.fileName}`)))] })] })] }) }) }));
|
|
1031
|
+
}
|
|
1032
|
+
export function BrandKitPage({ bannerControls, canUseDevActions = true, endpoints, manifest, }) {
|
|
1033
|
+
const [selectedAsset, setSelectedAsset] = useState(null);
|
|
1034
|
+
const [toasts, setToasts] = useState([]);
|
|
1035
|
+
const toastSequence = useRef(0);
|
|
1036
|
+
const [bannerPreviewVersion, setBannerPreviewVersion] = useState(0);
|
|
1037
|
+
const [customBannerIds, setCustomBannerIds] = useState(() => new Set(manifest.bannerGroups.flatMap((group) => group.items.filter((asset) => asset.isCustom).map((asset) => asset.id))));
|
|
1038
|
+
const avatarAssets = useMemo(() => findAvatarAssets(manifest.assetGroups), [manifest.assetGroups]);
|
|
1039
|
+
const brandColorRows = useMemo(() => createColorRows(manifest), [manifest]);
|
|
1040
|
+
const assetCount = useMemo(() => manifest.assetGroups.reduce((total, group) => total + group.items.length, 0), [manifest.assetGroups]);
|
|
1041
|
+
const bannerAssetCount = useMemo(() => manifest.bannerGroups.reduce((total, group) => total + group.items.length, 0), [manifest.bannerGroups]);
|
|
1042
|
+
const totalAssetCount = assetCount + bannerAssetCount;
|
|
1043
|
+
const heroAsset = useMemo(() => findHeroAsset(manifest.assetGroups), [manifest.assetGroups]);
|
|
1044
|
+
const footerAsset = useMemo(() => findFooterAsset(manifest.assetGroups), [manifest.assetGroups]);
|
|
1045
|
+
const currentYear = new Date().getFullYear();
|
|
1046
|
+
const brandLabel = manifest.brand.shortName ?? manifest.brand.name;
|
|
1047
|
+
const homeUrl = manifest.brand.homeUrl ?? '/';
|
|
1048
|
+
const allDownloadHref = `${manifest.route}/download/all`;
|
|
1049
|
+
const bannerDownloadHref = `${manifest.route}/download/banners`;
|
|
1050
|
+
const canInstallFavicons = canUseDevActions && process.env.NODE_ENV !== 'production';
|
|
1051
|
+
const canUseBannerActions = canUseDevActions;
|
|
1052
|
+
const canUseCustomBannerUploads = canUseDevActions && process.env.NODE_ENV !== 'production';
|
|
1053
|
+
function showToast(message, tone = 'success') {
|
|
1054
|
+
const id = Date.now() + toastSequence.current;
|
|
1055
|
+
toastSequence.current += 1;
|
|
1056
|
+
setToasts((current) => [...current, { id, message, tone }].slice(-4));
|
|
1057
|
+
window.setTimeout(() => {
|
|
1058
|
+
setToasts((current) => current.filter((toast) => toast.id !== id));
|
|
1059
|
+
}, 3200);
|
|
1060
|
+
}
|
|
1061
|
+
function updateCustomBannerState(assetId, isCustom) {
|
|
1062
|
+
setCustomBannerIds((current) => {
|
|
1063
|
+
const next = new Set(current);
|
|
1064
|
+
if (isCustom) {
|
|
1065
|
+
next.add(assetId);
|
|
1066
|
+
}
|
|
1067
|
+
else {
|
|
1068
|
+
next.delete(assetId);
|
|
1069
|
+
}
|
|
1070
|
+
return next;
|
|
1071
|
+
});
|
|
1072
|
+
}
|
|
1073
|
+
function scrollToSection(event, sectionId) {
|
|
1074
|
+
event.preventDefault();
|
|
1075
|
+
const target = document.getElementById(sectionId);
|
|
1076
|
+
if (!target)
|
|
1077
|
+
return;
|
|
1078
|
+
window.history.pushState(null, '', `#${sectionId}`);
|
|
1079
|
+
target.scrollIntoView({ behavior: 'smooth' });
|
|
1080
|
+
}
|
|
1081
|
+
return (_jsxs("div", { className: "min-h-screen bg-slate-50 text-slate-950", children: [_jsx("header", { className: "border-b border-slate-200 bg-white", children: _jsx("div", { className: "mx-auto max-w-7xl px-4 py-12 sm:px-6 lg:px-8", children: _jsxs("div", { className: "grid gap-10 lg:grid-cols-[1fr_360px] lg:items-center", children: [_jsxs("div", { children: [_jsx("a", { className: "text-xs font-semibold tracking-[0.2em] text-neutral-500 uppercase transition-colors hover:text-neutral-950", href: homeUrl, children: brandLabel }), _jsx("h1", { className: "mt-3 font-display text-5xl font-medium text-slate-950", children: "Brand Kit" }), _jsx("p", { className: "mt-4 max-w-2xl text-base leading-7 text-slate-600", children: deterministicIntro }), _jsxs("nav", { className: "mt-8 flex flex-wrap items-center gap-3", "aria-label": "Brand Kit sections", children: [_jsxs("a", { className: "inline-flex items-center gap-1.5 text-sm font-medium text-neutral-700 underline-offset-4 transition-colors hover:text-neutral-950 hover:underline", href: "#logos", onClick: (event) => scrollToSection(event, 'logos'), children: [_jsx(ArrowDown, { "aria-hidden": true, className: "h-3.5 w-3.5" }), _jsx("span", { children: "Logos" })] }), _jsxs("a", { className: "inline-flex items-center gap-1.5 text-sm font-medium text-neutral-700 underline-offset-4 transition-colors hover:text-neutral-950 hover:underline", href: "#colors", onClick: (event) => scrollToSection(event, 'colors'), children: [_jsx(ArrowDown, { "aria-hidden": true, className: "h-3.5 w-3.5" }), _jsx("span", { children: "Colors" })] }), manifest.bannerGroups.length ? (_jsxs("a", { className: "inline-flex items-center gap-1.5 pr-2 text-sm font-medium text-neutral-700 underline-offset-4 transition-colors hover:text-neutral-950 hover:underline sm:pr-4", href: "#banners", onClick: (event) => scrollToSection(event, 'banners'), children: [_jsx(ArrowDown, { "aria-hidden": true, className: "h-3.5 w-3.5" }), _jsx("span", { children: "Banners" })] })) : null, _jsx("span", { className: "text-sm text-slate-500", children: formatAssetCount(totalAssetCount) }), manifest.downloads.allAssets ? (_jsx(DownloadAllButton, { href: allDownloadHref })) : null] })] }), heroAsset ? (_jsx("div", { className: "flex items-center justify-start lg:justify-end", children: _jsx("div", { className: "relative flex aspect-[673/489] w-full max-w-xs items-center justify-center", children: _jsx("img", { src: heroAsset.previewUrl, alt: heroAsset.title, className: "h-full w-full object-contain" }) }) })) : null] }) }) }), _jsxs("main", { children: [_jsx("section", { className: "border-b border-slate-200 bg-slate-50", id: "logos", children: _jsxs("div", { className: "mx-auto max-w-7xl px-4 py-14 sm:px-6 lg:px-8", children: [_jsxs("div", { className: "max-w-2xl", children: [_jsx("p", { className: "text-xs font-semibold tracking-[0.2em] text-neutral-500 uppercase", children: "Logos" }), _jsx("h2", { className: "mt-3 font-display text-4xl font-medium text-slate-950", children: "Approved marks" })] }), _jsxs("div", { className: "mt-10 space-y-12", children: [manifest.assetGroups.map((group) => (_jsx(AssetGroup, { downloadHref: `${manifest.route}/download/${group.key}`, group: group, onPreview: setSelectedAsset }, group.key))), _jsx(AvatarGenerator, { assets: avatarAssets, brandLabel: brandLabel, canUseDevActions: canInstallFavicons, colorSections: manifest.colorSections, colors: manifest.brandColors, endpoints: endpoints })] })] }) }), _jsx("section", { className: "bg-white", id: "colors", children: _jsxs("div", { className: "mx-auto max-w-7xl px-4 py-14 sm:px-6 lg:px-8", children: [_jsxs("div", { className: "max-w-2xl", children: [_jsx("p", { className: "text-xs font-semibold tracking-[0.2em] text-neutral-500 uppercase", children: "Colors" }), _jsx("h2", { className: "mt-3 font-display text-4xl font-medium text-slate-950", children: "Color system" })] }), _jsxs("div", { className: "mt-12 space-y-14", children: [_jsxs("section", { className: "space-y-5", children: [_jsxs("div", { children: [_jsx("h3", { className: "text-lg font-semibold text-slate-950", children: "Brand Colors" }), _jsx("p", { className: "mt-1 text-sm text-slate-600", children: "Core digital colors for product and web work." })] }), _jsx("div", { className: "space-y-4", children: brandColorRows.map((section) => (_jsxs("section", { className: "space-y-3", children: [_jsx("h4", { className: "text-sm font-semibold tracking-[0.12em] text-neutral-500 uppercase", children: section.label }), _jsx("div", { className: "space-y-4", children: section.rows.map((row, index) => (_jsx("div", { className: `grid gap-4 ${section.columns === 2
|
|
1082
|
+
? 'sm:grid-cols-2'
|
|
1083
|
+
: 'sm:grid-cols-2 lg:grid-cols-3'}`, children: row.map((color) => (_jsx(ColorCard, { color: color, onToast: showToast }, color.name))) }, `${section.label}-${index}`))) })] }, section.label))) })] }), _jsx(PrintColorGroups, { groups: manifest.printColorGroups ?? [], onToast: showToast })] })] }) }), manifest.bannerGroups.length ? (_jsx("section", { className: "border-y border-slate-200 bg-slate-50", id: "banners", children: _jsxs("div", { className: "mx-auto max-w-7xl px-4 py-14 sm:px-6 lg:px-8", children: [_jsxs("div", { className: "flex flex-col gap-3 sm:flex-row sm:items-end sm:justify-between", children: [_jsxs("div", { className: "max-w-2xl", children: [_jsx("p", { className: "text-xs font-semibold tracking-[0.2em] text-neutral-500 uppercase", children: "Banners" }), _jsx("h2", { className: "mt-3 font-display text-4xl font-medium text-slate-950", children: "Social profile assets" }), _jsx("p", { className: "mt-4 text-base leading-7 text-slate-600", children: "Ready-to-use PNG cover images sized for each platform." })] }), _jsxs("div", { className: "flex items-center gap-3", children: [_jsx("span", { className: "text-sm text-slate-500", children: formatAssetCount(bannerAssetCount) }), manifest.downloads.bannerAssets ? (_jsx(DownloadAllButton, { href: bannerDownloadHref })) : null] })] }), canUseBannerActions && endpoints?.bannerPresets && bannerControls ? (_jsx(BannerPresetControls, { controls: bannerControls, endpoint: endpoints.bannerPresets, onToast: showToast, onUpdated: () => setBannerPreviewVersion(Date.now()) })) : null, _jsx("div", { className: "mt-12 space-y-10", children: manifest.bannerGroups.map((group) => (_jsx(BannerGroup, { canUseDevActions: canUseCustomBannerUploads, customBannerIds: customBannerIds, endpoints: endpoints, group: group, onCustomStateChange: updateCustomBannerState, onToast: showToast, onUpdated: () => setBannerPreviewVersion(Date.now()), previewVersion: bannerPreviewVersion }, group.key))) })] }) })) : null] }), _jsx("footer", { className: "border-t border-slate-200 bg-white", children: _jsxs("div", { className: "mx-auto flex max-w-7xl flex-col gap-4 px-4 py-8 sm:px-6 md:flex-row md:items-center md:justify-between lg:px-8", children: [footerAsset ? (_jsx("div", { className: "relative h-12 w-40", children: _jsx("img", { src: footerAsset.previewUrl, alt: footerAsset.title, className: "h-full w-full object-contain" }) })) : null, _jsxs("p", { className: "text-sm text-slate-500 md:text-right", children: ["\u00A9 ", currentYear, " ", manifest.brand.name, ". All rights reserved."] })] }) }), _jsx(Lightbox, { asset: selectedAsset, onClose: () => setSelectedAsset(null) }), _jsx(ToastStack, { toasts: toasts })] }));
|
|
1084
|
+
}
|
|
1085
|
+
//# sourceMappingURL=brandkit-page.js.map
|