@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.
Files changed (34) hide show
  1. package/dist/AppLogo.svelte +146 -0
  2. package/dist/AppLogo.svelte.d.ts +7 -0
  3. package/dist/LeftiumLogo.svelte +267 -186
  4. package/dist/LeftiumLogo.svelte.d.ts +1 -0
  5. package/dist/app-logo/color-transform.d.ts +40 -0
  6. package/dist/app-logo/color-transform.js +142 -0
  7. package/dist/app-logo/config-serialization.d.ts +80 -0
  8. package/dist/app-logo/config-serialization.js +489 -0
  9. package/dist/app-logo/defaults.d.ts +60 -0
  10. package/dist/app-logo/defaults.js +55 -0
  11. package/dist/app-logo/generate-favicon-set.d.ts +44 -0
  12. package/dist/app-logo/generate-favicon-set.js +97 -0
  13. package/dist/app-logo/generate-ico.d.ts +18 -0
  14. package/dist/app-logo/generate-ico.js +63 -0
  15. package/dist/app-logo/generate-png.d.ts +16 -0
  16. package/dist/app-logo/generate-png.js +60 -0
  17. package/dist/app-logo/generate-svg.d.ts +9 -0
  18. package/dist/app-logo/generate-svg.js +160 -0
  19. package/dist/app-logo/iconify.d.ts +35 -0
  20. package/dist/app-logo/iconify.js +223 -0
  21. package/dist/app-logo/squircle.d.ts +43 -0
  22. package/dist/app-logo/squircle.js +213 -0
  23. package/dist/app-logo/types.d.ts +39 -0
  24. package/dist/app-logo/types.js +1 -0
  25. package/dist/assets/logo-parts/glow-squircle.svg +44 -0
  26. package/dist/index.d.ts +8 -3
  27. package/dist/index.js +9 -3
  28. package/dist/leftium-logo/generate-svg.d.ts +29 -0
  29. package/dist/leftium-logo/generate-svg.js +470 -0
  30. package/dist/tooltip.d.ts +18 -0
  31. package/dist/tooltip.js +38 -0
  32. package/dist/webgl-ripples/webgl-ripples.d.ts +0 -4
  33. package/dist/webgl-ripples/webgl-ripples.js +1 -1
  34. 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>;