feat(skills): add create-website skill for zero-build responsive SPAs and showcases
This commit is contained in:
@@ -84,6 +84,12 @@ Instant chart and diagram generation for chat and Telegram.
|
||||
- **Delivery:** Generates local PNGs and outputs `MEDIA:<path>` for native Telegram image rendering.
|
||||
- **Tools:** Standalone CLI at `scripts/chart.py`.
|
||||
|
||||
### [create-website](skills/create-website/SKILL.md)
|
||||
Creative Web Director and Senior Frontend Engineer for single-page applications and interactive showcases.
|
||||
- **Features:** Builds fancy, responsive, zero-build SPAs (Tailwind CSS Play CDN, Alpine.js reactivity, Lucide icons, Google Fonts) with anti-AI-slop design archetypes (Dark Obsidian, Editorial Warmth, Neo-Brutalism, Modern Clean SaaS).
|
||||
- **Hosting:** Direct validation and upload to `https://hosting.haumdaucher.de/showcase/...` via the `hosting` skill pipeline, with native Telegram document delivery and QR code generation.
|
||||
- **Tools:** Standalone validation & publisher CLI at `scripts/publish_website.py`.
|
||||
|
||||
### [grill-me](skills/grill-me/SKILL.md)
|
||||
Relentless, structured interview to stress-test and sharpen plans, architectures, decisions, or ideas before writing code.
|
||||
- **Features:** Decision tree & frontier exploration, Telegram/mobile-optimized batching (1-3 questions per round), facts-first environment research, anti-sycophancy challenge, and final specification/ADR synthesis.
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
---
|
||||
name: create-website
|
||||
description: Build and host fancy, production-grade, interactive single-page websites, SPAs, and HTML showcases using modern Zero-Build technologies (Tailwind CSS, Alpine.js, Lucide Icons). Use when the user asks to create, build, design, or show a website, landing page, interactive calculator, showcase, recipe app, dashboard, or web tool.
|
||||
---
|
||||
|
||||
# Create Website & Interactive Single-Page Applications 🌐✨
|
||||
|
||||
This skill empowers OpenClaw to act as a **Creative Web Director and Senior Frontend Engineer**. It generates visually stunning, responsive, fully interactive single-page applications (SPAs) contained within a single HTML file—without requiring complex build steps or Node.js compilers.
|
||||
|
||||
---
|
||||
|
||||
## ⚡ Core Philosophy: The Zero-Build Reactive Engine
|
||||
|
||||
1. **Self-Contained Single-File**: HTML markup, Tailwind CSS utility styling, Alpine.js reactivity, and Lucide icons reside in a single standalone file.
|
||||
2. **Zero-Build Latency**: Runs directly in the browser via fast CDNs. Zero compile time, 100% reliable execution across desktop, tablet, and mobile (iOS Safari, Telegram in-app browser).
|
||||
3. **Anti-AI-Slop Aesthetics**: Reject generic AI defaults (no plain Inter font, no purple-to-blue gradients, no boring centered 3-card monotony). Every site must have an intentional aesthetic direction with strong typography, depth, and micro-interactions.
|
||||
|
||||
---
|
||||
|
||||
## 🎨 1. Choose a Visual Archetype
|
||||
|
||||
Before writing code, commit strictly to one of the 4 design archetypes (detailed in [references/archetypes.md](references/archetypes.md)):
|
||||
|
||||
* **Dark Obsidian & Cyber-Luxury**: Deep dark canvas (`#090A0F`), glassmorphism cards (`backdrop-blur-xl border border-white/10`), electric cyan/violet/emerald glows. Fonts: *Syne* + *Plus Jakarta Sans*. (Ideal for tech showcases, tools, AI, fitness trackers).
|
||||
* **Editorial Warmth & Swiss Typographic**: Linen/ivory background (`#FDFBF7`), hairline borders (`border-stone-200`), warm terracotta/forest green accents. Fonts: *Fraunces* (Serif) + *Instrument Sans*. (Ideal for research dossiers, recipes, long-form stories).
|
||||
* **Neo-Brutalism & High-Energy**: High contrast, thick black borders (`border-2 border-black`), offset hard drop shadows (`shadow-[4px_4px_0px_0px_#000]`), vivid canary yellow and lime accents. Fonts: *Space Grotesk* + *JetBrains Mono*. (Ideal for events, party invites, challenges, games).
|
||||
* **Modern Clean SaaS & Dynamic Dashboard**: Crisp slate canvas, automatic dark/light mode toggle via Alpine, KPI cards, filterable tables. Fonts: *Outfit* + *Plus Jakarta Sans*. (Ideal for data apps, comparisons, calculators).
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ 2. The Standard CDN Skeleton
|
||||
|
||||
Use this bulletproof template structure for all generated websites:
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html lang="de" x-data="{ darkMode: localStorage.getItem('theme') === 'dark' }" :class="{ 'dark': darkMode }">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Titel der Website</title>
|
||||
|
||||
<!-- 1. Google Fonts -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@300;400;500;600;700&family=Syne:wght@600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||||
|
||||
<!-- 2. Tailwind CSS v3 Play CDN -->
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script>
|
||||
tailwind.config = {
|
||||
darkMode: 'class',
|
||||
theme: {
|
||||
extend: {
|
||||
fontFamily: {
|
||||
heading: ['Syne', 'sans-serif'],
|
||||
sans: ['Plus Jakarta Sans', 'sans-serif'],
|
||||
mono: ['JetBrains Mono', 'monospace'],
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- 3. Alpine.js for snappy reactivity -->
|
||||
<script defer src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
||||
|
||||
<!-- 4. Lucide Icons CDN -->
|
||||
<script src="https://unpkg.com/lucide@latest"></script>
|
||||
|
||||
<!-- 5. Optional Confetti for Micro-Delight -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/canvas-confetti@1.9.2/dist/confetti.browser.min.js"></script>
|
||||
</head>
|
||||
<body class="bg-[#090A0F] text-zinc-100 font-sans min-h-screen flex flex-col antialiased selection:bg-cyan-500/30 selection:text-cyan-200">
|
||||
|
||||
<!-- Main Application Content (Tabs, Filters, Interactive Cards, Calculators) -->
|
||||
<main class="flex-1 max-w-6xl w-full mx-auto px-4 py-8 md:py-16">
|
||||
<!-- Component Code Here -->
|
||||
</main>
|
||||
|
||||
<!-- 6. Lucide Icons Initialization (Always at the end of body) -->
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
lucide.createIcons();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⚡ 3. Add Interactivity with Alpine.js
|
||||
|
||||
Review [references/alpine-patterns.md](references/alpine-patterns.md) for pre-built patterns:
|
||||
* **Dynamic Tabs**: Seamless view switching without page reloads.
|
||||
* **Instant Search & Filter**: Real-time filtering of cards/recipes/items based on text input or category pills.
|
||||
* **Reactive Calculators / Sliders**: Live computation (portions, prices, nutrition, splits) with instant UI updates.
|
||||
* **Modal Dialogs & Drawers**: Smooth transitions with backdrop blur and escape key handling.
|
||||
* **Celebration Effects**: Triggering confetti on successful calculations or milestones (`confetti({ particleCount: 80, spread: 70 })`).
|
||||
|
||||
---
|
||||
|
||||
## 🚀 4. Publishing & Sharing Workflow
|
||||
|
||||
Once the website HTML has been generated and saved locally (e.g. to `/tmp/websites/<slug>.html`), publish it to public hosting:
|
||||
|
||||
### Step 1: Run the Publisher Script
|
||||
```bash
|
||||
uv run /home/node/mop-skills/mop-skills/skills/create-website/scripts/publish_website.py \
|
||||
--file /tmp/websites/<slug>.html \
|
||||
--category showcase \
|
||||
--slug <slug>
|
||||
```
|
||||
|
||||
This validates the HTML and uploads it to MinIO, returning the public HTTPS URL:
|
||||
`https://hosting.haumdaucher.de/showcase/{YYYYMMDD}-{slug}.html`
|
||||
|
||||
### Step 2: Present to User in Telegram
|
||||
Always deliver both the clickable link and the local file for offline access:
|
||||
```markdown
|
||||
🚀 **Deine Website ist live!**
|
||||
🔗 https://hosting.haumdaucher.de/showcase/20260928-ninja-creami-calc.html
|
||||
|
||||
*(Die Datei ist zum lokalen Öffnen ebenfalls beigefügt)*
|
||||
MEDIA:/tmp/websites/<slug>.html
|
||||
```
|
||||
|
||||
### Step 3: Offer QR Code
|
||||
Immediately trigger `ask_user` to offer generating a scannable QR code:
|
||||
```json
|
||||
{
|
||||
"question": "Möchtest du für diesen Link einen QR-Code generieren?",
|
||||
"options": ["Ja, QR-Code erstellen", "Nein, danke"]
|
||||
}
|
||||
```
|
||||
If approved, execute the `qr-code` skill:
|
||||
```bash
|
||||
uv run /home/node/mop-skills/mop-skills/skills/qr-code/scripts/generate_qr.py "https://hosting.haumdaucher.de/showcase/20260928-ninja-creami-calc.html"
|
||||
```
|
||||
and send via `MEDIA:<path>`.
|
||||
|
||||
---
|
||||
|
||||
## 🛡️ Quality Gate & Anti-Slop Checklist
|
||||
Always check the site against [references/anti-slop-checklist.md](references/anti-slop-checklist.md) before publishing:
|
||||
1. Are font pairings distinct and imported via Google Fonts?
|
||||
2. Are all buttons responsive with hover and active states (`active:scale-95`)?
|
||||
3. Does the layout scale gracefully on small mobile screens (`grid-cols-1 md:grid-cols-2 lg:grid-cols-3`)?
|
||||
4. Are Lucide icons properly rendered via `lucide.createIcons()`?
|
||||
@@ -0,0 +1,210 @@
|
||||
# Alpine.js Core Patterns for Single-Page Applications ⚡
|
||||
|
||||
These zero-build Alpine.js patterns provide reactive state management directly in HTML. Use these snippets to make your single-page websites snappy, interactive, and responsive.
|
||||
|
||||
---
|
||||
|
||||
## 1. Dark/Light Mode Switcher with Persistence
|
||||
Place this in `<html x-data="{ darkMode: localStorage.getItem('theme') === 'dark' }" :class="{ 'dark': darkMode }">`:
|
||||
|
||||
```html
|
||||
<button
|
||||
@click="darkMode = !darkMode; localStorage.setItem('theme', darkMode ? 'dark' : 'light')"
|
||||
class="p-2.5 rounded-xl border border-zinc-200 dark:border-zinc-800 bg-white dark:bg-zinc-900 text-zinc-700 dark:text-zinc-300 hover:bg-zinc-100 dark:hover:bg-zinc-800 transition-all cursor-pointer"
|
||||
title="Toggle Theme">
|
||||
<i x-show="!darkMode" data-lucide="moon" class="w-5 h-5"></i>
|
||||
<i x-show="darkMode" data-lucide="sun" class="w-5 h-5"></i>
|
||||
</button>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Dynamic Tab Navigation
|
||||
Switch between multiple views without page reloads:
|
||||
|
||||
```html
|
||||
<div x-data="{ activeTab: 'overview' }" class="w-full">
|
||||
<!-- Tab Bar -->
|
||||
<div class="flex gap-2 p-1.5 bg-zinc-100 dark:bg-zinc-900 rounded-xl max-w-md mx-auto mb-8 border border-zinc-200 dark:border-zinc-800">
|
||||
<button
|
||||
@click="activeTab = 'overview'"
|
||||
:class="activeTab === 'overview' ? 'bg-white dark:bg-zinc-800 text-zinc-900 dark:text-white shadow-sm' : 'text-zinc-500 hover:text-zinc-900 dark:hover:text-white'"
|
||||
class="flex-1 py-2 px-4 rounded-lg font-medium text-sm transition-all flex items-center justify-center gap-2">
|
||||
<i data-lucide="layout-grid" class="w-4 h-4"></i> Overview
|
||||
</button>
|
||||
<button
|
||||
@click="activeTab = 'calculator'"
|
||||
:class="activeTab === 'calculator' ? 'bg-white dark:bg-zinc-800 text-zinc-900 dark:text-white shadow-sm' : 'text-zinc-500 hover:text-zinc-900 dark:hover:text-white'"
|
||||
class="flex-1 py-2 px-4 rounded-lg font-medium text-sm transition-all flex items-center justify-center gap-2">
|
||||
<i data-lucide="calculator" class="w-4 h-4"></i> Calculator
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Tab Contents with Transitions -->
|
||||
<div x-show="activeTab === 'overview'" x-transition:enter="transition ease-out duration-200" x-transition:enter-start="opacity-0 translate-y-2" x-transition:enter-end="opacity-100 translate-y-0">
|
||||
<!-- Overview View Content -->
|
||||
</div>
|
||||
|
||||
<div x-show="activeTab === 'calculator'" x-transition:enter="transition ease-out duration-200" x-transition:enter-start="opacity-0 translate-y-2" x-transition:enter-end="opacity-100 translate-y-0">
|
||||
<!-- Calculator View Content -->
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Instant Search & Category Filter
|
||||
Real-time filtering of cards or list items:
|
||||
|
||||
```html
|
||||
<div x-data="{
|
||||
search: '',
|
||||
activeCategory: 'all',
|
||||
items: [
|
||||
{ title: 'Pistazien Gelato', category: 'creami', protein: '28g', calories: 340, desc: 'Reichhaltiges Gelato mit echter sizilianischer Pistazienpaste.' },
|
||||
{ title: 'Protein Erdbeer Sorbet', category: 'protein', protein: '35g', calories: 210, desc: 'Frisches Erdbeersorbet mit Clear Whey Isolat.' },
|
||||
{ title: 'Mango Maracuja Slush', category: 'fruit', protein: '2g', calories: 140, desc: 'Perfekte Sommererfrischung mit Mango-Chunks.' }
|
||||
],
|
||||
get filteredItems() {
|
||||
return this.items.filter(item => {
|
||||
const matchesSearch = item.title.toLowerCase().includes(this.search.toLowerCase()) || item.desc.toLowerCase().includes(this.search.toLowerCase());
|
||||
const matchesCategory = this.activeCategory === 'all' || item.category === this.activeCategory;
|
||||
return matchesSearch && matchesCategory;
|
||||
});
|
||||
}
|
||||
}">
|
||||
<!-- Search & Pills -->
|
||||
<div class="flex flex-col sm:flex-row gap-4 mb-8">
|
||||
<div class="relative flex-1">
|
||||
<i data-lucide="search" class="w-4 h-4 absolute left-3.5 top-3.5 text-zinc-400"></i>
|
||||
<input
|
||||
x-model="search"
|
||||
type="text"
|
||||
placeholder="Rezepte durchsuchen..."
|
||||
class="w-full pl-10 pr-4 py-2.5 rounded-xl border border-zinc-200 dark:border-zinc-800 bg-white dark:bg-zinc-900 text-zinc-900 dark:text-white placeholder-zinc-400 focus:outline-none focus:ring-2 focus:ring-cyan-500 text-sm">
|
||||
</div>
|
||||
<div class="flex gap-2 overflow-x-auto pb-1">
|
||||
<template x-for="cat in ['all', 'creami', 'protein', 'fruit']" :key="cat">
|
||||
<button
|
||||
@click="activeCategory = cat"
|
||||
:class="activeCategory === cat ? 'bg-cyan-500 text-white font-semibold' : 'bg-zinc-100 dark:bg-zinc-800 text-zinc-600 dark:text-zinc-400 hover:bg-zinc-200'"
|
||||
class="px-4 py-2 rounded-xl text-xs uppercase tracking-wider capitalize whitespace-nowrap transition-all cursor-pointer"
|
||||
x-text="cat === 'all' ? 'Alle Rezepte' : cat">
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Cards Grid -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<template x-for="item in filteredItems" :key="item.title">
|
||||
<div class="p-6 rounded-2xl bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 hover:border-cyan-500/50 shadow-sm hover:shadow-xl transition-all">
|
||||
<h3 class="text-lg font-bold text-zinc-900 dark:text-white mb-2" x-text="item.title"></h3>
|
||||
<p class="text-sm text-zinc-500 dark:text-zinc-400 mb-4" x-text="item.desc"></p>
|
||||
<div class="flex justify-between items-center text-xs font-semibold text-cyan-600 dark:text-cyan-400">
|
||||
<span x-text="item.protein + ' Protein'"></span>
|
||||
<span x-text="item.calories + ' kcal'"></span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Live Reactive Slider & Calculator
|
||||
Instant calculations with automatic UI updates:
|
||||
|
||||
```html
|
||||
<div x-data="{
|
||||
pints: 1,
|
||||
baseMilk: 300,
|
||||
baseWhey: 35,
|
||||
basePistachio: 30,
|
||||
get milk() { return this.baseMilk * this.pints; },
|
||||
get whey() { return this.baseWhey * this.pints; },
|
||||
get pistachio() { return this.basePistachio * this.pints; },
|
||||
get proteinTotal() { return Math.round(this.whey * 0.8 + (this.milk * 0.034)); },
|
||||
celebrate() {
|
||||
if (typeof confetti === 'function') {
|
||||
confetti({ particleCount: 75, spread: 60, origin: { y: 0.8 } });
|
||||
}
|
||||
}
|
||||
}" class="p-6 rounded-2xl bg-zinc-900 border border-white/10 text-white">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h3 class="text-xl font-bold font-heading">Portions-Rechner</h3>
|
||||
<span class="px-3 py-1 bg-cyan-500/20 text-cyan-400 rounded-full text-xs font-semibold" x-text="pints + (pints === 1 ? ' Pint' : ' Pints')"></span>
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<label class="block text-xs uppercase tracking-wider text-zinc-400 mb-2">Anzahl Ninja CREAMi Deluxe Pints (680ml)</label>
|
||||
<input type="range" min="1" max="4" step="1" x-model.number="pints" class="w-full accent-cyan-400 cursor-pointer">
|
||||
</div>
|
||||
|
||||
<!-- Computed Ingredients -->
|
||||
<div class="space-y-3 mb-6">
|
||||
<div class="flex justify-between py-2 border-b border-white/5 text-sm">
|
||||
<span class="text-zinc-400">Milch 1.5% oder Hafermilch</span>
|
||||
<span class="font-bold text-white" x-text="milk + ' ml'"></span>
|
||||
</div>
|
||||
<div class="flex justify-between py-2 border-b border-white/5 text-sm">
|
||||
<span class="text-zinc-400">Proteinpulver (Whey/Casein)</span>
|
||||
<span class="font-bold text-cyan-400" x-text="whey + ' g'"></span>
|
||||
</div>
|
||||
<div class="flex justify-between py-2 border-b border-white/5 text-sm">
|
||||
<span class="text-zinc-400">Pistazienmus (100%)</span>
|
||||
<span class="font-bold text-emerald-400" x-text="pistachio + ' g'"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button @click="celebrate()" class="w-full py-3 bg-gradient-to-r from-cyan-500 to-blue-600 rounded-xl font-bold text-sm hover:brightness-110 active:scale-95 transition-all">
|
||||
Zutaten vorbereitet! 🎉
|
||||
</button>
|
||||
</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Modal Dialog with Escape & Backdrop Blur
|
||||
Accessible modal overlay:
|
||||
|
||||
```html
|
||||
<div x-data="{ open: false }">
|
||||
<button @click="open = true" class="px-4 py-2 bg-zinc-900 text-white rounded-xl text-sm font-medium">
|
||||
Details öffnen
|
||||
</button>
|
||||
|
||||
<div
|
||||
x-show="open"
|
||||
@keydown.escape.window="open = false"
|
||||
class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm"
|
||||
x-transition:enter="transition ease-out duration-200"
|
||||
x-transition:enter-start="opacity-0"
|
||||
x-transition:enter-end="opacity-100"
|
||||
style="display: none;">
|
||||
|
||||
<div
|
||||
@click.outside="open = false"
|
||||
class="bg-white dark:bg-zinc-900 rounded-2xl max-w-lg w-full p-6 border border-zinc-200 dark:border-zinc-800 shadow-2xl"
|
||||
x-transition:enter="transition ease-out duration-200"
|
||||
x-transition:enter-start="opacity-0 scale-95"
|
||||
x-transition:enter-end="opacity-100 scale-100">
|
||||
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h3 class="text-lg font-bold text-zinc-900 dark:text-white">Modal Titel</h3>
|
||||
<button @click="open = false" class="text-zinc-400 hover:text-zinc-600 dark:hover:text-zinc-200">
|
||||
<i data-lucide="x" class="w-5 h-5"></i>
|
||||
</button>
|
||||
</div>
|
||||
<p class="text-sm text-zinc-600 dark:text-zinc-400 mb-6">
|
||||
Hier befindet sich detaillierter Inhalt mit Reaktivität oder Formularen.
|
||||
</p>
|
||||
<div class="flex justify-end gap-3">
|
||||
<button @click="open = false" class="px-4 py-2 text-sm text-zinc-500 hover:text-zinc-800 dark:hover:text-white">Schließen</button>
|
||||
<button @click="open = false" class="px-4 py-2 text-sm font-medium bg-cyan-500 text-white rounded-xl">Bestätigen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
@@ -0,0 +1,38 @@
|
||||
# Anti-AI-Slop Quality Checklist 🛡️
|
||||
|
||||
Before finalizing and publishing any website or HTML artifact, verify every item on this checklist. This ensures the output feels designed by a human senior product designer rather than a generic LLM.
|
||||
|
||||
---
|
||||
|
||||
## ❌ Forbidden Trappings of "AI Slop"
|
||||
1. **Never use generic default fonts**:
|
||||
* 🚫 No raw Inter, Roboto, Arial, or unstyled system-ui.
|
||||
* ✅ Always import characterful fonts (e.g. *Syne*, *Cabinet Grotesk*, *Fraunces*, *Space Grotesk*, *Plus Jakarta Sans*).
|
||||
2. **Never use the generic purple-to-indigo gradient**:
|
||||
* 🚫 `bg-gradient-to-r from-purple-600 to-indigo-600` on white background.
|
||||
* ✅ Use intentional palettes: deep obsidian with electric cyan accents, warm ivory with terracotta, neo-brutalist yellow with black borders, or slate with emerald glow.
|
||||
3. **Never generate static, boring centered 3-card rows**:
|
||||
* 🚫 Three identical white boxes with light gray text centered on a plain gray canvas.
|
||||
* ✅ Vary card sizes, emphasize a primary card with a border glow or badge, use grid spans (`md:col-span-2`), or add asymmetry.
|
||||
4. **Never forget interactive micro-interactions**:
|
||||
* 🚫 Dead buttons that don't react to hover or click.
|
||||
* ✅ Always add `hover:-translate-y-0.5 hover:shadow-lg transition-all duration-200` and `active:scale-95`.
|
||||
5. **Never rely on broken external images**:
|
||||
* 🚫 Using fake URLs like `https://example.com/image.jpg` or placeholder services that fail.
|
||||
* ✅ Use SVG patterns, Lucide icons, gradient mesh backgrounds, or reliable Unsplash source links with explicit fallback styling.
|
||||
|
||||
---
|
||||
|
||||
## ✅ Mandatory Pre-Flight Verification
|
||||
|
||||
| Check | Requirement | Verified? |
|
||||
|---|---|:---:|
|
||||
| **Viewport & Encoding** | `<meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">` | [ ] |
|
||||
| **Tailwind CDN** | `<script src="https://cdn.tailwindcss.com"></script>` included in `<head>` | [ ] |
|
||||
| **Alpine.js CDN** | `<script defer src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>` in `<head>` | [ ] |
|
||||
| **Lucide Icons** | `<script src="https://unpkg.com/lucide@latest"></script>` + `lucide.createIcons()` at bottom of `<body>` | [ ] |
|
||||
| **Google Fonts** | Preconnect + Google Font `<link>` + custom `<style>` setting `font-family` | [ ] |
|
||||
| **Touch Targets** | Buttons and clickable elements have at least `py-2.5 px-4` and `cursor-pointer` | [ ] |
|
||||
| **Dark Mode Consistency** | If dark mode is supported, all cards, borders, text, and inputs have `dark:` variants | [ ] |
|
||||
| **Mobile Responsiveness** | Layout uses `grid-cols-1 md:grid-cols-2 lg:grid-cols-3` and responsive paddings (`p-4 md:p-8`) | [ ] |
|
||||
| **Content Richness** | Real, realistic domain content (no repeated "Lorem Ipsum" filler) | [ ] |
|
||||
@@ -0,0 +1,122 @@
|
||||
# Design Archetypes & Visual Systems 🎨
|
||||
|
||||
This document defines the 4 core design archetypes used by the `create-website` skill. Every generated website or single-page application must commit fully to one of these visual directions to ensure high aesthetic distinction and eliminate generic AI aesthetics.
|
||||
|
||||
---
|
||||
|
||||
## 1. Dark Obsidian & Cyber-Luxury 🌌
|
||||
*Best for: Tech products, developer tools, AI showcases, crypto/finance, dark-mode dashboards, interactive apps.*
|
||||
|
||||
### Google Fonts Embed
|
||||
```html
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@300;400;500;600;700&family=Syne:wght@600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
body { font-family: 'Plus Jakarta Sans', sans-serif; }
|
||||
h1, h2, h3, .font-heading { font-family: 'Syne', sans-serif; }
|
||||
code, pre, .font-mono { font-family: 'JetBrains Mono', monospace; }
|
||||
</style>
|
||||
```
|
||||
|
||||
### Color Palette & Classes
|
||||
* **Canvas Background**: `bg-[#090A0F]` with subtle radial gradients:
|
||||
`bg-[radial-gradient(ellipse_80%_80%_at_50%_-20%,rgba(120,119,198,0.15),rgba(255,255,255,0))]`
|
||||
* **Card Surface (Glassmorphism)**: `bg-white/[0.03] backdrop-blur-xl border border-white/10 hover:border-white/20 transition-all`
|
||||
* **Secondary Surface**: `bg-[#12151E] border border-white/5`
|
||||
* **Text Hierarchy**: Headings `text-white font-bold tracking-tight`, Body `text-zinc-400`, Subtle `text-zinc-500`
|
||||
* **Accent Highlights**:
|
||||
* Electric Cyan: `text-cyan-400 bg-cyan-500/10 border-cyan-500/20 shadow-[0_0_20px_rgba(6,182,212,0.15)]`
|
||||
* Neon Violet: `text-purple-400 bg-purple-500/10 border-purple-500/20`
|
||||
* Emerald Glow: `text-emerald-400 bg-emerald-500/10 border-emerald-500/20`
|
||||
* **Buttons**:
|
||||
* Primary: `bg-gradient-to-r from-cyan-500 to-blue-600 hover:from-cyan-400 hover:to-blue-500 text-white font-medium px-5 py-2.5 rounded-xl shadow-lg shadow-cyan-500/20 active:scale-95 transition-all`
|
||||
* Secondary: `bg-white/5 hover:bg-white/10 text-white border border-white/10 px-5 py-2.5 rounded-xl active:scale-95 transition-all`
|
||||
|
||||
---
|
||||
|
||||
## 2. Editorial Warmth & Swiss Typographic 📰
|
||||
*Best for: In-depth research dossiers, long-form journalism, recipe collections, personal manifestos, architectural notes, knowledge collections.*
|
||||
|
||||
### Google Fonts Embed
|
||||
```html
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Fraunces:ital,opsz,wght@0,9..144,300..700;1,9..144,300..700&family=Instrument+Sans:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
body { font-family: 'Instrument Sans', sans-serif; }
|
||||
h1, h2, h3, .font-heading { font-family: 'Fraunces', serif; }
|
||||
</style>
|
||||
```
|
||||
|
||||
### Color Palette & Classes
|
||||
* **Canvas Background**: Warm Ivory `bg-[#FDFBF7]` or Linen `bg-[#F7F4EE]`
|
||||
* **Card Surface**: Paper White `bg-white border border-stone-200/80 shadow-sm rounded-2xl`
|
||||
* **Text Hierarchy**: Headings `text-stone-900 font-semibold tracking-tight`, Body `text-stone-600 leading-relaxed`, Captions `text-stone-500 text-sm`
|
||||
* **Accent Highlights**:
|
||||
* Heritage Rust / Terracotta: `text-[#C2410C] bg-[#FFF7ED] border-[#FDBA74]`
|
||||
* Deep Forest: `text-[#166534] bg-[#F0FDF4] border-[#BBF7D0]`
|
||||
* Warm Amber: `text-[#B45309] bg-[#FEF3C7] border-[#FDE68A]`
|
||||
* **Styling Rules**:
|
||||
* Generous margins and breathing room (`py-16 md:py-24 max-w-4xl mx-auto`).
|
||||
* Elegant divider rules (`border-t border-stone-200`).
|
||||
* Dropcaps or serif quotes with subtle italic styling (`italic text-stone-700`).
|
||||
|
||||
---
|
||||
|
||||
## 3. Neo-Brutalism & High-Energy ⚡
|
||||
*Best for: Interactive party invites, festival schedules, fitness challenges, hackathon pitches, playful calculators, game companions.*
|
||||
|
||||
### Google Fonts Embed
|
||||
```html
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@500;700&family=JetBrains+Mono:wght@500;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
body { font-family: 'Space Grotesk', sans-serif; }
|
||||
code, pre, .font-mono { font-family: 'JetBrains Mono', monospace; }
|
||||
</style>
|
||||
```
|
||||
|
||||
### Color Palette & Classes
|
||||
* **Canvas Background**: High-contrast Pale Pastel `bg-[#FAF8F5]` or Off-Yellow `bg-[#FEFCE8]`
|
||||
* **Borders & Shadows**:
|
||||
* Signature Shadow: `border-2 border-black shadow-[4px_4px_0px_0px_#000] hover:shadow-[2px_2px_0px_0px_#000] hover:translate-x-[2px] hover:translate-y-[2px] transition-all`
|
||||
* Card: `bg-white border-2 border-black shadow-[5px_5px_0px_0px_#000] rounded-xl p-6`
|
||||
* **Color Accents**:
|
||||
* Electric Canary: `bg-[#FDE047]`
|
||||
* Acid Lime: `bg-[#A3E635]`
|
||||
* Bright Coral: `bg-[#FB7185]`
|
||||
* Cyan Pill: `bg-[#38BDF8]`
|
||||
* **Interactive Elements**:
|
||||
* Badges: `inline-block px-3 py-1 text-xs font-bold uppercase tracking-wider bg-black text-white rounded-md`
|
||||
* Buttons: `bg-[#FDE047] hover:bg-[#FACC15] text-black font-bold px-6 py-3 border-2 border-black shadow-[4px_4px_0px_0px_#000] active:translate-x-[4px] active:translate-y-[4px] active:shadow-none transition-all rounded-xl cursor-pointer`
|
||||
|
||||
---
|
||||
|
||||
## 4. Modern Clean SaaS & Dynamic Dashboard 📊
|
||||
*Best for: Multi-tab trackers, data analytics, KPI summaries, filterable catalogues, interactive calculators, recipe managers.*
|
||||
|
||||
### Google Fonts Embed
|
||||
```html
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700&family=Plus+Jakarta+Sans:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
body { font-family: 'Plus Jakarta Sans', sans-serif; }
|
||||
h1, h2, h3, .font-heading { font-family: 'Outfit', sans-serif; }
|
||||
</style>
|
||||
```
|
||||
|
||||
### Color Palette & Classes
|
||||
* **Canvas Background**: Adaptive Light/Dark:
|
||||
`bg-slate-50 text-slate-900 dark:bg-slate-950 dark:text-slate-100 transition-colors duration-200`
|
||||
* **Card Surface**:
|
||||
`bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-2xl shadow-sm hover:shadow-md transition-all`
|
||||
* **KPI Metrics**:
|
||||
* Value: `text-3xl md:text-4xl font-bold font-heading tracking-tight text-slate-900 dark:text-white`
|
||||
* Label: `text-xs font-medium uppercase tracking-wider text-slate-500 dark:text-slate-400 mt-1`
|
||||
* **Interactive Controls**:
|
||||
* Inputs: `bg-slate-100 dark:bg-slate-800 border-none rounded-xl px-4 py-2 text-sm focus:ring-2 focus:ring-indigo-500`
|
||||
* Active Tabs: `bg-indigo-600 text-white rounded-lg px-4 py-2 font-medium shadow-sm`
|
||||
* Inactive Tabs: `text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white px-4 py-2 rounded-lg`
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
#!/usr/bin/env python3
|
||||
# /// script
|
||||
# dependencies = [
|
||||
# "boto3>=1.34.0",
|
||||
# ]
|
||||
# ///
|
||||
"""
|
||||
Publish single-page websites to hosting.haumdaucher.de with validation and clean URL reporting.
|
||||
Integrates with the existing 'hosting' skill publisher.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
def validate_html(content: str) -> list[str]:
|
||||
"""Basic pre-flight checks to ensure the generated website isn't malformed."""
|
||||
warnings = []
|
||||
content_lower = content.lower()
|
||||
|
||||
if "<!doctype html>" not in content_lower:
|
||||
warnings.append("Missing <!DOCTYPE html> declaration.")
|
||||
if "viewport" not in content_lower:
|
||||
warnings.append("Missing responsive viewport meta tag.")
|
||||
if "cdn.tailwindcss.com" not in content_lower and "tailwindcss" not in content_lower:
|
||||
warnings.append("Tailwind CSS CDN not detected in HTML.")
|
||||
if "alpine" not in content_lower:
|
||||
warnings.append("Alpine.js script not detected (interactivity might be missing).")
|
||||
if "lucide" in content_lower and "lucide.createicons" not in content_lower:
|
||||
warnings.append("Lucide icons included, but 'lucide.createIcons()' initialization is missing.")
|
||||
|
||||
return warnings
|
||||
|
||||
def resolve_hosting_script() -> Path:
|
||||
"""Finds the publish_file.py script relative to this script or in standard locations."""
|
||||
current_dir = Path(__file__).resolve().parent
|
||||
# Standard relative location in mop-skills
|
||||
sibling_hosting = current_dir.parent.parent / "hosting" / "scripts" / "publish_file.py"
|
||||
if sibling_hosting.exists():
|
||||
return sibling_hosting
|
||||
|
||||
# Fallback in container
|
||||
pod_hosting = Path("/home/node/mop-skills/mop-skills/skills/hosting/scripts/publish_file.py")
|
||||
if pod_hosting.exists():
|
||||
return pod_hosting
|
||||
|
||||
raise FileNotFoundError(f"Cannot find publish_file.py in {sibling_hosting} or {pod_hosting}")
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Publish websites to hosting.haumdaucher.de")
|
||||
parser.add_argument("--file", "-f", help="Local HTML file to upload")
|
||||
parser.add_argument("--content", "-c", help="Inline HTML content string")
|
||||
parser.add_argument("--slug", "-s", required=True, help="Slug identifier for URL (e.g. creami-calculator)")
|
||||
parser.add_argument("--category", default="showcase", help="Category folder (default: showcase)")
|
||||
parser.add_argument("--skip-validation", action="store_true", help="Skip HTML quality checks")
|
||||
parser.add_argument("--url-only", action="store_true", help="Print only public URL")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if not args.file and args.content is None:
|
||||
parser.error("You must provide either --file or --content.")
|
||||
|
||||
# Read content for validation
|
||||
html_content = ""
|
||||
if args.file:
|
||||
file_path = Path(args.file)
|
||||
if not file_path.exists():
|
||||
sys.exit(f"Error: File not found: {args.file}")
|
||||
html_content = file_path.read_text(encoding="utf-8", errors="replace")
|
||||
else:
|
||||
html_content = args.content
|
||||
|
||||
# Quality validation
|
||||
if not args.skip_validation:
|
||||
warnings = validate_html(html_content)
|
||||
if warnings:
|
||||
print("⚠️ Pre-flight HTML Warnings:", file=sys.stderr)
|
||||
for w in warnings:
|
||||
print(f" - {w}", file=sys.stderr)
|
||||
|
||||
# Delegate to hosting/scripts/publish_file.py
|
||||
publisher_script = resolve_hosting_script()
|
||||
cmd = [
|
||||
sys.executable,
|
||||
str(publisher_script),
|
||||
"--category", args.category,
|
||||
"--slug", args.slug,
|
||||
"--ext", "html",
|
||||
]
|
||||
|
||||
if args.file:
|
||||
cmd.extend(["--file", str(args.file)])
|
||||
else:
|
||||
cmd.extend(["--content", args.content])
|
||||
|
||||
if args.url_only:
|
||||
cmd.append("--url-only")
|
||||
|
||||
result = subprocess.run(cmd, capture_output=True, text=True)
|
||||
if result.returncode != 0:
|
||||
sys.stderr.write(result.stderr)
|
||||
sys.exit(result.returncode)
|
||||
|
||||
print(result.stdout.strip())
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,45 @@
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
# Add scripts directory to path
|
||||
script_dir = Path(__file__).resolve().parent.parent / "scripts"
|
||||
sys.path.insert(0, str(script_dir))
|
||||
|
||||
from publish_website import validate_html, resolve_hosting_script
|
||||
|
||||
class TestPublishWebsite(unittest.TestCase):
|
||||
def test_validate_html_valid(self):
|
||||
valid_html = """
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
||||
<script src="https://unpkg.com/lucide@latest"></script>
|
||||
</head>
|
||||
<body>
|
||||
<i data-lucide="sparkles"></i>
|
||||
<script>lucide.createIcons();</script>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
warnings = validate_html(valid_html)
|
||||
self.assertEqual(warnings, [])
|
||||
|
||||
def test_validate_html_warnings(self):
|
||||
broken_html = "<html><body>Hello</body></html>"
|
||||
warnings = validate_html(broken_html)
|
||||
self.assertIn("Missing <!DOCTYPE html> declaration.", warnings)
|
||||
self.assertIn("Missing responsive viewport meta tag.", warnings)
|
||||
self.assertIn("Tailwind CSS CDN not detected in HTML.", warnings)
|
||||
self.assertIn("Alpine.js script not detected (interactivity might be missing).", warnings)
|
||||
|
||||
def test_resolve_hosting_script(self):
|
||||
hosting_script = resolve_hosting_script()
|
||||
self.assertTrue(hosting_script.exists())
|
||||
self.assertEqual(hosting_script.name, "publish_file.py")
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user