@olwiba/dx 0.0.22 → 0.0.23

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/README.md CHANGED
@@ -81,6 +81,36 @@ bunx @olwiba/dx ascii-gif \
81
81
  --out ./public/olwibaDX.gif
82
82
  ```
83
83
 
84
+ ### Asset Generator
85
+
86
+ Generate favicons, app icons, manifest files, robots.txt, and an `og-image.png` from a Lucide icon or SVG mark.
87
+
88
+ ```bash
89
+ bunx @olwiba/dx generate-assets \
90
+ --name "myApp" \
91
+ --icon ./public/logo.svg \
92
+ --color "#0d9488" \
93
+ --og-component ./public/search-bar.svg \
94
+ --out ./public
95
+ ```
96
+
97
+ `--og-component` is optional and only affects the social image: it renders the given SVG/PNG/JPG/WebP
98
+ component bottom-middle on a solid `--color` background, with a small `--icon` + `--name` wordmark
99
+ grouped above it. Without it, `og-image.png` uses the default large-logo layout. Favicons and app
100
+ icons always use `--icon`.
101
+
102
+ ```ts
103
+ import { generateAssets } from "@olwiba/dx/generate-assets";
104
+
105
+ await generateAssets({
106
+ name: "myApp",
107
+ icon: "./public/logo.svg",
108
+ color: "#0d9488",
109
+ outputDir: "./public",
110
+ ogComponent: "./public/search-bar.svg",
111
+ });
112
+ ```
113
+
84
114
  ### Preview Generator
85
115
 
86
116
  Puppeteer-based screenshot tool used to pre-render the isometric preview tiles on docs sites.
package/dist/cli.js CHANGED
@@ -1740,6 +1740,14 @@ function getSvgInner(svgPath) {
1740
1740
  const inner = svg.replace(/<svg[^>]*>/, "").replace(/<\/svg>/, "").trim();
1741
1741
  return { inner, viewBox };
1742
1742
  }
1743
+ function resolveIcon(icon) {
1744
+ const mode = detectIconMode(icon);
1745
+ if (mode === "svg") {
1746
+ const { inner, viewBox } = getSvgInner(icon);
1747
+ return { mode, inner, viewBox };
1748
+ }
1749
+ return { mode, inner: getLucideInner(icon), viewBox: 24 };
1750
+ }
1743
1751
  function buildIconSvgLucide(inner, color, size) {
1744
1752
  const pad = Math.round(size * 0.18);
1745
1753
  const area = size - pad * 2;
@@ -1798,8 +1806,44 @@ function buildOgSvgCustom(inner, viewBox, color) {
1798
1806
  </g>
1799
1807
  </svg>`;
1800
1808
  }
