@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.
- package/dist/AppLogo.svelte +16 -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
package/dist/app-logo/iconify.js
CHANGED
|
@@ -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,
|
|
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
|
-
//
|
|
125
|
-
|
|
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',
|
package/dist/app-logo/types.d.ts
CHANGED
|
@@ -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 {};
|
package/dist/tooltip.js
ADDED
|
@@ -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
|
+
}
|