@leftium/logo 0.1.0 → 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.
@@ -1,6 +1,8 @@
1
- import { detectIconSource } from './defaults.js';
1
+ import { detectIconSource, DEFAULT_EMOJI_STYLE } from './defaults.js';
2
2
  /** Cached Iconify SVG strings keyed by icon ID */
3
3
  const iconCache = new Map();
4
+ /** Cached emoji slug resolution keyed by `${prefix}:${emoji}` */
5
+ const emojiSlugCache = new Map();
4
6
  /**
5
7
  * Fetch an Iconify icon SVG by its ID (e.g. "mdi:rocket-launch").
6
8
  * Results are cached in memory for the session.
@@ -84,13 +86,87 @@ function detectMonochrome(svg) {
84
86
  // No colors found at all -- treat as monochrome (will use currentColor behavior)
85
87
  return true;
86
88
  }
89
+ // ─── Emoji → Iconify slug resolution ────────────────────────────────────
90
+ /**
91
+ * Convert an emoji string to its Unicode codepoint sequence (e.g. "🚀" → "1f680").
92
+ * Strips variation selectors (U+FE0E, U+FE0F).
93
+ */
94
+ function emojiToCodepoints(emoji) {
95
+ const codepoints = [];
96
+ for (const codePoint of emoji) {
97
+ const cp = codePoint.codePointAt(0);
98
+ if (cp === undefined)
99
+ continue;
100
+ // Skip variation selectors
101
+ if (cp === 0xfe0e || cp === 0xfe0f)
102
+ continue;
103
+ codepoints.push(cp.toString(16));
104
+ }
105
+ return codepoints.join('-');
106
+ }
107
+ /**
108
+ * Resolve an emoji to its Iconify icon slug within a given set.
109
+ *
110
+ * Strategy A: Use the Iconify search API, which accepts emoji characters
111
+ * directly and returns matching icon names.
112
+ *
113
+ * Returns the icon name (slug) or null if not found.
114
+ * Results are cached by `${prefix}:${emoji}`.
115
+ */
116
+ export async function resolveEmojiSlug(emoji, prefix) {
117
+ const cacheKey = `${prefix}:${emoji}`;
118
+ const cached = emojiSlugCache.get(cacheKey);
119
+ if (cached !== undefined)
120
+ return cached;
121
+ try {
122
+ // Strategy A: Iconify search API
123
+ const encoded = encodeURIComponent(emoji);
124
+ const url = `https://api.iconify.design/search?query=${encoded}&prefix=${prefix}&limit=1`;
125
+ const response = await fetch(url);
126
+ if (response.ok) {
127
+ const data = await response.json();
128
+ // Response format: { icons: ["prefix:name", ...] }
129
+ if (data.icons && data.icons.length > 0) {
130
+ const fullId = data.icons[0];
131
+ // Extract just the name part (after the prefix:)
132
+ const colonIndex = fullId.indexOf(':');
133
+ const slug = colonIndex >= 0 ? fullId.substring(colonIndex + 1) : fullId;
134
+ emojiSlugCache.set(cacheKey, slug);
135
+ return slug;
136
+ }
137
+ }
138
+ // Strategy B fallback: Try codepoint-based name (many sets use this)
139
+ const codepoints = emojiToCodepoints(emoji);
140
+ if (codepoints) {
141
+ // Try fetching directly — if it exists, the codepoint IS the name
142
+ const directUrl = `https://api.iconify.design/${prefix}/${codepoints}.svg`;
143
+ const directResponse = await fetch(directUrl, { method: 'HEAD' });
144
+ if (directResponse.ok) {
145
+ emojiSlugCache.set(cacheKey, codepoints);
146
+ return codepoints;
147
+ }
148
+ }
149
+ // Not found
150
+ emojiSlugCache.set(cacheKey, null);
151
+ return null;
152
+ }
153
+ catch {
154
+ // Network error — don't cache failures
155
+ return null;
156
+ }
157
+ }
158
+ // ─── Main resolution ────────────────────────────────────────────────────
87
159
  /**
88
160
  * Resolve an icon prop value to SVG data ready for rendering.
89
161
  *
90
162
  * Handles all 4 source types: Iconify ID, emoji, inline SVG, data URL.
91
- * For emoji, returns a special SVG text element instead of path data.
163
+ * For emoji, auto-maps to an Iconify emoji set unless emojiStyle is 'native'.
164
+ *
165
+ * @param icon - The icon prop value
166
+ * @param emojiStyle - Iconify emoji set prefix (default: DEFAULT_EMOJI_STYLE).
167
+ * Use 'native' to disable auto-mapping and render as <text>.
92
168
  */