1809
+ function buildOgGlowSvg(box) {
1810
+ const spread = 40;
1811
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="${OG_WIDTH}" height="${OG_HEIGHT}">
1812
+ <defs>
1813
+ <filter id="glow" x="-40%" y="-40%" width="180%" height="180%">
1814
+ <feGaussianBlur stdDeviation="46"/>
1815
+ </filter>
1816
+ </defs>
1817
+ <rect x="${box.left - spread}" y="${box.top - spread}" width="${box.width + spread * 2}" height="${box.height + spread * 2}" rx="48" fill="#ffffff" opacity="0.22" filter="url(#glow)"/>
1818
+ </svg>`;
1819
+ }
1820
+ function buildOgSvgWithSmallLogo(mode, inner, viewBox, color, name) {
1821
+ const markSize = OG_HEADER.markSize;
1822
+ const gap = 28;
1823
+ const fontSize = 56;
1824
+ const groupY = OG_HEADER.top;
1825
+ const scale = markSize / viewBox;
1826
+ const textWidth = Math.round(name.length * fontSize * 0.58);
1827
+ const totalWidth = markSize + gap + textWidth;
1828
+ const groupX = Math.round((1200 - totalWidth) / 2);
1829
+ const logoMarkup = mode === "lucide" ? (() => {
1830
+ const sw = (2.5 / scale).toFixed(4);
1831
+ return `<g transform="translate(${groupX} ${groupY}) scale(${scale})" stroke="white" fill="none" stroke-width="${sw}" stroke-linecap="round" stroke-linejoin="round">
1832
+ ${inner}
1833
+ </g>`;
1834
+ })() : `<g transform="translate(${groupX} ${groupY}) scale(${scale})">
1835
+ ${inner}
1836
+ </g>`;
1837
+ const textX = groupX + markSize + gap;
1838
+ const textY = groupY + markSize / 2;
1839
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="${OG_WIDTH}" height="${OG_HEIGHT}">
1840
+ <rect width="${OG_WIDTH}" height="${OG_HEIGHT}" fill="${color}"/>
1841
+ ${logoMarkup}
1842
+ <text x="${textX}" y="${textY}" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="${fontSize}" font-weight="700" fill="white" dominant-baseline="middle" letter-spacing="-1">${escapeXml(name)}</text>
1843
+ </svg>`;
1844
+ }
1801
1845
  async function generateAssets(config) {
1802
- const { name, icon, color, outputDir } = config;
1846
+ const { name, icon, color, outputDir, ogComponent } = config;
1803
1847
  let sharpFn;
1804
1848
  try {
1805
1849
  sharpFn = (await import('sharp')).default;
@@ -1811,17 +1855,8 @@ async function generateAssets(config) {
1811
1855
  mkdirSync(outputDir, { recursive: true });
1812
1856
  const faviconDir = join(outputDir, "favicon");
1813
1857
  mkdirSync(faviconDir, { recursive: true });
1814
- const mode = detectIconMode(icon);
1815
1858
  const written = [];
1816
- let inner;
1817
- let viewBox = 24;
1818
- if (mode === "svg") {
1819
- const parsed = getSvgInner(icon);
1820
- inner = parsed.inner;
1821
- viewBox = parsed.viewBox;
1822
- } else {
1823
- inner = getLucideInner(icon);
1824
- }
1859
+ const { mode, inner, viewBox } = resolveIcon(icon);
1825
1860
  const buildIcon = (size) => mode === "svg" ? buildIconSvgCustom(inner, viewBox, color, size) : buildIconSvgLucide(inner, color, size);
1826
1861
  for (const size of FAVICON_SIZES) {
1827
1862
  const png = await sharpFn(Buffer.from(buildIcon(size))).png().toBuffer();
@@ -1835,8 +1870,31 @@ async function generateAssets(config) {
1835
1870
  writeFileSync(dest, png);
1836
1871
  written.push(dest);
1837
1872
  }
1838
- const ogSvg = mode === "svg" ? buildOgSvgCustom(inner, viewBox, color) : buildOgSvgLucide(inner, name, color);
1839
- const ogPng = await sharpFn(Buffer.from(ogSvg)).png().toBuffer();
1873
+ const ogSvg = ogComponent ? buildOgSvgWithSmallLogo(mode, inner, viewBox, color, name) : mode === "svg" ? buildOgSvgCustom(inner, viewBox, color) : buildOgSvgLucide(inner, name, color);
1874
+ let ogPng = await sharpFn(Buffer.from(ogSvg)).png().toBuffer();
1875
+ if (ogComponent) {
1876
+ const sideMargin = 72;
1877
+ const maxWidth = OG_WIDTH - sideMargin * 2;
1878
+ const headerBottom = OG_HEADER.top + OG_HEADER.markSize + OG_HEADER.gapBelow;
1879
+ const componentBuf = readFileSync(resolve(process.cwd(), ogComponent));
1880
+ const resized = await sharpFn(componentBuf, { density: 300 }).resize({ width: maxWidth, withoutEnlargement: true }).png().toBuffer();
1881
+ const { width: compWidth = maxWidth, height: compHeight = 0 } = await sharpFn(resized).metadata();
1882
+ const left = Math.round((OG_WIDTH - compWidth) / 2);
1883
+ const fits = headerBottom + compHeight <= OG_HEIGHT;
1884
+ const top = fits ? Math.round(headerBottom + (OG_HEIGHT - headerBottom - compHeight) / 2) : headerBottom;
1885
+ ogPng = await sharpFn(ogPng).composite([
1886
+ // Glow first: it has to sit under the component, and sharp applies
1887
+ // composites in array order.
1888
+ {
1889
+ input: Buffer.from(
1890
+ buildOgGlowSvg({ left, top, width: compWidth, height: compHeight })
1891
+ ),
1892
+ left: 0,
1893
+ top: 0
1894
+ },
1895
+ { input: resized, left, top }
1896
+ ]).png().toBuffer();
1897
+ }
1840
1898
  const ogDest = join(outputDir, "og-image.png");
1841
1899
  writeFileSync(ogDest, ogPng);
1842
1900
  written.push(ogDest);
@@ -1859,9 +1917,17 @@ async function generateAssets(config) {
1859
1917
  written.push(robotsDest);
1860
1918
  return { files: written };
1861
1919
  }
1862
- var FAVICON_SIZES, NAMED_ICONS;
1920
+ var OG_WIDTH, OG_HEIGHT, OG_HEADER, FAVICON_SIZES, NAMED_ICONS;
1863
1921
  var init_generate_assets = __esm({
1864
1922
  "src/generate-assets.ts"() {
1923
+ OG_WIDTH = 1200;
1924
+ OG_HEIGHT = 630;
1925
+ OG_HEADER = {
1926
+ top: 54,
1927
+ markSize: 96,
1928
+ /** Clear space between the wordmark's baseline and the component. */
1929
+ gapBelow: 30
1930
+ };
1865
1931
  FAVICON_SIZES = [16, 32, 48, 64, 192, 512];
1866
1932
  NAMED_ICONS = [
1867
1933
  ["apple-touch-icon.png", 180],
@@ -1880,7 +1946,7 @@ if (command === "skills" && subcommand === "install") {
1880
1946
  await runGenerateAssets();
1881
1947
  } else {
1882
1948
  process.stdout.write(
1883
- "Usage:\n dx skills install [--source <url>] [--target claude|amp] [--all] [--name a,b,c]\n dx ascii-gif --text <text> --out <file.gif>\n dx generate-assets --name <app> --icon <lucide-icon> --color <#hex> [--out <dir>]\n"
1949
+ "Usage:\n dx skills install [--source <url>] [--target claude|amp] [--all] [--name a,b,c]\n dx ascii-gif --text <text> --out <file.gif>\n dx generate-assets --name <app> --icon <lucide-icon> --color <#hex> [--out <dir>] [--og-component <svg-or-image-path>]\n"
1884
1950
  );
1885
1951
  }
1886
1952
  async function runSkillsInstall() {
@@ -2044,9 +2110,10 @@ async function runGenerateAssets() {
2044
2110
  const icon = flags.icon;
2045
2111
  const color = flags.color;
2046
2112
  const outputDir = flags.out ?? flags.output ?? "public";
2113
+ const ogComponent = flags["og-component"];
2047
2114
  if (!name || !icon || !color) {
2048
2115
  process.stderr.write(
2049
- "Usage: dx generate-assets --name <app> --icon <lucide-icon> --color <#hex> [--out <dir>]\n"
2116
+ "Usage: dx generate-assets --name <app> --icon <lucide-icon> --color <#hex> [--out <dir>] [--og-component <svg-or-image-path>]\n"
2050
2117
  );
2051
2118
  process.exitCode = 1;
2052
2119
  return;
@@ -2054,7 +2121,7 @@ async function runGenerateAssets() {
2054
2121
  process.stdout.write(`Generating assets for "${name}"\u2026
2055
2122
  `);
2056
2123
  try {
2057
- const result = await generateAssets2({ name, icon, color, outputDir });
2124
+ const result = await generateAssets2({ name, icon, color, outputDir, ogComponent });
2058
2125
  process.stdout.write(`Generated ${result.files.length} files in ${outputDir}/
2059
2126
  `);
2060
2127
  for (const f of result.files) process.stdout.write(` ${f}
@@ -4,6 +4,12 @@ interface GenerateAssetsConfig {
4
4
  icon: string;
5
5
  color: string;
6
6
  outputDir: string;
7
+ /**
8
+ * Optional path to a rendered component (SVG, PNG, JPG, or WebP) placed bottom-middle
9
+ * in og-image.png. When set, the OG image uses a solid `color` background with a smaller
10
+ * `icon` above the component instead of the default large-logo layout.
11
+ */
12
+ ogComponent?: string;
7
13
  }
8
14
  interface GenerateAssetsResult {
9
15
  files: string[];
@@ -34,6 +34,14 @@ function getSvgInner(svgPath) {
34
34
  const inner = svg.replace(/<svg[^>]*>/, "").replace(/<\/svg>/, "").trim();
35
35
  return { inner, viewBox };
36
36
  }
37
+ function resolveIcon(icon) {
38
+ const mode = detectIconMode(icon);
39
+ if (mode === "svg") {
40
+ const { inner, viewBox } = getSvgInner(icon);
41
+ return { mode, inner, viewBox };
42
+ }
43
+ return { mode, inner: getLucideInner(icon), viewBox: 24 };
44
+ }
37
45
  function buildIconSvgLucide(inner, color, size) {
38
46
  const pad = Math.round(size * 0.18);
39
47
  const area = size - pad * 2;
@@ -92,6 +100,50 @@ function buildOgSvgCustom(inner, viewBox, color) {
92
100
  </g>
93
101
  </svg>`;
94
102
  }
103
+ var OG_WIDTH = 1200;
104
+ var OG_HEIGHT = 630;
105
+ var OG_HEADER = {
106
+ top: 54,
107
+ markSize: 96,
108
+ /** Clear space between the wordmark's baseline and the component. */
109
+ gapBelow: 30
110
+ };
111
+ function buildOgGlowSvg(box) {
112
+ const spread = 40;
113
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="${OG_WIDTH}" height="${OG_HEIGHT}">
114
+ <defs>
115
+ <filter id="glow" x="-40%" y="-40%" width="180%" height="180%">
116
+ <feGaussianBlur stdDeviation="46"/>
117
+ </filter>
118
+ </defs>
119
+ <rect x="${box.left - spread}" y="${box.top - spread}" width="${box.width + spread * 2}" height="${box.height + spread * 2}" rx="48" fill="#ffffff" opacity="0.22" filter="url(#glow)"/>
120
+ </svg>`;
121
+ }
122
+ function buildOgSvgWithSmallLogo(mode, inner, viewBox, color, name) {
123
+ const markSize = OG_HEADER.markSize;
124
+ const gap = 28;
125
+ const fontSize = 56;
126
+ const groupY = OG_HEADER.top;
127
+ const scale = markSize / viewBox;
128
+ const textWidth = Math.round(name.length * fontSize * 0.58);
129
+ const totalWidth = markSize + gap + textWidth;
130
+ const groupX = Math.round((1200 - totalWidth) / 2);
131
+ const logoMarkup = mode === "lucide" ? (() => {
132
+ const sw = (2.5 / scale).toFixed(4);
133
+ return `<g transform="translate(${groupX} ${groupY}) scale(${scale})" stroke="white" fill="none" stroke-width="${sw}" stroke-linecap="round" stroke-linejoin="round">
134
+ ${inner}
135
+ </g>`;
136
+ })() : `<g transform="translate(${groupX} ${groupY}) scale(${scale})">
137
+ ${inner}
138
+ </g>`;
139
+ const textX = groupX + markSize + gap;
140
+ const textY = groupY + markSize / 2;
141
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="${OG_WIDTH}" height="${OG_HEIGHT}">
142
+ <rect width="${OG_WIDTH}" height="${OG_HEIGHT}" fill="${color}"/>
143
+ ${logoMarkup}
144
+ <text x="${textX}" y="${textY}" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="${fontSize}" font-weight="700" fill="white" dominant-baseline="middle" letter-spacing="-1">${escapeXml(name)}</text>
145
+ </svg>`;
146
+ }
95
147
  var FAVICON_SIZES = [16, 32, 48, 64, 192, 512];
96
148
  var NAMED_ICONS = [
97
149
  ["apple-touch-icon.png", 180],
@@ -99,7 +151,7 @@ var NAMED_ICONS = [
99
151
  ["android-chrome-512x512.png", 512]
100
152
  ];
101
153
  async function generateAssets(config) {
102
- const { name, icon, color, outputDir } = config;
154
+ const { name, icon, color, outputDir, ogComponent } = config;
103
155
  let sharpFn;
104
156
  try {
105
157
  sharpFn = (await import('sharp')).default;
@@ -111,17 +163,8 @@ async function generateAssets(config) {
111
163
  mkdirSync(outputDir, { recursive: true });
112
164
  const faviconDir = join(outputDir, "favicon");
113
165
  mkdirSync(faviconDir, { recursive: true });
114
- const mode = detectIconMode(icon);
115
166
  const written = [];
116
- let inner;
117
- let viewBox = 24;
118
- if (mode === "svg") {
119
- const parsed = getSvgInner(icon);
120
- inner = parsed.inner;
121
- viewBox = parsed.viewBox;
122
- } else {
123
- inner = getLucideInner(icon);
124
- }
167
+ const { mode, inner, viewBox } = resolveIcon(icon);
125
168
  const buildIcon = (size) => mode === "svg" ? buildIconSvgCustom(inner, viewBox, color, size) : buildIconSvgLucide(inner, color, size);
126
169
  for (const size of FAVICON_SIZES) {
127
170
  const png = await sharpFn(Buffer.from(buildIcon(size))).png().toBuffer();
@@ -135,8 +178,31 @@ async function generateAssets(config) {
135
178
  writeFileSync(dest, png);
136
179
  written.push(dest);
137
180
  }
138
- const ogSvg = mode === "svg" ? buildOgSvgCustom(inner, viewBox, color) : buildOgSvgLucide(inner, name, color);
139
- const ogPng = await sharpFn(Buffer.from(ogSvg)).png().toBuffer();
181
+ const ogSvg = ogComponent ? buildOgSvgWithSmallLogo(mode, inner, viewBox, color, name) : mode === "svg" ? buildOgSvgCustom(inner, viewBox, color) : buildOgSvgLucide(inner, name, color);
182
+ let ogPng = await sharpFn(Buffer.from(ogSvg)).png().toBuffer();
183
+ if (ogComponent) {
184
+ const sideMargin = 72;
185
+ const maxWidth = OG_WIDTH - sideMargin * 2;
186
+ const headerBottom = OG_HEADER.top + OG_HEADER.markSize + OG_HEADER.gapBelow;
187
+ const componentBuf = readFileSync(resolve(process.cwd(), ogComponent));
188
+ const resized = await sharpFn(componentBuf, { density: 300 }).resize({ width: maxWidth, withoutEnlargement: true }).png().toBuffer();
189
+ const { width: compWidth = maxWidth, height: compHeight = 0 } = await sharpFn(resized).metadata();
190
+ const left = Math.round((OG_WIDTH - compWidth) / 2);
191
+ const fits = headerBottom + compHeight <= OG_HEIGHT;
192
+ const top = fits ? Math.round(headerBottom + (OG_HEIGHT - headerBottom - compHeight) / 2) : headerBottom;
193
+ ogPng = await sharpFn(ogPng).composite([
194
+ // Glow first: it has to sit under the component, and sharp applies
195
+ // composites in array order.
196
+ {
197
+ input: Buffer.from(
198
+ buildOgGlowSvg({ left, top, width: compWidth, height: compHeight })
199
+ ),
200
+ left: 0,
201
+ top: 0
202
+ },
203
+ { input: resized, left, top }
204
+ ]).png().toBuffer();
205
+ }
140
206
  const ogDest = join(outputDir, "og-image.png");
141
207
  writeFileSync(ogDest, ogPng);
142
208
  written.push(ogDest);
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/generate-assets.ts"],"names":[],"mappings":"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","file":"generate-assets.js","sourcesContent":["import { mkdirSync, readFileSync, writeFileSync } from \"node:fs\"\nimport { createRequire } from \"node:module\"\nimport { join, resolve } from \"node:path\"\n\nexport interface GenerateAssetsConfig {\n name: string\n /** Lucide icon name (e.g. \"Zap\") OR path to an SVG file (e.g. \"./logo.svg\") */\n icon: string\n color: string\n outputDir: string\n}\n\nexport interface GenerateAssetsResult {\n files: string[]\n}\n\ntype IconMode = \"lucide\" | \"svg\"\n\nfunction detectIconMode(icon: string): IconMode {\n return icon.endsWith(\".svg\") || icon.includes(\"/\") || icon.includes(\"\\\\\") ? \"svg\" : \"lucide\"\n}\n\nfunction toKebabCase(name: string): string {\n return name\n .replace(/([A-Z])/g, (_, ch: string) => `-${ch.toLowerCase()}`)\n .replace(/^-/, \"\")\n .toLowerCase()\n}\n\nfunction escapeXml(str: string): string {\n return str\n .replace(/&/g, \"&amp;\")\n .replace(/</g, \"&lt;\")\n .replace(/>/g, \"&gt;\")\n .replace(/\"/g, \"&quot;\")\n .replace(/'/g, \"&apos;\")\n}\n\nfunction getLucideInner(iconName: string): string {\n const kebab = toKebabCase(iconName)\n const req = createRequire(import.meta.url)\n let iconPath: string\n try {\n iconPath = req.resolve(`lucide-static/icons/${kebab}.svg`)\n } catch {\n throw new Error(\n `Unknown Lucide icon: \"${iconName}\" (tried \"${kebab}.svg\"). ` +\n `Browse icons at https://lucide.dev/icons`,\n )\n }\n const svg = readFileSync(iconPath, \"utf-8\")\n return svg\n .replace(/<svg[^>]*>/, \"\")\n .replace(/<\\/svg>/, \"\")\n .replace(/\\sstroke=\"currentColor\"/g, \"\")\n .replace(/\\sfill=\"none\"/g, \"\")\n .trim()\n}\n\nfunction getSvgInner(svgPath: string): { inner: string; viewBox: number } {\n const abs = resolve(process.cwd(), svgPath)\n const svg = readFileSync(abs, \"utf-8\")\n const vbMatch = svg.match(/viewBox=\"0 0 (\\d+\\.?\\d*) \\d/)\n const viewBox = vbMatch ? parseFloat(vbMatch[1]) : 24\n const inner = svg\n .replace(/<svg[^>]*>/, \"\")\n .replace(/<\\/svg>/, \"\")\n .trim()\n return { inner, viewBox }\n}\n\nfunction buildIconSvgLucide(inner: string, color: string, size: number): string {\n const pad = Math.round(size * 0.18)\n const area = size - pad * 2\n const scale = area / 24\n const rx = Math.round(size * 0.16)\n const sw = (2 / scale).toFixed(4)\n return `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"${size}\" height=\"${size}\">\n <rect width=\"${size}\" height=\"${size}\" rx=\"${rx}\" fill=\"${color}\"/>\n <g transform=\"translate(${pad} ${pad}) scale(${scale})\" stroke=\"white\" fill=\"none\" stroke-width=\"${sw}\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n ${inner}\n </g>\n</svg>`\n}\n\nfunction buildIconSvgCustom(inner: string, viewBox: number, color: string, size: number): string {\n const pad = Math.round(size * 0.08)\n const area = size - pad * 2\n const scale = area / viewBox\n const rx = Math.round(size * 0.16)\n return `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"${size}\" height=\"${size}\">\n <rect width=\"${size}\" height=\"${size}\" rx=\"${rx}\" fill=\"${color}\"/>\n <g transform=\"translate(${pad} ${pad}) scale(${scale})\">\n ${inner}\n </g>\n</svg>`\n}\n\nfunction buildOgSvgLucide(inner: string, name: string, color: string): string {\n const iconPx = 180\n const iconX = Math.round((1200 - iconPx) / 2)\n const iconY = 160\n const scale = iconPx / 24\n const sw = (2.5 / scale).toFixed(4)\n return `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"1200\" height=\"630\">\n <defs>\n <radialGradient id=\"rg\" cx=\"50%\" cy=\"40%\" r=\"65%\">\n <stop offset=\"0%\" stop-color=\"${color}\" stop-opacity=\"0.22\"/>\n <stop offset=\"100%\" stop-color=\"#000000\" stop-opacity=\"0\"/>\n </radialGradient>\n </defs>\n <rect width=\"1200\" height=\"630\" fill=\"#09090b\"/>\n <rect width=\"1200\" height=\"630\" fill=\"url(#rg)\"/>\n <g transform=\"translate(${iconX} ${iconY}) scale(${scale})\" stroke=\"${color}\" fill=\"none\" stroke-width=\"${sw}\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n ${inner}\n </g>\n <text x=\"600\" y=\"470\" font-family=\"-apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif\" font-size=\"72\" font-weight=\"700\" fill=\"white\" text-anchor=\"middle\" letter-spacing=\"-1\">${escapeXml(name)}</text>\n</svg>`\n}\n\nfunction buildOgSvgCustom(inner: string, viewBox: number, color: string): string {\n const s = 470\n const x = Math.round((1200 - s) / 2)\n const y = Math.round((630 - s) / 2)\n const scale = s / viewBox\n return `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"1200\" height=\"630\">\n <rect width=\"1200\" height=\"630\" fill=\"${color}\"/>\n <g transform=\"translate(${x} ${y}) scale(${scale})\">\n ${inner}\n </g>\n</svg>`\n}\n\nconst FAVICON_SIZES = [16, 32, 48, 64, 192, 512] as const\n\nconst NAMED_ICONS: Array<[string, number]> = [\n [\"apple-touch-icon.png\", 180],\n [\"android-chrome-192x192.png\", 192],\n [\"android-chrome-512x512.png\", 512],\n]\n\nexport async function generateAssets(\n config: GenerateAssetsConfig,\n): Promise<GenerateAssetsResult> {\n const { name, icon, color, outputDir } = config\n\n let sharpFn: typeof import(\"sharp\")\n try {\n sharpFn = (await import(\"sharp\")).default\n } catch {\n throw new Error(\n \"sharp is required for asset generation.\\n\" +\n \"Install it in your project: bun add sharp lucide-static\",\n )\n }\n\n mkdirSync(outputDir, { recursive: true })\n const faviconDir = join(outputDir, \"favicon\")\n mkdirSync(faviconDir, { recursive: true })\n\n const mode = detectIconMode(icon)\n const written: string[] = []\n\n let inner: string\n let viewBox = 24\n\n if (mode === \"svg\") {\n const parsed = getSvgInner(icon)\n inner = parsed.inner\n viewBox = parsed.viewBox\n } else {\n inner = getLucideInner(icon)\n }\n\n const buildIcon = (size: number) =>\n mode === \"svg\"\n ? buildIconSvgCustom(inner, viewBox, color, size)\n : buildIconSvgLucide(inner, color, size)\n\n for (const size of FAVICON_SIZES) {\n const png = await sharpFn(Buffer.from(buildIcon(size))).png().toBuffer()\n const dest = join(faviconDir, `favicon-${size}.png`)\n writeFileSync(dest, png)\n written.push(dest)\n }\n\n for (const [filename, size] of NAMED_ICONS) {\n const png = await sharpFn(Buffer.from(buildIcon(size))).png().toBuffer()\n const dest = join(outputDir, filename)\n writeFileSync(dest, png)\n written.push(dest)\n }\n\n const ogSvg =\n mode === \"svg\"\n ? buildOgSvgCustom(inner, viewBox, color)\n : buildOgSvgLucide(inner, name, color)\n\n const ogPng = await sharpFn(Buffer.from(ogSvg)).png().toBuffer()\n const ogDest = join(outputDir, \"og-image.png\")\n writeFileSync(ogDest, ogPng)\n written.push(ogDest)\n\n const manifest = {\n name,\n short_name: name,\n theme_color: color,\n background_color: mode === \"svg\" ? color : \"#09090b\",\n display: \"standalone\",\n icons: [\n { src: \"/android-chrome-192x192.png\", sizes: \"192x192\", type: \"image/png\" },\n { src: \"/android-chrome-512x512.png\", sizes: \"512x512\", type: \"image/png\", purpose: \"maskable\" },\n ],\n }\n const manifestDest = join(outputDir, \"manifest.json\")\n writeFileSync(manifestDest, JSON.stringify(manifest, null, 2) + \"\\n\")\n written.push(manifestDest)\n\n const robotsDest = join(outputDir, \"robots.txt\")\n writeFileSync(robotsDest, \"User-agent: *\\nAllow: /\\n\")\n written.push(robotsDest)\n\n return { files: written }\n}\n"]}
1
+ {"version":3,"sources":["../src/generate-assets.ts"],"names":[],"mappings":"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","file":"generate-assets.js","sourcesContent":["import { mkdirSync, readFileSync, writeFileSync } from \"node:fs\"\nimport { createRequire } from \"node:module\"\nimport { join, resolve } from \"node:path\"\n\nexport interface GenerateAssetsConfig {\n name: string\n /** Lucide icon name (e.g. \"Zap\") OR path to an SVG file (e.g. \"./logo.svg\") */\n icon: string\n color: string\n outputDir: string\n /**\n * Optional path to a rendered component (SVG, PNG, JPG, or WebP) placed bottom-middle\n * in og-image.png. When set, the OG image uses a solid `color` background with a smaller\n * `icon` above the component instead of the default large-logo layout.\n */\n ogComponent?: string\n}\n\nexport interface GenerateAssetsResult {\n files: string[]\n}\n\ntype IconMode = \"lucide\" | \"svg\"\n\nfunction detectIconMode(icon: string): IconMode {\n return icon.endsWith(\".svg\") || icon.includes(\"/\") || icon.includes(\"\\\\\") ? \"svg\" : \"lucide\"\n}\n\nfunction toKebabCase(name: string): string {\n return name\n .replace(/([A-Z])/g, (_, ch: string) => `-${ch.toLowerCase()}`)\n .replace(/^-/, \"\")\n .toLowerCase()\n}\n\nfunction escapeXml(str: string): string {\n return str\n .replace(/&/g, \"&amp;\")\n .replace(/</g, \"&lt;\")\n .replace(/>/g, \"&gt;\")\n .replace(/\"/g, \"&quot;\")\n .replace(/'/g, \"&apos;\")\n}\n\nfunction getLucideInner(iconName: string): string {\n const kebab = toKebabCase(iconName)\n const req = createRequire(import.meta.url)\n let iconPath: string\n try {\n iconPath = req.resolve(`lucide-static/icons/${kebab}.svg`)\n } catch {\n throw new Error(\n `Unknown Lucide icon: \"${iconName}\" (tried \"${kebab}.svg\"). ` +\n `Browse icons at https://lucide.dev/icons`,\n )\n }\n const svg = readFileSync(iconPath, \"utf-8\")\n return svg\n .replace(/<svg[^>]*>/, \"\")\n .replace(/<\\/svg>/, \"\")\n .replace(/\\sstroke=\"currentColor\"/g, \"\")\n .replace(/\\sfill=\"none\"/g, \"\")\n .trim()\n}\n\nfunction getSvgInner(svgPath: string): { inner: string; viewBox: number } {\n const abs = resolve(process.cwd(), svgPath)\n const svg = readFileSync(abs, \"utf-8\")\n const vbMatch = svg.match(/viewBox=\"0 0 (\\d+\\.?\\d*) \\d/)\n const viewBox = vbMatch ? parseFloat(vbMatch[1]) : 24\n const inner = svg\n .replace(/<svg[^>]*>/, \"\")\n .replace(/<\\/svg>/, \"\")\n .trim()\n return { inner, viewBox }\n}\n\nfunction resolveIcon(icon: string): { mode: IconMode; inner: string; viewBox: number } {\n const mode = detectIconMode(icon)\n if (mode === \"svg\") {\n const { inner, viewBox } = getSvgInner(icon)\n return { mode, inner, viewBox }\n }\n return { mode, inner: getLucideInner(icon), viewBox: 24 }\n}\n\nfunction buildIconSvgLucide(inner: string, color: string, size: number): string {\n const pad = Math.round(size * 0.18)\n const area = size - pad * 2\n const scale = area / 24\n const rx = Math.round(size * 0.16)\n const sw = (2 / scale).toFixed(4)\n return `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"${size}\" height=\"${size}\">\n <rect width=\"${size}\" height=\"${size}\" rx=\"${rx}\" fill=\"${color}\"/>\n <g transform=\"translate(${pad} ${pad}) scale(${scale})\" stroke=\"white\" fill=\"none\" stroke-width=\"${sw}\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n ${inner}\n </g>\n</svg>`\n}\n\nfunction buildIconSvgCustom(inner: string, viewBox: number, color: string, size: number): string {\n const pad = Math.round(size * 0.08)\n const area = size - pad * 2\n const scale = area / viewBox\n const rx = Math.round(size * 0.16)\n return `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"${size}\" height=\"${size}\">\n <rect width=\"${size}\" height=\"${size}\" rx=\"${rx}\" fill=\"${color}\"/>\n <g transform=\"translate(${pad} ${pad}) scale(${scale})\">\n ${inner}\n </g>\n</svg>`\n}\n\nfunction buildOgSvgLucide(inner: string, name: string, color: string): string {\n const iconPx = 180\n const iconX = Math.round((1200 - iconPx) / 2)\n const iconY = 160\n const scale = iconPx / 24\n const sw = (2.5 / scale).toFixed(4)\n return `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"1200\" height=\"630\">\n <defs>\n <radialGradient id=\"rg\" cx=\"50%\" cy=\"40%\" r=\"65%\">\n <stop offset=\"0%\" stop-color=\"${color}\" stop-opacity=\"0.22\"/>\n <stop offset=\"100%\" stop-color=\"#000000\" stop-opacity=\"0\"/>\n </radialGradient>\n </defs>\n <rect width=\"1200\" height=\"630\" fill=\"#09090b\"/>\n <rect width=\"1200\" height=\"630\" fill=\"url(#rg)\"/>\n <g transform=\"translate(${iconX} ${iconY}) scale(${scale})\" stroke=\"${color}\" fill=\"none\" stroke-width=\"${sw}\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n ${inner}\n </g>\n <text x=\"600\" y=\"470\" font-family=\"-apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif\" font-size=\"72\" font-weight=\"700\" fill=\"white\" text-anchor=\"middle\" letter-spacing=\"-1\">${escapeXml(name)}</text>\n</svg>`\n}\n\nfunction buildOgSvgCustom(inner: string, viewBox: number, color: string): string {\n const s = 470\n const x = Math.round((1200 - s) / 2)\n const y = Math.round((630 - s) / 2)\n const scale = s / viewBox\n return `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"1200\" height=\"630\">\n <rect width=\"1200\" height=\"630\" fill=\"${color}\"/>\n <g transform=\"translate(${x} ${y}) scale(${scale})\">\n ${inner}\n </g>\n</svg>`\n}\n\nconst OG_WIDTH = 1200\nconst OG_HEIGHT = 630\n\n/**\n * The wordmark band at the top, and the space left under it for the component.\n *\n * Shared by the SVG that draws the header and the compositing that places the\n * component, so the two cannot disagree about where one ends and the other\n * begins — the failure mode being a component that creeps up under the logo.\n */\nconst OG_HEADER = {\n top: 54,\n markSize: 96,\n /** Clear space between the wordmark's baseline and the component. */\n gapBelow: 30,\n} as const\n\n/**\n * A soft bloom behind the component, so it sits on the brand field rather than\n * on top of it. White at low opacity rather than a tint, so it reads as light\n * on any brand colour instead of as a second colour competing with it.\n */\nfunction buildOgGlowSvg(box: { left: number; top: number; width: number; height: number }): string {\n const spread = 40\n return `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"${OG_WIDTH}\" height=\"${OG_HEIGHT}\">\n <defs>\n <filter id=\"glow\" x=\"-40%\" y=\"-40%\" width=\"180%\" height=\"180%\">\n <feGaussianBlur stdDeviation=\"46\"/>\n </filter>\n </defs>\n <rect x=\"${box.left - spread}\" y=\"${box.top - spread}\" width=\"${box.width + spread * 2}\" height=\"${box.height + spread * 2}\" rx=\"48\" fill=\"#ffffff\" opacity=\"0.22\" filter=\"url(#glow)\"/>\n</svg>`\n}\n\n/**\n * OG base image used when `ogComponent` is set: solid brand-`color` background with a\n * small logo mark + name wordmark centered as a group near the top. The component itself\n * is composited on top afterward, filling the space beneath.\n */\nfunction buildOgSvgWithSmallLogo(\n mode: IconMode,\n inner: string,\n viewBox: number,\n color: string,\n name: string,\n): string {\n const markSize = OG_HEADER.markSize\n const gap = 28\n const fontSize = 56\n const groupY = OG_HEADER.top\n const scale = markSize / viewBox\n\n // No text-measurement API in raw SVG generation, so estimate wordmark width from\n // character count to center the mark+text group as a whole.\n const textWidth = Math.round(name.length * fontSize * 0.58)\n const totalWidth = markSize + gap + textWidth\n const groupX = Math.round((1200 - totalWidth) / 2)\n\n const logoMarkup =\n mode === \"lucide\"\n ? (() => {\n const sw = (2.5 / scale).toFixed(4)\n return `<g transform=\"translate(${groupX} ${groupY}) scale(${scale})\" stroke=\"white\" fill=\"none\" stroke-width=\"${sw}\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n ${inner}\n </g>`\n })()\n : `<g transform=\"translate(${groupX} ${groupY}) scale(${scale})\">\n ${inner}\n </g>`\n\n const textX = groupX + markSize + gap\n const textY = groupY + markSize / 2\n\n return `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"${OG_WIDTH}\" height=\"${OG_HEIGHT}\">\n <rect width=\"${OG_WIDTH}\" height=\"${OG_HEIGHT}\" fill=\"${color}\"/>\n ${logoMarkup}\n <text x=\"${textX}\" y=\"${textY}\" font-family=\"-apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif\" font-size=\"${fontSize}\" font-weight=\"700\" fill=\"white\" dominant-baseline=\"middle\" letter-spacing=\"-1\">${escapeXml(name)}</text>\n</svg>`\n}\n\nconst FAVICON_SIZES = [16, 32, 48, 64, 192, 512] as const\n\nconst NAMED_ICONS: Array<[string, number]> = [\n [\"apple-touch-icon.png\", 180],\n [\"android-chrome-192x192.png\", 192],\n [\"android-chrome-512x512.png\", 512],\n]\n\nexport async function generateAssets(\n config: GenerateAssetsConfig,\n): Promise<GenerateAssetsResult> {\n const { name, icon, color, outputDir, ogComponent } = config\n\n let sharpFn: typeof import(\"sharp\")\n try {\n sharpFn = (await import(\"sharp\")).default\n } catch {\n throw new Error(\n \"sharp is required for asset generation.\\n\" +\n \"Install it in your project: bun add sharp lucide-static\",\n )\n }\n\n mkdirSync(outputDir, { recursive: true })\n const faviconDir = join(outputDir, \"favicon\")\n mkdirSync(faviconDir, { recursive: true })\n\n const written: string[] = []\n\n const { mode, inner, viewBox } = resolveIcon(icon)\n\n const buildIcon = (size: number) =>\n mode === \"svg\"\n ? buildIconSvgCustom(inner, viewBox, color, size)\n : buildIconSvgLucide(inner, color, size)\n\n for (const size of FAVICON_SIZES) {\n const png = await sharpFn(Buffer.from(buildIcon(size))).png().toBuffer()\n const dest = join(faviconDir, `favicon-${size}.png`)\n writeFileSync(dest, png)\n written.push(dest)\n }\n\n for (const [filename, size] of NAMED_ICONS) {\n const png = await sharpFn(Buffer.from(buildIcon(size))).png().toBuffer()\n const dest = join(outputDir, filename)\n writeFileSync(dest, png)\n written.push(dest)\n }\n\n const ogSvg = ogComponent\n ? buildOgSvgWithSmallLogo(mode, inner, viewBox, color, name)\n : mode === \"svg\"\n ? buildOgSvgCustom(inner, viewBox, color)\n : buildOgSvgLucide(inner, name, color)\n\n let ogPng = await sharpFn(Buffer.from(ogSvg)).png().toBuffer()\n\n if (ogComponent) {\n // Scaled to width and allowed to run off the bottom edge, rather than\n // shrunk to fit a box. A screenshot that fits entirely inside the frame\n // reads as a picture of an app; one that continues past the edge reads as\n // the app itself, and the detail stays legible at the size a link preview\n // is actually seen.\n //\n // The previous fixed 880x300 box assumed a wide strip — a composer, a\n // toolbar — and shrank anything squarer to fit its height: a 1.87:1\n // dashboard landed at 561x300, adrift in the middle of a 1200px canvas.\n const sideMargin = 72\n const maxWidth = OG_WIDTH - sideMargin * 2\n const headerBottom = OG_HEADER.top + OG_HEADER.markSize + OG_HEADER.gapBelow\n\n const componentBuf = readFileSync(resolve(process.cwd(), ogComponent))\n const resized = await sharpFn(componentBuf, { density: 300 })\n .resize({ width: maxWidth, withoutEnlargement: true })\n .png()\n .toBuffer()\n const { width: compWidth = maxWidth, height: compHeight = 0 } =\n await sharpFn(resized).metadata()\n\n const left = Math.round((OG_WIDTH - compWidth) / 2)\n // A component short enough to fit sits centred in the space under the\n // header; a tall one starts there and bleeds off the bottom. Without this\n // branch a wide strip would cling to the header with a gulf beneath it.\n const fits = headerBottom + compHeight <= OG_HEIGHT\n const top = fits\n ? Math.round(headerBottom + (OG_HEIGHT - headerBottom - compHeight) / 2)\n : headerBottom\n\n ogPng = await sharpFn(ogPng)\n .composite([\n // Glow first: it has to sit under the component, and sharp applies\n // composites in array order.\n {\n input: Buffer.from(\n buildOgGlowSvg({ left, top, width: compWidth, height: compHeight }),\n ),\n left: 0,\n top: 0,\n },\n { input: resized, left, top },\n ])\n .png()\n .toBuffer()\n }\n\n const ogDest = join(outputDir, \"og-image.png\")\n writeFileSync(ogDest, ogPng)\n written.push(ogDest)\n\n const manifest = {\n name,\n short_name: name,\n theme_color: color,\n background_color: mode === \"svg\" ? color : \"#09090b\",\n display: \"standalone\",\n icons: [\n { src: \"/android-chrome-192x192.png\", sizes: \"192x192\", type: \"image/png\" },\n { src: \"/android-chrome-512x512.png\", sizes: \"512x512\", type: \"image/png\", purpose: \"maskable\" },\n ],\n }\n const manifestDest = join(outputDir, \"manifest.json\")\n writeFileSync(manifestDest, JSON.stringify(manifest, null, 2) + \"\\n\")\n written.push(manifestDest)\n\n const robotsDest = join(outputDir, \"robots.txt\")\n writeFileSync(robotsDest, \"User-agent: *\\nAllow: /\\n\")\n written.push(robotsDest)\n\n return { files: written }\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@olwiba/dx",
3
- "version": "0.0.22",
3
+ "version": "0.0.23",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "sideEffects": false,