mirror of
https://github.com/zenlm/logo.git
synced 2026-07-26 22:08:56 +00:00
feat: scaffold @zenlm/logo — animated ensō mark, brand-neutral SVG + React, getAnimatedSVG()
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
name: Publish on Tag
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*' # Match version tags (e.g., v1.0.4)
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Run Tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Build package
|
||||
run: npm run build
|
||||
|
||||
- name: Run tests
|
||||
run: npm test
|
||||
continue-on-error: true # Don't fail if no tests configured yet
|
||||
|
||||
publish:
|
||||
name: Publish to NPM
|
||||
needs: test
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'npm'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Build package
|
||||
run: npm run build
|
||||
|
||||
- name: Configure npm authentication
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }}
|
||||
run: |
|
||||
npm config set //registry.npmjs.org/:_authToken $NODE_AUTH_TOKEN
|
||||
npm whoami
|
||||
|
||||
- name: Check and publish package
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }}
|
||||
run: |
|
||||
CURRENT_VERSION=$(node -p "require('./package.json').version")
|
||||
PACKAGE_NAME=$(node -p "require('./package.json').name")
|
||||
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo "📦 Checking $PACKAGE_NAME@$CURRENT_VERSION"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
|
||||
# Check if this version already exists on npm
|
||||
if npm view "$PACKAGE_NAME@$CURRENT_VERSION" version 2>/dev/null; then
|
||||
echo "⏭️ Already published - skipping"
|
||||
echo "ALREADY_PUBLISHED=true" >> $GITHUB_ENV
|
||||
else
|
||||
echo "🚀 Publishing to npm..."
|
||||
npm publish --access public
|
||||
echo "✅ Successfully published $PACKAGE_NAME@$CURRENT_VERSION"
|
||||
echo "PUBLISHED=true" >> $GITHUB_ENV
|
||||
echo "PACKAGE_VERSION=$CURRENT_VERSION" >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
- name: Create GitHub Release
|
||||
if: env.PUBLISHED == 'true'
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
generate_release_notes: true
|
||||
body: |
|
||||
## 📦 NPM Package Published
|
||||
|
||||
- @zenlm/logo@${{ env.PACKAGE_VERSION }}
|
||||
|
||||
### Installation
|
||||
|
||||
```bash
|
||||
npm install @zenlm/logo@${{ env.PACKAGE_VERSION }}
|
||||
```
|
||||
|
||||
### Changes
|
||||
|
||||
See [CHANGELOG.md](https://github.com/zenlm/logo/blob/main/CHANGELOG.md) for details.
|
||||
files: |
|
||||
CHANGELOG.md
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -0,0 +1,82 @@
|
||||
{
|
||||
"name": "@zenlm/logo",
|
||||
"version": "0.1.0",
|
||||
"description": "Official Zen ensō logo — animated, brand-neutral SVG",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"module": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"files": [
|
||||
"dist",
|
||||
"README.md",
|
||||
"svg"
|
||||
],
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.js",
|
||||
"require": "./dist/index.js"
|
||||
},
|
||||
"./logos": {
|
||||
"types": "./dist/logos.d.ts",
|
||||
"import": "./dist/logos.js",
|
||||
"require": "./dist/logos.js"
|
||||
},
|
||||
"./react": {
|
||||
"types": "./dist/react.d.ts",
|
||||
"import": "./dist/react.js",
|
||||
"require": "./dist/react.js"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "npm run clean && npm run compile && npm run build:cli",
|
||||
"build:cli": "tsc src/build.ts --outDir dist --module esnext --target es2020 --moduleResolution node --esModuleInterop --skipLibCheck",
|
||||
"compile": "tsc",
|
||||
"clean": "rm -rf dist",
|
||||
"dev": "tsc --watch",
|
||||
"generate": "node dist/build.js",
|
||||
"preview": "npm run generate && open menu-bar-preview.html",
|
||||
"lint": "eslint src --ext .ts,.tsx",
|
||||
"format": "prettier --write 'src/**/*.{ts,tsx,json}'",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"test": "npm run typecheck && npm run build",
|
||||
"watch": "tsc --watch",
|
||||
"prepublishOnly": "npm run build"
|
||||
},
|
||||
"keywords": [
|
||||
"zen",
|
||||
"enso",
|
||||
"logo",
|
||||
"icon",
|
||||
"favicon",
|
||||
"react",
|
||||
"typescript"
|
||||
],
|
||||
"author": "Zen LM",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"sharp": "^0.34.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/minimatch": "^5.1.2",
|
||||
"@types/node": "^20.19.17",
|
||||
"@types/react": "^18.3.24",
|
||||
"react": "^18.3.1",
|
||||
"typescript": "^5.9.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react": {
|
||||
"optional": true
|
||||
}
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/zenlm/logo.git"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
+246
@@ -0,0 +1,246 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Zen Logo Build Script
|
||||
* Generates all required icons for the Zen ecosystem
|
||||
*/
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import sharp from 'sharp';
|
||||
import { getColorSVG, getMonoSVG, getMenuBarSVG, getFaviconSVG, getWhiteSVG } from './logos.js';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
interface IconConfig {
|
||||
name: string;
|
||||
size: number;
|
||||
svg?: string;
|
||||
addBackground?: boolean;
|
||||
bgColor?: string;
|
||||
cornerRadius?: number;
|
||||
aspectRatio?: { width: number; height: number };
|
||||
}
|
||||
|
||||
async function generateIcon(
|
||||
svgString: string,
|
||||
outputPath: string,
|
||||
size: number,
|
||||
options: {
|
||||
addBackground?: boolean;
|
||||
bgColor?: string;
|
||||
cornerRadius?: number;
|
||||
aspectRatio?: { width: number; height: number };
|
||||
} = {}
|
||||
): Promise<void> {
|
||||
const dir = path.dirname(outputPath);
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
|
||||
const { addBackground = false, bgColor = 'black', cornerRadius, aspectRatio } = options;
|
||||
|
||||
if (aspectRatio) {
|
||||
const { width, height } = aspectRatio;
|
||||
const logoSize = Math.min(width, height) * 0.4;
|
||||
const logoX = Math.floor((width - logoSize) / 2);
|
||||
const logoY = Math.floor((height - logoSize) / 2);
|
||||
const radius = cornerRadius ?? 0;
|
||||
|
||||
const bgSvg = `<svg width="${width}" height="${height}" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="0" y="0" width="${width}" height="${height}" rx="${radius}" ry="${radius}" fill="${bgColor}"/>
|
||||
</svg>`;
|
||||
|
||||
const bg = await sharp(Buffer.from(bgSvg)).png().toBuffer();
|
||||
const logo = await sharp(Buffer.from(svgString))
|
||||
.resize(Math.floor(logoSize), Math.floor(logoSize))
|
||||
.png()
|
||||
.toBuffer();
|
||||
|
||||
await sharp(bg)
|
||||
.composite([{ input: logo, top: logoY, left: logoX }])
|
||||
.toFile(outputPath);
|
||||
} else if (addBackground) {
|
||||
const logoSize = Math.floor(size * 0.65);
|
||||
const padding = Math.floor((size - logoSize) / 2);
|
||||
const radius = cornerRadius ?? Math.floor(size * 0.22);
|
||||
|
||||
const bgSvg = `<svg width="${size}" height="${size}" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="0" y="0" width="${size}" height="${size}" rx="${radius}" ry="${radius}" fill="${bgColor}"/>
|
||||
</svg>`;
|
||||
|
||||
const bg = await sharp(Buffer.from(bgSvg)).png().toBuffer();
|
||||
const logo = await sharp(Buffer.from(svgString))
|
||||
.resize(logoSize, logoSize)
|
||||
.png()
|
||||
.toBuffer();
|
||||
|
||||
await sharp(bg)
|
||||
.composite([{ input: logo, top: padding, left: padding }])
|
||||
.toFile(outputPath);
|
||||
} else {
|
||||
await sharp(Buffer.from(svgString))
|
||||
.resize(size, size)
|
||||
.png()
|
||||
.toFile(outputPath);
|
||||
}
|
||||
console.log(`✓ ${path.relative(process.cwd(), outputPath)} (${options.aspectRatio ? `${options.aspectRatio.width}×${options.aspectRatio.height}` : `${size}×${size}`})`);
|
||||
}
|
||||
|
||||
async function buildAll(): Promise<void> {
|
||||
console.log('🎨 Zen Logo Builder\n');
|
||||
|
||||
const colorSVG = getColorSVG();
|
||||
const monoSVG = getMonoSVG();
|
||||
const menuBarSVG = getMenuBarSVG();
|
||||
const faviconSVG = getFaviconSVG();
|
||||
const whiteSVG = getWhiteSVG();
|
||||
|
||||
const dirs = ['dist', 'dist/icons', 'dist/favicon', 'dist/og', 'dist/apple', 'dist/dock', 'dist/menubar'];
|
||||
for (const dir of dirs) {
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
// Save SVG sources
|
||||
console.log('📁 SVG Sources:');
|
||||
fs.writeFileSync('dist/zen-logo.svg', colorSVG);
|
||||
fs.writeFileSync('dist/zen-logo-mono.svg', monoSVG);
|
||||
fs.writeFileSync('dist/zen-logo-white.svg', whiteSVG);
|
||||
fs.writeFileSync('dist/zen-logo-menubar.svg', menuBarSVG);
|
||||
fs.writeFileSync('dist/zen-favicon.svg', faviconSVG);
|
||||
console.log('✓ Generated 5 SVG sources\n');
|
||||
|
||||
// Standard Icons
|
||||
console.log('📁 Standard Icons (dist/icons/):');
|
||||
for (const size of [16, 32, 64, 128, 256, 512, 1024]) {
|
||||
await generateIcon(colorSVG, `dist/icons/logo-${size}.png`, size);
|
||||
}
|
||||
for (const size of [16, 32, 64, 128]) {
|
||||
await generateIcon(monoSVG, `dist/icons/logo-mono-${size}.png`, size);
|
||||
}
|
||||
|
||||
// Favicons
|
||||
console.log('\n📁 Favicons (dist/favicon/):');
|
||||
for (const size of [16, 32, 48, 64, 96, 128, 192, 256, 512]) {
|
||||
await generateIcon(faviconSVG, `dist/favicon/favicon-${size}.png`, size);
|
||||
}
|
||||
|
||||
// Apple Touch Icons
|
||||
console.log('\n📁 Apple Touch Icons (dist/apple/):');
|
||||
for (const size of [57, 60, 72, 76, 114, 120, 144, 152, 167, 180]) {
|
||||
await generateIcon(colorSVG, `dist/apple/apple-touch-icon-${size}.png`, size, {
|
||||
addBackground: true, bgColor: '#000000', cornerRadius: Math.floor(size * 0.156)
|
||||
});
|
||||
}
|
||||
|
||||
// OG Graph Images
|
||||
console.log('\n📁 Open Graph Images (dist/og/):');
|
||||
await generateIcon(whiteSVG, 'dist/og/og-image.png', 1200, { aspectRatio: { width: 1200, height: 630 }, bgColor: '#000000' });
|
||||
await generateIcon(whiteSVG, 'dist/og/twitter-card.png', 1200, { aspectRatio: { width: 1200, height: 600 }, bgColor: '#000000' });
|
||||
await generateIcon(whiteSVG, 'dist/og/og-square.png', 1200, { aspectRatio: { width: 1200, height: 1200 }, bgColor: '#000000' });
|
||||
|
||||
// Dock Icons
|
||||
console.log('\n📁 Dock Icons (dist/dock/):');
|
||||
for (const size of [64, 128, 256, 512, 1024]) {
|
||||
await generateIcon(whiteSVG, `dist/dock/dock-${size}.png`, size, { addBackground: true, bgColor: '#000000' });
|
||||
}
|
||||
for (const base of [128, 256, 512]) {
|
||||
await generateIcon(whiteSVG, `dist/dock/dock-${base}@2x.png`, base * 2, { addBackground: true, bgColor: '#000000' });
|
||||
}
|
||||
|
||||
// Menu Bar Icons
|
||||
console.log('\n📁 Menu Bar Icons (dist/menubar/):');
|
||||
await generateIcon(monoSVG, 'dist/menubar/menubar-16.png', 16);
|
||||
await generateIcon(monoSVG, 'dist/menubar/menubar-16@2x.png', 32);
|
||||
await generateIcon(monoSVG, 'dist/menubar/menubar-16@3x.png', 48);
|
||||
await generateIcon(monoSVG, 'dist/menubar/menubar-22.png', 22);
|
||||
await generateIcon(monoSVG, 'dist/menubar/menubar-22@2x.png', 44);
|
||||
await generateIcon(menuBarSVG, 'dist/menubar/iconTemplate.png', 16);
|
||||
await generateIcon(menuBarSVG, 'dist/menubar/iconTemplate@2x.png', 32);
|
||||
await generateIcon(menuBarSVG, 'dist/menubar/iconTemplate@3x.png', 48);
|
||||
|
||||
// Generate showcase
|
||||
console.log('\n📄 Generating showcase...');
|
||||
generateShowcase();
|
||||
|
||||
console.log('\n✅ Build complete!');
|
||||
console.log(' Open dist/showcase.html in browser to verify all assets\n');
|
||||
}
|
||||
|
||||
function generateShowcase(): void {
|
||||
const html = `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Zen Logo Assets Showcase</title>
|
||||
<style>
|
||||
:root { --bg-dark: #111; --bg-light: #f5f5f5; --grid-bg: repeating-conic-gradient(#ccc 0% 25%, #fff 0% 50%) 50% / 20px 20px; }
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #1a1a1a; color: #fff; padding: 40px; line-height: 1.6; }
|
||||
h1 { margin-bottom: 10px; font-size: 2rem; }
|
||||
.subtitle { color: #888; margin-bottom: 40px; }
|
||||
h2 { margin: 40px 0 20px; padding-bottom: 10px; border-bottom: 1px solid #333; }
|
||||
.section { margin-bottom: 60px; }
|
||||
.grid { display: flex; flex-wrap: wrap; gap: 20px; align-items: flex-end; }
|
||||
.icon-item { display: flex; flex-direction: column; align-items: center; gap: 10px; }
|
||||
.icon-box { display: flex; align-items: center; justify-content: center; background: var(--grid-bg); border-radius: 8px; padding: 10px; }
|
||||
.icon-box.dark { background: var(--bg-dark); }
|
||||
.icon-box.light { background: var(--bg-light); }
|
||||
.icon-box img { display: block; max-width: 100%; height: auto; }
|
||||
.label { font-size: 12px; color: #888; text-align: center; }
|
||||
.og-preview { max-width: 600px; border-radius: 8px; overflow: hidden; box-shadow: 0 4px 20px rgba(0,0,0,0.5); }
|
||||
.og-preview img { width: 100%; height: auto; display: block; }
|
||||
.dock-demo { background: rgba(255,255,255,0.1); backdrop-filter: blur(20px); padding: 8px; border-radius: 20px; display: inline-flex; gap: 8px; align-items: flex-end; }
|
||||
.dock-demo img { border-radius: 22%; }
|
||||
.timestamp { position: fixed; bottom: 20px; right: 20px; font-size: 12px; color: #666; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>◯ Zen Logo Assets</h1>
|
||||
<p class="subtitle">Generated: ${new Date().toISOString()}</p>
|
||||
<section class="section"><h2>📁 SVG Sources</h2><div class="grid">
|
||||
<div class="icon-item"><div class="icon-box dark" style="width:120px;height:120px;"><img src="zen-logo.svg" style="width:100px;"></div><span class="label">zen-logo.svg</span></div>
|
||||
<div class="icon-item"><div class="icon-box light" style="width:120px;height:120px;"><img src="zen-logo-mono.svg" style="width:100px;"></div><span class="label">zen-logo-mono.svg</span></div>
|
||||
<div class="icon-item"><div class="icon-box" style="width:120px;height:120px;"><img src="zen-favicon.svg" style="width:100px;"></div><span class="label">zen-favicon.svg</span></div>
|
||||
</div></section>
|
||||
<section class="section"><h2>🖼️ Standard Icons</h2><div class="grid">
|
||||
<div class="icon-item"><div class="icon-box dark"><img src="icons/logo-32.png"></div><span class="label">32px</span></div>
|
||||
<div class="icon-item"><div class="icon-box dark"><img src="icons/logo-64.png"></div><span class="label">64px</span></div>
|
||||
<div class="icon-item"><div class="icon-box dark"><img src="icons/logo-128.png"></div><span class="label">128px</span></div>
|
||||
<div class="icon-item"><div class="icon-box dark"><img src="icons/logo-256.png"></div><span class="label">256px</span></div>
|
||||
</div></section>
|
||||
<section class="section"><h2>⭐ Favicons</h2><div class="grid">
|
||||
<div class="icon-item"><div class="icon-box"><img src="favicon/favicon-16.png"></div><span class="label">16px</span></div>
|
||||
<div class="icon-item"><div class="icon-box"><img src="favicon/favicon-32.png"></div><span class="label">32px</span></div>
|
||||
<div class="icon-item"><div class="icon-box"><img src="favicon/favicon-48.png"></div><span class="label">48px</span></div>
|
||||
<div class="icon-item"><div class="icon-box"><img src="favicon/favicon-192.png"></div><span class="label">192px</span></div>
|
||||
</div></section>
|
||||
<section class="section"><h2>🍎 Apple Touch Icons</h2><div class="grid">
|
||||
<div class="icon-item"><div class="icon-box"><img src="apple/apple-touch-icon-60.png"></div><span class="label">60px</span></div>
|
||||
<div class="icon-item"><div class="icon-box"><img src="apple/apple-touch-icon-120.png"></div><span class="label">120px</span></div>
|
||||
<div class="icon-item"><div class="icon-box"><img src="apple/apple-touch-icon-180.png"></div><span class="label">180px</span></div>
|
||||
</div></section>
|
||||
<section class="section"><h2>🖥️ Dock Icons</h2>
|
||||
<div class="dock-demo"><img src="dock/dock-64.png" style="width:48px;"><img src="dock/dock-128.png" style="width:64px;"></div>
|
||||
<div class="grid" style="margin-top:20px;">
|
||||
<div class="icon-item"><div class="icon-box"><img src="dock/dock-128.png"></div><span class="label">128px</span></div>
|
||||
<div class="icon-item"><div class="icon-box"><img src="dock/dock-256.png"></div><span class="label">256px</span></div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="section"><h2>📣 Open Graph Images</h2>
|
||||
<p style="margin-bottom:10px;font-size:14px;">OG Image (1200×630)</p>
|
||||
<div class="og-preview"><img src="og/og-image.png"></div>
|
||||
</section>
|
||||
<div class="timestamp">Last generated: ${new Date().toLocaleString()}</div>
|
||||
</body>
|
||||
</html>`;
|
||||
fs.writeFileSync('dist/showcase.html', html);
|
||||
console.log('✓ dist/showcase.html');
|
||||
}
|
||||
|
||||
await buildAll().catch(console.error);
|
||||
@@ -0,0 +1,65 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import sharp from 'sharp';
|
||||
import { getColorSVG, getMonoSVG } from './logos';
|
||||
|
||||
/**
|
||||
* Generate a PNG icon from SVG
|
||||
*/
|
||||
export async function generateIcon(
|
||||
svgString: string,
|
||||
outputPath: string,
|
||||
size: number
|
||||
): Promise<void> {
|
||||
const dir = path.dirname(outputPath);
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
|
||||
await sharp(Buffer.from(svgString))
|
||||
.resize(size, size)
|
||||
.png()
|
||||
.toFile(outputPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate all standard icons
|
||||
*/
|
||||
export async function generateAllIcons(outputDir: string = 'dist/icons'): Promise<void> {
|
||||
const colorSVG = getColorSVG();
|
||||
const monoSVG = getMonoSVG();
|
||||
|
||||
// Standard sizes
|
||||
const sizes = [16, 32, 64, 128, 256, 512, 1024];
|
||||
|
||||
console.log('Generating icons...');
|
||||
|
||||
for (const size of sizes) {
|
||||
await generateIcon(colorSVG, path.join(outputDir, `zen-${size}.png`), size);
|
||||
await generateIcon(monoSVG, path.join(outputDir, `zen-mono-${size}.png`), size);
|
||||
}
|
||||
|
||||
// Generate @2x versions for macOS
|
||||
const macSizes = [
|
||||
{ base: 16, scale: 2 },
|
||||
{ base: 32, scale: 2 },
|
||||
{ base: 128, scale: 2 },
|
||||
{ base: 256, scale: 2 },
|
||||
{ base: 512, scale: 2 }
|
||||
];
|
||||
|
||||
for (const { base, scale } of macSizes) {
|
||||
await generateIcon(
|
||||
colorSVG,
|
||||
path.join(outputDir, `zen-${base}@${scale}x.png`),
|
||||
base * scale
|
||||
);
|
||||
}
|
||||
|
||||
// Menu bar templates
|
||||
await generateIcon(monoSVG, path.join(outputDir, 'iconTemplate.png'), 16);
|
||||
await generateIcon(monoSVG, path.join(outputDir, 'iconTemplate@2x.png'), 32);
|
||||
await generateIcon(monoSVG, path.join(outputDir, 'iconTemplate@3x.png'), 48);
|
||||
|
||||
console.log('✅ Icons generated successfully!');
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* @zenlm/logo - Official Zen Logo Package
|
||||
*
|
||||
* Provides Zen logos in various formats for use in applications
|
||||
*/
|
||||
|
||||
export * from './logos.js';
|
||||
export * from './types.js';
|
||||
export * from './react.js';
|
||||
+150
@@ -0,0 +1,150 @@
|
||||
import type { LogoSettings, LogoOptions } from './types.js';
|
||||
import { ANIMATED_SVG } from './animated.js';
|
||||
|
||||
// Zen logo: ◯ ensō — a brush-drawn circle with a deliberate gap.
|
||||
// This is the brand-neutral mark. White-label products use their own brand override.
|
||||
// When no brand is configured, the ensō shows — intentional, clean, not broken.
|
||||
|
||||
export const LOGO_SETTINGS: LogoSettings = {
|
||||
color: {
|
||||
viewBox: '0 0 100 100',
|
||||
width: 100,
|
||||
height: 100
|
||||
},
|
||||
mono: {
|
||||
strokeWidth: 11
|
||||
}
|
||||
};
|
||||
|
||||
// ◯ — the single ensō arc, drawn as a stroked open circle with a gap
|
||||
const ENSO = 'M66.22 83.26 A37 37 0 1 1 85.57 60.20';
|
||||
|
||||
/** Color SVG logo (white ensō stroke on transparent) */
|
||||
export function getColorSVG(): string {
|
||||
return `<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="${ENSO}" fill="none" stroke="#ffffff" stroke-width="11" stroke-linecap="round"/>
|
||||
</svg>`;
|
||||
}
|
||||
|
||||
/** Monochrome SVG logo (ensō in currentColor) */
|
||||
export function getMonoSVG(): string {
|
||||
return `<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="${ENSO}" fill="none" stroke="currentColor" stroke-width="11" stroke-linecap="round"/>
|
||||
</svg>`;
|
||||
}
|
||||
|
||||
/** Tightly cropped color SVG */
|
||||
export function getColorSVGCropped(): string {
|
||||
return getColorSVG();
|
||||
}
|
||||
|
||||
/** White SVG logo (for dark backgrounds) */
|
||||
export function getWhiteSVG(): string {
|
||||
return `<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="${ENSO}" fill="none" stroke="#ffffff" stroke-width="11" stroke-linecap="round"/>
|
||||
</svg>`;
|
||||
}
|
||||
|
||||
/** Menu bar SVG (uses currentColor for OS theme) */
|
||||
export function getMenuBarSVG(): string {
|
||||
return `<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="${ENSO}" fill="none" stroke="currentColor" stroke-width="11" stroke-linecap="round"/>
|
||||
</svg>`;
|
||||
}
|
||||
|
||||
/** Favicon SVG (white ensō on black rounded rect) */
|
||||
export function getFaviconSVG(): string {
|
||||
return `<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="100" height="100" rx="12" fill="#000000"/>
|
||||
<path d="${ENSO}" fill="none" stroke="#ffffff" stroke-width="11" stroke-linecap="round"/>
|
||||
</svg>`;
|
||||
}
|
||||
|
||||
/** Get logo as data URL */
|
||||
export function getLogoDataUrl(options: LogoOptions = {}): string {
|
||||
const svg = getLogo({ ...options, format: 'svg' });
|
||||
const base64 = btoa(unescape(encodeURIComponent(svg)));
|
||||
return `data:image/svg+xml;base64,${base64}`;
|
||||
}
|
||||
|
||||
/** Get logo as base64 string */
|
||||
export function getLogoBase64(options: LogoOptions = {}): string {
|
||||
const svg = getLogo({ ...options, format: 'svg' });
|
||||
return btoa(unescape(encodeURIComponent(svg)));
|
||||
}
|
||||
|
||||
/** Get logo in requested format */
|
||||
export function getLogo(options: LogoOptions = {}): string {
|
||||
const { variant = 'color', format = 'svg' } = options;
|
||||
if (variant === 'animated') {
|
||||
if (format === 'dataUrl') return getAnimatedDataUrl();
|
||||
if (format === 'base64') return btoa(unescape(encodeURIComponent(getAnimatedSVG())));
|
||||
return getAnimatedSVG();
|
||||
}
|
||||
|
||||
if (format === 'dataUrl') return getLogoDataUrl({ variant });
|
||||
if (format === 'base64') return getLogoBase64({ variant });
|
||||
|
||||
switch (variant) {
|
||||
case 'mono': return getMonoSVG();
|
||||
case 'white': return getWhiteSVG();
|
||||
default: return getColorSVG();
|
||||
}
|
||||
}
|
||||
|
||||
/** Wordmark SVG — Zen's wordmark is the ensō mark itself (white) */
|
||||
export function getWordmarkSVG(): string {
|
||||
return getWhiteSVG();
|
||||
}
|
||||
|
||||
/** Wordmark with metal gradient stroke */
|
||||
export function getWordmarkGradientSVG(): string {
|
||||
return `<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<linearGradient id="metalGradient" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#666666"/>
|
||||
<stop offset="30%" style="stop-color:#ffffff"/>
|
||||
<stop offset="70%" style="stop-color:#ffffff"/>
|
||||
<stop offset="100%" style="stop-color:#888888"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path d="${ENSO}" fill="none" stroke="url(#metalGradient)" stroke-width="11" stroke-linecap="round"/>
|
||||
</svg>`;
|
||||
}
|
||||
|
||||
// Pre-generated exports
|
||||
export const logo = getColorSVG();
|
||||
export const logoMono = getMonoSVG();
|
||||
export const logoWhite = getWhiteSVG();
|
||||
export const wordmark = getWordmarkSVG();
|
||||
export const wordmarkGradient = getWordmarkGradientSVG();
|
||||
export const logoDataUrl = getLogoDataUrl();
|
||||
export const logoMonoDataUrl = getLogoDataUrl({ variant: 'mono' });
|
||||
export const logoWhiteDataUrl = getLogoDataUrl({ variant: 'white' });
|
||||
|
||||
// Brand aliases
|
||||
export const zenLogo = logo;
|
||||
export const zenLogoMono = logoMono;
|
||||
export const zenLogoWhite = logoWhite;
|
||||
export const zenWordmark = wordmark;
|
||||
export const zenWordmarkGradient = wordmarkGradient;
|
||||
export const zenLogoDataUrl = logoDataUrl;
|
||||
export const zenLogoMonoDataUrl = logoMonoDataUrl;
|
||||
export const zenLogoWhiteDataUrl = logoWhiteDataUrl;
|
||||
|
||||
/**
|
||||
* Interactive animated mark — load (assemble) → hover (one graceful turn,
|
||||
* returns to rest) → press (squash). Pure CSS, no JS. Inline this SVG into
|
||||
* the DOM (not <img>) for the hover/press interactions to fire.
|
||||
*/
|
||||
export function getAnimatedSVG(): string {
|
||||
return ANIMATED_SVG;
|
||||
}
|
||||
|
||||
/** Animated mark as a data: URL (load animation runs even in <img>). */
|
||||
export function getAnimatedDataUrl(): string {
|
||||
return 'data:image/svg+xml;base64,' + btoa(unescape(encodeURIComponent(ANIMATED_SVG)));
|
||||
}
|
||||
|
||||
export const zenLogoAnimated = ANIMATED_SVG;
|
||||
export const zenLogoAnimatedDataUrl = getAnimatedDataUrl();
|
||||
@@ -0,0 +1,71 @@
|
||||
import React from 'react';
|
||||
import { getColorSVG, getMonoSVG, getWhiteSVG, getAnimatedSVG } from './logos.js';
|
||||
import type { LogoVariant } from './types.js';
|
||||
|
||||
export interface LogoProps {
|
||||
variant?: LogoVariant;
|
||||
size?: number | string;
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
|
||||
/**
|
||||
* ◯ Zen ensō Logo component
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* import { ZenLogo } from '@zenlm/logo';
|
||||
*
|
||||
* <ZenLogo size={64} />
|
||||
* <ZenLogo variant="mono" size="2rem" />
|
||||
* <ZenLogo variant="white" className="w-16 h-16" />
|
||||
* <ZenLogo variant="animated" size={96} />
|
||||
* ```
|
||||
*/
|
||||
export const ZenLogo: React.FC<LogoProps> = ({
|
||||
variant = 'color',
|
||||
size = 64,
|
||||
className,
|
||||
style
|
||||
}) => {
|
||||
let svg = '';
|
||||
switch (variant) {
|
||||
case 'mono': svg = getMonoSVG(); break;
|
||||
case 'white': svg = getWhiteSVG(); break;
|
||||
case 'animated': svg = getAnimatedSVG(); break;
|
||||
default: svg = getColorSVG();
|
||||
}
|
||||
|
||||
const dim = typeof size === 'number' ? { width: size, height: size } : { width: size, height: size };
|
||||
|
||||
return (
|
||||
<div
|
||||
className={className}
|
||||
style={{ ...dim, ...style }}
|
||||
dangerouslySetInnerHTML={{ __html: svg }}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Favicon component for <head>
|
||||
*/
|
||||
export const Favicon: React.FC = () => {
|
||||
const svg = getColorSVG();
|
||||
const dataUrl = `data:image/svg+xml,${encodeURIComponent(svg)}`;
|
||||
return (
|
||||
<>
|
||||
<link rel="icon" type="image/svg+xml" href={dataUrl} />
|
||||
<link rel="apple-touch-icon" href={dataUrl} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
// Aliases
|
||||
export { ZenLogo as Logo, ZenLogo as ZenlmLogo };
|
||||
export { Favicon as ZenFavicon, Favicon as ZenlmFavicon };
|
||||
|
||||
/** Wordmark = white ensō mark */
|
||||
export const Wordmark: React.FC<Omit<LogoProps, 'variant'>> = (props) => (
|
||||
<ZenLogo {...props} variant="white" />
|
||||
);
|
||||
@@ -0,0 +1,19 @@
|
||||
export interface LogoSettings {
|
||||
color: {
|
||||
viewBox: string;
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
mono: {
|
||||
strokeWidth: number;
|
||||
};
|
||||
}
|
||||
|
||||
export type LogoVariant = 'color' | 'mono' | 'white' | 'animated';
|
||||
export type LogoFormat = 'svg' | 'dataUrl' | 'base64';
|
||||
|
||||
export interface LogoOptions {
|
||||
variant?: LogoVariant;
|
||||
format?: LogoFormat;
|
||||
size?: number;
|
||||
}
|
||||
+17
-12
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 875 B After Width: | Height: | Size: 13 KiB |
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "ESNext",
|
||||
"lib": ["ES2020", "DOM"],
|
||||
"jsx": "react-jsx",
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist", "build.js"]
|
||||
}
|
||||
Reference in New Issue
Block a user