@leftium/logo 0.0.2 → 0.2.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 +146 -0
- package/dist/AppLogo.svelte.d.ts +7 -0
- package/dist/LeftiumLogo.svelte +267 -186
- package/dist/LeftiumLogo.svelte.d.ts +1 -0
- package/dist/app-logo/color-transform.d.ts +40 -0
- package/dist/app-logo/color-transform.js +142 -0
- 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 +60 -0
- package/dist/app-logo/defaults.js +55 -0
- package/dist/app-logo/generate-favicon-set.d.ts +44 -0
- package/dist/app-logo/generate-favicon-set.js +97 -0
- package/dist/app-logo/generate-ico.d.ts +18 -0
- package/dist/app-logo/generate-ico.js +63 -0
- package/dist/app-logo/generate-png.d.ts +16 -0
- package/dist/app-logo/generate-png.js +60 -0
- package/dist/app-logo/generate-svg.d.ts +9 -0
- package/dist/app-logo/generate-svg.js +160 -0
- package/dist/app-logo/iconify.d.ts +35 -0
- package/dist/app-logo/iconify.js +223 -0
- package/dist/app-logo/squircle.d.ts +43 -0
- package/dist/app-logo/squircle.js +213 -0
- package/dist/app-logo/types.d.ts +39 -0
- package/dist/app-logo/types.js +1 -0
- package/dist/assets/logo-parts/glow-squircle.svg +44 -0
- package/dist/index.d.ts +8 -3
- package/dist/index.js +9 -3
- 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/dist/webgl-ripples/webgl-ripples.d.ts +0 -4
- package/dist/webgl-ripples/webgl-ripples.js +1 -1
- package/package.json +35 -20
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { GradientConfig, AppLogoProps, IconSourceType } from './types.js';
|
|
2
|
+
/** The Leftium brand gradient: 45deg diagonal, dark blue -> bright blue -> dark blue */
|
|
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
|
+
}];
|
|
48
|
+
/** Default values for all AppLogo props */
|
|
49
|
+
export declare const APP_LOGO_DEFAULTS: Required<Pick<AppLogoProps, 'icon' | 'iconColor' | 'iconColorMode' | 'iconSize' | 'iconOffsetX' | 'iconOffsetY' | 'iconRotation' | 'cornerRadius' | 'cornerShape' | 'size'>> & {
|
|
50
|
+
background: GradientConfig;
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* Detect icon source type from the icon prop string.
|
|
54
|
+
*
|
|
55
|
+
* - Starts with `<svg` or `<SVG` -> inline SVG
|
|
56
|
+
* - Starts with `data:` -> data URL
|
|
57
|
+
* - Contains `:` (but not `data:`) -> Iconify ID
|
|
58
|
+
* - Otherwise -> Unicode/emoji text
|
|
59
|
+
*/
|
|
60
|
+
export declare function detectIconSource(icon: string): IconSourceType;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/** The Leftium brand gradient: 45deg diagonal, dark blue -> bright blue -> dark blue */
|
|
2
|
+
export const LEFTIUM_GRADIENT = {
|
|
3
|
+
colors: ['#0029c1', '#3973ff', '#0029c1'],
|
|
4
|
+
stops: [0, 0.29, 1],
|
|
5
|
+
angle: 45 // bottom-left -> top-right
|
|
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
|
+
];
|
|
24
|
+
/** Default values for all AppLogo props */
|
|
25
|
+
export const APP_LOGO_DEFAULTS = {
|
|
26
|
+
icon: 'fxemoji:rocket',
|
|
27
|
+
iconColor: '#ffffff',
|
|
28
|
+
iconColorMode: 'auto',
|
|
29
|
+
iconSize: 60,
|
|
30
|
+
iconOffsetX: 0,
|
|
31
|
+
iconOffsetY: 0,
|
|
32
|
+
iconRotation: 0,
|
|
33
|
+
cornerRadius: 0,
|
|
34
|
+
cornerShape: 'round',
|
|
35
|
+
background: LEFTIUM_GRADIENT,
|
|
36
|
+
size: 512
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Detect icon source type from the icon prop string.
|
|
40
|
+
*
|
|
41
|
+
* - Starts with `<svg` or `<SVG` -> inline SVG
|
|
42
|
+
* - Starts with `data:` -> data URL
|
|
43
|
+
* - Contains `:` (but not `data:`) -> Iconify ID
|
|
44
|
+
* - Otherwise -> Unicode/emoji text
|
|
45
|
+
*/
|
|
46
|
+
export function detectIconSource(icon) {
|
|
47
|
+
const trimmed = icon.trim();
|
|
48
|
+
if (trimmed.startsWith('<svg') || trimmed.startsWith('<SVG'))
|
|
49
|
+
return 'svg';
|
|
50
|
+
if (trimmed.startsWith('data:'))
|
|
51
|
+
return 'data-url';
|
|
52
|
+
if (trimmed.includes(':'))
|
|
53
|
+
return 'iconify';
|
|
54
|
+
return 'emoji';
|
|
55
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { AppLogoConfig } from './types.js';
|
|
2
|
+
export interface FaviconSetResult {
|
|
3
|
+
svg: string;
|
|
4
|
+
ico: Blob;
|
|
5
|
+
appleTouchIcon: Blob;
|
|
6
|
+
icon192: Blob;
|
|
7
|
+
icon512: Blob;
|
|
8
|
+
}
|
|
9
|
+
export interface AppInfo {
|
|
10
|
+
name?: string;
|
|
11
|
+
shortName?: string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Generate the full favicon file set from an AppLogo configuration.
|
|
15
|
+
*
|
|
16
|
+
* Returns SVG, ICO, and PNG blobs for all standard favicon sizes.
|
|
17
|
+
* Browser-only (requires canvas).
|
|
18
|
+
*/
|
|
19
|
+
export declare function generateFaviconSet(config: AppLogoConfig): Promise<FaviconSetResult>;
|
|
20
|
+
/**
|
|
21
|
+
* Generate a manifest.webmanifest JSON string.
|
|
22
|
+
*/
|
|
23
|
+
export declare function generateManifest(appInfo: AppInfo): string;
|
|
24
|
+
/**
|
|
25
|
+
* Generate the HTML snippet for pasting into app.html.
|
|
26
|
+
*/
|
|
27
|
+
export declare function generateFaviconHtml(appInfo: AppInfo): string;
|
|
28
|
+
/**
|
|
29
|
+
* Build the full zip kit as a Blob.
|
|
30
|
+
*
|
|
31
|
+
* Zip structure mirrors a SvelteKit project root:
|
|
32
|
+
* static/favicon.ico
|
|
33
|
+
* static/icon.svg
|
|
34
|
+
* static/apple-touch-icon.png
|
|
35
|
+
* static/icon-192.png
|
|
36
|
+
* static/icon-512.png
|
|
37
|
+
* static/logo.png
|
|
38
|
+
* static/logo.webp
|
|
39
|
+
* static/logo.svg
|
|
40
|
+
* static/manifest.webmanifest
|
|
41
|
+
* static/_app-logo/config.json
|
|
42
|
+
* _snippets/favicon-html.html
|
|
43
|
+
*/
|
|
44
|
+
export declare function generateZipKit(config: AppLogoConfig, appInfo: AppInfo): Promise<Blob>;
|