Add tabbed case studies, CLAUDE.md, and various improvements

- Convert "See It In Action" section to tabbed interface for space efficiency
- Add CLAUDE.md with project instructions for CSS build process
- Update README with additional documentation
- Enhance glass terminal component
- Add new API handlers and server routes

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Paul Bakaus
2026-01-07 18:08:04 -08:00
co-authored by Claude Opus 4.5
parent 0b9c1846a6
commit af21bf2d34
9 changed files with 730 additions and 30 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
# Impeccable
The vocabulary you didn't know you needed. 1 skill, 15 commands, and curated anti-patterns for impeccable style. Works with Cursor, Claude Code, Gemini CLI, and Codex CLI.
The vocabulary you didn't know you needed. 1 skill, 17 commands, and curated anti-patterns for impeccable style. Works with Cursor, Claude Code, Gemini CLI, and Codex CLI.
## Repository Purpose
+36
View File
@@ -0,0 +1,36 @@
# Project Instructions for Claude
## CSS Build Process
**IMPORTANT**: After modifying any CSS files in `public/css/` (especially `workflow.css` or `main.css`), you MUST rebuild the Tailwind CSS:
```bash
bunx @tailwindcss/cli -i public/css/main.css -o public/css/styles.css
```
The CSS architecture:
- `public/css/main.css` - Main entry point, imports Tailwind and all other CSS files
- `public/css/workflow.css` - Commands section, glass terminal, case studies styles
- `public/css/styles.css` - **Compiled output** (do not edit directly)
## Development Server
```bash
bun run dev
```
Runs at http://localhost:3000
## Build System
The build system compiles skills and commands from `source/` to provider-specific formats in `dist/`:
```bash
bun run build # Build all providers
bun run rebuild # Clean and rebuild
```
Source files use placeholders that get replaced per-provider:
- `{{model}}` - Model name (Claude, Gemini, GPT, etc.)
- `{{config_file}}` - Config file name (CLAUDE.md, .cursorrules, etc.)
- `{{ask_instruction}}` - How to ask user questions
+14 -10
View File
@@ -11,25 +11,25 @@ Anthropic created [frontend-design](https://github.com/anthropics/skills/tree/ma
Every LLM learned from the same generic templates. Without guidance, you get the same predictable mistakes: Inter font, purple gradients, cards nested in cards, gray text on colored backgrounds.
Impeccable fights that bias with:
- **An expanded skill** with 7 domain-specific reference files
- **15 steering commands** to audit, polish, simplify, animate, and more
- **An expanded skill** with 7 domain-specific reference files ([view source](source/skills/frontend-design/))
- **17 steering commands** to audit, review, polish, simplify, animate, and more
- **Curated anti-patterns** that explicitly tell the AI what NOT to do
## What's Included
### The Skill: frontend-design
A comprehensive design skill with 7 domain-specific references:
A comprehensive design skill with 7 domain-specific references ([view skill](source/skills/frontend-design/SKILL.md)):
| Reference | Covers |
|-----------|--------|
| typography | Type systems, font pairing, modular scales, OpenType |
| color-and-contrast | OKLCH, tinted neutrals, dark mode, accessibility |
| spatial-design | Spacing systems, grids, visual hierarchy |
| motion-design | Easing curves, staggering, reduced motion |
| interaction-design | Forms, focus states, loading patterns |
| responsive-design | Mobile-first, fluid design, container queries |
| ux-writing | Button labels, error messages, empty states |
| [typography](source/skills/frontend-design/reference/typography.md) | Type systems, font pairing, modular scales, OpenType |
| [color-and-contrast](source/skills/frontend-design/reference/color-and-contrast.md) | OKLCH, tinted neutrals, dark mode, accessibility |
| [spatial-design](source/skills/frontend-design/reference/spatial-design.md) | Spacing systems, grids, visual hierarchy |
| [motion-design](source/skills/frontend-design/reference/motion-design.md) | Easing curves, staggering, reduced motion |
| [interaction-design](source/skills/frontend-design/reference/interaction-design.md) | Forms, focus states, loading patterns |
| [responsive-design](source/skills/frontend-design/reference/responsive-design.md) | Mobile-first, fluid design, container queries |
| [ux-writing](source/skills/frontend-design/reference/ux-writing.md) | Button labels, error messages, empty states |
### 17 Commands
@@ -63,6 +63,10 @@ The skill includes explicit guidance on what to avoid:
- Don't wrap everything in cards or nest cards inside cards
- Don't use bounce/elastic easing (feels dated)
## See It In Action
Visit [impeccable.style](https://impeccable.style#casestudies) to see before/after case studies of real projects transformed with Impeccable commands.
## Installation
### Option 1: Download from Website (Recommended)
+75
View File
@@ -224,6 +224,79 @@ document.addEventListener("click", (e) => {
}
});
// ============================================
// CASE STUDIES TABS
// ============================================
function initCaseStudyTabs() {
const container = document.querySelector('.transformations-tabbed');
if (!container) return;
const tabs = container.querySelectorAll('.transformation-tab');
const panels = container.querySelectorAll('.transformation-panel');
tabs.forEach(tab => {
tab.addEventListener('click', () => {
const tabIndex = tab.dataset.tab;
// Update tabs
tabs.forEach(t => {
t.classList.remove('active');
t.setAttribute('aria-selected', 'false');
});
tab.classList.add('active');
tab.setAttribute('aria-selected', 'true');
// Update panels
panels.forEach(p => {
p.classList.remove('active');
});
const activePanel = container.querySelector(`[data-panel="${tabIndex}"]`);
if (activePanel) {
activePanel.classList.add('active');
}
});
});
}
// ============================================
// LIGHTBOX
// ============================================
function initLightbox() {
const lightbox = document.getElementById('image-lightbox');
if (!lightbox) return;
const lightboxImage = lightbox.querySelector('.lightbox-image');
const closeBtn = lightbox.querySelector('.lightbox-close');
// Click on transformation images to open lightbox
document.querySelectorAll('.transformation-before img, .transformation-after img').forEach(img => {
img.addEventListener('click', () => {
lightboxImage.src = img.src;
lightboxImage.alt = img.alt;
lightbox.classList.add('active');
document.body.style.overflow = 'hidden';
});
});
// Close lightbox
function closeLightbox() {
lightbox.classList.remove('active');
document.body.style.overflow = '';
}
closeBtn.addEventListener('click', closeLightbox);
lightbox.addEventListener('click', (e) => {
if (e.target === lightbox) closeLightbox();
});
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && lightbox.classList.contains('active')) {
closeLightbox();
}
});
}
// ============================================
// STARTUP
// ============================================
@@ -237,6 +310,8 @@ function init() {
initScrollReveal();
initGlassTerminal();
initFrameworkViz();
initLightbox();
initCaseStudyTabs();
loadContent();
document.body.classList.add("loaded");
+367 -1
View File
@@ -109,7 +109,7 @@
color: var(--color-ink);
}
/* Right: The Terminal (Sticky) */
/* Right: The Terminal Stack (Sticky) */
.glass-terminal-wrapper {
position: sticky;
top: var(--spacing-lg);
@@ -119,6 +119,138 @@
min-height: 500px;
}
/* Stacked Windows Container */
.terminal-stack {
position: relative;
height: 100%;
perspective: 1200px;
}
.terminal-stack-tabs {
position: absolute;
top: -31px;
right: 8px;
display: flex;
gap: 4px;
z-index: 10;
}
.terminal-stack-tab {
font-family: var(--font-mono);
font-size: 0.75rem;
padding: 5px 12px;
background: var(--color-mist);
border: 1px solid var(--color-mist);
border-bottom: none;
border-radius: 6px 6px 0 0;
color: var(--color-ash);
cursor: pointer;
transition: all 0.2s ease;
}
.terminal-stack-tab:hover {
background: var(--color-paper);
color: var(--color-charcoal);
}
.terminal-stack-tab.active {
background: var(--color-paper);
color: var(--color-ink);
border-color: var(--color-mist);
}
/* Individual Windows */
.terminal-window {
position: absolute;
inset: 0;
transition: transform 0.4s cubic-bezier(0.4, 0, 0.2, 1),
opacity 0.3s ease,
filter 0.3s ease;
transform-origin: center bottom;
}
/* Demo window (front by default) */
.terminal-window--demo {
z-index: 2;
}
.terminal-window--demo.is-back {
transform: translateY(16px) translateX(12px) scale(0.96);
opacity: 0.6;
filter: brightness(0.92);
pointer-events: none;
z-index: 1;
}
/* Source window (back by default) */
.terminal-window--source {
z-index: 1;
transform: translateY(16px) translateX(12px) scale(0.96);
opacity: 0.6;
filter: brightness(0.92);
pointer-events: none;
}
.terminal-window--source.is-front {
transform: translateY(0) translateX(0) scale(1);
opacity: 1;
filter: brightness(1);
pointer-events: auto;
z-index: 2;
}
/* Source Window Content */
.source-window {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid var(--color-mist);
border-radius: 8px;
box-shadow:
0 20px 60px -10px rgba(0,0,0,0.15),
0 0 0 1px rgba(255,255,255,0.5) inset;
height: 100%;
display: flex;
flex-direction: column;
overflow: hidden;
}
.source-header {
background: rgba(0,0,0,0.02);
padding: 12px 16px;
display: flex;
align-items: center;
gap: 8px;
border-bottom: 1px solid var(--color-mist);
flex-shrink: 0;
}
.source-title {
font-family: var(--font-mono);
font-size: 0.875rem;
color: var(--color-ink);
font-weight: 500;
}
.source-body {
flex: 1;
padding: var(--spacing-md);
font-family: var(--font-mono);
font-size: 0.75rem;
line-height: 1.5;
color: var(--color-charcoal);
overflow-y: auto;
overscroll-behavior: contain;
white-space: pre-wrap;
word-break: break-word;
background: var(--color-cream);
}
.source-loading {
color: var(--color-ash);
font-style: italic;
}
@media (max-width: 900px) {
.glass-terminal-wrapper {
display: none; /* Hide on mobile for now, or stack */
@@ -329,3 +461,237 @@
}
@keyframes blink { 50% { opacity: 0; } }
/* ============================================
CASE STUDIES / TRANSFORMATIONS SECTION
============================================ */
.casestudies-section {
position: relative;
padding: var(--spacing-2xl) 0;
border-top: 1px solid var(--color-mist);
}
/* Tabbed transformations */
.transformations-tabbed {
margin-top: var(--spacing-xl);
}
.transformation-tabs {
display: flex;
gap: var(--spacing-xs);
border-bottom: 1px solid var(--color-mist);
margin-bottom: var(--spacing-lg);
}
.transformation-tab {
font-family: var(--font-display);
font-size: 0.9375rem;
font-weight: 500;
color: var(--color-ash);
background: none;
border: none;
padding: var(--spacing-sm) var(--spacing-md);
cursor: pointer;
position: relative;
transition: color 0.2s ease;
}
.transformation-tab:hover {
color: var(--color-charcoal);
}
.transformation-tab.active {
color: var(--color-ink);
}
.transformation-tab.active::after {
content: '';
position: absolute;
bottom: -1px;
left: 0;
right: 0;
height: 2px;
background: var(--color-accent);
}
.transformation-panels {
position: relative;
}
.transformation-panel {
display: none;
flex-direction: column;
gap: var(--spacing-lg);
animation: fadeInPanel 0.3s ease;
}
.transformation-panel.active {
display: flex;
}
@keyframes fadeInPanel {
from {
opacity: 0;
transform: translateY(8px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Side-by-side images */
.transformation-images {
display: flex;
align-items: center;
gap: var(--spacing-md);
}
.transformation-before,
.transformation-after {
flex: 1;
margin: 0;
}
.transformation-before img,
.transformation-after img,
.transformation-placeholder {
width: 100%;
aspect-ratio: 16 / 10;
object-fit: cover;
border-radius: 8px;
border: 1px solid var(--color-mist);
cursor: pointer;
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.transformation-before img:hover,
.transformation-after img:hover,
.transformation-placeholder:hover {
transform: scale(1.02);
box-shadow: 0 8px 24px -4px rgba(0,0,0,0.15);
}
.transformation-placeholder {
background: linear-gradient(135deg, var(--color-mist) 0%, var(--color-cream) 100%);
display: flex;
align-items: center;
justify-content: center;
color: var(--color-ash);
font-size: 0.8125rem;
font-style: italic;
}
.transformation-before figcaption,
.transformation-after figcaption {
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--color-ash);
margin-top: var(--spacing-xs);
text-align: center;
}
.transformation-arrow {
font-size: 1.5rem;
color: var(--color-accent);
font-weight: 300;
flex-shrink: 0;
}
/* Info section */
.transformation-info {
max-width: 600px;
}
.transformation-title {
font-family: var(--font-display);
font-size: 1.25rem;
font-weight: 600;
color: var(--color-ink);
margin: 0 0 var(--spacing-xs);
}
.transformation-desc {
font-size: 0.9375rem;
color: var(--color-charcoal);
line-height: 1.6;
margin: 0 0 var(--spacing-sm);
}
.transformation-commands {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.transformation-command {
font-family: var(--font-mono);
font-size: 0.75rem;
background: var(--color-mist);
color: var(--color-charcoal);
padding: 4px 10px;
border-radius: 4px;
}
/* Lightbox */
.lightbox {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.9);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
opacity: 0;
visibility: hidden;
transition: opacity 0.3s ease, visibility 0.3s ease;
}
.lightbox.active {
opacity: 1;
visibility: visible;
}
.lightbox-close {
position: absolute;
top: 20px;
right: 24px;
background: none;
border: none;
color: white;
font-size: 2.5rem;
cursor: pointer;
opacity: 0.7;
transition: opacity 0.2s ease;
line-height: 1;
}
.lightbox-close:hover {
opacity: 1;
}
.lightbox-image {
max-width: 90vw;
max-height: 85vh;
object-fit: contain;
border-radius: 8px;
box-shadow: 0 20px 60px rgba(0,0,0,0.5);
}
@media (max-width: 768px) {
.transformation-images {
flex-direction: column;
}
.transformation-arrow {
transform: rotate(90deg);
}
.transformation-before,
.transformation-after {
width: 100%;
}
}
+105 -8
View File
@@ -4,20 +4,20 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Impeccable: The missing upgrade to Anthropic's frontend-design skill</title>
<meta name="description" content="1 skill, 15 commands, and curated anti-patterns for impeccable frontend design. Works with Cursor, Claude Code, Gemini CLI, and Codex CLI.">
<meta name="description" content="1 skill, 17 commands, and curated anti-patterns for impeccable frontend design. Works with Cursor, Claude Code, Gemini CLI, and Codex CLI.">
<!-- OpenGraph -->
<meta property="og:type" content="website">
<meta property="og:url" content="https://impeccable.style">
<meta property="og:title" content="Impeccable: Design skills for AI coding tools">
<meta property="og:description" content="1 skill, 15 commands, and curated anti-patterns for impeccable frontend design. Works with Cursor, Claude Code, Gemini CLI, and Codex CLI.">
<meta property="og:description" content="1 skill, 17 commands, and curated anti-patterns for impeccable frontend design. Works with Cursor, Claude Code, Gemini CLI, and Codex CLI.">
<meta property="og:image" content="https://impeccable.style/og-image.svg">
<!-- Twitter -->
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:url" content="https://impeccable.style">
<meta name="twitter:title" content="Impeccable: Design skills for AI coding tools">
<meta name="twitter:description" content="1 skill, 15 commands, and curated anti-patterns for impeccable frontend design.">
<meta name="twitter:description" content="1 skill, 17 commands, and curated anti-patterns for impeccable frontend design.">
<meta name="twitter:image" content="https://impeccable.style/og-image.svg">
<link rel="icon" type="image/svg+xml" href="./favicon.svg">
@@ -66,11 +66,11 @@
<div class="equation-block">
<span class="equation-label">Steering Commands</span>
<span class="equation-title">15 commands</span>
<span class="equation-title">17 commands</span>
<ul class="equation-list">
<li>/polish, /audit, /simplify</li>
<li>/bolder, /quieter, /animate</li>
<li>and 9 more...</li>
<li>and 11 more...</li>
</ul>
</div>
</div>
@@ -206,7 +206,7 @@
<h2 class="section-title">The Framework</h2>
</div>
<div class="solution-content">
<p class="section-lead" data-reveal>One comprehensive skill with deep expertise, plus 15 commands that form the language of design.</p>
<p class="section-lead" data-reveal>One comprehensive skill with deep expertise, plus 17 commands that form the language of design.</p>
<div class="solution-visual-interactive" id="framework-viz-container" data-reveal>
<!-- Subway map generated by JS -->
@@ -232,10 +232,106 @@
</div>
</section>
<!-- 5. PLATFORMS -->
<section class="platforms-section" id="downloads">
<!-- 5. CASE STUDIES -->
<section class="casestudies-section" id="casestudies">
<div class="section-header" data-reveal>
<span class="section-number">05</span>
<h2 class="section-title">See It In Action</h2>
<p class="section-subtitle">Sample projects transformed with Impeccable commands</p>
</div>
<div class="transformations-tabbed" data-reveal>
<!-- Tab buttons -->
<div class="transformation-tabs" role="tablist">
<button class="transformation-tab active" role="tab" aria-selected="true" data-tab="0">Dashboard</button>
<button class="transformation-tab" role="tab" aria-selected="false" data-tab="1">Landing Page</button>
<button class="transformation-tab" role="tab" aria-selected="false" data-tab="2">Form UX</button>
</div>
<!-- Tab panels -->
<div class="transformation-panels">
<article class="transformation-panel active" role="tabpanel" data-panel="0">
<div class="transformation-images">
<figure class="transformation-before">
<div class="transformation-placeholder">Before screenshot</div>
<figcaption>Before</figcaption>
</figure>
<span class="transformation-arrow"></span>
<figure class="transformation-after">
<div class="transformation-placeholder">After screenshot</div>
<figcaption>After</figcaption>
</figure>
</div>
<div class="transformation-info">
<h3 class="transformation-title">Dashboard Redesign</h3>
<p class="transformation-desc">Generic analytics dashboard transformed into a distinctive, scannable interface.</p>
<div class="transformation-commands">
<span class="transformation-command">/audit</span>
<span class="transformation-command">/normalize</span>
<span class="transformation-command">/bolder</span>
</div>
</div>
</article>
<article class="transformation-panel" role="tabpanel" data-panel="1">
<div class="transformation-images">
<figure class="transformation-before">
<div class="transformation-placeholder">Before screenshot</div>
<figcaption>Before</figcaption>
</figure>
<span class="transformation-arrow"></span>
<figure class="transformation-after">
<div class="transformation-placeholder">After screenshot</div>
<figcaption>After</figcaption>
</figure>
</div>
<div class="transformation-info">
<h3 class="transformation-title">Landing Page Polish</h3>
<p class="transformation-desc">AI-generated landing page refined from generic template to memorable brand experience.</p>
<div class="transformation-commands">
<span class="transformation-command">/review</span>
<span class="transformation-command">/polish</span>
<span class="transformation-command">/animate</span>
</div>
</div>
</article>
<article class="transformation-panel" role="tabpanel" data-panel="2">
<div class="transformation-images">
<figure class="transformation-before">
<div class="transformation-placeholder">Before screenshot</div>
<figcaption>Before</figcaption>
</figure>
<span class="transformation-arrow"></span>
<figure class="transformation-after">
<div class="transformation-placeholder">After screenshot</div>
<figcaption>After</figcaption>
</figure>
</div>
<div class="transformation-info">
<h3 class="transformation-title">Form UX Overhaul</h3>
<p class="transformation-desc">Complex multi-step form simplified and clarified for better completion rates.</p>
<div class="transformation-commands">
<span class="transformation-command">/simplify</span>
<span class="transformation-command">/clarify</span>
<span class="transformation-command">/harden</span>
</div>
</div>
</article>
</div>
</div>
<!-- Lightbox for expanded view -->
<div class="lightbox" id="image-lightbox">
<button class="lightbox-close" aria-label="Close">&times;</button>
<img class="lightbox-image" src="" alt="">
</div>
</section>
<!-- 6. PLATFORMS -->
<section class="platforms-section" id="downloads">
<div class="section-header" data-reveal>
<span class="section-number">06</span>
<h2 class="section-title">Download for Your AI Harness</h2>
<p class="section-subtitle">Same design intelligence, adapted for your workflow. Download and start using in seconds.</p>
</div>
@@ -336,6 +432,7 @@
<div class="footer-links">
<a href="#antidote">Anti-Patterns</a>
<a href="#commands-section">Commands</a>
<a href="#casestudies">Case Studies</a>
<a href="#downloads">Downloads</a>
<a href="https://github.com/pbakaus/impeccable">GitHub</a>
</div>
+104 -10
View File
@@ -5,6 +5,7 @@ import { commandProcessSteps, commandCategories, commandRelationships } from "..
// Track current split instance and command for cleanup
let currentSplitInstance = null;
let currentCommandId = null;
let sourceCache = {}; // Cache fetched source content
const MOBILE_BREAKPOINT = 900;
@@ -79,17 +80,35 @@ function renderDesktopLayout(container, commands) {
${manualHTML}
</div>
<div class="glass-terminal-wrapper">
<div class="glass-terminal">
<div class="terminal-header">
<span class="terminal-dot red"></span>
<span class="terminal-dot yellow"></span>
<span class="terminal-dot green"></span>
<span class="terminal-title">zsh — 80x24</span>
<div class="terminal-stack">
<div class="terminal-stack-tabs">
<button class="terminal-stack-tab active" data-view="demo">Demo</button>
<button class="terminal-stack-tab" data-view="source">Source</button>
</div>
<div class="terminal-body" id="terminal-content">
<div class="terminal-line">
<span class="terminal-prompt">➜</span>
<span>Waiting for input...</span>
<div class="terminal-window terminal-window--source">
<div class="source-window">
<div class="source-header">
<span class="source-title" id="source-title">command.md</span>
</div>
<div class="source-body" id="source-content" data-lenis-prevent>
<span class="source-loading">Select a command to view source...</span>
</div>
</div>
</div>
<div class="terminal-window terminal-window--demo">
<div class="glass-terminal">
<div class="terminal-header">
<span class="terminal-dot red"></span>
<span class="terminal-dot yellow"></span>
<span class="terminal-dot green"></span>
<span class="terminal-title">zsh — 80x24</span>
</div>
<div class="terminal-body" id="terminal-content">
<div class="terminal-line">
<span class="terminal-prompt">➜</span>
<span>Waiting for input...</span>
</div>
</div>
</div>
</div>
</div>
@@ -97,6 +116,8 @@ function renderDesktopLayout(container, commands) {
</div>
`;
setupStackTabs();
setupDesktopScrollSpy(commands);
if (commands.length > 0) {
@@ -175,6 +196,9 @@ function updateTerminal(cmd, container, allCommands) {
if (currentCommandId === cmd.id) return;
currentCommandId = cmd.id;
// Also update source content
updateSourceContent(cmd.id);
if (currentSplitInstance) {
currentSplitInstance.destroy();
currentSplitInstance = null;
@@ -314,3 +338,73 @@ function setupMobileInteractions(commands) {
});
}
// ============================================
// STACKED WINDOWS - Tab Switching
// ============================================
function setupStackTabs() {
const tabs = document.querySelectorAll('.terminal-stack-tab');
const demoWindow = document.querySelector('.terminal-window--demo');
const sourceWindow = document.querySelector('.terminal-window--source');
tabs.forEach(tab => {
tab.addEventListener('click', () => {
const view = tab.dataset.view;
// Update tab states
tabs.forEach(t => t.classList.remove('active'));
tab.classList.add('active');
// Switch windows
if (view === 'source') {
demoWindow.classList.add('is-back');
sourceWindow.classList.add('is-front');
} else {
demoWindow.classList.remove('is-back');
sourceWindow.classList.remove('is-front');
}
});
});
}
async function fetchCommandSource(cmdId) {
// Check cache first
if (sourceCache[cmdId]) {
return sourceCache[cmdId];
}
try {
const response = await fetch(`/api/command-source/${cmdId}`);
if (!response.ok) throw new Error('Failed to fetch source');
const data = await response.json();
sourceCache[cmdId] = data.content;
return data.content;
} catch (error) {
console.error('Error fetching command source:', error);
return null;
}
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
async function updateSourceContent(cmdId) {
const titleEl = document.getElementById('source-title');
const contentEl = document.getElementById('source-content');
if (!titleEl || !contentEl) return;
titleEl.textContent = `${cmdId}.md`;
contentEl.innerHTML = '<span class="source-loading">Loading...</span>';
const source = await fetchCommandSource(cmdId);
if (source) {
contentEl.textContent = source;
} else {
contentEl.innerHTML = '<span class="source-loading">Source not available</span>';
}
}
+11
View File
@@ -3,6 +3,7 @@ import homepage from "../public/index.html";
import {
getSkills,
getCommands,
getCommandSource,
getPatterns,
handleFileDownload,
handleBundleDownload
@@ -49,6 +50,16 @@ const server = serve({
},
},
// API: Get command source content
"/api/command-source/:id": async (req) => {
const { id } = req.params;
const content = await getCommandSource(id);
if (!content) {
return Response.json({ error: "Command not found" }, { status: 404 });
}
return Response.json({ content });
},
// API: Download individual file
"/api/download/:type/:provider/:id": async (req) => {
const { type, provider, id } = req.params;
+17
View File
@@ -72,6 +72,23 @@ export async function getCommands() {
return commands;
}
// Get command source content
export async function getCommandSource(id) {
const sourceDir = join(PROJECT_ROOT, "source");
const commandPath = join(sourceDir, "commands", `${id}.md`);
try {
if (!existsSync(commandPath)) {
return null;
}
const content = await readFileContent(commandPath);
return content;
} catch (error) {
console.error("Error reading command source:", error);
return null;
}
}
// Get the appropriate file path for a provider
export function getFilePath(type, provider, id) {
const distDir = join(PROJECT_ROOT, "dist");