Initial commit

This commit is contained in:
Zhongwei Li
2025-11-30 08:36:56 +08:00
commit 904d09e3f6
5 changed files with 425 additions and 0 deletions

View File

@@ -0,0 +1,12 @@
{
"name": "pwa-assets-generator",
"description": "Generate all required PWA assets from a single 1024x1024px image",
"version": "1.0.0",
"author": {
"name": "Laststance",
"email": "ryota.murakami@laststance.io"
},
"skills": [
"./skills"
]
}

3
README.md Normal file
View File

@@ -0,0 +1,3 @@
# pwa-assets-generator
Generate all required PWA assets from a single 1024x1024px image

49
plugin.lock.json Normal file
View File

@@ -0,0 +1,49 @@
{
"$schema": "internal://schemas/plugin.lock.v1.json",
"pluginId": "gh:laststance/claude-code-marketplace:pwa-assets-generator",
"normalized": {
"repo": null,
"ref": "refs/tags/v20251128.0",
"commit": "9cbda3b3c0a10faed9ac8e931d57f8c19b379565",
"treeHash": "0d259dd5f8bc9f0ce73f2bb07b230f3844ae3d4a328fd2368634961038cf9ed6",
"generatedAt": "2025-11-28T10:20:04.008729Z",
"toolVersion": "publish_plugins.py@0.2.0"
},
"origin": {
"remote": "git@github.com:zhongweili/42plugin-data.git",
"branch": "master",
"commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390",
"repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data"
},
"manifest": {
"name": "pwa-assets-generator",
"description": "Generate all required PWA assets from a single 1024x1024px image",
"version": "1.0.0"
},
"content": {
"files": [
{
"path": "README.md",
"sha256": "4829bcf1fcb10aa00853d708a571ceb42d789563b01677aecb6eb93a640c32a0"
},
{
"path": ".claude-plugin/plugin.json",
"sha256": "0d3dcd463b0766186c5ad88b48a68e51f6b8e6331bc15b41d6c0a96a2179917d"
},
{
"path": "skills/pwa-assets-generator/SKILL.md",
"sha256": "2e2e300ba6a316b9992223a4f6992b321e5d8eb65ca13599ee97542df0471e3e"
},
{
"path": "skills/pwa-assets-generator/scripts/generate-pwa-assets.js",
"sha256": "24893395c10bae08ccbf0ca6adb95df0b9218bfd6d3ea7b697465ccc2abd809e"
}
],
"dirSha256": "0d259dd5f8bc9f0ce73f2bb07b230f3844ae3d4a328fd2368634961038cf9ed6"
},
"security": {
"scannedAt": null,
"scannerVersion": null,
"flags": []
}
}

View File

@@ -0,0 +1,104 @@
---
name: pwa-assets-generator
description: Generate all required PWA assets from a single 1024x1024px image. This skill creates app icons, favicons, Apple touch icons, maskable icons, badge icons, and placeholder screenshots for PWAs. Use when developers need to prepare PWA manifest assets, generate multiple icon sizes from a source image, or create a complete set of PWA-compliant images.
allowed-tools: Bash, Read, Write
---
# PWA Assets Generator
Generate complete set of PWA assets from a single 1024x1024px source image.
## Prerequisites
Ensure Node.js and npm are installed. The script will automatically install required dependencies.
## Quick Start
1. Place your 1024x1024px source image in the project directory
2. Run the generation script:
```bash
node scripts/generate-pwa-assets.js <source-image-path> <output-directory>
```
## Generated Assets
The script creates the following PWA-compliant assets:
### App Icons (Standard)
- `icon-144x144.png` - Standard PWA icon
- `icon-192x192.png` - Android Chrome icon
- `icon-512x512.png` - High-resolution PWA icon
### Maskable Icons
- `icon-192x192-safe.png` - Maskable variant with safe area padding (20% padding)
### iOS Support
- `apple-touch-icon.png` - 180×180px for iOS devices
### Favicon
- `favicon.ico` - Multi-resolution icon (16×16, 32×32, 48×48)
### Badge Icon
- `badge.png` - 96×96px monochrome white badge
### Screenshot Placeholders
- `screenshots/desktop-wide.png` - 1280×720px desktop placeholder
- `screenshots/mobile-narrow.png` - 375×812px mobile placeholder
### Shortcut Icons
- `shortcuts/start.png` - 96×96px with play overlay
- `shortcuts/settings.png` - 96×96px with gear overlay
## Script Features
- **Automatic dependency installation**: Installs sharp and png-to-ico if not present
- **Smart resizing**: Uses sharp for high-quality image processing
- **Maskable icon generation**: Adds proper padding for maskable icons
- **Multi-resolution favicon**: Creates proper .ico file with multiple sizes
- **Badge creation**: Converts to monochrome white for notification badges
- **Placeholder screenshots**: Generates branded placeholders with instructions
- **Overlay icons**: Adds symbolic overlays for shortcut icons
- **Progress tracking**: Shows real-time generation progress
## Customization
### Maskable Icons
The script adds 20% padding to maskable icons by default. Adjust the `MASKABLE_PADDING` constant in the script if needed.
### Screenshot Placeholders
The generated screenshots include instructional text. Replace these with actual app screenshots before deployment.
### Badge Color
The badge is converted to white monochrome. Edit the script's badge generation section for different color schemes.
## Usage Example
```bash
# Generate all assets from logo.png into public/ directory
node scripts/generate-pwa-assets.js ./logo.png ./public
```
## Output Structure
```
output-directory/
├── icon-144x144.png
├── icon-192x192.png
├── icon-512x512.png
├── icon-192x192-safe.png
├── apple-touch-icon.png
├── favicon.ico
├── badge.png
├── screenshots/
│ ├── desktop-wide.png
│ └── mobile-narrow.png
└── shortcuts/
├── start.png
└── settings.png
```
## Troubleshooting
- **Image too small**: Source image must be at least 1024×1024px
- **Transparency issues**: PNG with alpha channel recommended for best results
- **Favicon not showing**: Clear browser cache after replacing favicon.ico