93
- export async function resolveIcon(icon) {
169
+ export async function resolveIcon(icon, emojiStyle = DEFAULT_EMOJI_STYLE) {
94
170
  const sourceType = detectIconSource(icon);
95
171
  switch (sourceType) {
96
172
  case 'iconify': {
@@ -121,8 +197,21 @@ export async function resolveIcon(icon) {
121
197
  };
122
198
  }
123
199
  case 'emoji': {
124
- // For emoji, we create a text-based SVG element
125
- // The viewBox is set to a standard size; the emoji is centered
200
+ // Auto-map emoji to Iconify SVG icon
201
+ if (emojiStyle !== 'native') {
202
+ const slug = await resolveEmojiSlug(icon, emojiStyle);
203
+ if (slug) {
204
+ const iconId = `${emojiStyle}:${slug}`;
205
+ const svg = await fetchIconifySvg(iconId);
206
+ return {
207
+ svgContent: extractSvgContent(svg),
208
+ viewBox: parseViewBox(svg),
209
+ isMonochrome: detectMonochrome(svg),
210
+ sourceType: 'iconify' // Resolved as Iconify, even though input was emoji
211
+ };
212
+ }
213
+ }
214
+ // Fallback: platform-native <text> rendering
126
215
  return {
127
216
  svgContent: `<text x="50%" y="50%" dominant-baseline="central" text-anchor="middle" font-size="80" font-family="'Apple Color Emoji','Segoe UI Emoji','Noto Color Emoji',sans-serif">${icon}</text>`,
128
217
  viewBox: '0 0 100 100',
@@ -18,6 +18,7 @@ export interface AppLogoProps {
18
18
  iconOffsetX?: number;
19
19
  iconOffsetY?: number;
20
20
  iconRotation?: number;
21
+ grayscaleLightness?: number;
21
22
  cornerRadius?: number;
22
23
  cornerShape?: CornerShape;
23
24
  background?: string | GradientConfig;
@@ -30,6 +31,7 @@ export interface AppLogoConfig {
30
31
  background?: AppLogoProps['background'];
31
32
  cornerRadius?: number;
32
33
  cornerShape?: CornerShape;
34
+ emojiStyle?: string;
33
35
  logo?: Partial<AppLogoProps>;
34
36
  favicon?: Partial<AppLogoProps>;
35
37
  }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Generate a self-contained static SVG for the Leftium logo.
3
+ *
4
+ * Replicates the LeftiumLogo.svelte layout geometry as pure SVG,
5
+ * with all assets inlined as base64 data URIs.
6
+ */
7
+ export type BoundingBox = 'square' | 'default' | 'encircled' | 'cropped';
8
+ export interface LeftiumLogoConfig {
9
+ /** Output canvas width in pixels. For 'cropped' mode the height will differ. Default: 800 */
10
+ size?: number;
11
+ /** Corner shape of the blue square. Default: true (squircle) */
12
+ squircle?: boolean;
13
+ /** Bounding box mode. Default: 'default' */
14
+ boundingBox?: BoundingBox;
15
+ /** Background fill. 'transparent' or a CSS color string e.g. '#ffffff'. Default: 'transparent' */
16
+ background?: string;
17
+ }
18
+ /**
19
+ * Generate a complete, self-contained SVG string for the static Leftium logo.
20
+ *
21
+ * All asset SVGs are inlined as base64 data URIs so the output is a single
22
+ * portable file with no external dependencies.
23
+ */
24
+ export declare function generateLeftiumLogoSvg(config?: LeftiumLogoConfig): string;
25
+ /**
26
+ * Rasterise the Leftium logo to a PNG or WebP Blob.
27
+ * Browser-only (requires canvas + Image).
28
+ */
29
+ export declare function generateLeftiumLogoPng(config: LeftiumLogoConfig, format?: 'png' | 'webp'): Promise<Blob>;
@@ -0,0 +1,470 @@
1
+ /**
2
+ * Generate a self-contained static SVG for the Leftium logo.
3
+ *
4
+ * Replicates the LeftiumLogo.svelte layout geometry as pure SVG,
5
+ * with all assets inlined as base64 data URIs.
6
+ */
7
+ // ─── Raw SVG asset strings (imported at build time via ?raw) ─────────────────
8
+ import squareSvgRaw from '../assets/logo-parts/square.svg?raw';
9
+ import glowSvgRaw from '../assets/logo-parts/glow.svg?raw';
10
+ import glowSquircleSvgRaw from '../assets/logo-parts/glow-squircle.svg?raw';
11
+ import ligatureSvgRaw from '../assets/logo-parts/ligature.svg?raw';
12
+ import shadowSvgRaw from '../assets/logo-parts/shadow.svg?raw';
13
+ // ─── Geometry constants (mirrored from LeftiumLogo.svelte) ───────────────────
14
+ // The inner grid (grid-logo) is always 532×532 in "native" units.
15
+ const GRID = 532;
16
+ // Glow is 647×647, centered on the square (overflows by 57.5 on each side)
17
+ const GLOW_W = 647;
18
+ const GLOW_OFFSET = -57.5; // (647-532)/2 = 57.5
19
+ // Ligature positioning — square mode
20
+ const LIG_ORIG_W = 440;
21
+ const LIG_ORIG_H = 666;
22
+ const LIG_ORIG_L = 133.5;
23
+ const LIG_ORIG_T = -65.75;
24
+ const BLUR_PAD_ORIG = 50;
25
+ // Ligature positioning — squircle mode
26
+ const LIG_SQRC_W = 425.2;
27
+ const LIG_SQRC_H = 643.6;
28
+ const LIG_SQRC_L = 129.5;
29
+ const LIG_SQRC_T = -47.6;
30
+ const BLUR_PAD_SQRC = 48.3;
31
+ // Bounding box scaling factors for the grid within the output canvas.
32
+ // CSS: grid width = canvas / scale_factor, centered.
33
+ const BBOX_SCALE = {
34
+ square: 1,
35
+ default: 1.2519,
36
+ encircled: 1.5037,
37
+ cropped: 1 // handled separately — non-square canvas
38
+ };
39
+ // Squircle encircled gets an extra 1.04× scale-up
40
+ const ENCIRCLED_SQUIRCLE_EXTRA = 1.04;
41
+ // Cropped mode dimensions (from LeftiumLogo.svelte CSS comments)
42
+ // Container aspect ratio ≈ 0.8906 (height/width = 1/0.8906)
43
+ const CROPPED_ASPECT = 1 / 0.8906; // height = width * this
44
+ const CROPPED_GRID_W_FRAC = 0.782; // grid is 78.2% of container width
45
+ const CROPPED_LEFT_FRAC = 0.0844; // 8.44% from left
46
+ const CROPPED_TOP_FRAC = 0.1523; // 15.23% from top
47
+ // Squircle clip polygon points (percentages → will be scaled to px).
48
+ // This is the same polygon as LeftiumLogo.svelte SQUIRCLE_CLIP but converted
49
+ // to absolute pixel coordinates at render time.
50
+ const SQUIRCLE_POLY_PCTS = [
51
+ [50, 0],
52
+ [53.05, 0],
53
+ [55.96, 0],
54
+ [58.74, 0],
55
+ [61.38, 0],
56
+ [63.89, 0],
57
+ [66.27, 0],
58
+ [68.54, 0.01],
59
+ [70.69, 0.01],
60
+ [72.73, 0.02],
61
+ [74.66, 0.03],
62
+ [76.48, 0.04],
63
+ [78.21, 0.06],
64
+ [79.84, 0.09],
65
+ [81.37, 0.11],
66
+ [82.82, 0.15],
67
+ [84.18, 0.2],
68
+ [85.46, 0.25],
69
+ [86.66, 0.31],
70
+ [87.78, 0.39],
71
+ [88.83, 0.48],
72
+ [89.81, 0.58],
73
+ [90.73, 0.7],
74
+ [91.58, 0.83],
75
+ [92.37, 0.99],
76
+ [93.11, 1.16],
77
+ [93.79, 1.36],
78
+ [94.41, 1.58],
79
+ [94.99, 1.83],
80
+ [95.53, 2.11],
81
+ [96.02, 2.41],
82
+ [96.47, 2.75],
83
+ [96.88, 3.13],
84
+ [97.25, 3.53],
85
+ [97.59, 3.98],
86
+ [97.89, 4.47],
87
+ [98.17, 5.01],
88
+ [98.42, 5.59],
89
+ [98.64, 6.21],
90
+ [98.84, 6.89],
91
+ [99.01, 7.63],
92
+ [99.17, 8.42],
93
+ [99.3, 9.27],
94
+ [99.42, 10.19],
95
+ [99.52, 11.17],
96
+ [99.61, 12.22],
97
+ [99.69, 13.34],
98
+ [99.75, 14.54],
99
+ [99.8, 15.82],
100
+ [99.85, 17.18],
101
+ [99.89, 18.63],
102
+ [99.91, 20.16],
103
+ [99.94, 21.79],
104
+ [99.96, 23.52],
105
+ [99.97, 25.34],
106
+ [99.98, 27.27],
107
+ [99.99, 29.31],
108
+ [99.99, 31.46],
109
+ [100, 33.73],
110
+ [100, 36.11],
111
+ [100, 38.62],
112
+ [100, 41.26],
113
+ [100, 44.04],
114
+ [100, 46.95],
115
+ [100, 50],
116
+ [100, 50],
117
+ [100, 53.05],
118
+ [100, 55.96],
119
+ [100, 58.74],
120
+ [100, 61.38],
121
+ [100, 63.89],
122
+ [100, 66.27],
123
+ [99.99, 68.54],
124
+ [99.99, 70.69],
125
+ [99.98, 72.73],
126
+ [99.97, 74.66],
127
+ [99.96, 76.48],
128
+ [99.94, 78.21],
129
+ [99.91, 79.84],
130
+ [99.89, 81.37],
131
+ [99.85, 82.82],
132
+ [99.8, 84.18],
133
+ [99.75, 85.46],
134
+ [99.69, 86.66],
135
+ [99.61, 87.78],
136
+ [99.52, 88.83],
137
+ [99.42, 89.81],
138
+ [99.3, 90.73],
139
+ [99.17, 91.58],
140
+ [99.01, 92.37],
141
+ [98.84, 93.11],
142
+ [98.64, 93.79],
143
+ [98.42, 94.41],
144
+ [98.17, 94.99],
145
+ [97.89, 95.53],
146
+ [97.59, 96.02],
147
+ [97.25, 96.47],
148
+ [96.88, 96.88],
149
+ [96.47, 97.25],
150
+ [96.02, 97.59],
151
+ [95.53, 97.89],
152
+ [94.99, 98.17],
153
+ [94.41, 98.42],
154
+ [93.79, 98.64],
155
+ [93.11, 98.84],
156
+ [92.37, 99.01],
157
+ [91.58, 99.17],
158
+ [90.73, 99.3],
159
+ [89.81, 99.42],
160
+ [88.83, 99.52],
161
+ [87.78, 99.61],
162
+ [86.66, 99.69],
163
+ [85.46, 99.75],
164
+ [84.18, 99.8],
165
+ [82.82, 99.85],
166
+ [81.37, 99.89],
167
+ [79.84, 99.91],
168
+ [78.21, 99.94],
169
+ [76.48, 99.96],
170
+ [74.66, 99.97],
171
+ [72.73, 99.98],
172
+ [70.69, 99.99],
173
+ [68.54, 99.99],
174
+ [66.27, 100],
175
+ [63.89, 100],
176
+ [61.38, 100],
177
+ [58.74, 100],
178
+ [55.96, 100],
179
+ [53.05, 100],
180
+ [50, 100],
181
+ [50, 100],
182
+ [46.95, 100],
183
+ [44.04, 100],
184
+ [41.26, 100],
185
+ [38.62, 100],
186
+ [36.11, 100],
187
+ [33.73, 100],
188
+ [31.46, 99.99],
189
+ [29.31, 99.99],
190
+ [27.27, 99.98],
191
+ [25.34, 99.97],
192
+ [23.52, 99.96],
193
+ [21.79, 99.94],
194
+ [20.16, 99.91],
195
+ [18.63, 99.89],
196
+ [17.18, 99.85],
197
+ [15.82, 99.8],
198
+ [14.54, 99.75],
199
+ [13.34, 99.69],
200
+ [12.22, 99.61],
201
+ [11.17, 99.52],
202
+ [10.19, 99.42],
203
+ [9.27, 99.3],
204
+ [8.42, 99.17],
205
+ [7.63, 99.01],
206
+ [6.89, 98.84],
207
+ [6.21, 98.64],
208
+ [5.59, 98.42],
209
+ [5.01, 98.17],
210
+ [4.47, 97.89],
211
+ [3.98, 97.59],
212
+ [3.53, 97.25],
213
+ [3.13, 96.88],
214
+ [2.75, 96.47],
215
+ [2.41, 96.02],
216
+ [2.11, 95.53],
217
+ [1.83, 94.99],
218
+ [1.58, 94.41],
219
+ [1.36, 93.79],
220
+ [1.16, 93.11],
221
+ [0.99, 92.37],
222
+ [0.83, 91.58],
223
+ [0.7, 90.73],
224
+ [0.58, 89.81],
225
+ [0.48, 88.83],
226
+ [0.39, 87.78],
227
+ [0.31, 86.66],
228
+ [0.25, 85.46],
229
+ [0.2, 84.18],
230
+ [0.15, 82.82],
231
+ [0.11, 81.37],
232
+ [0.09, 79.84],
233
+ [0.06, 78.21],
234
+ [0.04, 76.48],
235
+ [0.03, 74.66],
236
+ [0.02, 72.73],
237
+ [0.01, 70.69],
238
+ [0.01, 68.54],
239
+ [0, 66.27],
240
+ [0, 63.89],
241
+ [0, 61.38],
242
+ [0, 58.74],
243
+ [0, 55.96],
244
+ [0, 53.05],
245
+ [0, 50],
246
+ [0, 50],
247
+ [0, 46.95],
248
+ [0, 44.04],
249
+ [0, 41.26],
250
+ [0, 38.62],
251
+ [0, 36.11],
252
+ [0, 33.73],
253
+ [0.01, 31.46],
254
+ [0.01, 29.31],
255
+ [0.02, 27.27],
256
+ [0.03, 25.34],
257
+ [0.04, 23.52],
258
+ [0.06, 21.79],
259
+ [0.09, 20.16],
260
+ [0.11, 18.63],
261
+ [0.15, 17.18],
262
+ [0.2, 15.82],
263
+ [0.25, 14.54],
264
+ [0.31, 13.34],
265
+ [0.39, 12.22],
266
+ [0.48, 11.17],
267
+ [0.58, 10.19],
268
+ [0.7, 9.27],
269
+ [0.83, 8.42],
270
+ [0.99, 7.63],
271
+ [1.16, 6.89],
272
+ [1.36, 6.21],
273
+ [1.58, 5.59],
274
+ [1.83, 5.01],
275
+ [2.11, 4.47],
276
+ [2.41, 3.98],
277
+ [2.75, 3.53],
278
+ [3.13, 3.13],
279
+ [3.53, 2.75],
280
+ [3.98, 2.41],
281
+ [4.47, 2.11],
282
+ [5.01, 1.83],
283
+ [5.59, 1.58],
284
+ [6.21, 1.36],
285
+ [6.89, 1.16],
286
+ [7.63, 0.99],
287
+ [8.42, 0.83],
288
+ [9.27, 0.7],
289
+ [10.19, 0.58],
290
+ [11.17, 0.48],
291
+ [12.22, 0.39],
292
+ [13.34, 0.31],
293
+ [14.54, 0.25],
294
+ [15.82, 0.2],
295
+ [17.18, 0.15],
296
+ [18.63, 0.11],
297
+ [20.16, 0.09],
298
+ [21.79, 0.06],
299
+ [23.52, 0.04],
300
+ [25.34, 0.03],
301
+ [27.27, 0.02],
302
+ [29.31, 0.01],
303
+ [31.46, 0.01],
304
+ [33.73, 0],
305
+ [36.11, 0],
306
+ [38.62, 0],
307
+ [41.26, 0],
308
+ [44.04, 0],
309
+ [46.95, 0],
310
+ [50, 0]
311
+ ];
312
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
313
+ /** Encode a raw SVG string as a base64 data URI */
314
+ function svgToDataUri(svgRaw) {
315
+ // btoa works in both browser and modern Node/Bun
316
+ const b64 = btoa(unescape(encodeURIComponent(svgRaw)));
317
+ return `data:image/svg+xml;base64,${b64}`;
318
+ }
319
+ /** Round to 4 decimal places to keep SVG compact */
320
+ function r(n) {
321
+ return Math.round(n * 10000) / 10000;
322
+ }
323
+ /**
324
+ * Convert the squircle polygon (% coords) to absolute px points
325
+ * offset by (ox, oy) within the SVG canvas.
326
+ */
327
+ function squircleClipPath(gridPx, ox, oy, clipId) {
328
+ const pts = SQUIRCLE_POLY_PCTS.map(([px, py]) => `${r(ox + (px / 100) * gridPx)},${r(oy + (py / 100) * gridPx)}`).join(' ');
329
+ return `<clipPath id="${clipId}"><polygon points="${pts}"/></clipPath>`;
330
+ }
331
+ // ─── Main export ─────────────────────────────────────────────────────────────
332
+ /**
333
+ * Generate a complete, self-contained SVG string for the static Leftium logo.
334
+ *
335
+ * All asset SVGs are inlined as base64 data URIs so the output is a single
336
+ * portable file with no external dependencies.
337
+ */
338
+ export function generateLeftiumLogoSvg(config = {}) {
339
+ const { size = 800, squircle = true, boundingBox = 'default', background = 'transparent' } = config;
340
+ // ── Choose ligature/shadow constants based on squircle mode ──────────────
341
+ const ligW = squircle ? LIG_SQRC_W : LIG_ORIG_W;
342
+ const ligH = squircle ? LIG_SQRC_H : LIG_ORIG_H;
343
+ const ligL = squircle ? LIG_SQRC_L : LIG_ORIG_L;
344
+ const ligT = squircle ? LIG_SQRC_T : LIG_ORIG_T;
345
+ const blurPad = squircle ? BLUR_PAD_SQRC : BLUR_PAD_ORIG;
346
+ const shadW = ligW + blurPad * 2;
347
+ const shadH = ligH + blurPad * 2;
348
+ const shadL = ligL - blurPad;
349
+ const shadT = ligT - blurPad;
350
+ // ── Compute canvas size and grid position ─────────────────────────────────
351
+ let canvasW;
352
+ let canvasH;
353
+ let gridPx; // rendered size of the 532-unit grid
354
+ let gridX; // left offset of grid on canvas
355
+ let gridY; // top offset of grid on canvas
356
+ if (boundingBox === 'cropped') {
357
+ canvasW = size;
358
+ canvasH = r(size * CROPPED_ASPECT);
359
+ gridPx = r(size * CROPPED_GRID_W_FRAC);
360
+ gridX = r(size * CROPPED_LEFT_FRAC);
361
+ gridY = r(canvasH * CROPPED_TOP_FRAC);
362
+ }
363
+ else {
364
+ canvasW = size;
365
+ canvasH = size;
366
+ let scale = BBOX_SCALE[boundingBox];
367
+ if (boundingBox === 'encircled' && squircle)
368
+ scale = scale / ENCIRCLED_SQUIRCLE_EXTRA;
369
+ gridPx = r(size / scale);
370
+ gridX = r((size - gridPx) / 2);
371
+ gridY = r((size - gridPx) / 2);
372
+ }
373
+ // Scale factor: native grid (532) → rendered pixels
374
+ const s = gridPx / GRID;
375
+ // ── Asset data URIs ───────────────────────────────────────────────────────
376
+ const squareUri = svgToDataUri(squareSvgRaw);
377
+ const glowUri = svgToDataUri(squircle ? glowSquircleSvgRaw : glowSvgRaw);
378
+ const ligUri = svgToDataUri(ligatureSvgRaw);
379
+ const shadUri = svgToDataUri(shadowSvgRaw);
380
+ // ── Element positions (all in canvas px coords) ───────────────────────────
381
+ // Square: fills the full grid
382
+ const sqX = gridX;
383
+ const sqY = gridY;
384
+ const sqW = gridPx;
385
+ const sqH = gridPx;
386
+ // Glow: 647/532 × gridPx, centered (overflows grid on all sides)
387
+ const glowPx = r((GLOW_W / GRID) * gridPx);
388
+ const glowX = r(gridX + (GLOW_OFFSET / GRID) * gridPx);
389
+ const glowY = r(gridY + (GLOW_OFFSET / GRID) * gridPx);
390
+ // Shadow: scaled from native 532 coords
391
+ const shadXpx = r(gridX + (shadL / GRID) * gridPx);
392
+ const shadYpx = r(gridY + (shadT / GRID) * gridPx);
393
+ const shadWpx = r((shadW / GRID) * gridPx);
394
+ const shadHpx = r((shadH / GRID) * gridPx);
395
+ // Ligature: scaled from native 532 coords
396
+ const ligXpx = r(gridX + (ligL / GRID) * gridPx);
397
+ const ligYpx = r(gridY + (ligT / GRID) * gridPx);
398
+ const ligWpx = r((ligW / GRID) * gridPx);
399
+ const ligHpx = r((ligH / GRID) * gridPx);
400
+ // ── Build SVG defs (background rect + optional squircle clip) ─────────────
401
+ const defs = [];
402
+ let squareClipAttr = '';
403
+ if (squircle) {
404
+ const clipId = 'leftium-squircle-clip';
405
+ defs.push(squircleClipPath(gridPx, gridX, gridY, clipId));
406
+ squareClipAttr = ` clip-path="url(#${clipId})"`;
407
+ }
408
+ const defsBlock = defs.length ? `\n <defs>\n ${defs.join('\n ')}\n </defs>` : '';
409
+ // ── Background ────────────────────────────────────────────────────────────
410
+ const bgEl = background === 'transparent'
411
+ ? ''
412
+ : `\n <rect width="${canvasW}" height="${canvasH}" fill="${background}"/>`;
413
+ // ── Assemble SVG ──────────────────────────────────────────────────────────
414
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="${canvasW}" height="${canvasH}" viewBox="0 0 ${canvasW} ${canvasH}">${defsBlock}${bgEl}
415
+ <!-- shadow (z=0) -->
416
+ <image href="${shadUri}" x="${shadXpx}" y="${shadYpx}" width="${shadWpx}" height="${shadHpx}"/>
417
+ <!-- glow (z=1) -->
418
+ <image href="${glowUri}" x="${glowX}" y="${glowY}" width="${glowPx}" height="${glowPx}"/>
419
+ <!-- square (z=2)${squircle ? ', squircle-clipped' : ''} -->
420
+ <image href="${squareUri}" x="${sqX}" y="${sqY}" width="${sqW}" height="${sqH}"${squareClipAttr}/>
421
+ <!-- ligature (z=3) -->
422
+ <image href="${ligUri}" x="${ligXpx}" y="${ligYpx}" width="${ligWpx}" height="${ligHpx}"/>
423
+ </svg>`;
424
+ }
425
+ // ─── PNG / WebP rasteriser ────────────────────────────────────────────────────
426
+ /**
427
+ * Rasterise the Leftium logo to a PNG or WebP Blob.
428
+ * Browser-only (requires canvas + Image).
429
+ */
430
+ export async function generateLeftiumLogoPng(config, format = 'png') {
431
+ const svg = generateLeftiumLogoSvg(config);
432
+ const size = config.size ?? 800;
433
+ const { squircle = true, boundingBox = 'default' } = config;
434
+ const canvasW = size;
435
+ const canvasH = boundingBox === 'cropped' ? Math.round(size * (1 / 0.8906)) : size;
436
+ return new Promise((resolve, reject) => {
437
+ const img = new Image();
438
+ const blob = new Blob([svg], { type: 'image/svg+xml;charset=utf-8' });
439
+ const url = URL.createObjectURL(blob);
440
+ img.onload = () => {
441
+ try {
442
+ const canvas = document.createElement('canvas');
443
+ canvas.width = canvasW;
444
+ canvas.height = canvasH;
445
+ const ctx = canvas.getContext('2d');
446
+ if (!ctx) {
447
+ reject(new Error('Failed to get canvas 2d context'));
448
+ return;
449
+ }
450
+ ctx.drawImage(img, 0, 0, canvasW, canvasH);
451
+ canvas.toBlob((outBlob) => {
452
+ URL.revokeObjectURL(url);
453
+ if (outBlob)
454
+ resolve(outBlob);
455
+ else
456
+ reject(new Error('Canvas toBlob returned null'));
457
+ }, format === 'webp' ? 'image/webp' : 'image/png');
458
+ }
459
+ catch (err) {
460
+ URL.revokeObjectURL(url);
461
+ reject(err);
462
+ }
463
+ };
464
+ img.onerror = () => {
465
+ URL.revokeObjectURL(url);
466
+ reject(new Error('Failed to load SVG into Image element'));
467
+ };
468
+ img.src = url;
469
+ });
470
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Svelte action wrapping tippy.js for instant, styled tooltips.
3
+ *
4
+ * Usage:
5
+ * <button use:tooltip={'Copy to clipboard'}>Copy</button>
6
+ * <button use:tooltip={{ content: 'Preview', placement: 'top' }}>Hover</button>
7
+ */
8
+ import type { Props } from 'tippy.js';
9
+ import 'tippy.js/dist/tippy.css';
10
+ type TooltipParam = string | Partial<Props> | null | undefined;
11
+ export declare function tooltip(node: HTMLElement, param: TooltipParam): {
12
+ update?: undefined;
13
+ destroy?: undefined;
14
+ } | {
15
+ update(newParam: TooltipParam): void;
16
+ destroy(): void;
17
+ };
18
+ export {};
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Svelte action wrapping tippy.js for instant, styled tooltips.
3
+ *
4
+ * Usage:
5
+ * <button use:tooltip={'Copy to clipboard'}>Copy</button>
6
+ * <button use:tooltip={{ content: 'Preview', placement: 'top' }}>Hover</button>
7
+ */
8
+ import tippy from 'tippy.js';
9
+ import 'tippy.js/dist/tippy.css';
10
+ export function tooltip(node, param) {
11
+ if (!param)
12
+ return {};
13
+ const opts = typeof param === 'string' ? { content: param } : param;
14
+ // @ts-expect-error -- tippy.js CJS/ESM interop under module:"NodeNext"
15
+ const instance = tippy(node, {
16
+ delay: [80, 0], // 80ms show delay (fast but not jumpy), instant hide
17
+ duration: [150, 100],
18
+ ...opts
19
+ });
20
+ // Remove native title so browser tooltip doesn't fight tippy
21
+ if (node.hasAttribute('title')) {
22
+ node.removeAttribute('title');
23
+ }
24
+ return {
25
+ update(newParam) {
26
+ if (!newParam) {
27
+ instance.disable();
28
+ return;
29
+ }
30
+ instance.enable();
31
+ const newOpts = typeof newParam === 'string' ? { content: newParam } : newParam;
32
+ instance.setProps(newOpts);
33
+ },
34
+ destroy() {
35
+ instance.destroy();
36
+ }
37
+ };
38
+ }