@leftium/logo 0.1.0 → 0.3.0
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/dist/AppLogo.svelte +21 -7
- package/dist/AppLogo.svelte.d.ts +4 -1
- package/dist/app-logo/color-transform.d.ts +7 -3
- package/dist/app-logo/color-transform.js +14 -8
- package/dist/app-logo/config-serialization.d.ts +80 -0
- package/dist/app-logo/config-serialization.js +489 -0
- package/dist/app-logo/defaults.d.ts +44 -0
- package/dist/app-logo/defaults.js +17 -0
- package/dist/app-logo/generate-svg.js +6 -4
- package/dist/app-logo/iconify.d.ts +16 -2
- package/dist/app-logo/iconify.js +94 -5
- package/dist/app-logo/types.d.ts +2 -0
- package/dist/leftium-logo/generate-svg.d.ts +29 -0
- package/dist/leftium-logo/generate-svg.js +470 -0
- package/dist/tooltip.d.ts +18 -0
- package/dist/tooltip.js +38 -0
- package/package.json +5 -3
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config serialization: bidirectional conversion between the UI-level ColumnState
|
|
3
|
+
* (flat, slider-friendly) and the export-level AppLogoConfig (structured, spec-compliant).
|
|
4
|
+
*
|
|
5
|
+
* Also handles URL hash encoding/decoding and Svelte snippet generation.
|
|
6
|
+
*/
|
|
7
|
+
import { LEFTIUM_GRADIENT, APP_LOGO_DEFAULTS, DEFAULT_EMOJI_STYLE } from './defaults.js';
|
|
8
|
+
import { generateFaviconHtml } from './generate-favicon-set.js';
|
|
9
|
+
export const DEFAULT_STATE = {
|
|
10
|
+
icon: 'fxemoji:rocket',
|
|
11
|
+
iconColor: '#ffffff',
|
|
12
|
+
iconColorModeKey: 'auto',
|
|
13
|
+
hueValue: 210,
|
|
14
|
+
saturationValue: 70,
|
|
15
|
+
iconSize: 60,
|
|
16
|
+
iconOffsetX: 0,
|
|
17
|
+
iconOffsetY: 0,
|
|
18
|
+
iconRotation: 0,
|
|
19
|
+
grayscaleLightness: 100,
|
|
20
|
+
cornerRadius: 50,
|
|
21
|
+
cornerK: 2,
|
|
22
|
+
solidColor: '#0029c1',
|
|
23
|
+
useGradient: true,
|
|
24
|
+
gradientAngle: 45,
|
|
25
|
+
gradientPosition: 0,
|
|
26
|
+
gradientScale: 1
|
|
27
|
+
};
|
|
28
|
+
export const DEFAULT_LOCKS = {
|
|
29
|
+
icon: true,
|
|
30
|
+
iconColor: true,
|
|
31
|
+
iconColorModeKey: true,
|
|
32
|
+
hueValue: true,
|
|
33
|
+
saturationValue: true,
|
|
34
|
+
iconSize: true,
|
|
35
|
+
iconOffsetX: true,
|
|
36
|
+
iconOffsetY: true,
|
|
37
|
+
iconRotation: true,
|
|
38
|
+
grayscaleLightness: true,
|
|
39
|
+
cornerRadius: true,
|
|
40
|
+
cornerK: true,
|
|
41
|
+
solidColor: true,
|
|
42
|
+
useGradient: true,
|
|
43
|
+
gradientAngle: true,
|
|
44
|
+
gradientPosition: true,
|
|
45
|
+
gradientScale: true
|
|
46
|
+
};
|
|
47
|
+
// ─── Corner K ↔ CornerShape conversion ──────────────────────────────────
|
|
48
|
+
/** Map a CornerShape keyword to its K value */
|
|
49
|
+
const SHAPE_TO_K = {
|
|
50
|
+
round: 1,
|
|
51
|
+
squircle: 2,
|
|
52
|
+
square: 10,
|
|
53
|
+
bevel: 0,
|
|
54
|
+
scoop: -1,
|
|
55
|
+
notch: -10
|
|
56
|
+
};
|
|
57
|
+
export function cornerKToShape(k) {
|
|
58
|
+
if (k === 1)
|
|
59
|
+
return 'round';
|
|
60
|
+
if (k === 0)
|
|
61
|
+
return 'bevel';
|
|
62
|
+
return `superellipse(${k})`;
|
|
63
|
+
}
|
|
64
|
+
export function cornerShapeToK(shape) {
|
|
65
|
+
if (shape in SHAPE_TO_K)
|
|
66
|
+
return SHAPE_TO_K[shape];
|
|
67
|
+
const match = shape.match(/^superellipse\((.+)\)$/);
|
|
68
|
+
if (match)
|
|
69
|
+
return parseFloat(match[1]);
|
|
70
|
+
return 1; // fallback to round
|
|
71
|
+
}
|
|
72
|
+
export function cornerKLabel(k) {
|
|
73
|
+
if (k === -10)
|
|
74
|
+
return 'notch';
|
|
75
|
+
if (k === -1)
|
|
76
|
+
return 'scoop';
|
|
77
|
+
if (k === 0)
|
|
78
|
+
return 'bevel';
|
|
79
|
+
if (k === 1)
|
|
80
|
+
return 'round';
|
|
81
|
+
if (k === 2)
|
|
82
|
+
return 'squircle';
|
|
83
|
+
if (k === 10)
|
|
84
|
+
return 'square';
|
|
85
|
+
return `K=${k}`;
|
|
86
|
+
}
|
|
87
|
+
// ─── IconColorMode ↔ flat fields conversion ─────────────────────────────
|
|
88
|
+
export function iconColorModeFromFlat(key, hue, saturation) {
|
|
89
|
+
if (key === 'hue')
|
|
90
|
+
return { hue, saturation };
|
|
91
|
+
return key;
|
|
92
|
+
}
|
|
93
|
+
export function iconColorModeToFlat(mode) {
|
|
94
|
+
if (typeof mode === 'object' && 'hue' in mode) {
|
|
95
|
+
return {
|
|
96
|
+
key: 'hue',
|
|
97
|
+
hue: mode.hue,
|
|
98
|
+
saturation: mode.saturation ?? 70
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
return { key: mode, hue: 210, saturation: 70 };
|
|
102
|
+
}
|
|
103
|
+
// ─── Background ↔ flat fields conversion ────────────────────────────────
|
|
104
|
+
export function backgroundFromFlat(col) {
|
|
105
|
+
if (!col.useGradient)
|
|
106
|
+
return col.solidColor;
|
|
107
|
+
return {
|
|
108
|
+
...LEFTIUM_GRADIENT,
|
|
109
|
+
angle: col.gradientAngle,
|
|
110
|
+
position: col.gradientPosition,
|
|
111
|
+
scale: col.gradientScale
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
function backgroundToFlat(bg) {
|
|
115
|
+
if (typeof bg === 'string') {
|
|
116
|
+
return {
|
|
117
|
+
solidColor: bg,
|
|
118
|
+
useGradient: false,
|
|
119
|
+
gradientAngle: DEFAULT_STATE.gradientAngle,
|
|
120
|
+
gradientPosition: DEFAULT_STATE.gradientPosition,
|
|
121
|
+
gradientScale: DEFAULT_STATE.gradientScale
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
if (bg && typeof bg === 'object') {
|
|
125
|
+
return {
|
|
126
|
+
solidColor: bg.colors?.[0] ?? DEFAULT_STATE.solidColor,
|
|
127
|
+
useGradient: true,
|
|
128
|
+
gradientAngle: bg.angle ?? DEFAULT_STATE.gradientAngle,
|
|
129
|
+
gradientPosition: bg.position ?? DEFAULT_STATE.gradientPosition,
|
|
130
|
+
gradientScale: bg.scale ?? DEFAULT_STATE.gradientScale
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
// undefined / default
|
|
134
|
+
return {
|
|
135
|
+
solidColor: DEFAULT_STATE.solidColor,
|
|
136
|
+
useGradient: DEFAULT_STATE.useGradient,
|
|
137
|
+
gradientAngle: DEFAULT_STATE.gradientAngle,
|
|
138
|
+
gradientPosition: DEFAULT_STATE.gradientPosition,
|
|
139
|
+
gradientScale: DEFAULT_STATE.gradientScale
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
// ─── ColumnState → AppLogoConfig ────────────────────────────────────────
|
|
143
|
+
/**
|
|
144
|
+
* Build an AppLogoConfig from the logo and effective favicon ColumnState.
|
|
145
|
+
* Also accepts the lock state to determine which favicon fields are overrides.
|
|
146
|
+
*/
|
|
147
|
+
export function buildFullConfig(logoState, effectiveFaviconState, lockState, emojiStyle) {
|
|
148
|
+
const logoColorMode = iconColorModeFromFlat(logoState.iconColorModeKey, logoState.hueValue, logoState.saturationValue);
|
|
149
|
+
const favColorMode = iconColorModeFromFlat(effectiveFaviconState.iconColorModeKey, effectiveFaviconState.hueValue, effectiveFaviconState.saturationValue);
|
|
150
|
+
const config = {
|
|
151
|
+
icon: logoState.icon,
|
|
152
|
+
iconColor: logoState.iconColor,
|
|
153
|
+
iconColorMode: logoColorMode,
|
|
154
|
+
background: backgroundFromFlat(logoState),
|
|
155
|
+
cornerRadius: logoState.cornerRadius,
|
|
156
|
+
cornerShape: cornerKToShape(logoState.cornerK),
|
|
157
|
+
logo: {
|
|
158
|
+
iconSize: logoState.iconSize,
|
|
159
|
+
iconOffsetX: logoState.iconOffsetX,
|
|
160
|
+
iconOffsetY: logoState.iconOffsetY,
|
|
161
|
+
iconRotation: logoState.iconRotation,
|
|
162
|
+
grayscaleLightness: logoState.grayscaleLightness
|
|
163
|
+
},
|
|
164
|
+
favicon: {
|
|
165
|
+
icon: effectiveFaviconState.icon,
|
|
166
|
+
iconColor: effectiveFaviconState.iconColor,
|
|
167
|
+
iconColorMode: favColorMode,
|
|
168
|
+
background: backgroundFromFlat(effectiveFaviconState),
|
|
169
|
+
cornerRadius: effectiveFaviconState.cornerRadius,
|
|
170
|
+
cornerShape: cornerKToShape(effectiveFaviconState.cornerK),
|
|
171
|
+
iconSize: effectiveFaviconState.iconSize,
|
|
172
|
+
iconOffsetX: effectiveFaviconState.iconOffsetX,
|
|
173
|
+
iconOffsetY: effectiveFaviconState.iconOffsetY,
|
|
174
|
+
iconRotation: effectiveFaviconState.iconRotation,
|
|
175
|
+
grayscaleLightness: effectiveFaviconState.grayscaleLightness
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
// Only include emojiStyle if it differs from default
|
|
179
|
+
if (emojiStyle && emojiStyle !== DEFAULT_EMOJI_STYLE) {
|
|
180
|
+
config.emojiStyle = emojiStyle;
|
|
181
|
+
}
|
|
182
|
+
return config;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Build a single-variant AppLogoConfig for export/rendering.
|
|
186
|
+
*/
|
|
187
|
+
export function buildVariantConfig(col, variant, emojiStyle) {
|
|
188
|
+
const colorMode = iconColorModeFromFlat(col.iconColorModeKey, col.hueValue, col.saturationValue);
|
|
189
|
+
const config = {
|
|
190
|
+
icon: col.icon,
|
|
191
|
+
iconColor: col.iconColor,
|
|
192
|
+
iconColorMode: colorMode,
|
|
193
|
+
background: backgroundFromFlat(col),
|
|
194
|
+
cornerRadius: col.cornerRadius,
|
|
195
|
+
cornerShape: cornerKToShape(col.cornerK),
|
|
196
|
+
[variant]: {
|
|
197
|
+
iconSize: col.iconSize,
|
|
198
|
+
iconOffsetX: col.iconOffsetX,
|
|
199
|
+
iconOffsetY: col.iconOffsetY,
|
|
200
|
+
iconRotation: col.iconRotation,
|
|
201
|
+
grayscaleLightness: col.grayscaleLightness
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
if (emojiStyle && emojiStyle !== DEFAULT_EMOJI_STYLE) {
|
|
205
|
+
config.emojiStyle = emojiStyle;
|
|
206
|
+
}
|
|
207
|
+
return config;
|
|
208
|
+
}
|
|
209
|
+
// ─── AppLogoConfig → ColumnState (import / URL decode) ──────────────────
|
|
210
|
+
/**
|
|
211
|
+
* Convert an AppLogoConfig back to ColumnState + lock state.
|
|
212
|
+
* Shared fields populate the logo column. Favicon overrides populate
|
|
213
|
+
* the favicon column and unlock the corresponding locks.
|
|
214
|
+
*/
|
|
215
|
+
export function configToUIState(config) {
|
|
216
|
+
// Build logo state from shared config fields
|
|
217
|
+
const colorFlat = iconColorModeToFlat(config.iconColorMode ?? 'auto');
|
|
218
|
+
const bgFlat = backgroundToFlat(config.background);
|
|
219
|
+
const logoState = {
|
|
220
|
+
icon: config.icon,
|
|
221
|
+
iconColor: config.iconColor ?? DEFAULT_STATE.iconColor,
|
|
222
|
+
iconColorModeKey: colorFlat.key,
|
|
223
|
+
hueValue: colorFlat.hue,
|
|
224
|
+
saturationValue: colorFlat.saturation,
|
|
225
|
+
iconSize: config.logo?.iconSize ?? DEFAULT_STATE.iconSize,
|
|
226
|
+
iconOffsetX: config.logo?.iconOffsetX ?? DEFAULT_STATE.iconOffsetX,
|
|
227
|
+
iconOffsetY: config.logo?.iconOffsetY ?? DEFAULT_STATE.iconOffsetY,
|
|
228
|
+
iconRotation: config.logo?.iconRotation ?? DEFAULT_STATE.iconRotation,
|
|
229
|
+
grayscaleLightness: config.logo?.grayscaleLightness ?? DEFAULT_STATE.grayscaleLightness,
|
|
230
|
+
cornerRadius: config.cornerRadius ?? DEFAULT_STATE.cornerRadius,
|
|
231
|
+
cornerK: cornerShapeToK(config.cornerShape ?? 'round'),
|
|
232
|
+
...bgFlat
|
|
233
|
+
};
|
|
234
|
+
// Start with all locks locked, favicon = copy of logo
|
|
235
|
+
const locks = { ...DEFAULT_LOCKS };
|
|
236
|
+
const faviconState = { ...logoState };
|
|
237
|
+
// Apply favicon overrides — each override unlocks the corresponding field
|
|
238
|
+
if (config.favicon) {
|
|
239
|
+
const fav = config.favicon;
|
|
240
|
+
if (fav.icon !== undefined && fav.icon !== logoState.icon) {
|
|
241
|
+
faviconState.icon = fav.icon;
|
|
242
|
+
locks.icon = false;
|
|
243
|
+
}
|
|
244
|
+
if (fav.iconColor !== undefined && fav.iconColor !== logoState.iconColor) {
|
|
245
|
+
faviconState.iconColor = fav.iconColor;
|
|
246
|
+
locks.iconColor = false;
|
|
247
|
+
}
|
|
248
|
+
if (fav.iconColorMode !== undefined) {
|
|
249
|
+
const favColorFlat = iconColorModeToFlat(fav.iconColorMode);
|
|
250
|
+
if (favColorFlat.key !== logoState.iconColorModeKey ||
|
|
251
|
+
favColorFlat.hue !== logoState.hueValue ||
|
|
252
|
+
favColorFlat.saturation !== logoState.saturationValue) {
|
|
253
|
+
faviconState.iconColorModeKey = favColorFlat.key;
|
|
254
|
+
faviconState.hueValue = favColorFlat.hue;
|
|
255
|
+
faviconState.saturationValue = favColorFlat.saturation;
|
|
256
|
+
locks.iconColorModeKey = false;
|
|
257
|
+
locks.hueValue = false;
|
|
258
|
+
locks.saturationValue = false;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
if (fav.iconSize !== undefined && fav.iconSize !== logoState.iconSize) {
|
|
262
|
+
faviconState.iconSize = fav.iconSize;
|
|
263
|
+
locks.iconSize = false;
|
|
264
|
+
}
|
|
265
|
+
if (fav.iconOffsetX !== undefined && fav.iconOffsetX !== logoState.iconOffsetX) {
|
|
266
|
+
faviconState.iconOffsetX = fav.iconOffsetX;
|
|
267
|
+
locks.iconOffsetX = false;
|
|
268
|
+
}
|
|
269
|
+
if (fav.iconOffsetY !== undefined && fav.iconOffsetY !== logoState.iconOffsetY) {
|
|
270
|
+
faviconState.iconOffsetY = fav.iconOffsetY;
|
|
271
|
+
locks.iconOffsetY = false;
|
|
272
|
+
}
|
|
273
|
+
if (fav.iconRotation !== undefined && fav.iconRotation !== logoState.iconRotation) {
|
|
274
|
+
faviconState.iconRotation = fav.iconRotation;
|
|
275
|
+
locks.iconRotation = false;
|
|
276
|
+
}
|
|
277
|
+
if (fav.grayscaleLightness !== undefined &&
|
|
278
|
+
fav.grayscaleLightness !== logoState.grayscaleLightness) {
|
|
279
|
+
faviconState.grayscaleLightness = fav.grayscaleLightness;
|
|
280
|
+
locks.grayscaleLightness = false;
|
|
281
|
+
}
|
|
282
|
+
if (fav.cornerRadius !== undefined && fav.cornerRadius !== logoState.cornerRadius) {
|
|
283
|
+
faviconState.cornerRadius = fav.cornerRadius;
|
|
284
|
+
locks.cornerRadius = false;
|
|
285
|
+
}
|
|
286
|
+
if (fav.cornerShape !== undefined) {
|
|
287
|
+
const favK = cornerShapeToK(fav.cornerShape);
|
|
288
|
+
if (favK !== logoState.cornerK) {
|
|
289
|
+
faviconState.cornerK = favK;
|
|
290
|
+
locks.cornerK = false;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
if (fav.background !== undefined) {
|
|
294
|
+
const favBgFlat = backgroundToFlat(fav.background);
|
|
295
|
+
if (favBgFlat.solidColor !== logoState.solidColor ||
|
|
296
|
+
favBgFlat.useGradient !== logoState.useGradient ||
|
|
297
|
+
favBgFlat.gradientAngle !== logoState.gradientAngle ||
|
|
298
|
+
favBgFlat.gradientPosition !== logoState.gradientPosition ||
|
|
299
|
+
favBgFlat.gradientScale !== logoState.gradientScale) {
|
|
300
|
+
faviconState.solidColor = favBgFlat.solidColor;
|
|
301
|
+
faviconState.useGradient = favBgFlat.useGradient;
|
|
302
|
+
faviconState.gradientAngle = favBgFlat.gradientAngle;
|
|
303
|
+
faviconState.gradientPosition = favBgFlat.gradientPosition;
|
|
304
|
+
faviconState.gradientScale = favBgFlat.gradientScale;
|
|
305
|
+
locks.solidColor = false;
|
|
306
|
+
locks.useGradient = false;
|
|
307
|
+
locks.gradientAngle = false;
|
|
308
|
+
locks.gradientPosition = false;
|
|
309
|
+
locks.gradientScale = false;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return {
|
|
314
|
+
logo: logoState,
|
|
315
|
+
favicon: faviconState,
|
|
316
|
+
locks,
|
|
317
|
+
emojiStyle: config.emojiStyle ?? DEFAULT_EMOJI_STYLE
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
// ─── URL hash encoding/decoding ─────────────────────────────────────────
|
|
321
|
+
/**
|
|
322
|
+
* Encode a config object to a base64url string for URL hash.
|
|
323
|
+
* Uses TextEncoder for Unicode safety (emoji icon names, non-ASCII app names).
|
|
324
|
+
*/
|
|
325
|
+
export function encodeConfigToHash(config) {
|
|
326
|
+
const json = JSON.stringify(config);
|
|
327
|
+
const bytes = new TextEncoder().encode(json);
|
|
328
|
+
// Manual base64 from Uint8Array
|
|
329
|
+
let binary = '';
|
|
330
|
+
for (const byte of bytes) {
|
|
331
|
+
binary += String.fromCharCode(byte);
|
|
332
|
+
}
|
|
333
|
+
const base64 = btoa(binary);
|
|
334
|
+
// URL-safe: + → -, / → _, strip trailing =
|
|
335
|
+
return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Decode a base64url string from URL hash back to AppLogoConfig.
|
|
339
|
+
* Returns null if decoding fails.
|
|
340
|
+
*/
|
|
341
|
+
export function decodeConfigFromHash(hash) {
|
|
342
|
+
try {
|
|
343
|
+
// Restore standard base64: - → +, _ → /
|
|
344
|
+
let base64 = hash.replace(/-/g, '+').replace(/_/g, '/');
|
|
345
|
+
// Restore padding
|
|
346
|
+
while (base64.length % 4)
|
|
347
|
+
base64 += '=';
|
|
348
|
+
const binary = atob(base64);
|
|
349
|
+
const bytes = new Uint8Array(binary.length);
|
|
350
|
+
for (let i = 0; i < binary.length; i++) {
|
|
351
|
+
bytes[i] = binary.charCodeAt(i);
|
|
352
|
+
}
|
|
353
|
+
const json = new TextDecoder().decode(bytes);
|
|
354
|
+
const parsed = JSON.parse(json);
|
|
355
|
+
// Basic validation: must have icon field
|
|
356
|
+
if (!parsed || typeof parsed.icon !== 'string')
|
|
357
|
+
return null;
|
|
358
|
+
return parsed;
|
|
359
|
+
}
|
|
360
|
+
catch {
|
|
361
|
+
return null;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
// ─── Svelte snippet generation ──────────────────────────────────────────
|
|
365
|
+
/**
|
|
366
|
+
* Generate a minimal <AppLogo> Svelte snippet with only non-default props.
|
|
367
|
+
*/
|
|
368
|
+
export function generateSvelteSnippet(config) {
|
|
369
|
+
const props = [];
|
|
370
|
+
// Compare against defaults
|
|
371
|
+
if (config.icon !== APP_LOGO_DEFAULTS.icon) {
|
|
372
|
+
props.push(`\ticon="${config.icon}"`);
|
|
373
|
+
}
|
|
374
|
+
if (config.iconColor && config.iconColor !== APP_LOGO_DEFAULTS.iconColor) {
|
|
375
|
+
props.push(`\ticonColor="${config.iconColor}"`);
|
|
376
|
+
}
|
|
377
|
+
if (config.iconColorMode) {
|
|
378
|
+
const mode = config.iconColorMode;
|
|
379
|
+
if (typeof mode === 'string' && mode !== APP_LOGO_DEFAULTS.iconColorMode) {
|
|
380
|
+
props.push(`\ticonColorMode="${mode}"`);
|
|
381
|
+
}
|
|
382
|
+
else if (typeof mode === 'object' && 'hue' in mode) {
|
|
383
|
+
const satPart = mode.saturation !== undefined && mode.saturation !== 70
|
|
384
|
+
? `, saturation: ${mode.saturation}`
|
|
385
|
+
: '';
|
|
386
|
+
props.push(`\ticonColorMode={{ hue: ${mode.hue}${satPart} }}`);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
// Logo-specific props (from logo override)
|
|
390
|
+
const logoOverride = config.logo;
|
|
391
|
+
if (logoOverride) {
|
|
392
|
+
if (logoOverride.iconSize !== undefined &&
|
|
393
|
+
logoOverride.iconSize !== APP_LOGO_DEFAULTS.iconSize) {
|
|
394
|
+
props.push(`\ticonSize={${logoOverride.iconSize}}`);
|
|
395
|
+
}
|
|
396
|
+
if (logoOverride.iconOffsetX !== undefined &&
|
|
397
|
+
logoOverride.iconOffsetX !== APP_LOGO_DEFAULTS.iconOffsetX) {
|
|
398
|
+
props.push(`\ticonOffsetX={${logoOverride.iconOffsetX}}`);
|
|
399
|
+
}
|
|
400
|
+
if (logoOverride.iconOffsetY !== undefined &&
|
|
401
|
+
logoOverride.iconOffsetY !== APP_LOGO_DEFAULTS.iconOffsetY) {
|
|
402
|
+
props.push(`\ticonOffsetY={${logoOverride.iconOffsetY}}`);
|
|
403
|
+
}
|
|
404
|
+
if (logoOverride.iconRotation !== undefined &&
|
|
405
|
+
logoOverride.iconRotation !== APP_LOGO_DEFAULTS.iconRotation) {
|
|
406
|
+
props.push(`\ticonRotation={${logoOverride.iconRotation}}`);
|
|
407
|
+
}
|
|
408
|
+
if (logoOverride.grayscaleLightness !== undefined && logoOverride.grayscaleLightness !== 100) {
|
|
409
|
+
props.push(`\tgrayscaleLightness={${logoOverride.grayscaleLightness}}`);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
if (config.cornerRadius !== undefined && config.cornerRadius !== APP_LOGO_DEFAULTS.cornerRadius) {
|
|
413
|
+
props.push(`\tcornerRadius={${config.cornerRadius}}`);
|
|
414
|
+
}
|
|
415
|
+
if (config.cornerShape !== undefined && config.cornerShape !== APP_LOGO_DEFAULTS.cornerShape) {
|
|
416
|
+
props.push(`\tcornerShape="${config.cornerShape}"`);
|
|
417
|
+
}
|
|
418
|
+
// Background
|
|
419
|
+
if (config.background !== undefined) {
|
|
420
|
+
if (typeof config.background === 'string') {
|
|
421
|
+
props.push(`\tbackground="${config.background}"`);
|
|
422
|
+
}
|
|
423
|
+
else if (typeof config.background === 'object') {
|
|
424
|
+
const bg = config.background;
|
|
425
|
+
const def = APP_LOGO_DEFAULTS.background;
|
|
426
|
+
// Check if it differs from default gradient
|
|
427
|
+
const isSameAsDefault = JSON.stringify(bg.colors) === JSON.stringify(def.colors) &&
|
|
428
|
+
JSON.stringify(bg.stops) === JSON.stringify(def.stops) &&
|
|
429
|
+
(bg.angle ?? 45) === (def.angle ?? 45) &&
|
|
430
|
+
(bg.position ?? 0) === (def.position ?? 0) &&
|
|
431
|
+
(bg.scale ?? 1) === (def.scale ?? 1);
|
|
432
|
+
if (!isSameAsDefault) {
|
|
433
|
+
const parts = [];
|
|
434
|
+
parts.push(`colors: ${JSON.stringify(bg.colors)}`);
|
|
435
|
+
if (bg.stops)
|
|
436
|
+
parts.push(`stops: ${JSON.stringify(bg.stops)}`);
|
|
437
|
+
if (bg.angle !== undefined)
|
|
438
|
+
parts.push(`angle: ${bg.angle}`);
|
|
439
|
+
if (bg.position !== undefined && bg.position !== 0)
|
|
440
|
+
parts.push(`position: ${bg.position}`);
|
|
441
|
+
if (bg.scale !== undefined && bg.scale !== 1)
|
|
442
|
+
parts.push(`scale: ${bg.scale}`);
|
|
443
|
+
props.push(`\tbackground={{ ${parts.join(', ')} }}`);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
const propsStr = props.length > 0 ? `\n${props.join('\n')}\n` : ' ';
|
|
448
|
+
let snippet = `<script>\n\timport { AppLogo } from '@leftium/logo';\n</script>\n\n<AppLogo${propsStr}/>`;
|
|
449
|
+
// Add favicon override comment if favicon differs
|
|
450
|
+
if (config.favicon) {
|
|
451
|
+
const favOverrides = [];
|
|
452
|
+
const fav = config.favicon;
|
|
453
|
+
if (fav.iconSize !== undefined)
|
|
454
|
+
favOverrides.push(`iconSize={${fav.iconSize}}`);
|
|
455
|
+
if (fav.iconOffsetX !== undefined && fav.iconOffsetX !== 0)
|
|
456
|
+
favOverrides.push(`iconOffsetX={${fav.iconOffsetX}}`);
|
|
457
|
+
if (fav.iconOffsetY !== undefined && fav.iconOffsetY !== 0)
|
|
458
|
+
favOverrides.push(`iconOffsetY={${fav.iconOffsetY}}`);
|
|
459
|
+
if (fav.iconRotation !== undefined && fav.iconRotation !== 0)
|
|
460
|
+
favOverrides.push(`iconRotation={${fav.iconRotation}}`);
|
|
461
|
+
if (fav.cornerRadius !== undefined && fav.cornerRadius !== config.cornerRadius)
|
|
462
|
+
favOverrides.push(`cornerRadius={${fav.cornerRadius}}`);
|
|
463
|
+
if (fav.cornerShape !== undefined && fav.cornerShape !== config.cornerShape)
|
|
464
|
+
favOverrides.push(`cornerShape="${fav.cornerShape}"`);
|
|
465
|
+
if (fav.icon !== undefined && fav.icon !== config.icon)
|
|
466
|
+
favOverrides.push(`icon="${fav.icon}"`);
|
|
467
|
+
if (fav.iconColor !== undefined && fav.iconColor !== config.iconColor)
|
|
468
|
+
favOverrides.push(`iconColor="${fav.iconColor}"`);
|
|
469
|
+
if (fav.grayscaleLightness !== undefined &&
|
|
470
|
+
fav.grayscaleLightness !== (config.logo?.grayscaleLightness ?? 100))
|
|
471
|
+
favOverrides.push(`grayscaleLightness={${fav.grayscaleLightness}}`);
|
|
472
|
+
// Filter out favicon overrides that match the logo
|
|
473
|
+
const meaningfulOverrides = favOverrides.filter((o) => {
|
|
474
|
+
// Each override is already only added when it differs, so all are meaningful
|
|
475
|
+
return true;
|
|
476
|
+
});
|
|
477
|
+
if (meaningfulOverrides.length > 0) {
|
|
478
|
+
snippet += `\n\n<!-- Favicon variant uses different settings:\n ${meaningfulOverrides.join(', ')} -->`;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
return snippet;
|
|
482
|
+
}
|
|
483
|
+
/**
|
|
484
|
+
* Generate the HTML favicon snippet for clipboard copy.
|
|
485
|
+
* Re-exports the existing function for convenience.
|
|
486
|
+
*/
|
|
487
|
+
export function generateHtmlSnippet(appInfo) {
|
|
488
|
+
return generateFaviconHtml(appInfo);
|
|
489
|
+
}
|
|
@@ -1,6 +1,50 @@
|
|
|
1
1
|
import type { GradientConfig, AppLogoProps, IconSourceType } from './types.js';
|
|
2
2
|
/** The Leftium brand gradient: 45deg diagonal, dark blue -> bright blue -> dark blue */
|
|
3
3
|
export declare const LEFTIUM_GRADIENT: GradientConfig;
|
|
4
|
+
/** Default emoji icon set for auto-mapping. "native" disables mapping. */
|
|
5
|
+
export declare const DEFAULT_EMOJI_STYLE = "twemoji";
|
|
6
|
+
/** Emoji icon sets that support automatic emoji-to-Iconify name resolution */
|
|
7
|
+
export declare const EMOJI_SETS: readonly [{
|
|
8
|
+
readonly prefix: "twemoji";
|
|
9
|
+
readonly name: "Twitter Emoji";
|
|
10
|
+
readonly monochrome: false;
|
|
11
|
+
}, {
|
|
12
|
+
readonly prefix: "noto";
|
|
13
|
+
readonly name: "Noto Emoji";
|
|
14
|
+
readonly monochrome: false;
|
|
15
|
+
}, {
|
|
16
|
+
readonly prefix: "openmoji";
|
|
17
|
+
readonly name: "OpenMoji";
|
|
18
|
+
readonly monochrome: false;
|
|
19
|
+
}, {
|
|
20
|
+
readonly prefix: "fluent-emoji";
|
|
21
|
+
readonly name: "Fluent Emoji";
|
|
22
|
+
readonly monochrome: false;
|
|
23
|
+
}, {
|
|
24
|
+
readonly prefix: "fluent-emoji-flat";
|
|
25
|
+
readonly name: "Fluent Flat";
|
|
26
|
+
readonly monochrome: false;
|
|
27
|
+
}, {
|
|
28
|
+
readonly prefix: "emojione";
|
|
29
|
+
readonly name: "Emoji One";
|
|
30
|
+
readonly monochrome: false;
|
|
31
|
+
}, {
|
|
32
|
+
readonly prefix: "noto-v1";
|
|
33
|
+
readonly name: "Noto v1";
|
|
34
|
+
readonly monochrome: false;
|
|
35
|
+
}, {
|
|
36
|
+
readonly prefix: "emojione-v1";
|
|
37
|
+
readonly name: "Emoji One v1";
|
|
38
|
+
readonly monochrome: false;
|
|
39
|
+
}, {
|
|
40
|
+
readonly prefix: "emojione-monotone";
|
|
41
|
+
readonly name: "Emoji One Mono";
|
|
42
|
+
readonly monochrome: true;
|
|
43
|
+
}, {
|
|
44
|
+
readonly prefix: "fluent-emoji-high-contrast";
|
|
45
|
+
readonly name: "Fluent HC";
|
|
46
|
+
readonly monochrome: true;
|
|
47
|
+
}];
|
|
4
48
|
/** Default values for all AppLogo props */
|
|
5
49
|
export declare const APP_LOGO_DEFAULTS: Required<Pick<AppLogoProps, 'icon' | 'iconColor' | 'iconColorMode' | 'iconSize' | 'iconOffsetX' | 'iconOffsetY' | 'iconRotation' | 'cornerRadius' | 'cornerShape' | 'size'>> & {
|
|
6
50
|
background: GradientConfig;
|
|
@@ -4,6 +4,23 @@ export const LEFTIUM_GRADIENT = {
|
|
|
4
4
|
stops: [0, 0.29, 1],
|
|
5
5
|
angle: 45 // bottom-left -> top-right
|
|
6
6
|
};
|
|
7
|
+
/** Default emoji icon set for auto-mapping. "native" disables mapping. */
|
|
8
|
+
export const DEFAULT_EMOJI_STYLE = 'twemoji';
|
|
9
|
+
/** Emoji icon sets that support automatic emoji-to-Iconify name resolution */
|
|
10
|
+
export const EMOJI_SETS = [
|
|
11
|
+
// Color sets
|
|
12
|
+
{ prefix: 'twemoji', name: 'Twitter Emoji', monochrome: false },
|
|
13
|
+
{ prefix: 'noto', name: 'Noto Emoji', monochrome: false },
|
|
14
|
+
{ prefix: 'openmoji', name: 'OpenMoji', monochrome: false },
|
|
15
|
+
{ prefix: 'fluent-emoji', name: 'Fluent Emoji', monochrome: false },
|
|
16
|
+
{ prefix: 'fluent-emoji-flat', name: 'Fluent Flat', monochrome: false },
|
|
17
|
+
{ prefix: 'emojione', name: 'Emoji One', monochrome: false },
|
|
18
|
+
{ prefix: 'noto-v1', name: 'Noto v1', monochrome: false },
|
|
19
|
+
{ prefix: 'emojione-v1', name: 'Emoji One v1', monochrome: false },
|
|
20
|
+
// Monochrome sets
|
|
21
|
+
{ prefix: 'emojione-monotone', name: 'Emoji One Mono', monochrome: true },
|
|
22
|
+
{ prefix: 'fluent-emoji-high-contrast', name: 'Fluent HC', monochrome: true }
|
|
23
|
+
];
|
|
7
24
|
/** Default values for all AppLogo props */
|
|
8
25
|
export const APP_LOGO_DEFAULTS = {
|
|
9
26
|
icon: 'fxemoji:rocket',
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { APP_LOGO_DEFAULTS } from './defaults.js';
|
|
1
|
+
import { APP_LOGO_DEFAULTS, DEFAULT_EMOJI_STYLE } from './defaults.js';
|
|
2
2
|
import { resolveIcon } from './iconify.js';
|
|
3
3
|
import { applyColorMode } from './color-transform.js';
|
|
4
4
|
import { generateCornerPath } from './squircle.js';
|
|
@@ -15,6 +15,7 @@ function resolveProps(config, variant) {
|
|
|
15
15
|
iconOffsetX: overrides?.iconOffsetX ?? APP_LOGO_DEFAULTS.iconOffsetX,
|
|
16
16
|
iconOffsetY: overrides?.iconOffsetY ?? APP_LOGO_DEFAULTS.iconOffsetY,
|
|
17
17
|
iconRotation: overrides?.iconRotation ?? APP_LOGO_DEFAULTS.iconRotation,
|
|
18
|
+
grayscaleLightness: overrides?.grayscaleLightness ?? 100,
|
|
18
19
|
cornerRadius: overrides?.cornerRadius ?? config.cornerRadius ?? APP_LOGO_DEFAULTS.cornerRadius,
|
|
19
20
|
cornerShape: overrides?.cornerShape ?? config.cornerShape ?? APP_LOGO_DEFAULTS.cornerShape,
|
|
20
21
|
background: overrides?.background ?? config.background ?? APP_LOGO_DEFAULTS.background,
|
|
@@ -136,10 +137,11 @@ export async function generateAppLogoSvg(config, variant) {
|
|
|
136
137
|
const props = resolveProps(config, variant);
|
|
137
138
|
const size = props.size;
|
|
138
139
|
const gradientId = 'app-logo-bg';
|
|
139
|
-
// Resolve icon
|
|
140
|
-
const
|
|
140
|
+
// Resolve icon (pass emojiStyle for emoji auto-mapping)
|
|
141
|
+
const emojiStyle = config.emojiStyle ?? DEFAULT_EMOJI_STYLE;
|
|
142
|
+
const resolved = await resolveIcon(props.icon, emojiStyle);
|
|
141
143
|
// Apply color mode (supports all IconColorMode values)
|
|
142
|
-
const coloredContent = applyColorMode(resolved.svgContent, resolved.isMonochrome, props.iconColorMode, props.iconColor);
|
|
144
|
+
const coloredContent = applyColorMode(resolved.svgContent, resolved.isMonochrome, props.iconColorMode, props.iconColor, props.grayscaleLightness);
|
|
143
145
|
// Build SVG parts
|
|
144
146
|
const isGradient = typeof props.background !== 'string';
|
|
145
147
|
const gradientEl = isGradient
|
|
@@ -12,10 +12,24 @@ export interface ResolvedIcon {
|
|
|
12
12
|
/** The detected source type */
|
|
13
13
|
sourceType: IconSourceType;
|
|
14
14
|
}
|
|
15
|
+
/**
|
|
16
|
+
* Resolve an emoji to its Iconify icon slug within a given set.
|
|
17
|
+
*
|
|
18
|
+
* Strategy A: Use the Iconify search API, which accepts emoji characters
|
|
19
|
+
* directly and returns matching icon names.
|
|
20
|
+
*
|
|
21
|
+
* Returns the icon name (slug) or null if not found.
|
|
22
|
+
* Results are cached by `${prefix}:${emoji}`.
|
|
23
|
+
*/
|
|
24
|
+
export declare function resolveEmojiSlug(emoji: string, prefix: string): Promise<string | null>;
|
|
15
25
|
/**
|
|
16
26
|
* Resolve an icon prop value to SVG data ready for rendering.
|
|
17
27
|
*
|
|
18
28
|
* Handles all 4 source types: Iconify ID, emoji, inline SVG, data URL.
|
|
19
|
-
* For emoji,
|
|
29
|
+
* For emoji, auto-maps to an Iconify emoji set unless emojiStyle is 'native'.
|
|
30
|
+
*
|
|
31
|
+
* @param icon - The icon prop value
|
|
32
|
+
* @param emojiStyle - Iconify emoji set prefix (default: DEFAULT_EMOJI_STYLE).
|
|
33
|
+
* Use 'native' to disable auto-mapping and render as <text>.
|
|
20
34
|
*/
|
|
21
|
-
export declare function resolveIcon(icon: string): Promise<ResolvedIcon>;
|
|
35
|
+
export declare function resolveIcon(icon: string, emojiStyle?: string): Promise<ResolvedIcon>;
|