feat: scaffold @zenlm/logo — animated ensō mark, brand-neutral SVG + React, getAnimatedSVG()

This commit is contained in:
hanzo-dev
2026-06-28 19:34:08 -07:00
parent d4501597de
commit 4412cdfd81
11 changed files with 790 additions and 12 deletions
+106
View File
@@ -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 }}
+82
View File
@@ -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"
}
}
+3
View File
File diff suppressed because one or more lines are too long
+246
View File
@@ -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);
+65
View File
@@ -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!');
}
+9
View File
@@ -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
View File
@@ -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();
+71
View File
@@ -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" />
);
+19
View File
@@ -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
View File
File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 875 B

After

Width:  |  Height:  |  Size: 13 KiB

+22
View File
@@ -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"]
}