SPB Git

spb/svgarden Public

SVGarden — searchable bank of 74 self-contained SVG+CSS animation snippets (svgarden.dev)

HTML 79.2% Astro 10.3% JavaScript 6% CSS 3.7% Shell 0.8%
3.3 KB · 83 lines javascript
Raw Blame History
1/**2 * ============================================================3 * SVGarden — https://www.svgarden.dev4 * Author : Simon-Pierre Boucher5 * Contact: contact@spboucher.ai6 * File   : scripts/new-snippet.mjs7 * Desc   : Interactive scaffolder — creates a compliant snippet file skeleton8 * ============================================================9 */10import { writeFile, mkdir, access } from 'node:fs/promises';11import path from 'node:path';12import readline from 'node:readline/promises';13import { SNIPPETS_DIR, CATEGORIES, DIFFICULTIES, attributionHeader } from '../src/lib/snippets.mjs';1415const rl = readline.createInterface({ input: process.stdin, output: process.stdout });1617const ask = async (q, { def, valid } = {}) => {18  for (;;) {19    const raw = (await rl.question(def ? `${q} [${def}]: ` : `${q}: `)).trim();20    const answer = raw || def || '';21    if (!answer) { console.log('  → required.'); continue; }22    if (valid && !valid.includes(answer)) { console.log(`  → one of: ${valid.join(', ')}`); continue; }23    return answer;24  }25};2627console.log('SVGarden — new snippet scaffolder\n');2829const title = await ask('Title (e.g. "Dash spinner")');30const slug = await ask('Slug (kebab-case)', { def: title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '') });31const category = await ask('Category', { valid: CATEGORIES, def: 'loaders' });32const difficulty = await ask('Difficulty', { valid: DIFFICULTIES, def: 'beginner' });33const desc = await ask('One-line description');34const tags = (await ask('Tags (comma-separated)', { def: category.replace(/s$/, '') })).split(',').map((t) => t.trim()).filter(Boolean);35rl.close();3637const relPath = `snippets/${category}/${slug}.html`;38const file = path.join(SNIPPETS_DIR, category, `${slug}.html`);3940try {41  await access(file);42  console.error(`✖ ${relPath} already exists.`);43  process.exit(1);44} catch { /* good — does not exist */ }4546const today = new Date().toISOString().slice(0, 10);47const cls = `sg-${slug}`;4849const content = `${attributionHeader(relPath, desc)}50<!--svgarden51title: ${title}52slug: ${slug}53category: ${category}54tags: [${tags.join(', ')}]55difficulty: ${difficulty}56techniques: [TODO]57how_it_works: >58  TODO — explain the technique in 2 to 4 sentences that genuinely teach it.59  What property animates, why it produces this visual effect, and what the60  reader should tweak first to make it their own.61customizable:62  - { var: "--sg-color",    label: "Color", type: color, default: "#7F77DD" }63  - { var: "--sg-size",     label: "Size",  type: range, min: 24, max: 120, default: 48, unit: px }64  - { var: "--sg-duration", label: "Speed", type: range, min: 0.5, max: 4, step: 0.1, default: 1.5, unit: s }65created: ${today}66-->67<div class="${cls}" style="--sg-color:#7F77DD; --sg-size:48px; --sg-duration:1.5s;">68  <svg viewBox="0 0 50 50" width="48" height="48" aria-hidden="true" focusable="false">69    <!-- TODO: your SVG -->70    <circle cx="25" cy="25" r="20" fill="none" stroke="var(--sg-color)" stroke-width="4"/>71  </svg>72</div>73<style>74  .${cls} { width: var(--sg-size); height: var(--sg-size); }75  .${cls} svg { display: block; width: 100%; height: 100%; }76  /* TODO: @keyframes ${cls}-anim { } */77</style>78`;7980await mkdir(path.dirname(file), { recursive: true });81await writeFile(file, content, 'utf8');82console.log(`✔ created ${relPath} — fill in the TODOs, then run: npm run validate`);83