View File

@@ -0,0 +1,257 @@
#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
// Configuration
const MASKABLE_PADDING = 0.2; // 20% padding for maskable icons
// Asset definitions
const ASSETS = [
{ name: 'icon-144x144.png', size: 144 },
{ name: 'icon-192x192.png', size: 192 },
{ name: 'icon-512x512.png', size: 512 },
{ name: 'icon-192x192-safe.png', size: 192, maskable: true },
{ name: 'apple-touch-icon.png', size: 180 },
{ name: 'badge.png', size: 96, monochrome: true },
];
const SCREENSHOTS = [
{ name: 'screenshots/desktop-wide.png', width: 1280, height: 720 },
{ name: 'screenshots/mobile-narrow.png', width: 375, height: 812 },
];
const SHORTCUTS = [
{ name: 'shortcuts/start.png', size: 96, overlay: 'play' },
{ name: 'shortcuts/settings.png', size: 96, overlay: 'gear' },
];
// Check and install dependencies
function ensureDependencies() {
const requiredPackages = ['sharp', 'png-to-ico'];
const missingPackages = [];
requiredPackages.forEach(pkg => {
try {
require.resolve(pkg);
} catch {
missingPackages.push(pkg);
}
});
if (missingPackages.length > 0) {
console.log('📦 Installing required dependencies...');
try {
execSync(`npm install ${missingPackages.join(' ')}`, { stdio: 'inherit' });
console.log('✅ Dependencies installed successfully!\n');
} catch (error) {
console.error('❌ Failed to install dependencies. Please run:');
console.error(` npm install ${missingPackages.join(' ')}`);
process.exit(1);
}
}
}
// Main function
async function generatePWAAssets(sourcePath, outputDir) {
ensureDependencies();
const sharp = require('sharp');
const pngToIco = require('png-to-ico');
// Validate source image
if (!fs.existsSync(sourcePath)) {
throw new Error(`Source image not found: ${sourcePath}`);
}
// Get image metadata
const metadata = await sharp(sourcePath).metadata();
if (metadata.width < 1024 || metadata.height < 1024) {
throw new Error(`Source image must be at least 1024x1024px. Current size: ${metadata.width}x${metadata.height}`);
}
// Create output directories
const dirs = [
outputDir,
path.join(outputDir, 'screenshots'),
path.join(outputDir, 'shortcuts'),
];
dirs.forEach(dir => {
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
});
console.log('🎨 Generating PWA Assets...\n');
// Generate standard icons
for (const asset of ASSETS) {
const outputPath = path.join(outputDir, asset.name);
console.log(`📱 Creating ${asset.name}...`);
let pipeline = sharp(sourcePath);
if (asset.maskable) {
// Add padding for maskable icon
const paddedSize = Math.round(asset.size * (1 + MASKABLE_PADDING * 2));
pipeline = pipeline
.resize(asset.size, asset.size)
.extend({
top: Math.round(asset.size * MASKABLE_PADDING),
bottom: Math.round(asset.size * MASKABLE_PADDING),
left: Math.round(asset.size * MASKABLE_PADDING),
right: Math.round(asset.size * MASKABLE_PADDING),
background: { r: 255, g: 255, b: 255, alpha: 0 }
})
.resize(asset.size, asset.size);
} else if (asset.monochrome) {
// Create monochrome white badge
pipeline = pipeline
.resize(asset.size, asset.size)
.grayscale()
.negate()
.threshold(128);
} else {
// Standard resize
pipeline = pipeline.resize(asset.size, asset.size);
}
await pipeline.png().toFile(outputPath);
}
// Generate favicon.ico
console.log('🌐 Creating favicon.ico...');
const faviconSizes = [16, 32, 48];
const faviconBuffers = [];
for (const size of faviconSizes) {
const buffer = await sharp(sourcePath)
.resize(size, size)
.png()
.toBuffer();
faviconBuffers.push(buffer);
}
const icoBuffer = await pngToIco(faviconBuffers);
fs.writeFileSync(path.join(outputDir, 'favicon.ico'), icoBuffer);
// Generate screenshot placeholders
for (const screenshot of SCREENSHOTS) {
const outputPath = path.join(outputDir, screenshot.name);
console.log(`📸 Creating ${screenshot.name}...`);
// Create a placeholder with the source image centered
const maxDimension = Math.min(screenshot.width, screenshot.height) * 0.4;
await sharp(sourcePath)
.resize(Math.round(maxDimension), Math.round(maxDimension), { fit: 'inside' })
.toBuffer()
.then(async (logoBuffer) => {
// Create background with gradient
const svg = `
<svg width="${screenshot.width}" height="${screenshot.height}" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="grad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#FEF3C7;stop-opacity:1" />
<stop offset="100%" style="stop-color:#FBBF24;stop-opacity:1" />
</linearGradient>
</defs>
<rect width="100%" height="100%" fill="url(#grad)" />
<text x="50%" y="85%" text-anchor="middle" font-family="Arial, sans-serif"
font-size="24" fill="#92400E">
Replace with actual app screenshot
</text>
</svg>
`;
const background = await sharp(Buffer.from(svg))
.png()
.toBuffer();
await sharp(background)
.composite([{
input: logoBuffer,
gravity: 'center'
}])
.toFile(outputPath);
});
}
// Generate shortcut icons with overlays
for (const shortcut of SHORTCUTS) {
const outputPath = path.join(outputDir, shortcut.name);
console.log(`⚡ Creating ${shortcut.name}...`);
// Resize base icon
const resizedIcon = await sharp(sourcePath)
.resize(shortcut.size, shortcut.size)
.toBuffer();
// Create overlay SVG based on type
let overlaySvg;
if (shortcut.overlay === 'play') {
overlaySvg = `
<svg width="${shortcut.size}" height="${shortcut.size}" xmlns="http://www.w3.org/2000/svg">
<g transform="translate(${shortcut.size * 0.6}, ${shortcut.size * 0.6})">
<circle cx="16" cy="16" r="16" fill="white" opacity="0.9"/>
<path d="M 11 9 L 11 23 L 23 16 Z" fill="#10B981"/>
</g>
</svg>
`;
} else if (shortcut.overlay === 'gear') {
overlaySvg = `
<svg width="${shortcut.size}" height="${shortcut.size}" xmlns="http://www.w3.org/2000/svg">
<g transform="translate(${shortcut.size * 0.6}, ${shortcut.size * 0.6})">
<circle cx="16" cy="16" r="16" fill="white" opacity="0.9"/>
<path d="M16 10 L18 12 L20 10 L22 12 L20 14 L22 16 L20 18 L22 20 L20 22 L18 20 L16 22 L14 20 L12 22 L10 20 L12 18 L10 16 L12 14 L10 12 L12 10 L14 12 L16 10 Z"
fill="#6B7280" transform="translate(-6, -6) scale(1.5)"/>
</g>
</svg>
`;
}
const overlay = await sharp(Buffer.from(overlaySvg))
.png()
.toBuffer();
await sharp(resizedIcon)
.composite([{ input: overlay }])
.toFile(outputPath);
}
console.log('\n✅ All PWA assets generated successfully!');
console.log(`📂 Output directory: ${outputDir}`);
console.log('\n📋 Generated files:');
console.log(' ✓ App icons (144x144, 192x192, 512x512)');
console.log(' ✓ Maskable icon (192x192-safe)');
console.log(' ✓ Apple touch icon (180x180)');
console.log(' ✓ Favicon.ico (multi-resolution)');
console.log(' ✓ Badge icon (96x96, monochrome)');
console.log(' ✓ Screenshot placeholders (desktop & mobile)');
console.log(' ✓ Shortcut icons (start & settings)');
console.log('\n💡 Remember to replace screenshot placeholders with actual app screenshots!');
}
// CLI handling
if (require.main === module) {
const args = process.argv.slice(2);
if (args.length < 2) {
console.log('Usage: node generate-pwa-assets.js <source-image> <output-directory>');
console.log('\nExample:');
console.log(' node generate-pwa-assets.js ./logo.png ./public');
process.exit(1);
}
const [sourcePath, outputDir] = args;
generatePWAAssets(sourcePath, outputDir)
.catch(error => {
console.error('\n❌ Error:', error.message);
process.exit(1);
});
}
module.exports = { generatePWAAssets };