Initial: pi-skill — 68 skills, 43 extensions, 11 themes for Pi
This commit is contained in:
15
skills/.claude-plugin/marketplace.json
Normal file
15
skills/.claude-plugin/marketplace.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "diagram-design",
|
||||
"metadata": {
|
||||
"description": "Editorial-quality technical and product diagrams — 13 types rendered as standalone HTML with inline SVG, skinnable to match your brand"
|
||||
},
|
||||
"owner": {
|
||||
"name": "Cathryn Lavery"
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
"name": "diagram-design",
|
||||
"source": "./"
|
||||
}
|
||||
]
|
||||
}
|
||||
17
skills/.claude-plugin/plugin.json
Normal file
17
skills/.claude-plugin/plugin.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "diagram-design",
|
||||
"description": "Editorial-quality technical and product diagrams — 13 types rendered as standalone HTML with inline SVG, skinnable to match your brand",
|
||||
"version": "1.0.0",
|
||||
"homepage": "https://github.com/cathrynlavery/diagram-design",
|
||||
"repository": "https://github.com/cathrynlavery/diagram-design",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"diagrams",
|
||||
"svg",
|
||||
"architecture",
|
||||
"flowchart",
|
||||
"visualization",
|
||||
"editorial"
|
||||
],
|
||||
"skills": ["./"]
|
||||
}
|
||||
12
skills/.clawscan-allow
Normal file
12
skills/.clawscan-allow
Normal file
@@ -0,0 +1,12 @@
|
||||
# Security scan allowlist for html-ppt-skill
|
||||
# These patterns are false positives from template content, not actual threats.
|
||||
|
||||
# Path traversal: templates reference shared assets via relative paths
|
||||
# e.g. templates/full-decks/weekly-report/ → ../../../assets/
|
||||
# This is the correct relative path to the skill root assets directory.
|
||||
traversal:templates/full-decks/*/index.html
|
||||
|
||||
# Destructive commands: testing-safety-alert template displays forbidden
|
||||
# commands as text examples in a security policy demo slide.
|
||||
# They are HTML content, not executable code.
|
||||
destructive:templates/full-decks/testing-safety-alert/index.html
|
||||
21
skills/LICENSE
Normal file
21
skills/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 Thomas Praun
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
296
skills/SKILL.md
Normal file
296
skills/SKILL.md
Normal file
@@ -0,0 +1,296 @@
|
||||
---
|
||||
name: writing-plans
|
||||
description: Use when you have a spec or requirements for a multi-step task. Creates comprehensive implementation plans with bite-sized tasks, exact file paths, and complete code examples.
|
||||
version: 1.1.0
|
||||
author: Hermes Agent (adapted from obra/superpowers)
|
||||
license: MIT
|
||||
metadata:
|
||||
hermes:
|
||||
tags: [planning, design, implementation, workflow, documentation]
|
||||
related_skills: [subagent-driven-development, test-driven-development, requesting-code-review]
|
||||
---
|
||||
|
||||
# Writing Implementation Plans
|
||||
|
||||
## Overview
|
||||
|
||||
Write comprehensive implementation plans assuming the implementer has zero context for the codebase and questionable taste. Document everything they need: which files to touch, complete code, testing commands, docs to check, how to verify. Give them bite-sized tasks. DRY. YAGNI. TDD. Frequent commits.
|
||||
|
||||
Assume the implementer is a skilled developer but knows almost nothing about the toolset or problem domain. Assume they don't know good test design very well.
|
||||
|
||||
**Core principle:** A good plan makes implementation obvious. If someone has to guess, the plan is incomplete.
|
||||
|
||||
## When to Use
|
||||
|
||||
**Always use before:**
|
||||
- Implementing multi-step features
|
||||
- Breaking down complex requirements
|
||||
- Delegating to subagents via subagent-driven-development
|
||||
|
||||
**Don't skip when:**
|
||||
- Feature seems simple (assumptions cause bugs)
|
||||
- You plan to implement it yourself (future you needs guidance)
|
||||
- Working alone (documentation matters)
|
||||
|
||||
## Bite-Sized Task Granularity
|
||||
|
||||
**Each task = 2-5 minutes of focused work.**
|
||||
|
||||
Every step is one action:
|
||||
- "Write the failing test" — step
|
||||
- "Run it to make sure it fails" — step
|
||||
- "Implement the minimal code to make the test pass" — step
|
||||
- "Run the tests and make sure they pass" — step
|
||||
- "Commit" — step
|
||||
|
||||
**Too big:**
|
||||
```markdown
|
||||
### Task 1: Build authentication system
|
||||
[50 lines of code across 5 files]
|
||||
```
|
||||
|
||||
**Right size:**
|
||||
```markdown
|
||||
### Task 1: Create User model with email field
|
||||
[10 lines, 1 file]
|
||||
|
||||
### Task 2: Add password hash field to User
|
||||
[8 lines, 1 file]
|
||||
|
||||
### Task 3: Create password hashing utility
|
||||
[15 lines, 1 file]
|
||||
```
|
||||
|
||||
## Plan Document Structure
|
||||
|
||||
### Header (Required)
|
||||
|
||||
Every plan MUST start with:
|
||||
|
||||
```markdown
|
||||
# [Feature Name] Implementation Plan
|
||||
|
||||
> **For Hermes:** Use subagent-driven-development skill to implement this plan task-by-task.
|
||||
|
||||
**Goal:** [One sentence describing what this builds]
|
||||
|
||||
**Architecture:** [2-3 sentences about approach]
|
||||
|
||||
**Tech Stack:** [Key technologies/libraries]
|
||||
|
||||
---
|
||||
```
|
||||
|
||||
### Task Structure
|
||||
|
||||
Each task follows this format:
|
||||
|
||||
````markdown
|
||||
### Task N: [Descriptive Name]
|
||||
|
||||
**Objective:** What this task accomplishes (one sentence)
|
||||
|
||||
**Files:**
|
||||
- Create: `exact/path/to/new_file.py`
|
||||
- Modify: `exact/path/to/existing.py:45-67` (line numbers if known)
|
||||
- Test: `tests/path/to/test_file.py`
|
||||
|
||||
**Step 1: Write failing test**
|
||||
|
||||
```python
|
||||
def test_specific_behavior():
|
||||
result = function(input)
|
||||
assert result == expected
|
||||
```
|
||||
|
||||
**Step 2: Run test to verify failure**
|
||||
|
||||
Run: `pytest tests/path/test.py::test_specific_behavior -v`
|
||||
Expected: FAIL — "function not defined"
|
||||
|
||||
**Step 3: Write minimal implementation**
|
||||
|
||||
```python
|
||||
def function(input):
|
||||
return expected
|
||||
```
|
||||
|
||||
**Step 4: Run test to verify pass**
|
||||
|
||||
Run: `pytest tests/path/test.py::test_specific_behavior -v`
|
||||
Expected: PASS
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add tests/path/test.py src/path/file.py
|
||||
git commit -m "feat: add specific feature"
|
||||
```
|
||||
````
|
||||
|
||||
## Writing Process
|
||||
|
||||
### Step 1: Understand Requirements
|
||||
|
||||
Read and understand:
|
||||
- Feature requirements
|
||||
- Design documents or user description
|
||||
- Acceptance criteria
|
||||
- Constraints
|
||||
|
||||
### Step 2: Explore the Codebase
|
||||
|
||||
Use Hermes tools to understand the project:
|
||||
|
||||
```python
|
||||
# Understand project structure
|
||||
search_files("*.py", target="files", path="src/")
|
||||
|
||||
# Look at similar features
|
||||
search_files("similar_pattern", path="src/", file_glob="*.py")
|
||||
|
||||
# Check existing tests
|
||||
search_files("*.py", target="files", path="tests/")
|
||||
|
||||
# Read key files
|
||||
read_file("src/app.py")
|
||||
```
|
||||
|
||||
### Step 3: Design Approach
|
||||
|
||||
Decide:
|
||||
- Architecture pattern
|
||||
- File organization
|
||||
- Dependencies needed
|
||||
- Testing strategy
|
||||
|
||||
### Step 4: Write Tasks
|
||||
|
||||
Create tasks in order:
|
||||
1. Setup/infrastructure
|
||||
2. Core functionality (TDD for each)
|
||||
3. Edge cases
|
||||
4. Integration
|
||||
5. Cleanup/documentation
|
||||
|
||||
### Step 5: Add Complete Details
|
||||
|
||||
For each task, include:
|
||||
- **Exact file paths** (not "the config file" but `src/config/settings.py`)
|
||||
- **Complete code examples** (not "add validation" but the actual code)
|
||||
- **Exact commands** with expected output
|
||||
- **Verification steps** that prove the task works
|
||||
|
||||
### Step 6: Review the Plan
|
||||
|
||||
Check:
|
||||
- [ ] Tasks are sequential and logical
|
||||
- [ ] Each task is bite-sized (2-5 min)
|
||||
- [ ] File paths are exact
|
||||
- [ ] Code examples are complete (copy-pasteable)
|
||||
- [ ] Commands are exact with expected output
|
||||
- [ ] No missing context
|
||||
- [ ] DRY, YAGNI, TDD principles applied
|
||||
|
||||
### Step 7: Save the Plan
|
||||
|
||||
```bash
|
||||
mkdir -p docs/plans
|
||||
# Save plan to docs/plans/YYYY-MM-DD-feature-name.md
|
||||
git add docs/plans/
|
||||
git commit -m "docs: add implementation plan for [feature]"
|
||||
```
|
||||
|
||||
## Principles
|
||||
|
||||
### DRY (Don't Repeat Yourself)
|
||||
|
||||
**Bad:** Copy-paste validation in 3 places
|
||||
**Good:** Extract validation function, use everywhere
|
||||
|
||||
### YAGNI (You Aren't Gonna Need It)
|
||||
|
||||
**Bad:** Add "flexibility" for future requirements
|
||||
**Good:** Implement only what's needed now
|
||||
|
||||
```python
|
||||
# Bad — YAGNI violation
|
||||
class User:
|
||||
def __init__(self, name, email):
|
||||
self.name = name
|
||||
self.email = email
|
||||
self.preferences = {} # Not needed yet!
|
||||
self.metadata = {} # Not needed yet!
|
||||
|
||||
# Good — YAGNI
|
||||
class User:
|
||||
def __init__(self, name, email):
|
||||
self.name = name
|
||||
self.email = email
|
||||
```
|
||||
|
||||
### TDD (Test-Driven Development)
|
||||
|
||||
Every task that produces code should include the full TDD cycle:
|
||||
1. Write failing test
|
||||
2. Run to verify failure
|
||||
3. Write minimal code
|
||||
4. Run to verify pass
|
||||
|
||||
See `test-driven-development` skill for details.
|
||||
|
||||
### Frequent Commits
|
||||
|
||||
Commit after every task:
|
||||
```bash
|
||||
git add [files]
|
||||
git commit -m "type: description"
|
||||
```
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
### Vague Tasks
|
||||
|
||||
**Bad:** "Add authentication"
|
||||
**Good:** "Create User model with email and password_hash fields"
|
||||
|
||||
### Incomplete Code
|
||||
|
||||
**Bad:** "Step 1: Add validation function"
|
||||
**Good:** "Step 1: Add validation function" followed by the complete function code
|
||||
|
||||
### Missing Verification
|
||||
|
||||
**Bad:** "Step 3: Test it works"
|
||||
**Good:** "Step 3: Run `pytest tests/test_auth.py -v`, expected: 3 passed"
|
||||
|
||||
### Missing File Paths
|
||||
|
||||
**Bad:** "Create the model file"
|
||||
**Good:** "Create: `src/models/user.py`"
|
||||
|
||||
## Execution Handoff
|
||||
|
||||
After saving the plan, offer the execution approach:
|
||||
|
||||
**"Plan complete and saved. Ready to execute using subagent-driven-development — I'll dispatch a fresh subagent per task with two-stage review (spec compliance then code quality). Shall I proceed?"**
|
||||
|
||||
When executing, use the `subagent-driven-development` skill:
|
||||
- Fresh `delegate_task` per task with full context
|
||||
- Spec compliance review after each task
|
||||
- Code quality review after spec passes
|
||||
- Proceed only when both reviews approve
|
||||
|
||||
## Remember
|
||||
|
||||
```
|
||||
Bite-sized tasks (2-5 min each)
|
||||
Exact file paths
|
||||
Complete code (copy-pasteable)
|
||||
Exact commands with expected output
|
||||
Verification steps
|
||||
DRY, YAGNI, TDD
|
||||
Frequent commits
|
||||
```
|
||||
|
||||
**A good plan makes implementation obvious.**
|
||||
122
skills/adversarial-reviewer/SKILL.md
Normal file
122
skills/adversarial-reviewer/SKILL.md
Normal file
@@ -0,0 +1,122 @@
|
||||
---
|
||||
name: adversarial-reviewer
|
||||
description: Adversarial code review that assumes bugs exist and hunts for them. Use when asked to review code, find bugs, audit for correctness, stress-test a PR, or when someone says "tear this apart" or "what's wrong with this". Give no benefit of the doubt — every line is guilty until proven innocent.
|
||||
---
|
||||
|
||||
# Adversarial Code Reviewer
|
||||
|
||||
You are a hostile reviewer. Your job is to find bugs, not to be helpful. Assume the code is broken and prove yourself right.
|
||||
|
||||
## Mindset
|
||||
|
||||
- **Guilty until proven innocent.** Every line of code is a suspect.
|
||||
- **No compliments.** Don't say what's good. Say what's wrong.
|
||||
- **No "potential issue" hedging.** If something looks wrong, say it's wrong. Be direct.
|
||||
- **Prove it.** Construct concrete inputs, sequences, or race conditions that trigger the bug. Don't hand-wave.
|
||||
- **Silence means approval.** If you don't mention something, that IS your approval. Don't waste tokens on "this looks fine".
|
||||
|
||||
## Review Checklist
|
||||
|
||||
Work through these categories in order. Skip a category only when it genuinely doesn't apply.
|
||||
|
||||
### 1. Logic Errors
|
||||
|
||||
- Off-by-one in loops, slices, ranges, pagination
|
||||
- Inverted or missing conditions (especially negation — `!` is easy to miss)
|
||||
- Fallthrough in switch/match without break
|
||||
- Short-circuit evaluation hiding side effects
|
||||
- Wrong operator (`=` vs `==`, `&&` vs `||`, `&` vs `&&`)
|
||||
- Integer overflow, floating point comparison, implicit coercion
|
||||
|
||||
### 2. Edge Cases & Boundaries
|
||||
|
||||
- Empty inputs: empty string, empty array, null, undefined, 0, NaN
|
||||
- Single-element collections
|
||||
- Maximum values, minimum values, negative numbers
|
||||
- Unicode, multi-byte characters, RTL text
|
||||
- Concurrent calls with identical arguments
|
||||
- What happens when it's called twice? What about zero times?
|
||||
|
||||
### 3. Error Handling
|
||||
|
||||
- Catch blocks that swallow errors silently
|
||||
- Missing error handling on async operations
|
||||
- Error handling that catches too broadly (bare `catch` / `catch(e)`)
|
||||
- Cleanup/finally blocks missing or incomplete
|
||||
- Error messages that leak internals to users
|
||||
- Thrown errors that aren't Error instances
|
||||
|
||||
### 4. State & Concurrency
|
||||
|
||||
- Shared mutable state without synchronization
|
||||
- TOCTOU (time-of-check-to-time-of-use) races
|
||||
- Stale closures capturing variables that mutate
|
||||
- Event handler registration without cleanup
|
||||
- Assumptions about execution order of async operations
|
||||
|
||||
### 5. Security
|
||||
|
||||
- Unsanitized user input reaching SQL, HTML, shell, or file paths
|
||||
- Missing or incorrect authorization checks
|
||||
- Information leakage in error responses
|
||||
- CSRF, open redirect, path traversal
|
||||
- Secrets in code, logs, or error messages
|
||||
- Timing attacks on comparison operations
|
||||
|
||||
### 6. Data Integrity
|
||||
|
||||
- Missing validation at system boundaries
|
||||
- Type coercion hiding bad data
|
||||
- Partial writes without transactions
|
||||
- Missing uniqueness constraints
|
||||
- Cascading deletes that orphan or destroy data
|
||||
- Schema mismatches between code and database
|
||||
|
||||
### 7. Resource Management
|
||||
|
||||
- Missing cleanup: file handles, connections, timers, listeners
|
||||
- Unbounded growth: caches without eviction, arrays without limits
|
||||
- Memory leaks from retained references
|
||||
- Missing timeouts on network operations
|
||||
- Retry loops without backoff or limits
|
||||
|
||||
## Output Format
|
||||
|
||||
For each bug found:
|
||||
|
||||
```
|
||||
**BUG: [short title]**
|
||||
File: path/to/file.ts:42
|
||||
Category: [from checklist above]
|
||||
Severity: CRITICAL | HIGH | MEDIUM | LOW
|
||||
|
||||
[What's wrong — one or two sentences, no filler]
|
||||
|
||||
Trigger: [concrete scenario that hits this bug]
|
||||
|
||||
Fix: [minimal code change or approach — don't rewrite the function]
|
||||
```
|
||||
|
||||
Order findings by severity (CRITICAL first).
|
||||
|
||||
## Severity Guide
|
||||
|
||||
- **CRITICAL**: Data loss, security vulnerability, crash in production
|
||||
- **HIGH**: Wrong behavior users will hit in normal usage
|
||||
- **MEDIUM**: Wrong behavior in edge cases, resource leaks under load
|
||||
- **LOW**: Cosmetic logic issues, unnecessary work, misleading names that could cause future bugs
|
||||
|
||||
## What This Review Is NOT
|
||||
|
||||
- Not a style review. Don't comment on formatting, naming conventions, or "I'd do it differently".
|
||||
- Not a feature review. Don't suggest additions, improvements, or refactors.
|
||||
- Not a test review. Don't say "this needs more tests" — say what's broken.
|
||||
- Not a compliment sandwich. There is no sandwich. There is only bugs.
|
||||
|
||||
## Process
|
||||
|
||||
1. Read ALL the code under review before writing anything. Form a mental model of the data flow.
|
||||
2. Trace the unhappy paths. What happens when things go wrong?
|
||||
3. Look for implicit assumptions. What does this code believe about its inputs that isn't enforced?
|
||||
4. Check the boundaries between components. Where does trust transfer happen?
|
||||
5. Write up findings. If you found nothing, say "No bugs found" and stop. Don't manufacture issues to seem thorough.
|
||||
139
skills/agent-browser/SKILL.md
Normal file
139
skills/agent-browser/SKILL.md
Normal file
@@ -0,0 +1,139 @@
|
||||
---
|
||||
name: agent-browser
|
||||
description: Browser automation for testing and verification. Use when you need to interact with web UIs, verify visual changes, fill forms, or capture screenshots.
|
||||
---
|
||||
|
||||
# Agent Browser Skill
|
||||
|
||||
Fast browser automation CLI for AI agents. Use this to verify web UI changes, test interactions, and capture screenshots.
|
||||
|
||||
## When to Use
|
||||
|
||||
- Verify visual changes after modifying frontend code
|
||||
- Test form interactions and user flows
|
||||
- Capture screenshots for documentation or debugging
|
||||
- Inspect rendered HTML/accessibility tree
|
||||
- Debug why something isn't working in the browser
|
||||
|
||||
## Core Workflow
|
||||
|
||||
### 1. Open a URL
|
||||
|
||||
```bash
|
||||
agent-browser open http://localhost:4321/
|
||||
```
|
||||
|
||||
### 2. Take a Snapshot
|
||||
|
||||
The snapshot command returns an accessibility tree with refs (`@e1`, `@e2`, etc.) that you can use for interactions:
|
||||
|
||||
```bash
|
||||
agent-browser snapshot -i # -i = interactive elements only (recommended)
|
||||
agent-browser snapshot -c # -c = compact (removes empty structural elements)
|
||||
agent-browser snapshot -i -c # Both flags work together
|
||||
```
|
||||
|
||||
### 3. Interact Using Refs
|
||||
|
||||
Use the `@ref` values from the snapshot to interact with elements:
|
||||
|
||||
```bash
|
||||
agent-browser click @e5 # Click element
|
||||
agent-browser fill @e3 "hello" # Clear and type
|
||||
agent-browser type @e3 "world" # Type without clearing
|
||||
agent-browser select @e7 "option" # Select dropdown
|
||||
agent-browser check @e9 # Check checkbox
|
||||
```
|
||||
|
||||
### 4. Screenshot
|
||||
|
||||
```bash
|
||||
agent-browser screenshot # Viewport only
|
||||
agent-browser screenshot --full # Full page
|
||||
agent-browser screenshot output.png # Save to file
|
||||
```
|
||||
|
||||
ALWAYS check the image size before attempting to load. If it is larger than 2MB, process it using a tool such as sips or ImageMagick to reduce the size. Large files cannot be loaded by your tools.
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Form Verification
|
||||
|
||||
```bash
|
||||
agent-browser open http://localhost:4321/_emdash/api/setup/dev-bypass?redirect=/_emdash/admin/content/posts/new
|
||||
agent-browser snapshot -i # Check what fields are visible
|
||||
agent-browser fill @e3 "Test Title"
|
||||
agent-browser fill @e5 "Test content here"
|
||||
agent-browser click @e7 # Save button
|
||||
```
|
||||
|
||||
### Get Element Info
|
||||
|
||||
```bash
|
||||
agent-browser get text @e1 # Get text content
|
||||
agent-browser get html @e1 # Get inner HTML
|
||||
agent-browser get value @e2 # Get input value
|
||||
agent-browser get url # Current URL
|
||||
agent-browser get title # Page title
|
||||
agent-browser get count "button" # Count matching elements
|
||||
```
|
||||
|
||||
### Check Element State
|
||||
|
||||
```bash
|
||||
agent-browser is visible @e1
|
||||
agent-browser is enabled @e2
|
||||
agent-browser is checked @e3
|
||||
```
|
||||
|
||||
### Find by Role/Label
|
||||
|
||||
```bash
|
||||
agent-browser find role button click --name "Submit"
|
||||
agent-browser find label "Email" fill "test@example.com"
|
||||
agent-browser find placeholder "Search..." type "query"
|
||||
```
|
||||
|
||||
## Sessions
|
||||
|
||||
Sessions keep browser state (cookies, storage) between commands:
|
||||
|
||||
```bash
|
||||
# Use named session (persists until closed)
|
||||
agent-browser --session mytest open http://localhost:4321
|
||||
agent-browser --session mytest snapshot -i
|
||||
|
||||
# Or set via environment
|
||||
export AGENT_BROWSER_SESSION=mytest
|
||||
agent-browser open http://localhost:3000
|
||||
```
|
||||
|
||||
## Useful Options
|
||||
|
||||
| Option | Description |
|
||||
| ------------------ | ----------------------------------- |
|
||||
| `--session <name>` | Isolated browser session |
|
||||
| `--headed` | Show browser window (not headless) |
|
||||
| `--json` | JSON output for programmatic use |
|
||||
| `--full` | Full page screenshot |
|
||||
| `-i` | Snapshot: interactive elements only |
|
||||
| `-c` | Snapshot: compact output |
|
||||
| `-d <n>` | Snapshot: limit tree depth |
|
||||
|
||||
## Debugging
|
||||
|
||||
```bash
|
||||
agent-browser --headed open http://localhost:3000 # See what's happening
|
||||
agent-browser console # View console logs
|
||||
agent-browser errors # View page errors
|
||||
agent-browser highlight @e5 # Highlight element
|
||||
agent-browser eval "document.title" # Run JS
|
||||
```
|
||||
|
||||
## Tips
|
||||
|
||||
1. **Always snapshot first** - Get refs before interacting
|
||||
2. **Use `-i` flag** - Interactive-only snapshots are much cleaner
|
||||
3. **Wait when needed** - Use `wait <ms>` or `wait <selector>` after actions that trigger loading
|
||||
4. **Sessions for auth** - Use named sessions to persist login state
|
||||
5. **Headed for debugging** - Use `--headed` when things aren't working as expected
|
||||
59
skills/agent-memory/SKILL.md.disabled
Normal file
59
skills/agent-memory/SKILL.md.disabled
Normal file
@@ -0,0 +1,59 @@
|
||||
# agent-memory — Local Hybrid Search Memory System
|
||||
|
||||
## What It Does
|
||||
|
||||
`agent-memory` is a CLI tool that indexes your markdown memory files (`~/.claude/agent-memory/`) into SQLite and provides **hybrid search** (0.7 vector + 0.3 BM25), fully local with zero API calls.
|
||||
|
||||
## When to Use
|
||||
|
||||
- **Searching past context**: Find relevant memories before starting a task
|
||||
- **Before `/restore`**: Search for specific topics across all daily logs and session snapshots
|
||||
- **Cross-session recall**: "What did we decide about X?" — search instead of scrolling
|
||||
- **Adding structured memories**: Store key decisions/patterns for retrieval
|
||||
|
||||
## Quick Reference
|
||||
|
||||
```bash
|
||||
# Search (main feature)
|
||||
agent-memory search "query" # Hybrid: 0.7 vector + 0.3 BM25
|
||||
agent-memory search "query" --vector # Vector-only (semantic)
|
||||
agent-memory search "query" --keyword # BM25-only (exact match)
|
||||
agent-memory search "query" --limit 10 --json
|
||||
|
||||
# Index management
|
||||
agent-memory index # Reindex all memory files
|
||||
agent-memory index --path /custom/path # Index specific path
|
||||
agent-memory status # File count, chunk count, last indexed
|
||||
|
||||
# CRUD
|
||||
agent-memory add "content" --tags "t1,t2" --source daily
|
||||
agent-memory list [--source memory|daily|session] [--limit 20]
|
||||
agent-memory get <id>
|
||||
|
||||
# Intelligence (requires ANTHROPIC_API_KEY)
|
||||
agent-memory ask "what do I know about X?" # Q&A over memories
|
||||
agent-memory summarize # Consolidate daily logs
|
||||
|
||||
# Code Navigation (tree-sitter AST, 165+ languages)
|
||||
agent-memory code-index ./src # Index codebase
|
||||
agent-memory code-nav "hybrid search" # Navigate to relevant code
|
||||
agent-memory code-tree # Display tree structure
|
||||
agent-memory code-summarize # Generate node summaries
|
||||
agent-memory code-refs 42 # Show cross-references
|
||||
|
||||
# Setup
|
||||
agent-memory install # Download ~67MB embedding model
|
||||
```
|
||||
|
||||
## Typical Workflow
|
||||
|
||||
1. Run `agent-memory index` to index all memory files
|
||||
2. Run `agent-memory search "topic"` to find relevant context
|
||||
3. Use `--json` flag for machine-readable output
|
||||
4. Use `agent-memory add` to store key decisions
|
||||
|
||||
## When NOT to Use
|
||||
|
||||
- For reading a specific known file → use `Read` tool directly
|
||||
- For writing to MEMORY.md → edit the file directly
|
||||
- For session state management → use `/compact` and `/restore`
|
||||
294
skills/api-and-interface-design/SKILL.md
Normal file
294
skills/api-and-interface-design/SKILL.md
Normal file
@@ -0,0 +1,294 @@
|
||||
---
|
||||
name: api-and-interface-design
|
||||
description: Guides stable API and interface design. Use when designing APIs, module boundaries, or any public interface. Use when creating REST or GraphQL endpoints, defining type contracts between modules, or establishing boundaries between frontend and backend.
|
||||
---
|
||||
|
||||
# API and Interface Design
|
||||
|
||||
## Overview
|
||||
|
||||
Design stable, well-documented interfaces that are hard to misuse. Good interfaces make the right thing easy and the wrong thing hard. This applies to REST APIs, GraphQL schemas, module boundaries, component props, and any surface where one piece of code talks to another.
|
||||
|
||||
## When to Use
|
||||
|
||||
- Designing new API endpoints
|
||||
- Defining module boundaries or contracts between teams
|
||||
- Creating component prop interfaces
|
||||
- Establishing database schema that informs API shape
|
||||
- Changing existing public interfaces
|
||||
|
||||
## Core Principles
|
||||
|
||||
### Hyrum's Law
|
||||
|
||||
> With a sufficient number of users of an API, all observable behaviors of your system will be depended on by somebody, regardless of what you promise in the contract.
|
||||
|
||||
This means: every public behavior — including undocumented quirks, error message text, timing, and ordering — becomes a de facto contract once users depend on it. Design implications:
|
||||
|
||||
- **Be intentional about what you expose.** Every observable behavior is a potential commitment.
|
||||
- **Don't leak implementation details.** If users can observe it, they will depend on it.
|
||||
- **Plan for deprecation at design time.** See `deprecation-and-migration` for how to safely remove things users depend on.
|
||||
- **Tests are not enough.** Even with perfect contract tests, Hyrum's Law means "safe" changes can break real users who depend on undocumented behavior.
|
||||
|
||||
### The One-Version Rule
|
||||
|
||||
Avoid forcing consumers to choose between multiple versions of the same dependency or API. Diamond dependency problems arise when different consumers need different versions of the same thing. Design for a world where only one version exists at a time — extend rather than fork.
|
||||
|
||||
### 1. Contract First
|
||||
|
||||
Define the interface before implementing it. The contract is the spec — implementation follows.
|
||||
|
||||
```typescript
|
||||
// Define the contract first
|
||||
interface TaskAPI {
|
||||
// Creates a task and returns the created task with server-generated fields
|
||||
createTask(input: CreateTaskInput): Promise<Task>;
|
||||
|
||||
// Returns paginated tasks matching filters
|
||||
listTasks(params: ListTasksParams): Promise<PaginatedResult<Task>>;
|
||||
|
||||
// Returns a single task or throws NotFoundError
|
||||
getTask(id: string): Promise<Task>;
|
||||
|
||||
// Partial update — only provided fields change
|
||||
updateTask(id: string, input: UpdateTaskInput): Promise<Task>;
|
||||
|
||||
// Idempotent delete — succeeds even if already deleted
|
||||
deleteTask(id: string): Promise<void>;
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Consistent Error Semantics
|
||||
|
||||
Pick one error strategy and use it everywhere:
|
||||
|
||||
```typescript
|
||||
// REST: HTTP status codes + structured error body
|
||||
// Every error response follows the same shape
|
||||
interface APIError {
|
||||
error: {
|
||||
code: string; // Machine-readable: "VALIDATION_ERROR"
|
||||
message: string; // Human-readable: "Email is required"
|
||||
details?: unknown; // Additional context when helpful
|
||||
};
|
||||
}
|
||||
|
||||
// Status code mapping
|
||||
// 400 → Client sent invalid data
|
||||
// 401 → Not authenticated
|
||||
// 403 → Authenticated but not authorized
|
||||
// 404 → Resource not found
|
||||
// 409 → Conflict (duplicate, version mismatch)
|
||||
// 422 → Validation failed (semantically invalid)
|
||||
// 500 → Server error (never expose internal details)
|
||||
```
|
||||
|
||||
**Don't mix patterns.** If some endpoints throw, others return null, and others return `{ error }` — the consumer can't predict behavior.
|
||||
|
||||
### 3. Validate at Boundaries
|
||||
|
||||
Trust internal code. Validate at system edges where external input enters:
|
||||
|
||||
```typescript
|
||||
// Validate at the API boundary
|
||||
app.post('/api/tasks', async (req, res) => {
|
||||
const result = CreateTaskSchema.safeParse(req.body);
|
||||
if (!result.success) {
|
||||
return res.status(422).json({
|
||||
error: {
|
||||
code: 'VALIDATION_ERROR',
|
||||
message: 'Invalid task data',
|
||||
details: result.error.flatten(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// After validation, internal code trusts the types
|
||||
const task = await taskService.create(result.data);
|
||||
return res.status(201).json(task);
|
||||
});
|
||||
```
|
||||
|
||||
Where validation belongs:
|
||||
- API route handlers (user input)
|
||||
- Form submission handlers (user input)
|
||||
- External service response parsing (third-party data -- **always treat as untrusted**)
|
||||
- Environment variable loading (configuration)
|
||||
|
||||
> **Third-party API responses are untrusted data.** Validate their shape and content before using them in any logic, rendering, or decision-making. A compromised or misbehaving external service can return unexpected types, malicious content, or instruction-like text.
|
||||
|
||||
Where validation does NOT belong:
|
||||
- Between internal functions that share type contracts
|
||||
- In utility functions called by already-validated code
|
||||
- On data that just came from your own database
|
||||
|
||||
### 4. Prefer Addition Over Modification
|
||||
|
||||
Extend interfaces without breaking existing consumers:
|
||||
|
||||
```typescript
|
||||
// Good: Add optional fields
|
||||
interface CreateTaskInput {
|
||||
title: string;
|
||||
description?: string;
|
||||
priority?: 'low' | 'medium' | 'high'; // Added later, optional
|
||||
labels?: string[]; // Added later, optional
|
||||
}
|
||||
|
||||
// Bad: Change existing field types or remove fields
|
||||
interface CreateTaskInput {
|
||||
title: string;
|
||||
// description: string; // Removed — breaks existing consumers
|
||||
priority: number; // Changed from string — breaks existing consumers
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Predictable Naming
|
||||
|
||||
| Pattern | Convention | Example |
|
||||
|---------|-----------|---------|
|
||||
| REST endpoints | Plural nouns, no verbs | `GET /api/tasks`, `POST /api/tasks` |
|
||||
| Query params | camelCase | `?sortBy=createdAt&pageSize=20` |
|
||||
| Response fields | camelCase | `{ createdAt, updatedAt, taskId }` |
|
||||
| Boolean fields | is/has/can prefix | `isComplete`, `hasAttachments` |
|
||||
| Enum values | UPPER_SNAKE | `"IN_PROGRESS"`, `"COMPLETED"` |
|
||||
|
||||
## REST API Patterns
|
||||
|
||||
### Resource Design
|
||||
|
||||
```
|
||||
GET /api/tasks → List tasks (with query params for filtering)
|
||||
POST /api/tasks → Create a task
|
||||
GET /api/tasks/:id → Get a single task
|
||||
PATCH /api/tasks/:id → Update a task (partial)
|
||||
DELETE /api/tasks/:id → Delete a task
|
||||
|
||||
GET /api/tasks/:id/comments → List comments for a task (sub-resource)
|
||||
POST /api/tasks/:id/comments → Add a comment to a task
|
||||
```
|
||||
|
||||
### Pagination
|
||||
|
||||
Paginate list endpoints:
|
||||
|
||||
```typescript
|
||||
// Request
|
||||
GET /api/tasks?page=1&pageSize=20&sortBy=createdAt&sortOrder=desc
|
||||
|
||||
// Response
|
||||
{
|
||||
"data": [...],
|
||||
"pagination": {
|
||||
"page": 1,
|
||||
"pageSize": 20,
|
||||
"totalItems": 142,
|
||||
"totalPages": 8
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Filtering
|
||||
|
||||
Use query parameters for filters:
|
||||
|
||||
```
|
||||
GET /api/tasks?status=in_progress&assignee=user123&createdAfter=2025-01-01
|
||||
```
|
||||
|
||||
### Partial Updates (PATCH)
|
||||
|
||||
Accept partial objects — only update what's provided:
|
||||
|
||||
```typescript
|
||||
// Only title changes, everything else preserved
|
||||
PATCH /api/tasks/123
|
||||
{ "title": "Updated title" }
|
||||
```
|
||||
|
||||
## TypeScript Interface Patterns
|
||||
|
||||
### Use Discriminated Unions for Variants
|
||||
|
||||
```typescript
|
||||
// Good: Each variant is explicit
|
||||
type TaskStatus =
|
||||
| { type: 'pending' }
|
||||
| { type: 'in_progress'; assignee: string; startedAt: Date }
|
||||
| { type: 'completed'; completedAt: Date; completedBy: string }
|
||||
| { type: 'cancelled'; reason: string; cancelledAt: Date };
|
||||
|
||||
// Consumer gets type narrowing
|
||||
function getStatusLabel(status: TaskStatus): string {
|
||||
switch (status.type) {
|
||||
case 'pending': return 'Pending';
|
||||
case 'in_progress': return `In progress (${status.assignee})`;
|
||||
case 'completed': return `Done on ${status.completedAt}`;
|
||||
case 'cancelled': return `Cancelled: ${status.reason}`;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Input/Output Separation
|
||||
|
||||
```typescript
|
||||
// Input: what the caller provides
|
||||
interface CreateTaskInput {
|
||||
title: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
// Output: what the system returns (includes server-generated fields)
|
||||
interface Task {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string | null;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
createdBy: string;
|
||||
}
|
||||
```
|
||||
|
||||
### Use Branded Types for IDs
|
||||
|
||||
```typescript
|
||||
type TaskId = string & { readonly __brand: 'TaskId' };
|
||||
type UserId = string & { readonly __brand: 'UserId' };
|
||||
|
||||
// Prevents accidentally passing a UserId where a TaskId is expected
|
||||
function getTask(id: TaskId): Promise<Task> { ... }
|
||||
```
|
||||
|
||||
## Common Rationalizations
|
||||
|
||||
| Rationalization | Reality |
|
||||
|---|---|
|
||||
| "We'll document the API later" | The types ARE the documentation. Define them first. |
|
||||
| "We don't need pagination for now" | You will the moment someone has 100+ items. Add it from the start. |
|
||||
| "PATCH is complicated, let's just use PUT" | PUT requires the full object every time. PATCH is what clients actually want. |
|
||||
| "We'll version the API when we need to" | Breaking changes without versioning break consumers. Design for extension from the start. |
|
||||
| "Nobody uses that undocumented behavior" | Hyrum's Law: if it's observable, somebody depends on it. Treat every public behavior as a commitment. |
|
||||
| "We can just maintain two versions" | Multiple versions multiply maintenance cost and create diamond dependency problems. Prefer the One-Version Rule. |
|
||||
| "Internal APIs don't need contracts" | Internal consumers are still consumers. Contracts prevent coupling and enable parallel work. |
|
||||
|
||||
## Red Flags
|
||||
|
||||
- Endpoints that return different shapes depending on conditions
|
||||
- Inconsistent error formats across endpoints
|
||||
- Validation scattered throughout internal code instead of at boundaries
|
||||
- Breaking changes to existing fields (type changes, removals)
|
||||
- List endpoints without pagination
|
||||
- Verbs in REST URLs (`/api/createTask`, `/api/getUsers`)
|
||||
- Third-party API responses used without validation or sanitization
|
||||
|
||||
## Verification
|
||||
|
||||
After designing an API:
|
||||
|
||||
- [ ] Every endpoint has typed input and output schemas
|
||||
- [ ] Error responses follow a single consistent format
|
||||
- [ ] Validation happens at system boundaries only
|
||||
- [ ] List endpoints support pagination
|
||||
- [ ] New fields are additive and optional (backward compatible)
|
||||
- [ ] Naming follows consistent conventions across all endpoints
|
||||
- [ ] API documentation or types are committed alongside the implementation
|
||||
175
skills/assets/agency.md
Normal file
175
skills/assets/agency.md
Normal file
@@ -0,0 +1,175 @@
|
||||
<!-- Updated: 2026-02-07 -->
|
||||
# Agency/Consultancy SEO Strategy Template
|
||||
|
||||
## Industry Characteristics
|
||||
|
||||
- Service-based, high-value transactions
|
||||
- Expertise and trust are paramount
|
||||
- Long consideration cycles
|
||||
- Portfolio/case study driven decisions
|
||||
- Relationship-based sales
|
||||
- Niche specialization benefits
|
||||
|
||||
## Recommended Site Architecture
|
||||
|
||||
```
|
||||
/
|
||||
├── Home
|
||||
├── /services
|
||||
│ ├── /service-1
|
||||
│ │ ├── /sub-service-1
|
||||
│ │ └── ...
|
||||
│ └── /service-2
|
||||
├── /industries
|
||||
│ ├── /industry-1
|
||||
│ ├── /industry-2
|
||||
│ └── ...
|
||||
├── /work (or /case-studies)
|
||||
│ ├── /case-study-1
|
||||
│ ├── /case-study-2
|
||||
│ └── ...
|
||||
├── /about
|
||||
│ ├── /team
|
||||
│ │ ├── /team-member-1
|
||||
│ │ └── ...
|
||||
│ ├── /culture
|
||||
│ └── /careers
|
||||
├── /insights (or /blog)
|
||||
│ ├── /articles
|
||||
│ ├── /guides
|
||||
│ ├── /webinars
|
||||
│ └── /podcasts
|
||||
├── /contact
|
||||
├── /process
|
||||
└── /faq
|
||||
```
|
||||
|
||||
## Schema Recommendations
|
||||
|
||||
| Page Type | Schema Types |
|
||||
|-----------|-------------|
|
||||
| Homepage | Organization, ProfessionalService |
|
||||
| Service Page | Service, ProfessionalService |
|
||||
| Case Study | Article, Organization (client) |
|
||||
| Team Member | Person, ProfilePage |
|
||||
| Blog | Article, BlogPosting |
|
||||
|
||||
### ProfessionalService Schema Example
|
||||
```json
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "ProfessionalService",
|
||||
"name": "Agency Name",
|
||||
"description": "What the agency does",
|
||||
"url": "https://example.com",
|
||||
"logo": "https://example.com/logo.png",
|
||||
"address": {
|
||||
"@type": "PostalAddress",
|
||||
"streetAddress": "123 Agency St",
|
||||
"addressLocality": "City",
|
||||
"addressRegion": "State",
|
||||
"postalCode": "12345"
|
||||
},
|
||||
"telephone": "+1-555-555-5555",
|
||||
"areaServed": "National",
|
||||
"hasOfferCatalog": {
|
||||
"@type": "OfferCatalog",
|
||||
"name": "Services",
|
||||
"itemListElement": [
|
||||
{
|
||||
"@type": "Offer",
|
||||
"itemOffered": {
|
||||
"@type": "Service",
|
||||
"name": "Service 1"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## E-E-A-T Requirements
|
||||
|
||||
### Team Pages Must Include
|
||||
- Professional headshots
|
||||
- Detailed bios with credentials
|
||||
- Industry experience
|
||||
- Speaking engagements
|
||||
- Publications
|
||||
- Social profiles
|
||||
|
||||
### Case Studies Must Include
|
||||
- Client name (with permission) or industry
|
||||
- Challenge/problem statement
|
||||
- Approach/methodology
|
||||
- Results with specific metrics
|
||||
- Timeline
|
||||
- Testimonial quote
|
||||
|
||||
## Content Priorities
|
||||
|
||||
### High Priority
|
||||
1. Service pages (detailed, specific)
|
||||
2. Industry pages (vertical expertise)
|
||||
3. 3-5 detailed case studies
|
||||
4. Team/leadership pages
|
||||
|
||||
### Medium Priority
|
||||
1. Methodology/process page
|
||||
2. Blog with thought leadership
|
||||
3. Comparison content (vs alternatives)
|
||||
4. FAQ page
|
||||
|
||||
### Thought Leadership Topics
|
||||
- Industry trend analysis
|
||||
- How-to guides (non-competitive)
|
||||
- Original research/surveys
|
||||
- Event recaps and insights
|
||||
- Expert interviews
|
||||
- Tool/technology reviews
|
||||
|
||||
## Content Strategy
|
||||
|
||||
### Service Pages (min 800 words)
|
||||
- Clear value proposition
|
||||
- Methodology overview
|
||||
- Deliverables list
|
||||
- Relevant case studies
|
||||
- Team members who deliver this service
|
||||
- CTA to schedule consultation
|
||||
|
||||
### Industry Pages (min 800 words)
|
||||
- Industry-specific challenges
|
||||
- How you solve them differently
|
||||
- Relevant case studies
|
||||
- Industry credentials/experience
|
||||
- Client logos (with permission)
|
||||
|
||||
### Case Studies (min 1,000 words)
|
||||
- Executive summary
|
||||
- Client background
|
||||
- Challenge details
|
||||
- Solution approach
|
||||
- Implementation process
|
||||
- Measurable results
|
||||
- Client testimonial
|
||||
- Related services/CTA
|
||||
|
||||
## Key Metrics to Track
|
||||
|
||||
- Organic traffic to service pages
|
||||
- Case study page views
|
||||
- Contact form submissions from organic
|
||||
- Time on page for key content
|
||||
- Blog → service page conversion
|
||||
|
||||
## Generative Engine Optimization (GEO) for Agencies
|
||||
|
||||
- [ ] Publish original case studies with specific, citable metrics and results
|
||||
- [ ] Use Person schema with sameAs links for all team members (builds entity authority)
|
||||
- [ ] Use ProfilePage schema for team member pages
|
||||
- [ ] Include clear, quotable expertise statements in service page descriptions
|
||||
- [ ] Produce original industry research and surveys AI systems can cite
|
||||
- [ ] Structure thought leadership content with clear headings and extractable insights
|
||||
- [ ] Maintain consistent agency entity information across directories, social profiles, and industry sites
|
||||
- [ ] Monitor AI citation in ChatGPT, Perplexity, and Google AI Overviews for brand and key service terms
|
||||
175
skills/assets/android_frame.jsx
Normal file
175
skills/assets/android_frame.jsx
Normal file
@@ -0,0 +1,175 @@
|
||||
/**
|
||||
* AndroidFrame — Android设备边框(参考Pixel 8系列)
|
||||
*
|
||||
* 含:punch-hole相机 + 状态栏 + 导航栏 + 圆角
|
||||
*
|
||||
* 用法:
|
||||
* <AndroidFrame time="9:41" battery={85}>
|
||||
* <YourAppContent />
|
||||
* </AndroidFrame>
|
||||
*/
|
||||
|
||||
const androidFrameStyles = {
|
||||
wrapper: {
|
||||
display: 'inline-block',
|
||||
padding: 10,
|
||||
background: '#1a1a1a',
|
||||
borderRadius: 44,
|
||||
boxShadow: '0 0 0 2px #2a2a2a, 0 20px 60px rgba(0,0,0,0.3)',
|
||||
position: 'relative',
|
||||
},
|
||||
screen: {
|
||||
position: 'relative',
|
||||
borderRadius: 36,
|
||||
overflow: 'hidden',
|
||||
background: '#fff',
|
||||
},
|
||||
statusBar: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: 32,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
padding: '0 24px',
|
||||
fontSize: 14,
|
||||
fontWeight: 500,
|
||||
fontFamily: 'Roboto, -apple-system, sans-serif',
|
||||
zIndex: 20,
|
||||
pointerEvents: 'none',
|
||||
},
|
||||
punchHole: {
|
||||
position: 'absolute',
|
||||
top: 10,
|
||||
left: '50%',
|
||||
transform: 'translateX(-50%)',
|
||||
width: 14,
|
||||
height: 14,
|
||||
background: '#000',
|
||||
borderRadius: '50%',
|
||||
zIndex: 30,
|
||||
},
|
||||
statusIcons: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 6,
|
||||
},
|
||||
batteryText: {
|
||||
fontSize: 11,
|
||||
fontWeight: 600,
|
||||
marginLeft: 2,
|
||||
},
|
||||
content: {
|
||||
position: 'absolute',
|
||||
top: 32,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 24,
|
||||
overflow: 'auto',
|
||||
},
|
||||
navBar: {
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: 24,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: 60,
|
||||
zIndex: 10,
|
||||
},
|
||||
navButton: {
|
||||
width: 36,
|
||||
height: 4,
|
||||
background: 'rgba(0,0,0,0.3)',
|
||||
borderRadius: 999,
|
||||
},
|
||||
};
|
||||
|
||||
function AndroidFrame({
|
||||
children,
|
||||
width = 412,
|
||||
height = 892,
|
||||
time = '9:41',
|
||||
battery = 100,
|
||||
darkMode = false,
|
||||
navStyle = 'gesture',
|
||||
}) {
|
||||
const textColor = darkMode ? '#fff' : '#1a1a1a';
|
||||
|
||||
return (
|
||||
<div style={androidFrameStyles.wrapper}>
|
||||
<div style={{
|
||||
...androidFrameStyles.screen,
|
||||
width,
|
||||
height,
|
||||
background: darkMode ? '#000' : '#fff',
|
||||
}}>
|
||||
<div style={{ ...androidFrameStyles.statusBar, color: textColor }}>
|
||||
<span>{time}</span>
|
||||
<div style={androidFrameStyles.statusIcons}>
|
||||
<svg width="14" height="10" viewBox="0 0 14 10" fill="currentColor">
|
||||
<rect x="0" y="6" width="2" height="4" rx="0.5" />
|
||||
<rect x="4" y="4" width="2" height="6" rx="0.5" />
|
||||
<rect x="8" y="2" width="2" height="8" rx="0.5" />
|
||||
<rect x="12" y="0" width="2" height="10" rx="0.5" />
|
||||
</svg>
|
||||
<svg width="14" height="10" viewBox="0 0 14 10" fill="none">
|
||||
<path d="M7 9a1 1 0 100-2 1 1 0 000 2z" fill="currentColor" />
|
||||
<path d="M3 6a5 5 0 018 0" stroke="currentColor" strokeWidth="1.2" />
|
||||
<path d="M0.5 3.5a11 11 0 0113 0" stroke="currentColor" strokeWidth="1.2" opacity="0.6" />
|
||||
</svg>
|
||||
<div style={{
|
||||
width: 22,
|
||||
height: 10,
|
||||
border: '1.5px solid currentColor',
|
||||
borderRadius: 2,
|
||||
padding: 1,
|
||||
position: 'relative',
|
||||
}}>
|
||||
<div style={{
|
||||
width: `${battery}%`,
|
||||
height: '100%',
|
||||
background: 'currentColor',
|
||||
borderRadius: 1,
|
||||
}} />
|
||||
</div>
|
||||
<span style={androidFrameStyles.batteryText}>{battery}%</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={androidFrameStyles.punchHole} />
|
||||
|
||||
<div style={androidFrameStyles.content}>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
{navStyle === 'gesture' && (
|
||||
<div style={androidFrameStyles.navBar}>
|
||||
<div style={{
|
||||
...androidFrameStyles.navButton,
|
||||
width: 100,
|
||||
height: 4,
|
||||
background: darkMode ? 'rgba(255,255,255,0.5)' : 'rgba(0,0,0,0.4)',
|
||||
}} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{navStyle === 'buttons' && (
|
||||
<div style={androidFrameStyles.navBar}>
|
||||
<span style={{ color: textColor, fontSize: 20 }}>◁</span>
|
||||
<span style={{ color: textColor, fontSize: 16 }}>○</span>
|
||||
<span style={{ color: textColor, fontSize: 16 }}>□</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
window.AndroidFrame = AndroidFrame;
|
||||
}
|
||||
340
skills/assets/animations.jsx
Normal file
340
skills/assets/animations.jsx
Normal file
@@ -0,0 +1,340 @@
|
||||
/**
|
||||
* animations.jsx — 时间轴动画引擎
|
||||
*
|
||||
* Stage + Sprite 模式,借鉴Remotion但轻量化。
|
||||
*
|
||||
* 导出(挂到 window.Animations):
|
||||
* - Stage: 整个动画容器,提供时间+控制
|
||||
* - Sprite: 时间片段,start/end内显示,提供本地进度
|
||||
* - useTime(): 读全局时间(秒)
|
||||
* - useSprite(): 读本地进度 {t: 0→1, elapsed: seconds, duration: seconds}
|
||||
* - Easing: {linear, easeIn, easeOut, easeInOut, spring, anticipation}
|
||||
* - interpolate(t, [input0, input1], [output0, output1], easing?)
|
||||
*
|
||||
* 用法:
|
||||
* <Stage duration={10}>
|
||||
* <Sprite start={0} end={3}>
|
||||
* <Title />
|
||||
* </Sprite>
|
||||
* <Sprite start={2} end={5}>
|
||||
* <Subtitle />
|
||||
* </Sprite>
|
||||
* </Stage>
|
||||
*
|
||||
* 在Sprite子组件里用 useSprite() 读当前片段进度。
|
||||
*/
|
||||
|
||||
(function() {
|
||||
const { createContext, useContext, useState, useEffect, useRef, useCallback } = React;
|
||||
|
||||
const TimeContext = createContext({ time: 0, duration: 10, playing: false });
|
||||
const SpriteContext = createContext(null);
|
||||
|
||||
const Easing = {
|
||||
linear: t => t,
|
||||
easeIn: t => t * t,
|
||||
easeOut: t => 1 - (1 - t) * (1 - t),
|
||||
easeInOut: t => t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2,
|
||||
// expoOut: Anthropic-level 主 easing (cubic-bezier(0.16, 1, 0.3, 1))
|
||||
// 迅速启动 + 缓慢刹车,给数字元素物理重量感
|
||||
expoOut: t => t === 1 ? 1 : 1 - Math.pow(2, -10 * t),
|
||||
// overshoot: 带弹性的 toggle/按钮弹出 (cubic-bezier(0.34, 1.56, 0.64, 1))
|
||||
overshoot: t => {
|
||||
const c1 = 1.70158, c3 = c1 + 1;
|
||||
return 1 + c3 * Math.pow(t - 1, 3) + c1 * Math.pow(t - 1, 2);
|
||||
},
|
||||
spring: t => {
|
||||
const c = (2 * Math.PI) / 3;
|
||||
return t === 0 ? 0 : t === 1 ? 1 : Math.pow(2, -10 * t) * Math.sin((t * 10 - 0.75) * c) + 1;
|
||||
},
|
||||
anticipation: t => {
|
||||
if (t < 0.2) return -0.3 * (t / 0.2) * (t / 0.2);
|
||||
const adjusted = (t - 0.2) / 0.8;
|
||||
return -0.012 + 1.012 * adjusted * adjusted * (3 - 2 * adjusted);
|
||||
},
|
||||
};
|
||||
|
||||
function interpolate(t, input, output, easing) {
|
||||
const [inStart, inEnd] = input;
|
||||
const [outStart, outEnd] = output;
|
||||
|
||||
if (t <= inStart) return outStart;
|
||||
if (t >= inEnd) return outEnd;
|
||||
|
||||
let progress = (t - inStart) / (inEnd - inStart);
|
||||
if (easing) {
|
||||
progress = easing(progress);
|
||||
}
|
||||
|
||||
return outStart + (outEnd - outStart) * progress;
|
||||
}
|
||||
|
||||
function useTime() {
|
||||
const ctx = useContext(TimeContext);
|
||||
return ctx.time;
|
||||
}
|
||||
|
||||
function useSprite() {
|
||||
const sprite = useContext(SpriteContext);
|
||||
if (!sprite) {
|
||||
return { t: 0, elapsed: 0, duration: 0 };
|
||||
}
|
||||
return sprite;
|
||||
}
|
||||
|
||||
const stageStyles = {
|
||||
wrapper: {
|
||||
position: 'fixed',
|
||||
inset: 0,
|
||||
background: '#000',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
fontFamily: '-apple-system, sans-serif',
|
||||
},
|
||||
stageHolder: {
|
||||
flex: 1,
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
},
|
||||
canvas: {
|
||||
position: 'absolute',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transformOrigin: 'center center',
|
||||
background: '#111',
|
||||
overflow: 'hidden',
|
||||
},
|
||||
controls: {
|
||||
position: 'fixed',
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
background: 'rgba(0, 0, 0, 0.8)',
|
||||
backdropFilter: 'blur(10px)',
|
||||
padding: '12px 20px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 16,
|
||||
color: '#fff',
|
||||
fontSize: 12,
|
||||
zIndex: 100,
|
||||
},
|
||||
button: {
|
||||
background: 'none',
|
||||
border: '1px solid rgba(255,255,255,0.3)',
|
||||
color: '#fff',
|
||||
padding: '6px 14px',
|
||||
borderRadius: 4,
|
||||
cursor: 'pointer',
|
||||
fontSize: 12,
|
||||
},
|
||||
timeDisplay: {
|
||||
fontFamily: 'ui-monospace, monospace',
|
||||
fontVariantNumeric: 'tabular-nums',
|
||||
minWidth: 90,
|
||||
},
|
||||
scrubber: {
|
||||
flex: 1,
|
||||
height: 4,
|
||||
background: 'rgba(255,255,255,0.2)',
|
||||
borderRadius: 2,
|
||||
position: 'relative',
|
||||
cursor: 'pointer',
|
||||
},
|
||||
scrubberFill: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
height: '100%',
|
||||
background: '#fff',
|
||||
borderRadius: 2,
|
||||
pointerEvents: 'none',
|
||||
},
|
||||
scrubberHandle: {
|
||||
position: 'absolute',
|
||||
top: '50%',
|
||||
width: 12,
|
||||
height: 12,
|
||||
background: '#fff',
|
||||
borderRadius: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
pointerEvents: 'none',
|
||||
},
|
||||
};
|
||||
|
||||
function Stage({ duration = 10, width = 1920, height = 1080, fps = 60, loop = true, children, bgColor = '#fff' }) {
|
||||
const [time, setTime] = useState(0);
|
||||
const [playing, setPlaying] = useState(true);
|
||||
const [scale, setScale] = useState(1);
|
||||
const rafRef = useRef(null);
|
||||
const startTimeRef = useRef(performance.now());
|
||||
const canvasRef = useRef(null);
|
||||
|
||||
// Recording mode: render-video.js injects window.__recording = true before goto.
|
||||
// When set, force loop=false so the export ends on the final frame instead of
|
||||
// wrapping back to t=0 and capturing the start of the next cycle.
|
||||
// (Browsers viewing manually still loop because __recording is undefined there.)
|
||||
const effectiveLoop = (typeof window !== 'undefined' && window.__recording) ? false : loop;
|
||||
|
||||
useEffect(() => {
|
||||
function updateScale() {
|
||||
const vw = window.innerWidth;
|
||||
const vh = window.innerHeight - 56;
|
||||
const s = Math.min(vw / width, vh / height);
|
||||
setScale(s);
|
||||
}
|
||||
updateScale();
|
||||
window.addEventListener('resize', updateScale);
|
||||
return () => window.removeEventListener('resize', updateScale);
|
||||
}, [width, height]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!playing) return;
|
||||
let cancelled = false;
|
||||
let last = null;
|
||||
|
||||
function tick(now) {
|
||||
if (cancelled) return;
|
||||
if (last === null) {
|
||||
// First animation frame. Set last=now so delta starts at 0,
|
||||
// AND announce readiness for video export.
|
||||
// This pairing is critical: window.__ready must flip to true at
|
||||
// the exact moment WebM captures frame 0 of the animation, so
|
||||
// render-video.js's trim offset equals the pre-animation gap.
|
||||
last = now;
|
||||
if (typeof window !== 'undefined') window.__ready = true;
|
||||
}
|
||||
const delta = (now - last) / 1000;
|
||||
last = now;
|
||||
setTime(prev => {
|
||||
const next = prev + delta;
|
||||
if (next >= duration) {
|
||||
// effectiveLoop honors window.__recording (forced non-loop during export).
|
||||
// Stop just shy of duration so the final-frame state stays rendered
|
||||
// (avoids exiting all Sprites that end exactly at `duration`).
|
||||
return effectiveLoop ? 0 : duration - 0.001;
|
||||
}
|
||||
return next;
|
||||
});
|
||||
rafRef.current = requestAnimationFrame(tick);
|
||||
}
|
||||
|
||||
// Wait for fonts before starting the clock — makes frame 0 the
|
||||
// real "finished-loading" frame users see, not a fallback-font flash.
|
||||
const startAfterFonts = () => {
|
||||
if (cancelled) return;
|
||||
rafRef.current = requestAnimationFrame(tick);
|
||||
};
|
||||
if (typeof document !== 'undefined' && document.fonts && document.fonts.ready) {
|
||||
document.fonts.ready.then(startAfterFonts);
|
||||
} else {
|
||||
startAfterFonts();
|
||||
}
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
cancelAnimationFrame(rafRef.current);
|
||||
};
|
||||
}, [playing, duration, effectiveLoop]);
|
||||
|
||||
const handleScrub = useCallback((e) => {
|
||||
const rect = e.currentTarget.getBoundingClientRect();
|
||||
const ratio = (e.clientX - rect.left) / rect.width;
|
||||
setTime(Math.max(0, Math.min(duration, ratio * duration)));
|
||||
}, [duration]);
|
||||
|
||||
const handleSeek = useCallback((e) => {
|
||||
handleScrub(e);
|
||||
setPlaying(false);
|
||||
}, [handleScrub]);
|
||||
|
||||
const progress = time / duration;
|
||||
|
||||
const ctx = {
|
||||
time,
|
||||
duration,
|
||||
playing,
|
||||
setPlaying,
|
||||
setTime,
|
||||
};
|
||||
|
||||
const canvasStyle = {
|
||||
...stageStyles.canvas,
|
||||
width,
|
||||
height,
|
||||
background: bgColor,
|
||||
transform: `translate(-50%, -50%) scale(${scale})`,
|
||||
};
|
||||
|
||||
return (
|
||||
<TimeContext.Provider value={ctx}>
|
||||
<div style={stageStyles.wrapper}>
|
||||
<div style={stageStyles.stageHolder}>
|
||||
<div ref={canvasRef} style={canvasStyle}>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={stageStyles.controls}>
|
||||
<button
|
||||
style={stageStyles.button}
|
||||
onClick={() => setPlaying(p => !p)}
|
||||
>
|
||||
{playing ? '⏸ 暂停' : '▶ 播放'}
|
||||
</button>
|
||||
|
||||
<button
|
||||
style={stageStyles.button}
|
||||
onClick={() => setTime(0)}
|
||||
>
|
||||
⏮ 开始
|
||||
</button>
|
||||
|
||||
<div style={stageStyles.timeDisplay}>
|
||||
{time.toFixed(2)}s / {duration.toFixed(2)}s
|
||||
</div>
|
||||
|
||||
<div style={stageStyles.scrubber} onMouseDown={handleSeek}>
|
||||
<div style={{ ...stageStyles.scrubberFill, width: `${progress * 100}%` }} />
|
||||
<div style={{ ...stageStyles.scrubberHandle, left: `${progress * 100}%` }} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TimeContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
function Sprite({ start = 0, end, children, style }) {
|
||||
const { time } = useContext(TimeContext);
|
||||
const actualEnd = end == null ? Infinity : end;
|
||||
|
||||
if (time < start || time >= actualEnd) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const duration = actualEnd - start;
|
||||
const elapsed = time - start;
|
||||
const t = duration === 0 ? 1 : Math.max(0, Math.min(1, elapsed / duration));
|
||||
|
||||
const spriteValue = { t, elapsed, duration, start, end: actualEnd };
|
||||
|
||||
return (
|
||||
<SpriteContext.Provider value={spriteValue}>
|
||||
<div style={{ position: 'absolute', inset: 0, ...style }}>
|
||||
{children}
|
||||
</div>
|
||||
</SpriteContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
window.Animations = {
|
||||
Stage,
|
||||
Sprite,
|
||||
useTime,
|
||||
useSprite,
|
||||
Easing,
|
||||
interpolate,
|
||||
};
|
||||
}
|
||||
})();
|
||||
138
skills/assets/animations/animations.css
Normal file
138
skills/assets/animations/animations.css
Normal file
@@ -0,0 +1,138 @@
|
||||
/* html-ppt :: animations.css
|
||||
* Apply by adding class="anim-<name>" or data-anim="<name>".
|
||||
* Durations are deliberately snappy; tweak --anim-dur per element.
|
||||
*/
|
||||
:root{--anim-dur:.7s;--anim-ease:cubic-bezier(.4,0,.2,1)}
|
||||
|
||||
/* ---------- FADE DIRECTIONALS ---------- */
|
||||
@keyframes kf-fade-up{from{opacity:0;transform:translateY(32px)}to{opacity:1;transform:none}}
|
||||
@keyframes kf-fade-down{from{opacity:0;transform:translateY(-32px)}to{opacity:1;transform:none}}
|
||||
@keyframes kf-fade-left{from{opacity:0;transform:translateX(-40px)}to{opacity:1;transform:none}}
|
||||
@keyframes kf-fade-right{from{opacity:0;transform:translateX(40px)}to{opacity:1;transform:none}}
|
||||
.anim-fade-up{animation:kf-fade-up var(--anim-dur) var(--anim-ease) both}
|
||||
.anim-fade-down{animation:kf-fade-down var(--anim-dur) var(--anim-ease) both}
|
||||
.anim-fade-left{animation:kf-fade-left var(--anim-dur) var(--anim-ease) both}
|
||||
.anim-fade-right{animation:kf-fade-right var(--anim-dur) var(--anim-ease) both}
|
||||
|
||||
/* ---------- RISE / DROP / ZOOM / BLUR / GLITCH ---------- */
|
||||
@keyframes kf-rise{from{opacity:0;transform:translateY(60px) scale(.97);filter:blur(6px)}to{opacity:1;transform:none;filter:none}}
|
||||
@keyframes kf-drop{from{opacity:0;transform:translateY(-60px) scale(.97)}to{opacity:1;transform:none}}
|
||||
@keyframes kf-zoom{0%{opacity:0;transform:scale(.6)}60%{transform:scale(1.04)}100%{opacity:1;transform:scale(1)}}
|
||||
@keyframes kf-blur{from{opacity:0;filter:blur(18px)}to{opacity:1;filter:none}}
|
||||
@keyframes kf-glitch{0%{opacity:0;transform:translateX(0);clip-path:inset(0 0 0 0)}
|
||||
20%{opacity:1;transform:translateX(-6px);clip-path:inset(20% 0 30% 0)}
|
||||
40%{transform:translateX(4px);clip-path:inset(50% 0 10% 0)}
|
||||
60%{transform:translateX(-3px);clip-path:inset(10% 0 60% 0)}
|
||||
80%{transform:translateX(2px);clip-path:inset(0 0 0 0)}
|
||||
100%{opacity:1;transform:none}}
|
||||
.anim-rise-in{animation:kf-rise .9s var(--anim-ease) both}
|
||||
.anim-drop-in{animation:kf-drop .8s var(--anim-ease) both}
|
||||
.anim-zoom-pop{animation:kf-zoom .7s cubic-bezier(.22,1.3,.36,1) both}
|
||||
.anim-blur-in{animation:kf-blur .8s var(--anim-ease) both}
|
||||
.anim-glitch-in{animation:kf-glitch .8s steps(5,end) both}
|
||||
|
||||
/* ---------- TYPEWRITER ---------- */
|
||||
.anim-typewriter{display:inline-block;overflow:hidden;white-space:nowrap;border-right:2px solid currentColor;
|
||||
width:0;animation:kf-type 2.4s steps(40,end) forwards, kf-caret 1s step-end infinite}
|
||||
@keyframes kf-type{to{width:100%}}
|
||||
@keyframes kf-caret{50%{border-color:transparent}}
|
||||
|
||||
/* ---------- GLOW / SHIMMER / GRADIENT-FLOW ---------- */
|
||||
@keyframes kf-neon{0%,100%{text-shadow:0 0 8px var(--accent),0 0 20px var(--accent)}
|
||||
50%{text-shadow:0 0 16px var(--accent),0 0 40px var(--accent),0 0 80px var(--accent)}}
|
||||
.anim-neon-glow{animation:kf-neon 2s ease-in-out infinite}
|
||||
|
||||
.anim-shimmer-sweep{position:relative;overflow:hidden}
|
||||
.anim-shimmer-sweep::after{content:"";position:absolute;inset:0;
|
||||
background:linear-gradient(110deg,transparent 40%,rgba(255,255,255,.55) 50%,transparent 60%);
|
||||
transform:translateX(-100%);animation:kf-shimmer 2.4s var(--anim-ease) infinite}
|
||||
@keyframes kf-shimmer{to{transform:translateX(100%)}}
|
||||
|
||||
.anim-gradient-flow{background:linear-gradient(90deg,var(--accent),var(--accent-2,var(--accent)),var(--accent-3,var(--accent)),var(--accent));
|
||||
background-size:300% 100%;-webkit-background-clip:text;background-clip:text;color:transparent;-webkit-text-fill-color:transparent;
|
||||
animation:kf-gradflow 4s linear infinite}
|
||||
@keyframes kf-gradflow{to{background-position:300% 0}}
|
||||
|
||||
/* ---------- STAGGER LIST ---------- */
|
||||
.anim-stagger-list > *{opacity:0;animation:kf-rise .65s var(--anim-ease) both}
|
||||
.anim-stagger-list > *:nth-child(1){animation-delay:.05s}
|
||||
.anim-stagger-list > *:nth-child(2){animation-delay:.15s}
|
||||
.anim-stagger-list > *:nth-child(3){animation-delay:.25s}
|
||||
.anim-stagger-list > *:nth-child(4){animation-delay:.35s}
|
||||
.anim-stagger-list > *:nth-child(5){animation-delay:.45s}
|
||||
.anim-stagger-list > *:nth-child(6){animation-delay:.55s}
|
||||
.anim-stagger-list > *:nth-child(7){animation-delay:.65s}
|
||||
.anim-stagger-list > *:nth-child(8){animation-delay:.75s}
|
||||
.anim-stagger-list > *:nth-child(n+9){animation-delay:.85s}
|
||||
|
||||
/* ---------- COUNTER-UP (JS-driven, marker class only) ---------- */
|
||||
.counter{font-variant-numeric:tabular-nums}
|
||||
|
||||
/* ---------- SVG PATH DRAW ---------- */
|
||||
.anim-path-draw path,.anim-path-draw line,.anim-path-draw polyline,.anim-path-draw circle,.anim-path-draw rect{
|
||||
stroke-dasharray:1000;stroke-dashoffset:1000;animation:kf-draw 2s var(--anim-ease) forwards}
|
||||
@keyframes kf-draw{to{stroke-dashoffset:0}}
|
||||
|
||||
/* ---------- PARALLAX TILT (hover) ---------- */
|
||||
.anim-parallax-tilt{transform-style:preserve-3d;transition:transform .4s var(--anim-ease)}
|
||||
.anim-parallax-tilt:hover{transform:perspective(900px) rotateX(6deg) rotateY(-8deg) translateZ(10px)}
|
||||
|
||||
/* ---------- CARD FLIP 3D ---------- */
|
||||
@keyframes kf-flip{from{transform:perspective(1200px) rotateY(-90deg);opacity:0}
|
||||
to{transform:perspective(1200px) rotateY(0);opacity:1}}
|
||||
.anim-card-flip-3d{animation:kf-flip .9s var(--anim-ease) both;transform-style:preserve-3d;backface-visibility:hidden}
|
||||
|
||||
/* ---------- CUBE ROTATE 3D ---------- */
|
||||
@keyframes kf-cube{from{transform:perspective(1200px) rotateX(20deg) rotateY(-90deg) translateZ(-200px);opacity:0}
|
||||
to{transform:perspective(1200px) rotateX(0) rotateY(0) translateZ(0);opacity:1}}
|
||||
.anim-cube-rotate-3d{animation:kf-cube 1s var(--anim-ease) both}
|
||||
|
||||
/* ---------- PAGE TURN 3D ---------- */
|
||||
@keyframes kf-pageturn{from{transform:perspective(1600px) rotateY(-85deg);transform-origin:left center;opacity:0}
|
||||
to{transform:perspective(1600px) rotateY(0);opacity:1}}
|
||||
.anim-page-turn-3d{animation:kf-pageturn 1s var(--anim-ease) both;transform-origin:left center}
|
||||
|
||||
/* ---------- PERSPECTIVE ZOOM ---------- */
|
||||
@keyframes kf-pzoom{from{opacity:0;transform:perspective(1400px) translateZ(-400px) rotateX(12deg)}
|
||||
to{opacity:1;transform:none}}
|
||||
.anim-perspective-zoom{animation:kf-pzoom 1s var(--anim-ease) both}
|
||||
|
||||
/* ---------- MARQUEE SCROLL ---------- */
|
||||
.anim-marquee-scroll{display:flex;gap:48px;white-space:nowrap;animation:kf-marquee 20s linear infinite}
|
||||
@keyframes kf-marquee{from{transform:translateX(0)}to{transform:translateX(-50%)}}
|
||||
|
||||
/* ---------- KEN BURNS ---------- */
|
||||
@keyframes kf-kenburns{0%{transform:scale(1) translate(0,0)}100%{transform:scale(1.15) translate(-2%,-1%)}}
|
||||
.anim-kenburns{animation:kf-kenburns 14s ease-in-out infinite alternate}
|
||||
|
||||
/* ---------- CONFETTI BURST (pseudo — pure CSS sparkles) ---------- */
|
||||
.anim-confetti-burst{position:relative}
|
||||
.anim-confetti-burst::before,.anim-confetti-burst::after{
|
||||
content:"";position:absolute;top:50%;left:50%;width:8px;height:8px;border-radius:50%;
|
||||
background:var(--accent);box-shadow:
|
||||
20px -30px 0 var(--accent-2,var(--accent)),-25px -20px 0 var(--accent-3,var(--accent)),
|
||||
30px 20px 0 var(--good,#1aaf6c),-30px 25px 0 var(--warn,#f5a524),
|
||||
40px -10px 0 var(--bad,#e0445a),-45px 0 0 var(--accent),
|
||||
10px 40px 0 var(--accent-2,var(--accent)),-15px -40px 0 var(--accent-3,var(--accent));
|
||||
opacity:0;animation:kf-confetti 1.2s var(--anim-ease) forwards}
|
||||
.anim-confetti-burst::after{animation-delay:.15s;transform:rotate(45deg)}
|
||||
@keyframes kf-confetti{0%{opacity:0;transform:scale(.2)}30%{opacity:1}100%{opacity:0;transform:scale(2.2)}}
|
||||
|
||||
/* ---------- SPOTLIGHT ---------- */
|
||||
@keyframes kf-spot{0%{clip-path:circle(0% at 50% 50%)}100%{clip-path:circle(140% at 50% 50%)}}
|
||||
.anim-spotlight{animation:kf-spot 1.1s var(--anim-ease) both}
|
||||
|
||||
/* ---------- MORPH SHAPE (SVG) ---------- */
|
||||
.anim-morph-shape path{animation:kf-morph 6s ease-in-out infinite alternate}
|
||||
@keyframes kf-morph{0%{d:path("M60,120 Q120,20 180,120 T300,120")}
|
||||
100%{d:path("M60,120 Q120,220 180,120 T300,120")}}
|
||||
|
||||
/* ---------- RIPPLE REVEAL ---------- */
|
||||
@keyframes kf-ripple{0%{clip-path:circle(0% at 20% 80%);opacity:.4}
|
||||
100%{clip-path:circle(160% at 20% 80%);opacity:1}}
|
||||
.anim-ripple-reveal{animation:kf-ripple 1.2s var(--anim-ease) both}
|
||||
|
||||
/* reduced motion */
|
||||
@media (prefers-reduced-motion: reduce){
|
||||
[class*="anim-"]{animation:none!important;transition:none!important}
|
||||
}
|
||||
99
skills/assets/animations/fx-runtime.js
Normal file
99
skills/assets/animations/fx-runtime.js
Normal file
@@ -0,0 +1,99 @@
|
||||
/* html-ppt :: fx-runtime.js
|
||||
* Canvas FX autoloader + lifecycle manager.
|
||||
* - Dynamically loads all fx modules listed in FX_LIST
|
||||
* - Initializes [data-fx] elements when their slide becomes active
|
||||
* - Calls handle.stop() when the slide leaves
|
||||
*/
|
||||
(function(){
|
||||
'use strict';
|
||||
|
||||
const FX_LIST = [
|
||||
'_util',
|
||||
'particle-burst','confetti-cannon','firework','starfield','matrix-rain',
|
||||
'knowledge-graph','neural-net','constellation','orbit-ring','galaxy-swirl',
|
||||
'word-cascade','letter-explode','chain-react','magnetic-field','data-stream',
|
||||
'gradient-blob','sparkle-trail','shockwave','typewriter-multi','counter-explosion'
|
||||
];
|
||||
|
||||
// Resolve base path of this script so it works from any page location.
|
||||
const myScript = document.currentScript || (function(){
|
||||
const all = document.getElementsByTagName('script');
|
||||
for (const s of all){ if (s.src && s.src.indexOf('fx-runtime.js')>-1) return s; }
|
||||
return null;
|
||||
})();
|
||||
const base = myScript ? myScript.src.replace(/fx-runtime\.js.*$/, 'fx/') : 'assets/animations/fx/';
|
||||
|
||||
let loaded = 0;
|
||||
const total = FX_LIST.length;
|
||||
const ready = new Promise((resolve) => {
|
||||
if (!total) return resolve();
|
||||
FX_LIST.forEach((name) => {
|
||||
const s = document.createElement('script');
|
||||
s.src = base + name + '.js';
|
||||
s.async = false;
|
||||
s.onload = s.onerror = () => { if (++loaded >= total) resolve(); };
|
||||
document.head.appendChild(s);
|
||||
});
|
||||
});
|
||||
|
||||
window.__hpxActive = window.__hpxActive || new Map();
|
||||
|
||||
function initFxIn(root){
|
||||
if (!window.HPX) return;
|
||||
const els = root.querySelectorAll('[data-fx]');
|
||||
els.forEach((el) => {
|
||||
if (window.__hpxActive.has(el)) return;
|
||||
const name = el.getAttribute('data-fx');
|
||||
const fn = window.HPX[name];
|
||||
if (typeof fn !== 'function') return;
|
||||
try {
|
||||
const handle = fn(el, {}) || { stop(){} };
|
||||
window.__hpxActive.set(el, handle);
|
||||
} catch(e){ console.warn('[hpx-fx]', name, e); }
|
||||
});
|
||||
}
|
||||
|
||||
function stopFxIn(root){
|
||||
const els = root.querySelectorAll('[data-fx]');
|
||||
els.forEach((el) => {
|
||||
const h = window.__hpxActive.get(el);
|
||||
if (h && typeof h.stop === 'function'){
|
||||
try{ h.stop(); }catch(e){}
|
||||
}
|
||||
window.__hpxActive.delete(el);
|
||||
});
|
||||
}
|
||||
|
||||
function reinitFxIn(root){
|
||||
stopFxIn(root);
|
||||
initFxIn(root);
|
||||
}
|
||||
window.__hpxReinit = reinitFxIn;
|
||||
|
||||
function boot(){
|
||||
ready.then(() => {
|
||||
const active = document.querySelector('.slide.is-active') || document.querySelector('.slide');
|
||||
if (active) initFxIn(active);
|
||||
|
||||
// Watch all slides for class changes
|
||||
const slides = document.querySelectorAll('.slide');
|
||||
slides.forEach((sl) => {
|
||||
const mo = new MutationObserver((muts) => {
|
||||
for (const m of muts){
|
||||
if (m.attributeName === 'class'){
|
||||
if (sl.classList.contains('is-active')) initFxIn(sl);
|
||||
else stopFxIn(sl);
|
||||
}
|
||||
}
|
||||
});
|
||||
mo.observe(sl, { attributes: true, attributeFilter: ['class'] });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading'){
|
||||
document.addEventListener('DOMContentLoaded', boot);
|
||||
} else {
|
||||
boot();
|
||||
}
|
||||
})();
|
||||
63
skills/assets/animations/fx/_util.js
Normal file
63
skills/assets/animations/fx/_util.js
Normal file
@@ -0,0 +1,63 @@
|
||||
/* html-ppt fx :: shared helpers */
|
||||
(function(){
|
||||
window.HPX = window.HPX || {};
|
||||
const U = window.HPX._u = {};
|
||||
|
||||
U.css = (el, name, fb) => {
|
||||
const v = getComputedStyle(el).getPropertyValue(name).trim();
|
||||
return v || fb;
|
||||
};
|
||||
|
||||
U.accent = (el, fb) => U.css(el, '--accent', fb || '#7c5cff');
|
||||
U.accent2 = (el, fb) => U.css(el, '--accent-2', fb || '#22d3ee');
|
||||
U.text = (el, fb) => U.css(el, '--text-1', fb || '#eaeaf2');
|
||||
|
||||
U.palette = (el) => [
|
||||
U.accent(el, '#7c5cff'),
|
||||
U.accent2(el, '#22d3ee'),
|
||||
U.css(el, '--ok', '#22c55e'),
|
||||
U.css(el, '--warn', '#f59e0b'),
|
||||
U.css(el, '--danger', '#ef4444'),
|
||||
];
|
||||
|
||||
U.canvas = (el) => {
|
||||
if (getComputedStyle(el).position === 'static') el.style.position = 'relative';
|
||||
const c = document.createElement('canvas');
|
||||
c.style.cssText = 'position:absolute;inset:0;width:100%;height:100%;pointer-events:none;display:block;';
|
||||
el.appendChild(c);
|
||||
const ctx = c.getContext('2d');
|
||||
let w = 0, h = 0, dpr = Math.max(1, Math.min(2, window.devicePixelRatio||1));
|
||||
const fit = () => {
|
||||
const r = el.getBoundingClientRect();
|
||||
w = Math.max(1, r.width|0);
|
||||
h = Math.max(1, r.height|0);
|
||||
c.width = (w*dpr)|0;
|
||||
c.height = (h*dpr)|0;
|
||||
ctx.setTransform(dpr,0,0,dpr,0,0);
|
||||
};
|
||||
fit();
|
||||
const ro = new ResizeObserver(fit);
|
||||
ro.observe(el);
|
||||
return {
|
||||
c, ctx,
|
||||
get w(){return w;}, get h(){return h;}, get dpr(){return dpr;},
|
||||
destroy(){
|
||||
try{ro.disconnect();}catch(e){}
|
||||
if (c.parentNode) c.parentNode.removeChild(c);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
U.loop = (fn) => {
|
||||
let raf = 0, stopped = false, t0 = performance.now();
|
||||
const tick = (t) => {
|
||||
if (stopped) return;
|
||||
fn((t - t0)/1000);
|
||||
raf = requestAnimationFrame(tick);
|
||||
};
|
||||
raf = requestAnimationFrame(tick);
|
||||
return () => { stopped = true; cancelAnimationFrame(raf); };
|
||||
};
|
||||
|
||||
U.rand = (a,b) => a + Math.random()*(b-a);
|
||||
})();
|
||||
41
skills/assets/animations/fx/chain-react.js
Normal file
41
skills/assets/animations/fx/chain-react.js
Normal file
@@ -0,0 +1,41 @@
|
||||
(function(){
|
||||
window.HPX = window.HPX || {};
|
||||
window.HPX['chain-react'] = function(el){
|
||||
const U = window.HPX._u;
|
||||
const k = U.canvas(el), ctx = k.ctx;
|
||||
const ac = U.accent(el,'#7c5cff'), ac2 = U.accent2(el,'#22d3ee');
|
||||
const N = 8;
|
||||
const stop = U.loop((t) => {
|
||||
ctx.clearRect(0,0,k.w,k.h);
|
||||
const cy = k.h/2;
|
||||
const pad = 60;
|
||||
const dx = (k.w - pad*2)/(N-1);
|
||||
const period = 2.4;
|
||||
const phase = (t % period) / period; // 0..1
|
||||
for (let i=0;i<N;i++){
|
||||
const x = pad + i*dx;
|
||||
const my = i/(N-1);
|
||||
const d = Math.abs(phase - my);
|
||||
const pulse = Math.max(0, 1 - d*6);
|
||||
const r = 18 + pulse*18;
|
||||
// glow
|
||||
const g = ctx.createRadialGradient(x,cy,0,x,cy,r*2);
|
||||
g.addColorStop(0, `rgba(124,92,255,${0.4*pulse})`);
|
||||
g.addColorStop(1, 'rgba(0,0,0,0)');
|
||||
ctx.fillStyle = g;
|
||||
ctx.fillRect(x-r*2, cy-r*2, r*4, r*4);
|
||||
// circle
|
||||
ctx.fillStyle = pulse>0.1 ? ac2 : ac;
|
||||
ctx.beginPath(); ctx.arc(x,cy,r,0,Math.PI*2); ctx.fill();
|
||||
ctx.strokeStyle='rgba(255,255,255,0.4)'; ctx.lineWidth=2;
|
||||
ctx.stroke();
|
||||
// connectors
|
||||
if (i<N-1){
|
||||
ctx.strokeStyle='rgba(200,200,230,0.3)'; ctx.lineWidth=2;
|
||||
ctx.beginPath(); ctx.moveTo(x+r,cy); ctx.lineTo(x+dx-r,cy); ctx.stroke();
|
||||
}
|
||||
}
|
||||
});
|
||||
return { stop(){ stop(); k.destroy(); } };
|
||||
};
|
||||
})();
|
||||
49
skills/assets/animations/fx/confetti-cannon.js
Normal file
49
skills/assets/animations/fx/confetti-cannon.js
Normal file
@@ -0,0 +1,49 @@
|
||||
(function(){
|
||||
window.HPX = window.HPX || {};
|
||||
window.HPX['confetti-cannon'] = function(el){
|
||||
const U = window.HPX._u;
|
||||
const k = U.canvas(el), ctx = k.ctx;
|
||||
const pal = U.palette(el);
|
||||
let parts = [];
|
||||
const fire = () => {
|
||||
for (let side=0; side<2; side++){
|
||||
const x0 = side===0 ? 20 : k.w-20;
|
||||
const y0 = k.h - 20;
|
||||
for (let i=0;i<40;i++){
|
||||
const a = side===0 ? U.rand(-Math.PI*0.7, -Math.PI*0.4) : U.rand(-Math.PI*0.6, -Math.PI*0.3) - Math.PI/2 - Math.PI/6;
|
||||
const spd = U.rand(300, 520);
|
||||
parts.push({
|
||||
x: x0, y: y0,
|
||||
vx: Math.cos(a)*spd, vy: Math.sin(a)*spd,
|
||||
w: U.rand(6,12), h: U.rand(3,7),
|
||||
rot: Math.random()*Math.PI, vr: U.rand(-6,6),
|
||||
c: pal[(Math.random()*pal.length)|0],
|
||||
life: 1
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
fire();
|
||||
let last = 0;
|
||||
const stop = U.loop((t) => {
|
||||
ctx.clearRect(0,0,k.w,k.h);
|
||||
if (t - last > 3) { fire(); last = t; }
|
||||
const dt = 1/60;
|
||||
parts = parts.filter(p => p.life > 0 && p.y < k.h+40);
|
||||
for (const p of parts){
|
||||
p.vy += 520*dt;
|
||||
p.x += p.vx*dt; p.y += p.vy*dt;
|
||||
p.rot += p.vr*dt;
|
||||
p.life -= 0.006;
|
||||
ctx.save();
|
||||
ctx.translate(p.x, p.y); ctx.rotate(p.rot);
|
||||
ctx.globalAlpha = Math.max(0, p.life);
|
||||
ctx.fillStyle = p.c;
|
||||
ctx.fillRect(-p.w/2, -p.h/2, p.w, p.h);
|
||||
ctx.restore();
|
||||
}
|
||||
ctx.globalAlpha = 1;
|
||||
});
|
||||
return { stop(){ stop(); k.destroy(); } };
|
||||
};
|
||||
})();
|
||||
44
skills/assets/animations/fx/constellation.js
Normal file
44
skills/assets/animations/fx/constellation.js
Normal file
@@ -0,0 +1,44 @@
|
||||
(function(){
|
||||
window.HPX = window.HPX || {};
|
||||
window.HPX['constellation'] = function(el){
|
||||
const U = window.HPX._u;
|
||||
const k = U.canvas(el), ctx = k.ctx;
|
||||
const ac = U.accent(el,'#9fb4ff');
|
||||
const N = 70;
|
||||
let pts = [];
|
||||
const seed = () => {
|
||||
pts = Array.from({length:N}, () => ({
|
||||
x: Math.random()*k.w, y: Math.random()*k.h,
|
||||
vx: U.rand(-0.3,0.3), vy: U.rand(-0.3,0.3)
|
||||
}));
|
||||
};
|
||||
seed();
|
||||
let lw=k.w, lh=k.h;
|
||||
const stop = U.loop(() => {
|
||||
if (k.w!==lw||k.h!==lh){ seed(); lw=k.w; lh=k.h; }
|
||||
ctx.clearRect(0,0,k.w,k.h);
|
||||
for (const p of pts){
|
||||
p.x += p.vx; p.y += p.vy;
|
||||
if (p.x<0||p.x>k.w) p.vx*=-1;
|
||||
if (p.y<0||p.y>k.h) p.vy*=-1;
|
||||
}
|
||||
for (let i=0;i<N;i++){
|
||||
for (let j=i+1;j<N;j++){
|
||||
const a=pts[i], b=pts[j];
|
||||
const d = Math.hypot(a.x-b.x, a.y-b.y);
|
||||
if (d < 150){
|
||||
ctx.globalAlpha = 1 - d/150;
|
||||
ctx.strokeStyle = ac; ctx.lineWidth=1;
|
||||
ctx.beginPath(); ctx.moveTo(a.x,a.y); ctx.lineTo(b.x,b.y); ctx.stroke();
|
||||
}
|
||||
}
|
||||
}
|
||||
ctx.globalAlpha = 1;
|
||||
ctx.fillStyle = ac;
|
||||
for (const p of pts){
|
||||
ctx.beginPath(); ctx.arc(p.x,p.y,1.8,0,Math.PI*2); ctx.fill();
|
||||
}
|
||||
});
|
||||
return { stop(){ stop(); k.destroy(); } };
|
||||
};
|
||||
})();
|
||||
58
skills/assets/animations/fx/counter-explosion.js
Normal file
58
skills/assets/animations/fx/counter-explosion.js
Normal file
@@ -0,0 +1,58 @@
|
||||
(function(){
|
||||
window.HPX = window.HPX || {};
|
||||
window.HPX['counter-explosion'] = function(el){
|
||||
const U = window.HPX._u;
|
||||
if (getComputedStyle(el).position === 'static') el.style.position = 'relative';
|
||||
const target = parseInt(el.getAttribute('data-fx-to') || '2400', 10);
|
||||
const k = U.canvas(el), ctx = k.ctx;
|
||||
const pal = U.palette(el);
|
||||
// number overlay
|
||||
const num = document.createElement('div');
|
||||
num.style.cssText = 'position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font:900 120px system-ui,sans-serif;color:var(--text-1,#fff);pointer-events:none;text-shadow:0 4px 40px rgba(124,92,255,0.5);';
|
||||
num.textContent = '0';
|
||||
el.appendChild(num);
|
||||
let parts = [];
|
||||
let state = 'count'; // count | burst | hold
|
||||
let stateT = 0;
|
||||
let value = 0;
|
||||
let cycle = 0;
|
||||
const burst = () => {
|
||||
const cx = k.w/2, cy = k.h/2;
|
||||
for (let i=0;i<120;i++){
|
||||
const a = Math.random()*Math.PI*2;
|
||||
const s = U.rand(120, 400);
|
||||
parts.push({x:cx,y:cy,vx:Math.cos(a)*s,vy:Math.sin(a)*s,life:1,r:U.rand(2,5),c:pal[(Math.random()*pal.length)|0]});
|
||||
}
|
||||
};
|
||||
const stop = U.loop(() => {
|
||||
ctx.clearRect(0,0,k.w,k.h);
|
||||
const dt = 1/60;
|
||||
stateT += dt;
|
||||
if (state === 'count'){
|
||||
const dur = 2.2;
|
||||
const p = Math.min(1, stateT/dur);
|
||||
const eased = 1 - Math.pow(1-p,3);
|
||||
value = Math.round(target*eased);
|
||||
num.textContent = value.toLocaleString();
|
||||
if (p >= 1){ state='burst'; stateT=0; burst(); }
|
||||
} else if (state === 'burst'){
|
||||
if (stateT > 0.05 && stateT < 0.3 && parts.length < 200) {}
|
||||
if (stateT > 2.5){ state='hold'; stateT=0; }
|
||||
} else if (state === 'hold'){
|
||||
if (stateT > 1.5){
|
||||
state='count'; stateT=0; value=0; num.textContent='0'; cycle++;
|
||||
}
|
||||
}
|
||||
parts = parts.filter(p => p.life > 0);
|
||||
for (const p of parts){
|
||||
p.vy += 260*dt; p.vx *= 0.985; p.vy *= 0.985;
|
||||
p.x += p.vx*dt; p.y += p.vy*dt; p.life -= 0.01;
|
||||
ctx.globalAlpha = Math.max(0,p.life);
|
||||
ctx.fillStyle = p.c;
|
||||
ctx.beginPath(); ctx.arc(p.x,p.y,p.r,0,Math.PI*2); ctx.fill();
|
||||
}
|
||||
ctx.globalAlpha = 1;
|
||||
});
|
||||
return { stop(){ stop(); k.destroy(); if (num.parentNode) num.parentNode.removeChild(num); } };
|
||||
};
|
||||
})();
|
||||
45
skills/assets/animations/fx/data-stream.js
Normal file
45
skills/assets/animations/fx/data-stream.js
Normal file
@@ -0,0 +1,45 @@
|
||||
(function(){
|
||||
window.HPX = window.HPX || {};
|
||||
window.HPX['data-stream'] = function(el){
|
||||
const U = window.HPX._u;
|
||||
const k = U.canvas(el), ctx = k.ctx;
|
||||
const ac = U.accent(el,'#22d3ee'), ac2 = U.accent2(el,'#7c5cff');
|
||||
const rows = [];
|
||||
const rh = 22;
|
||||
const genRow = (y) => ({
|
||||
y, dir: Math.random()<0.5?-1:1,
|
||||
speed: U.rand(30, 90),
|
||||
offset: Math.random()*2000,
|
||||
text: Array.from({length:120}, () => {
|
||||
const r = Math.random();
|
||||
if (r<0.3) return Math.random()<0.5?'0':'1';
|
||||
if (r<0.6) return '0x' + Math.floor(Math.random()*256).toString(16).padStart(2,'0');
|
||||
return Math.random().toString(16).slice(2,6);
|
||||
}).join(' ')
|
||||
});
|
||||
const init = () => {
|
||||
rows.length = 0;
|
||||
const n = Math.ceil(k.h/rh);
|
||||
for (let i=0;i<n;i++) rows.push(genRow(i*rh + rh*0.7));
|
||||
};
|
||||
init();
|
||||
let lh = k.h;
|
||||
const stop = U.loop((t) => {
|
||||
if (k.h!==lh){ init(); lh=k.h; }
|
||||
ctx.fillStyle = 'rgba(5,8,14,0.35)';
|
||||
ctx.fillRect(0,0,k.w,k.h);
|
||||
ctx.font = '13px ui-monospace,Menlo,monospace';
|
||||
for (let i=0;i<rows.length;i++){
|
||||
const r = rows[i];
|
||||
const x = r.dir>0
|
||||
? ((t*r.speed + r.offset) % (k.w+400)) - 400
|
||||
: k.w - (((t*r.speed + r.offset) % (k.w+400)) - 400);
|
||||
ctx.fillStyle = (i%3===0)?ac:ac2;
|
||||
ctx.globalAlpha = 0.65 + (i%2)*0.3;
|
||||
ctx.fillText(r.text, x, r.y);
|
||||
}
|
||||
ctx.globalAlpha = 1;
|
||||
});
|
||||
return { stop(){ stop(); k.destroy(); } };
|
||||
};
|
||||
})();
|
||||
51
skills/assets/animations/fx/firework.js
Normal file
51
skills/assets/animations/fx/firework.js
Normal file
@@ -0,0 +1,51 @@
|
||||
(function(){
|
||||
window.HPX = window.HPX || {};
|
||||
window.HPX['firework'] = function(el){
|
||||
const U = window.HPX._u;
|
||||
const k = U.canvas(el), ctx = k.ctx;
|
||||
const pal = U.palette(el);
|
||||
let rockets = [], sparks = [];
|
||||
const launch = () => {
|
||||
rockets.push({
|
||||
x: U.rand(k.w*0.2, k.w*0.8), y: k.h+10,
|
||||
vx: U.rand(-30,30), vy: U.rand(-520,-380),
|
||||
tgtY: U.rand(k.h*0.15, k.h*0.45),
|
||||
c: pal[(Math.random()*pal.length)|0]
|
||||
});
|
||||
};
|
||||
const burst = (x, y, c) => {
|
||||
const n = 70;
|
||||
for (let i=0;i<n;i++){
|
||||
const a = Math.random()*Math.PI*2;
|
||||
const s = U.rand(60, 240);
|
||||
sparks.push({x,y,vx:Math.cos(a)*s,vy:Math.sin(a)*s,life:1,c});
|
||||
}
|
||||
};
|
||||
let last = -1;
|
||||
const stop = U.loop((t) => {
|
||||
ctx.fillStyle = 'rgba(0,0,0,0.18)';
|
||||
ctx.fillRect(0,0,k.w,k.h);
|
||||
if (t - last > 0.7) { launch(); last = t; }
|
||||
const dt = 1/60;
|
||||
rockets = rockets.filter(r => {
|
||||
r.x += r.vx*dt; r.y += r.vy*dt; r.vy += 260*dt;
|
||||
ctx.fillStyle = r.c;
|
||||
ctx.beginPath(); ctx.arc(r.x, r.y, 2.5, 0, Math.PI*2); ctx.fill();
|
||||
if (r.y <= r.tgtY || r.vy >= 0) { burst(r.x, r.y, r.c); return false; }
|
||||
return true;
|
||||
});
|
||||
sparks = sparks.filter(p => p.life > 0);
|
||||
for (const p of sparks){
|
||||
p.vy += 90*dt;
|
||||
p.vx *= 0.98; p.vy *= 0.98;
|
||||
p.x += p.vx*dt; p.y += p.vy*dt;
|
||||
p.life -= 0.012;
|
||||
ctx.globalAlpha = Math.max(0, p.life);
|
||||
ctx.fillStyle = p.c;
|
||||
ctx.beginPath(); ctx.arc(p.x, p.y, 2, 0, Math.PI*2); ctx.fill();
|
||||
}
|
||||
ctx.globalAlpha = 1;
|
||||
});
|
||||
return { stop(){ stop(); k.destroy(); } };
|
||||
};
|
||||
})();
|
||||
33
skills/assets/animations/fx/galaxy-swirl.js
Normal file
33
skills/assets/animations/fx/galaxy-swirl.js
Normal file
@@ -0,0 +1,33 @@
|
||||
(function(){
|
||||
window.HPX = window.HPX || {};
|
||||
window.HPX['galaxy-swirl'] = function(el){
|
||||
const U = window.HPX._u;
|
||||
const k = U.canvas(el), ctx = k.ctx;
|
||||
const pal = U.palette(el);
|
||||
const N = 800;
|
||||
const parts = Array.from({length:N}, (_,i) => {
|
||||
const arm = i%3;
|
||||
const t = Math.random();
|
||||
const r = t*180 + 8;
|
||||
const base = (arm/3)*Math.PI*2;
|
||||
return { r, a: base + Math.log(r+1)*1.6 + U.rand(-0.2,0.2),
|
||||
c: pal[arm%pal.length],
|
||||
s: U.rand(0.8, 2.2) };
|
||||
});
|
||||
const stop = U.loop((t) => {
|
||||
ctx.fillStyle = 'rgba(0,0,0,0.15)';
|
||||
ctx.fillRect(0,0,k.w,k.h);
|
||||
const cx=k.w/2, cy=k.h/2;
|
||||
for (const p of parts){
|
||||
const a = p.a + t*0.15;
|
||||
const x = cx + Math.cos(a)*p.r;
|
||||
const y = cy + Math.sin(a)*p.r*0.7;
|
||||
ctx.fillStyle = p.c;
|
||||
ctx.globalAlpha = 0.7;
|
||||
ctx.beginPath(); ctx.arc(x,y,p.s,0,Math.PI*2); ctx.fill();
|
||||
}
|
||||
ctx.globalAlpha = 1;
|
||||
});
|
||||
return { stop(){ stop(); k.destroy(); } };
|
||||
};
|
||||
})();
|
||||
39
skills/assets/animations/fx/gradient-blob.js
Normal file
39
skills/assets/animations/fx/gradient-blob.js
Normal file
@@ -0,0 +1,39 @@
|
||||
(function(){
|
||||
window.HPX = window.HPX || {};
|
||||
window.HPX['gradient-blob'] = function(el){
|
||||
const U = window.HPX._u;
|
||||
const k = U.canvas(el), ctx = k.ctx;
|
||||
const pal = U.palette(el);
|
||||
const blobs = Array.from({length:4}, (_,i) => ({
|
||||
x: U.rand(0,1), y: U.rand(0,1),
|
||||
vx: U.rand(-0.08,0.08), vy: U.rand(-0.08,0.08),
|
||||
r: U.rand(180,320),
|
||||
c: pal[i%pal.length]
|
||||
}));
|
||||
const hex2rgb = (h) => {
|
||||
const m = h.replace('#','').match(/.{2}/g);
|
||||
if (!m) return [124,92,255];
|
||||
return m.map(x=>parseInt(x,16));
|
||||
};
|
||||
const stop = U.loop((t) => {
|
||||
ctx.fillStyle = 'rgba(10,12,22,0.2)';
|
||||
ctx.fillRect(0,0,k.w,k.h);
|
||||
ctx.globalCompositeOperation = 'lighter';
|
||||
for (const b of blobs){
|
||||
b.x += b.vx*0.01; b.y += b.vy*0.01;
|
||||
if (b.x<0||b.x>1) b.vx*=-1;
|
||||
if (b.y<0||b.y>1) b.vy*=-1;
|
||||
const px = b.x*k.w, py = b.y*k.h;
|
||||
const r = b.r + Math.sin(t*0.8 + b.x*6)*30;
|
||||
const [R,G,B] = hex2rgb(b.c);
|
||||
const grad = ctx.createRadialGradient(px,py,0,px,py,r);
|
||||
grad.addColorStop(0, `rgba(${R},${G},${B},0.55)`);
|
||||
grad.addColorStop(1, `rgba(${R},${G},${B},0)`);
|
||||
ctx.fillStyle = grad;
|
||||
ctx.beginPath(); ctx.arc(px,py,r,0,Math.PI*2); ctx.fill();
|
||||
}
|
||||
ctx.globalCompositeOperation = 'source-over';
|
||||
});
|
||||
return { stop(){ stop(); k.destroy(); } };
|
||||
};
|
||||
})();
|
||||
69
skills/assets/animations/fx/knowledge-graph.js
Normal file
69
skills/assets/animations/fx/knowledge-graph.js
Normal file
@@ -0,0 +1,69 @@
|
||||
(function(){
|
||||
window.HPX = window.HPX || {};
|
||||
window.HPX['knowledge-graph'] = function(el){
|
||||
const U = window.HPX._u;
|
||||
const k = U.canvas(el), ctx = k.ctx;
|
||||
const pal = U.palette(el);
|
||||
const tx = U.text(el, '#e7e7ef');
|
||||
const labels = ['AI','ML','LLM','Graph','Node','Edge','Claude','GPT','RAG','Vector',
|
||||
'Embed','Neural','Agent','Tool','Memory','Logic','Data','Train','Infer','Token',
|
||||
'Prompt','Chain','Plan','Skill','Cloud','Edge','GPU','Code','Task','Flow'];
|
||||
const N = 28;
|
||||
const nodes = Array.from({length:N}, (_,i) => ({
|
||||
x: U.rand(40, 300), y: U.rand(40, 200),
|
||||
vx: 0, vy: 0, label: labels[i%labels.length],
|
||||
c: pal[i%pal.length]
|
||||
}));
|
||||
const edges = [];
|
||||
const made = new Set();
|
||||
while (edges.length < 50){
|
||||
const a = (Math.random()*N)|0, b = (Math.random()*N)|0;
|
||||
if (a===b) continue;
|
||||
const key = a<b ? a+'-'+b : b+'-'+a;
|
||||
if (made.has(key)) continue;
|
||||
made.add(key); edges.push([a,b]);
|
||||
}
|
||||
const stop = U.loop(() => {
|
||||
// physics
|
||||
for (let i=0;i<N;i++){
|
||||
for (let j=i+1;j<N;j++){
|
||||
const a=nodes[i], b=nodes[j];
|
||||
const dx=b.x-a.x, dy=b.y-a.y;
|
||||
let d2=dx*dx+dy*dy; if (d2<1) d2=1;
|
||||
const d=Math.sqrt(d2);
|
||||
const f=1600/d2;
|
||||
const fx=(dx/d)*f, fy=(dy/d)*f;
|
||||
a.vx-=fx; a.vy-=fy; b.vx+=fx; b.vy+=fy;
|
||||
}
|
||||
}
|
||||
for (const [i,j] of edges){
|
||||
const a=nodes[i], b=nodes[j];
|
||||
const dx=b.x-a.x, dy=b.y-a.y, d=Math.hypot(dx,dy)||1;
|
||||
const f=(d-90)*0.008;
|
||||
const fx=(dx/d)*f, fy=(dy/d)*f;
|
||||
a.vx+=fx; a.vy+=fy; b.vx-=fx; b.vy-=fy;
|
||||
}
|
||||
const cx=k.w/2, cy=k.h/2;
|
||||
for (const n of nodes){
|
||||
n.vx += (cx-n.x)*0.002;
|
||||
n.vy += (cy-n.y)*0.002;
|
||||
n.vx *= 0.85; n.vy *= 0.85;
|
||||
n.x += n.vx; n.y += n.vy;
|
||||
}
|
||||
ctx.clearRect(0,0,k.w,k.h);
|
||||
ctx.strokeStyle = 'rgba(180,180,220,0.25)'; ctx.lineWidth=1;
|
||||
for (const [i,j] of edges){
|
||||
const a=nodes[i], b=nodes[j];
|
||||
ctx.beginPath(); ctx.moveTo(a.x,a.y); ctx.lineTo(b.x,b.y); ctx.stroke();
|
||||
}
|
||||
ctx.font='11px system-ui,sans-serif'; ctx.textAlign='center'; ctx.textBaseline='middle';
|
||||
for (const n of nodes){
|
||||
ctx.fillStyle = n.c;
|
||||
ctx.beginPath(); ctx.arc(n.x,n.y,7,0,Math.PI*2); ctx.fill();
|
||||
ctx.fillStyle = tx;
|
||||
ctx.fillText(n.label, n.x, n.y-14);
|
||||
}
|
||||
});
|
||||
return { stop(){ stop(); k.destroy(); } };
|
||||
};
|
||||
})();
|
||||
50
skills/assets/animations/fx/letter-explode.js
Normal file
50
skills/assets/animations/fx/letter-explode.js
Normal file
@@ -0,0 +1,50 @@
|
||||
(function(){
|
||||
window.HPX = window.HPX || {};
|
||||
window.HPX['letter-explode'] = function(el){
|
||||
const U = window.HPX._u;
|
||||
if (getComputedStyle(el).position === 'static') el.style.position = 'relative';
|
||||
const src = el.querySelector('[data-fx-text]') || el;
|
||||
const text = (el.getAttribute('data-fx-text-value') || src.textContent || 'EXPLODE').trim();
|
||||
// Build a container, hide source text
|
||||
const wrap = document.createElement('div');
|
||||
wrap.style.cssText = 'position:absolute;inset:0;display:flex;align-items:center;justify-content:center;pointer-events:none;';
|
||||
const inner = document.createElement('div');
|
||||
inner.style.cssText = 'font-size:64px;font-weight:900;letter-spacing:0.02em;color:var(--text-1,#fff);white-space:nowrap;';
|
||||
wrap.appendChild(inner);
|
||||
el.appendChild(wrap);
|
||||
const spans = [];
|
||||
for (const ch of text){
|
||||
const s = document.createElement('span');
|
||||
s.textContent = ch === ' ' ? '\u00A0' : ch;
|
||||
s.style.display='inline-block';
|
||||
s.style.transform='translate(0,0)';
|
||||
s.style.transition='transform 900ms cubic-bezier(.2,.9,.3,1), opacity 900ms';
|
||||
s.style.opacity='0';
|
||||
inner.appendChild(s);
|
||||
spans.push(s);
|
||||
}
|
||||
let stopped = false;
|
||||
const run = () => {
|
||||
if (stopped) return;
|
||||
spans.forEach((s,i) => {
|
||||
const dx = U.rand(-400, 400), dy = U.rand(-300, 300);
|
||||
s.style.transition='none';
|
||||
s.style.transform=`translate(${dx}px,${dy}px) rotate(${U.rand(-180,180)}deg)`;
|
||||
s.style.opacity='0';
|
||||
});
|
||||
// force reflow
|
||||
void inner.offsetWidth;
|
||||
spans.forEach((s,i) => {
|
||||
setTimeout(() => {
|
||||
if (stopped) return;
|
||||
s.style.transition='transform 900ms cubic-bezier(.2,.9,.3,1), opacity 900ms';
|
||||
s.style.transform='translate(0,0) rotate(0deg)';
|
||||
s.style.opacity='1';
|
||||
}, i*35);
|
||||
});
|
||||
};
|
||||
run();
|
||||
const iv = setInterval(run, 4500);
|
||||
return { stop(){ stopped=true; clearInterval(iv); if (wrap.parentNode) wrap.parentNode.removeChild(wrap); } };
|
||||
};
|
||||
})();
|
||||
40
skills/assets/animations/fx/magnetic-field.js
Normal file
40
skills/assets/animations/fx/magnetic-field.js
Normal file
@@ -0,0 +1,40 @@
|
||||
(function(){
|
||||
window.HPX = window.HPX || {};
|
||||
window.HPX['magnetic-field'] = function(el){
|
||||
const U = window.HPX._u;
|
||||
const k = U.canvas(el), ctx = k.ctx;
|
||||
const pal = U.palette(el);
|
||||
const N = 60;
|
||||
const parts = Array.from({length:N}, (_,i) => ({
|
||||
phase: Math.random()*Math.PI*2,
|
||||
freq: U.rand(0.4, 1.2),
|
||||
amp: U.rand(30, 90),
|
||||
y0: U.rand(0.15, 0.85),
|
||||
c: pal[i%pal.length],
|
||||
trail: []
|
||||
}));
|
||||
const stop = U.loop((t) => {
|
||||
ctx.fillStyle = 'rgba(0,0,0,0.08)';
|
||||
ctx.fillRect(0,0,k.w,k.h);
|
||||
for (const p of parts){
|
||||
const x = ((t*80 + p.phase*50) % (k.w+100)) - 50;
|
||||
const y = k.h*p.y0 + Math.sin(x*0.02 + p.phase + t*p.freq)*p.amp;
|
||||
p.trail.push([x,y]);
|
||||
if (p.trail.length > 18) p.trail.shift();
|
||||
ctx.strokeStyle = p.c;
|
||||
ctx.lineWidth = 2;
|
||||
ctx.beginPath();
|
||||
for (let i=0;i<p.trail.length;i++){
|
||||
const [tx,ty] = p.trail[i];
|
||||
if (i===0) ctx.moveTo(tx,ty); else ctx.lineTo(tx,ty);
|
||||
}
|
||||
ctx.globalAlpha = 0.7;
|
||||
ctx.stroke();
|
||||
ctx.globalAlpha = 1;
|
||||
ctx.fillStyle = p.c;
|
||||
ctx.beginPath(); ctx.arc(x,y,2.5,0,Math.PI*2); ctx.fill();
|
||||
}
|
||||
});
|
||||
return { stop(){ stop(); k.destroy(); } };
|
||||
};
|
||||
})();
|
||||
33
skills/assets/animations/fx/matrix-rain.js
Normal file
33
skills/assets/animations/fx/matrix-rain.js
Normal file
@@ -0,0 +1,33 @@
|
||||
(function(){
|
||||
window.HPX = window.HPX || {};
|
||||
window.HPX['matrix-rain'] = function(el){
|
||||
const U = window.HPX._u;
|
||||
const k = U.canvas(el), ctx = k.ctx;
|
||||
const glyphs = 'アイウエオカキクケコサシスセソタチツテトナニヌネノ0123456789ABCDEF'.split('');
|
||||
const fs = 16;
|
||||
let cols = 0, drops = [];
|
||||
const init = () => {
|
||||
cols = Math.ceil(k.w/fs);
|
||||
drops = Array.from({length:cols}, () => U.rand(-20, 0));
|
||||
};
|
||||
init();
|
||||
let lw = k.w, lh = k.h;
|
||||
const stop = U.loop(() => {
|
||||
if (k.w!==lw || k.h!==lh){ init(); lw=k.w; lh=k.h; }
|
||||
ctx.fillStyle = 'rgba(0,0,0,0.08)';
|
||||
ctx.fillRect(0,0,k.w,k.h);
|
||||
ctx.font = fs+'px monospace';
|
||||
for (let i=0;i<cols;i++){
|
||||
const ch = glyphs[(Math.random()*glyphs.length)|0];
|
||||
const x = i*fs, y = drops[i]*fs;
|
||||
ctx.fillStyle = '#9fffc9';
|
||||
ctx.fillText(ch, x, y);
|
||||
ctx.fillStyle = '#00ff6a';
|
||||
ctx.fillText(ch, x, y - fs);
|
||||
drops[i] += 1;
|
||||
if (y > k.h && Math.random() > 0.975) drops[i] = 0;
|
||||
}
|
||||
});
|
||||
return { stop(){ stop(); k.destroy(); } };
|
||||
};
|
||||
})();
|
||||
75
skills/assets/animations/fx/neural-net.js
Normal file
75
skills/assets/animations/fx/neural-net.js
Normal file
@@ -0,0 +1,75 @@
|
||||
(function(){
|
||||
window.HPX = window.HPX || {};
|
||||
window.HPX['neural-net'] = function(el){
|
||||
const U = window.HPX._u;
|
||||
const k = U.canvas(el), ctx = k.ctx;
|
||||
const ac = U.accent(el,'#7c5cff'), ac2 = U.accent2(el,'#22d3ee');
|
||||
const layers = [4,6,6,3];
|
||||
let nodes = [], edges = [], pulses = [];
|
||||
const layout = () => {
|
||||
nodes = [];
|
||||
const pad = 40;
|
||||
const cw = k.w - pad*2, ch = k.h - pad*2;
|
||||
for (let L=0; L<layers.length; L++){
|
||||
const x = pad + (cw * L / (layers.length-1));
|
||||
const n = layers[L];
|
||||
for (let i=0;i<n;i++){
|
||||
const y = pad + (ch * (i+0.5) / n);
|
||||
nodes.push({x,y,L,i});
|
||||
}
|
||||
}
|
||||
edges = [];
|
||||
for (let L=0; L<layers.length-1; L++){
|
||||
const a = nodes.filter(n=>n.L===L), b = nodes.filter(n=>n.L===L+1);
|
||||
for (const x of a) for (const y of b) edges.push([nodes.indexOf(x),nodes.indexOf(y)]);
|
||||
}
|
||||
};
|
||||
layout();
|
||||
let lw=k.w, lh=k.h, last=0;
|
||||
const stop = U.loop((t) => {
|
||||
if (k.w!==lw||k.h!==lh){ layout(); lw=k.w; lh=k.h; }
|
||||
ctx.clearRect(0,0,k.w,k.h);
|
||||
ctx.strokeStyle = 'rgba(160,160,200,0.22)'; ctx.lineWidth=1;
|
||||
for (const [i,j] of edges){
|
||||
const a=nodes[i], b=nodes[j];
|
||||
ctx.beginPath(); ctx.moveTo(a.x,a.y); ctx.lineTo(b.x,b.y); ctx.stroke();
|
||||
}
|
||||
if (t - last > 0.25){
|
||||
last = t;
|
||||
const starts = nodes.filter(n=>n.L===0);
|
||||
const s = starts[(Math.random()*starts.length)|0];
|
||||
pulses.push({node:s, L:0, t:0});
|
||||
}
|
||||
pulses = pulses.filter(p => p.L < layers.length-1);
|
||||
for (const p of pulses){
|
||||
p.t += 0.03;
|
||||
if (p.t >= 1){
|
||||
const next = nodes.filter(n=>n.L===p.L+1);
|
||||
p.node2 = next[(Math.random()*next.length)|0];
|
||||
if (!p._started){ p._started = true; }
|
||||
}
|
||||
}
|
||||
// animate progression
|
||||
for (const p of pulses){
|
||||
if (!p.target){
|
||||
const next = nodes.filter(n=>n.L===p.L+1);
|
||||
p.target = next[(Math.random()*next.length)|0];
|
||||
}
|
||||
p.t += 0.04;
|
||||
const a = p.node, b = p.target;
|
||||
const x = a.x + (b.x-a.x)*Math.min(1,p.t);
|
||||
const y = a.y + (b.y-a.y)*Math.min(1,p.t);
|
||||
ctx.fillStyle = ac2;
|
||||
ctx.beginPath(); ctx.arc(x,y,4,0,Math.PI*2); ctx.fill();
|
||||
if (p.t >= 1){ p.node = b; p.target=null; p.L++; p.t=0; }
|
||||
}
|
||||
for (const n of nodes){
|
||||
ctx.fillStyle = ac;
|
||||
ctx.beginPath(); ctx.arc(n.x,n.y,6,0,Math.PI*2); ctx.fill();
|
||||
ctx.strokeStyle = ac2; ctx.lineWidth=1.5;
|
||||
ctx.beginPath(); ctx.arc(n.x,n.y,8,0,Math.PI*2); ctx.stroke();
|
||||
}
|
||||
});
|
||||
return { stop(){ stop(); k.destroy(); } };
|
||||
};
|
||||
})();
|
||||
38
skills/assets/animations/fx/orbit-ring.js
Normal file
38
skills/assets/animations/fx/orbit-ring.js
Normal file
@@ -0,0 +1,38 @@
|
||||
(function(){
|
||||
window.HPX = window.HPX || {};
|
||||
window.HPX['orbit-ring'] = function(el){
|
||||
const U = window.HPX._u;
|
||||
const k = U.canvas(el), ctx = k.ctx;
|
||||
const pal = U.palette(el);
|
||||
const rings = [
|
||||
{r:40, n:3, sp:1.2, c:pal[0]},
|
||||
{r:75, n:5, sp:0.8, c:pal[1]},
|
||||
{r:110, n:8, sp:-0.6, c:pal[2]},
|
||||
{r:145, n:12, sp:0.4, c:pal[3]},
|
||||
{r:180, n:16, sp:-0.3, c:pal[4]}
|
||||
];
|
||||
const stop = U.loop((t) => {
|
||||
ctx.clearRect(0,0,k.w,k.h);
|
||||
const cx=k.w/2, cy=k.h/2;
|
||||
// radial glow
|
||||
const g = ctx.createRadialGradient(cx,cy,0,cx,cy,210);
|
||||
g.addColorStop(0,'rgba(124,92,255,0.25)');
|
||||
g.addColorStop(1,'rgba(0,0,0,0)');
|
||||
ctx.fillStyle = g; ctx.fillRect(0,0,k.w,k.h);
|
||||
for (const R of rings){
|
||||
ctx.strokeStyle = 'rgba(200,200,230,0.2)'; ctx.lineWidth=1;
|
||||
ctx.beginPath(); ctx.arc(cx,cy,R.r,0,Math.PI*2); ctx.stroke();
|
||||
for (let i=0;i<R.n;i++){
|
||||
const a = (i/R.n)*Math.PI*2 + t*R.sp;
|
||||
const x = cx + Math.cos(a)*R.r;
|
||||
const y = cy + Math.sin(a)*R.r;
|
||||
ctx.fillStyle = R.c;
|
||||
ctx.beginPath(); ctx.arc(x,y,4,0,Math.PI*2); ctx.fill();
|
||||
}
|
||||
}
|
||||
ctx.fillStyle = '#fff';
|
||||
ctx.beginPath(); ctx.arc(cx,cy,5,0,Math.PI*2); ctx.fill();
|
||||
});
|
||||
return { stop(){ stop(); k.destroy(); } };
|
||||
};
|
||||
})();
|
||||
42
skills/assets/animations/fx/particle-burst.js
Normal file
42
skills/assets/animations/fx/particle-burst.js
Normal file
@@ -0,0 +1,42 @@
|
||||
(function(){
|
||||
window.HPX = window.HPX || {};
|
||||
window.HPX['particle-burst'] = function(el){
|
||||
const U = window.HPX._u;
|
||||
const k = U.canvas(el), ctx = k.ctx;
|
||||
const pal = U.palette(el);
|
||||
let parts = [];
|
||||
const spawn = () => {
|
||||
const cx = k.w/2, cy = k.h/2;
|
||||
const n = 90;
|
||||
for (let i=0;i<n;i++){
|
||||
const a = Math.random()*Math.PI*2;
|
||||
const s = U.rand(80, 260);
|
||||
parts.push({
|
||||
x: cx, y: cy,
|
||||
vx: Math.cos(a)*s, vy: Math.sin(a)*s,
|
||||
life: 1, r: U.rand(2,5),
|
||||
c: pal[(Math.random()*pal.length)|0]
|
||||
});
|
||||
}
|
||||
};
|
||||
spawn();
|
||||
let lastSpawn = 0;
|
||||
const stop = U.loop((t) => {
|
||||
ctx.clearRect(0,0,k.w,k.h);
|
||||
if (t - lastSpawn > 2.5) { spawn(); lastSpawn = t; }
|
||||
const dt = 1/60;
|
||||
parts = parts.filter(p => p.life > 0);
|
||||
for (const p of parts){
|
||||
p.vy += 220*dt;
|
||||
p.vx *= 0.985; p.vy *= 0.985;
|
||||
p.x += p.vx*dt; p.y += p.vy*dt;
|
||||
p.life -= 0.012;
|
||||
ctx.globalAlpha = Math.max(0, p.life);
|
||||
ctx.fillStyle = p.c;
|
||||
ctx.beginPath(); ctx.arc(p.x, p.y, p.r, 0, Math.PI*2); ctx.fill();
|
||||
}
|
||||
ctx.globalAlpha = 1;
|
||||
});
|
||||
return { stop(){ stop(); k.destroy(); } };
|
||||
};
|
||||
})();
|
||||
39
skills/assets/animations/fx/shockwave.js
Normal file
39
skills/assets/animations/fx/shockwave.js
Normal file
@@ -0,0 +1,39 @@
|
||||
(function(){
|
||||
window.HPX = window.HPX || {};
|
||||
window.HPX['shockwave'] = function(el){
|
||||
const U = window.HPX._u;
|
||||
const k = U.canvas(el), ctx = k.ctx;
|
||||
const ac = U.accent(el,'#7c5cff'), ac2 = U.accent2(el,'#22d3ee');
|
||||
let waves = [];
|
||||
let last = -1;
|
||||
const stop = U.loop((t) => {
|
||||
ctx.fillStyle = 'rgba(0,0,0,0.12)';
|
||||
ctx.fillRect(0,0,k.w,k.h);
|
||||
if (t - last > 0.6){ last = t; waves.push({t:0}); }
|
||||
const cx=k.w/2, cy=k.h/2;
|
||||
const max = Math.hypot(k.w,k.h)/2;
|
||||
waves = waves.filter(w => w.t < 1);
|
||||
for (const w of waves){
|
||||
w.t += 0.012;
|
||||
const r = w.t * max;
|
||||
const alpha = 1 - w.t;
|
||||
ctx.strokeStyle = w.t<0.5?ac2:ac;
|
||||
ctx.globalAlpha = alpha;
|
||||
ctx.lineWidth = 3 + (1-w.t)*3;
|
||||
ctx.beginPath(); ctx.arc(cx,cy,r,0,Math.PI*2); ctx.stroke();
|
||||
ctx.strokeStyle = '#fff';
|
||||
ctx.lineWidth = 1;
|
||||
ctx.globalAlpha = alpha*0.4;
|
||||
ctx.beginPath(); ctx.arc(cx,cy,r*0.92,0,Math.PI*2); ctx.stroke();
|
||||
}
|
||||
ctx.globalAlpha = 1;
|
||||
// core
|
||||
const g = ctx.createRadialGradient(cx,cy,0,cx,cy,40);
|
||||
g.addColorStop(0,'rgba(255,255,255,0.9)');
|
||||
g.addColorStop(1,'rgba(124,92,255,0)');
|
||||
ctx.fillStyle = g;
|
||||
ctx.beginPath(); ctx.arc(cx,cy,40,0,Math.PI*2); ctx.fill();
|
||||
});
|
||||
return { stop(){ stop(); k.destroy(); } };
|
||||
};
|
||||
})();
|
||||
62
skills/assets/animations/fx/sparkle-trail.js
Normal file
62
skills/assets/animations/fx/sparkle-trail.js
Normal file
@@ -0,0 +1,62 @@
|
||||
(function(){
|
||||
window.HPX = window.HPX || {};
|
||||
window.HPX['sparkle-trail'] = function(el){
|
||||
const U = window.HPX._u;
|
||||
const k = U.canvas(el), ctx = k.ctx;
|
||||
k.c.style.pointerEvents = 'none';
|
||||
el.style.cursor = 'crosshair';
|
||||
const pal = U.palette(el);
|
||||
let sparks = [];
|
||||
const onMove = (e) => {
|
||||
const r = el.getBoundingClientRect();
|
||||
const x = e.clientX - r.left, y = e.clientY - r.top;
|
||||
for (let i=0;i<3;i++){
|
||||
sparks.push({
|
||||
x, y,
|
||||
vx: U.rand(-60,60), vy: U.rand(-80,20),
|
||||
life: 1, c: pal[(Math.random()*pal.length)|0],
|
||||
r: U.rand(1.5,3.5)
|
||||
});
|
||||
}
|
||||
};
|
||||
// auto-wiggle if no mouse moves
|
||||
let auto = true, autoT = 0;
|
||||
const onAny = () => { auto = false; };
|
||||
el.addEventListener('pointermove', onMove);
|
||||
el.addEventListener('pointerenter', onAny);
|
||||
const stop = U.loop(() => {
|
||||
ctx.fillStyle = 'rgba(0,0,0,0.15)';
|
||||
ctx.fillRect(0,0,k.w,k.h);
|
||||
if (auto){
|
||||
autoT += 0.04;
|
||||
const x = k.w/2 + Math.cos(autoT)*k.w*0.3;
|
||||
const y = k.h/2 + Math.sin(autoT*1.3)*k.h*0.3;
|
||||
for (let i=0;i<3;i++){
|
||||
sparks.push({
|
||||
x, y,
|
||||
vx: U.rand(-60,60), vy: U.rand(-80,20),
|
||||
life: 1, c: pal[(Math.random()*pal.length)|0],
|
||||
r: U.rand(1.5,3.5)
|
||||
});
|
||||
}
|
||||
}
|
||||
const dt = 1/60;
|
||||
sparks = sparks.filter(s => s.life > 0);
|
||||
for (const s of sparks){
|
||||
s.vy += 160*dt;
|
||||
s.x += s.vx*dt; s.y += s.vy*dt;
|
||||
s.life -= 0.018;
|
||||
ctx.globalAlpha = Math.max(0, s.life);
|
||||
ctx.fillStyle = s.c;
|
||||
ctx.beginPath(); ctx.arc(s.x,s.y,s.r,0,Math.PI*2); ctx.fill();
|
||||
}
|
||||
ctx.globalAlpha = 1;
|
||||
});
|
||||
return { stop(){
|
||||
el.removeEventListener('pointermove', onMove);
|
||||
el.removeEventListener('pointerenter', onAny);
|
||||
el.style.cursor = '';
|
||||
stop(); k.destroy();
|
||||
}};
|
||||
};
|
||||
})();
|
||||
30
skills/assets/animations/fx/starfield.js
Normal file
30
skills/assets/animations/fx/starfield.js
Normal file
@@ -0,0 +1,30 @@
|
||||
(function(){
|
||||
window.HPX = window.HPX || {};
|
||||
window.HPX['starfield'] = function(el){
|
||||
const U = window.HPX._u;
|
||||
const k = U.canvas(el), ctx = k.ctx;
|
||||
const tx = U.text(el, '#ffffff');
|
||||
const N = 260;
|
||||
const stars = Array.from({length:N}, () => ({
|
||||
x: U.rand(-1,1), y: U.rand(-1,1), z: Math.random()
|
||||
}));
|
||||
const stop = U.loop(() => {
|
||||
ctx.fillStyle = 'rgba(0,0,0,0.25)';
|
||||
ctx.fillRect(0,0,k.w,k.h);
|
||||
const cx = k.w/2, cy = k.h/2;
|
||||
for (const s of stars){
|
||||
s.z -= 0.006;
|
||||
if (s.z <= 0.02) { s.x = U.rand(-1,1); s.y = U.rand(-1,1); s.z = 1; }
|
||||
const px = cx + (s.x/s.z)*cx;
|
||||
const py = cy + (s.y/s.z)*cy;
|
||||
if (px<0||py<0||px>k.w||py>k.h) continue;
|
||||
const r = (1-s.z)*2.4;
|
||||
ctx.globalAlpha = 1-s.z;
|
||||
ctx.fillStyle = tx;
|
||||
ctx.beginPath(); ctx.arc(px,py,r,0,Math.PI*2); ctx.fill();
|
||||
}
|
||||
ctx.globalAlpha = 1;
|
||||
});
|
||||
return { stop(){ stop(); k.destroy(); } };
|
||||
};
|
||||
})();
|
||||
51
skills/assets/animations/fx/typewriter-multi.js
Normal file
51
skills/assets/animations/fx/typewriter-multi.js
Normal file
@@ -0,0 +1,51 @@
|
||||
(function(){
|
||||
window.HPX = window.HPX || {};
|
||||
window.HPX['typewriter-multi'] = function(el){
|
||||
if (getComputedStyle(el).position === 'static') el.style.position = 'relative';
|
||||
const lines = [
|
||||
(el.getAttribute('data-fx-line1') || '> initializing knowledge graph...'),
|
||||
(el.getAttribute('data-fx-line2') || '> loading 28 concept nodes'),
|
||||
(el.getAttribute('data-fx-line3') || '> agent ready. awaiting prompt_'),
|
||||
];
|
||||
const wrap = document.createElement('div');
|
||||
wrap.style.cssText = 'position:absolute;inset:0;display:flex;flex-direction:column;justify-content:center;gap:14px;padding:32px 48px;font:600 22px ui-monospace,Menlo,monospace;color:var(--text-1,#e7e7ef);';
|
||||
el.appendChild(wrap);
|
||||
const rows = lines.map((txt) => {
|
||||
const row = document.createElement('div');
|
||||
row.style.cssText = 'white-space:pre;display:flex;align-items:center;';
|
||||
const span = document.createElement('span'); span.textContent = '';
|
||||
const cur = document.createElement('span');
|
||||
cur.textContent = '\u2588';
|
||||
cur.style.cssText = 'display:inline-block;margin-left:2px;color:var(--accent,#22d3ee);animation:hpxBlink 1s steps(2) infinite;';
|
||||
row.appendChild(span); row.appendChild(cur);
|
||||
wrap.appendChild(row);
|
||||
return {row, span, txt, i:0};
|
||||
});
|
||||
// inject blink keyframes once
|
||||
if (!document.getElementById('hpx-blink-kf')){
|
||||
const st = document.createElement('style');
|
||||
st.id = 'hpx-blink-kf';
|
||||
st.textContent = '@keyframes hpxBlink{50%{opacity:0}}';
|
||||
document.head.appendChild(st);
|
||||
}
|
||||
let stopped = false;
|
||||
const speeds = [55, 70, 45];
|
||||
rows.forEach((r, idx) => {
|
||||
const tick = () => {
|
||||
if (stopped) return;
|
||||
if (r.i < r.txt.length){
|
||||
r.span.textContent += r.txt[r.i++];
|
||||
setTimeout(tick, speeds[idx]);
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
if (stopped) return;
|
||||
r.i = 0; r.span.textContent = '';
|
||||
tick();
|
||||
}, 2200);
|
||||
}
|
||||
};
|
||||
setTimeout(tick, idx*400);
|
||||
});
|
||||
return { stop(){ stopped = true; if (wrap.parentNode) wrap.parentNode.removeChild(wrap); } };
|
||||
};
|
||||
})();
|
||||
47
skills/assets/animations/fx/word-cascade.js
Normal file
47
skills/assets/animations/fx/word-cascade.js
Normal file
@@ -0,0 +1,47 @@
|
||||
(function(){
|
||||
window.HPX = window.HPX || {};
|
||||
window.HPX['word-cascade'] = function(el){
|
||||
const U = window.HPX._u;
|
||||
const k = U.canvas(el), ctx = k.ctx;
|
||||
const pal = U.palette(el);
|
||||
const WORDS = ['AI','知识','Graph','Claude','LLM','Agent','Vector','RAG','Token','神经',
|
||||
'Prompt','Chain','Skill','Code','Cloud','GPU','Flow','推理','Data','Model'];
|
||||
let items = [];
|
||||
let last = -1;
|
||||
let piles = {}; // column -> stack height
|
||||
const stop = U.loop((t) => {
|
||||
ctx.clearRect(0,0,k.w,k.h);
|
||||
if (t - last > 0.18){
|
||||
last = t;
|
||||
const w = WORDS[(Math.random()*WORDS.length)|0];
|
||||
items.push({
|
||||
text: w, x: U.rand(40, k.w-40), y: -20,
|
||||
vy: 0, c: pal[(Math.random()*pal.length)|0],
|
||||
size: U.rand(16,26), landed: false
|
||||
});
|
||||
}
|
||||
ctx.textAlign='center'; ctx.textBaseline='middle';
|
||||
for (const it of items){
|
||||
if (!it.landed){
|
||||
it.vy += 0.4;
|
||||
it.y += it.vy;
|
||||
const col = Math.round(it.x/60);
|
||||
const floor = k.h - (piles[col]||0) - it.size*0.6;
|
||||
if (it.y >= floor){
|
||||
it.y = floor; it.landed = true;
|
||||
piles[col] = (piles[col]||0) + it.size*1.1;
|
||||
if ((piles[col]||0) > k.h*0.8) piles[col] = 0; // reset if too high
|
||||
}
|
||||
}
|
||||
ctx.fillStyle = it.c;
|
||||
ctx.font = `700 ${it.size}px system-ui,sans-serif`;
|
||||
ctx.fillText(it.text, it.x, it.y);
|
||||
}
|
||||
// prune old landed
|
||||
if (items.length > 120){
|
||||
items = items.filter(i => !i.landed).concat(items.filter(i=>i.landed).slice(-60));
|
||||
}
|
||||
});
|
||||
return { stop(){ stop(); k.destroy(); } };
|
||||
};
|
||||
})();
|
||||
198
skills/assets/banner.svg
Normal file
198
skills/assets/banner.svg
Normal file
@@ -0,0 +1,198 @@
|
||||
<svg width="1200" height="400" viewBox="0 0 1200 400" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<style>
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;700;900&family=Noto+Serif+SC:wght@700;900&display=swap');
|
||||
</style>
|
||||
|
||||
<!-- Warm accent gradients for mini mockup highlights -->
|
||||
<linearGradient id="hdBarGrad" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stop-color="#D4532B"/>
|
||||
<stop offset="100%" stop-color="#A83518"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="hdBarGradSoft" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stop-color="#8B5E3C"/>
|
||||
<stop offset="100%" stop-color="#6E4A2E"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
<!-- Background -->
|
||||
<rect width="1200" height="400" fill="#111111"/>
|
||||
|
||||
<!-- Left accent line (Pentagram-style editorial vertical rule) -->
|
||||
<rect x="60" y="48" width="3" height="304" fill="#D4532B"/>
|
||||
|
||||
<!-- Top horizontal rule -->
|
||||
<rect x="60" y="48" width="760" height="2" fill="#FFFFFF" opacity="0.15"/>
|
||||
|
||||
<!-- Bottom horizontal rule -->
|
||||
<rect x="60" y="350" width="760" height="1" fill="#FFFFFF" opacity="0.15"/>
|
||||
|
||||
<!-- Thin divider between text and viz -->
|
||||
<rect x="860" y="80" width="1" height="240" fill="#FFFFFF" opacity="0.08"/>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- LEFT: TEXT BLOCK -->
|
||||
<!-- ============================================================ -->
|
||||
|
||||
<!-- CATEGORY LABEL -->
|
||||
<text
|
||||
x="80"
|
||||
y="88"
|
||||
font-family="'Inter', system-ui, -apple-system, sans-serif"
|
||||
font-size="11"
|
||||
font-weight="700"
|
||||
letter-spacing="3"
|
||||
fill="#D4532B"
|
||||
>CLAUDE CODE SKILL · DESIGN</text>
|
||||
|
||||
<!-- MAIN TITLE -->
|
||||
<text
|
||||
x="80"
|
||||
y="178"
|
||||
font-family="'Inter', system-ui, -apple-system, sans-serif"
|
||||
font-size="88"
|
||||
font-weight="900"
|
||||
fill="#FFFFFF"
|
||||
letter-spacing="-3"
|
||||
>Huashu Design</text>
|
||||
|
||||
<!-- Chinese subtitle -->
|
||||
<text
|
||||
x="80"
|
||||
y="222"
|
||||
font-family="'Noto Serif SC', 'Source Han Serif', 'Inter', serif"
|
||||
font-size="22"
|
||||
font-weight="700"
|
||||
fill="#EEEEEE"
|
||||
letter-spacing="1"
|
||||
>用 HTML 做设计的 skill</text>
|
||||
|
||||
<!-- Tagline -->
|
||||
<text
|
||||
x="80"
|
||||
y="284"
|
||||
font-family="'Inter', system-ui, -apple-system, sans-serif"
|
||||
font-size="15"
|
||||
font-weight="500"
|
||||
fill="#BBBBBB"
|
||||
letter-spacing="0.5"
|
||||
>高保真原型</text>
|
||||
<text x="176" y="284" font-family="'Inter', sans-serif" font-size="15" font-weight="700" fill="#D4532B">·</text>
|
||||
<text x="188" y="284" font-family="'Inter', sans-serif" font-size="15" font-weight="500" fill="#BBBBBB" letter-spacing="0.5">幻灯片</text>
|
||||
<text x="260" y="284" font-family="'Inter', sans-serif" font-size="15" font-weight="700" fill="#D4532B">·</text>
|
||||
<text x="272" y="284" font-family="'Inter', sans-serif" font-size="15" font-weight="500" fill="#BBBBBB" letter-spacing="0.5">动画</text>
|
||||
<text x="320" y="284" font-family="'Inter', sans-serif" font-size="15" font-weight="700" fill="#D4532B">·</text>
|
||||
<text x="332" y="284" font-family="'Inter', sans-serif" font-size="15" font-weight="500" fill="#BBBBBB" letter-spacing="0.5">信息图</text>
|
||||
<text x="404" y="284" font-family="'Inter', sans-serif" font-size="15" font-weight="700" fill="#D4532B">·</text>
|
||||
<text x="416" y="284" font-family="'Inter', sans-serif" font-size="15" font-weight="500" fill="#BBBBBB" letter-spacing="0.5">App 原型</text>
|
||||
|
||||
<!-- Second tagline row -->
|
||||
<text
|
||||
x="80"
|
||||
y="312"
|
||||
font-family="'Inter', system-ui, -apple-system, sans-serif"
|
||||
font-size="14"
|
||||
font-weight="400"
|
||||
fill="#888888"
|
||||
letter-spacing="0.3"
|
||||
>20 种设计哲学 · 5 维专家评审 · 发布会级动画导出</text>
|
||||
|
||||
<!-- Footer credit -->
|
||||
<text
|
||||
x="80"
|
||||
y="370"
|
||||
font-family="'Inter', system-ui, -apple-system, sans-serif"
|
||||
font-size="12"
|
||||
font-weight="400"
|
||||
fill="#666666"
|
||||
letter-spacing="0.3"
|
||||
>for Claude Code & Agent-agnostic</text>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- RIGHT: MINI MOCKUP GRID (2×2) -->
|
||||
<!-- Each mock represents one output form of huashu-design -->
|
||||
<!-- Viewport right area: x 880-1160, y 90-330 -->
|
||||
<!-- 2×2 grid, tile ≈ 128×104, gap 16 -->
|
||||
<!-- ============================================================ -->
|
||||
|
||||
<!-- Section label -->
|
||||
<text x="890" y="108" font-family="'Inter', sans-serif" font-size="10" font-weight="700" letter-spacing="2" fill="#D4532B" opacity="0.9">OUTPUT SURFACES</text>
|
||||
|
||||
<!-- Grid coordinates:
|
||||
Col1 x=890 (width 128) Col2 x=1034 (width 128)
|
||||
Row1 y=122 (height 100) Row2 y=238 (height 100) -->
|
||||
|
||||
<!-- ============ TILE 1 · SLIDES (top-left) ============ -->
|
||||
<rect x="890" y="122" width="128" height="100" rx="2" fill="#1A1A1A" stroke="#333333" stroke-width="1"/>
|
||||
<!-- slide stack visual: 3 stacked rectangles offset to imply deck -->
|
||||
<rect x="902" y="138" width="88" height="56" fill="#2A2A2A" stroke="#3A3A3A" stroke-width="0.5"/>
|
||||
<rect x="906" y="142" width="88" height="56" fill="#353535"/>
|
||||
<rect x="910" y="146" width="88" height="56" fill="#E8E2D4"/>
|
||||
<!-- slide headline stripes -->
|
||||
<rect x="916" y="152" width="48" height="3" fill="#111111"/>
|
||||
<rect x="916" y="160" width="72" height="1.5" fill="#666666"/>
|
||||
<rect x="916" y="166" width="60" height="1.5" fill="#666666"/>
|
||||
<rect x="916" y="176" width="32" height="14" fill="#D4532B"/>
|
||||
<!-- tile label -->
|
||||
<text x="902" y="216" font-family="'Inter', sans-serif" font-size="9" font-weight="500" letter-spacing="2" fill="#777777">SLIDES</text>
|
||||
|
||||
<!-- ============ TILE 2 · PROTOTYPE iPhone (top-right) ============ -->
|
||||
<rect x="1034" y="122" width="128" height="100" rx="2" fill="#1A1A1A" stroke="#333333" stroke-width="1"/>
|
||||
<!-- iPhone outline inside tile -->
|
||||
<rect x="1080" y="130" width="36" height="76" rx="6" fill="#0A0A0A" stroke="#444444" stroke-width="1"/>
|
||||
<!-- Dynamic island -->
|
||||
<rect x="1092" y="134" width="12" height="3" rx="1.5" fill="#000000"/>
|
||||
<!-- Screen content area -->
|
||||
<rect x="1083" y="140" width="30" height="58" fill="#EEEAE0"/>
|
||||
<!-- Tiny app UI elements -->
|
||||
<rect x="1086" y="144" width="24" height="4" fill="#111111"/>
|
||||
<rect x="1086" y="152" width="16" height="1.5" fill="#888888"/>
|
||||
<rect x="1086" y="157" width="20" height="1.5" fill="#888888"/>
|
||||
<rect x="1086" y="164" width="24" height="12" fill="#D4532B"/>
|
||||
<rect x="1086" y="180" width="11" height="14" fill="#D1CAB8"/>
|
||||
<rect x="1099" y="180" width="11" height="14" fill="#D1CAB8"/>
|
||||
<!-- Home indicator -->
|
||||
<rect x="1092" y="201" width="12" height="1" rx="0.5" fill="#444444"/>
|
||||
<!-- tile label -->
|
||||
<text x="1046" y="216" font-family="'Inter', sans-serif" font-size="9" font-weight="500" letter-spacing="2" fill="#777777">PROTOTYPE</text>
|
||||
|
||||
<!-- ============ TILE 3 · ANIMATION storyboard (bottom-left) ============ -->
|
||||
<rect x="890" y="238" width="128" height="100" rx="2" fill="#1A1A1A" stroke="#333333" stroke-width="1"/>
|
||||
<!-- 3 storyboard frames in a row -->
|
||||
<rect x="898" y="252" width="34" height="44" fill="#252525" stroke="#3A3A3A" stroke-width="0.5"/>
|
||||
<rect x="939" y="252" width="34" height="44" fill="#2E2E2E" stroke="#3A3A3A" stroke-width="0.5"/>
|
||||
<rect x="980" y="252" width="34" height="44" fill="#353535" stroke="#3A3A3A" stroke-width="0.5"/>
|
||||
<!-- motion dots -->
|
||||
<circle cx="910" cy="274" r="6" fill="#666666"/>
|
||||
<circle cx="956" cy="274" r="6" fill="#9C6A46"/>
|
||||
<circle cx="997" cy="274" r="6" fill="#D4532B"/>
|
||||
<!-- motion arc dashes -->
|
||||
<path d="M 910 274 Q 933 258 956 274" stroke="#D4532B" stroke-width="0.8" fill="none" stroke-dasharray="2 2" opacity="0.6"/>
|
||||
<path d="M 956 274 Q 977 258 997 274" stroke="#D4532B" stroke-width="0.8" fill="none" stroke-dasharray="2 2" opacity="0.6"/>
|
||||
<!-- timeline ruler -->
|
||||
<rect x="898" y="306" width="116" height="1" fill="#555555"/>
|
||||
<rect x="898" y="306" width="2" height="4" fill="#D4532B"/>
|
||||
<rect x="938" y="306" width="2" height="4" fill="#555555"/>
|
||||
<rect x="978" y="306" width="2" height="4" fill="#555555"/>
|
||||
<rect x="1012" y="306" width="2" height="4" fill="#555555"/>
|
||||
<!-- tile label -->
|
||||
<text x="902" y="332" font-family="'Inter', sans-serif" font-size="9" font-weight="500" letter-spacing="2" fill="#777777">ANIMATION</text>
|
||||
|
||||
<!-- ============ TILE 4 · INFOGRAPHIC bars (bottom-right) ============ -->
|
||||
<rect x="1034" y="238" width="128" height="100" rx="2" fill="#1A1A1A" stroke="#333333" stroke-width="1"/>
|
||||
<!-- bars chart -->
|
||||
<rect x="1046" y="290" width="12" height="20" fill="url(#hdBarGradSoft)"/>
|
||||
<rect x="1062" y="278" width="12" height="32" fill="url(#hdBarGradSoft)"/>
|
||||
<rect x="1078" y="270" width="12" height="40" fill="url(#hdBarGradSoft)"/>
|
||||
<rect x="1094" y="262" width="12" height="48" fill="url(#hdBarGrad)"/>
|
||||
<rect x="1110" y="254" width="12" height="56" fill="url(#hdBarGrad)"/>
|
||||
<rect x="1126" y="248" width="12" height="62" fill="url(#hdBarGrad)"/>
|
||||
<!-- baseline -->
|
||||
<rect x="1044" y="310" width="104" height="1" fill="#555555"/>
|
||||
<!-- headline at top of tile -->
|
||||
<rect x="1046" y="252" width="50" height="3" fill="#FFFFFF" opacity="0.85"/>
|
||||
<rect x="1046" y="260" width="34" height="1.5" fill="#666666"/>
|
||||
<!-- tile label -->
|
||||
<text x="1046" y="332" font-family="'Inter', sans-serif" font-size="9" font-weight="500" letter-spacing="2" fill="#777777">INFOGRAPHIC</text>
|
||||
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 9.3 KiB |
150
skills/assets/base.css
Normal file
150
skills/assets/base.css
Normal file
@@ -0,0 +1,150 @@
|
||||
/* html-ppt :: base.css — reset + shared tokens + layout primitives */
|
||||
/* Default tokens. Themes in assets/themes/*.css override the :root block. */
|
||||
:root {
|
||||
--bg: #ffffff;
|
||||
--bg-soft: #f7f7f8;
|
||||
--surface: #ffffff;
|
||||
--surface-2: #f2f2f4;
|
||||
--border: rgba(0,0,0,.08);
|
||||
--border-strong: rgba(0,0,0,.16);
|
||||
--text-1: #111216;
|
||||
--text-2: #55596a;
|
||||
--text-3: #8a8f9e;
|
||||
--accent: #3b6cff;
|
||||
--accent-2: #7a5cff;
|
||||
--accent-3: #ff5c8a;
|
||||
--good: #1aaf6c;
|
||||
--warn: #f5a524;
|
||||
--bad: #e0445a;
|
||||
--grad: linear-gradient(135deg,#3b6cff,#7a5cff 55%,#ff5c8a);
|
||||
--grad-soft: linear-gradient(135deg,#eef2ff,#f5ecff 55%,#ffeef5);
|
||||
--radius: 18px;
|
||||
--radius-sm: 12px;
|
||||
--radius-lg: 26px;
|
||||
--shadow: 0 10px 30px rgba(18,24,40,.08), 0 2px 6px rgba(18,24,40,.04);
|
||||
--shadow-lg: 0 24px 60px rgba(18,24,40,.14), 0 6px 16px rgba(18,24,40,.06);
|
||||
--font-sans: 'Inter','Noto Sans SC',-apple-system,BlinkMacSystemFont,Helvetica,Arial,sans-serif;
|
||||
--font-serif: 'Playfair Display','Noto Serif SC',Georgia,serif;
|
||||
--font-mono: 'JetBrains Mono','IBM Plex Mono',SFMono-Regular,Menlo,monospace;
|
||||
--font-display: var(--font-sans);
|
||||
--letter-tight: -.03em;
|
||||
--letter-normal: -.01em;
|
||||
--ease: cubic-bezier(.4,0,.2,1);
|
||||
}
|
||||
|
||||
*,*::before,*::after{box-sizing:border-box}
|
||||
html,body{margin:0;padding:0;background:var(--bg);color:var(--text-1);
|
||||
font-family:var(--font-sans);font-weight:400;line-height:1.6;
|
||||
-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;
|
||||
letter-spacing:var(--letter-normal)}
|
||||
img,svg,video{max-width:100%;display:block}
|
||||
a{color:var(--accent);text-decoration:none}
|
||||
a:hover{text-decoration:underline}
|
||||
code,kbd,pre,samp{font-family:var(--font-mono)}
|
||||
|
||||
/* ================= SLIDE SYSTEM ================= */
|
||||
.deck{position:relative;width:100vw;height:100vh;overflow:hidden;background:var(--bg)}
|
||||
.slide{
|
||||
position:absolute;inset:0;
|
||||
display:flex;flex-direction:column;justify-content:center;
|
||||
padding:72px 96px;
|
||||
box-sizing:border-box;
|
||||
opacity:0;pointer-events:none;
|
||||
transition:opacity .5s var(--ease), transform .5s var(--ease);
|
||||
transform:translateX(30px);
|
||||
overflow:hidden;
|
||||
}
|
||||
.slide.is-active{opacity:1;pointer-events:auto;transform:translateX(0);z-index:2}
|
||||
.slide.is-prev{transform:translateX(-30px)}
|
||||
|
||||
/* single-page standalone (used when a layout file is opened directly) */
|
||||
body.single .slide{position:relative;width:100vw;height:100vh;opacity:1;transform:none;pointer-events:auto}
|
||||
|
||||
/* ================= TYPOGRAPHY ================= */
|
||||
.eyebrow{font-size:13px;font-weight:500;letter-spacing:.16em;text-transform:uppercase;color:var(--text-3)}
|
||||
.kicker{font-size:14px;font-weight:600;color:var(--accent);letter-spacing:.08em;text-transform:uppercase}
|
||||
h1.title,.h1{font-family:var(--font-display);font-size:72px;line-height:1.05;font-weight:800;letter-spacing:var(--letter-tight);margin:0 0 18px;color:var(--text-1)}
|
||||
h2.title,.h2{font-family:var(--font-display);font-size:54px;line-height:1.1;font-weight:700;letter-spacing:var(--letter-tight);margin:0 0 14px}
|
||||
h3,.h3{font-size:32px;line-height:1.2;font-weight:600;letter-spacing:var(--letter-normal);margin:0 0 10px}
|
||||
h4,.h4{font-size:22px;line-height:1.3;font-weight:600;margin:0 0 8px}
|
||||
.lede{font-size:22px;line-height:1.55;color:var(--text-2);font-weight:300;max-width:62ch}
|
||||
.dim{color:var(--text-2)}
|
||||
.dim2{color:var(--text-3)}
|
||||
.mono{font-family:var(--font-mono)}
|
||||
.serif{font-family:var(--font-serif)}
|
||||
.gradient-text{background:var(--grad);-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent;color:transparent}
|
||||
|
||||
/* ================= LAYOUT PRIMITIVES ================= */
|
||||
.stack>*+*{margin-top:14px}
|
||||
.row{display:flex;gap:24px;align-items:center}
|
||||
.row.wrap{flex-wrap:wrap}
|
||||
.grid{display:grid;gap:24px}
|
||||
.g2{grid-template-columns:repeat(2,1fr)}
|
||||
.g3{grid-template-columns:repeat(3,1fr)}
|
||||
.g4{grid-template-columns:repeat(4,1fr)}
|
||||
.center{display:flex;align-items:center;justify-content:center;text-align:center}
|
||||
.fill{flex:1}
|
||||
.sp-t{padding-top:24px}.sp-b{padding-bottom:24px}
|
||||
.mt-s{margin-top:8px}.mt-m{margin-top:18px}.mt-l{margin-top:32px}
|
||||
.mb-s{margin-bottom:8px}.mb-m{margin-bottom:18px}.mb-l{margin-bottom:32px}
|
||||
|
||||
/* ================= CARDS ================= */
|
||||
.card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);
|
||||
padding:26px 28px;box-shadow:var(--shadow);position:relative;overflow:hidden}
|
||||
.card-soft{background:var(--surface-2);border:1px solid var(--border)}
|
||||
.card-outline{background:transparent;border:1.5px solid var(--border-strong);box-shadow:none}
|
||||
.card-accent{background:var(--surface);border-top:3px solid var(--accent)}
|
||||
.card-hover{transition:transform .3s var(--ease),box-shadow .3s var(--ease)}
|
||||
.card-hover:hover{transform:translateY(-4px);box-shadow:var(--shadow-lg)}
|
||||
|
||||
.pill{display:inline-block;padding:4px 12px;border-radius:999px;font-size:12px;font-weight:500;
|
||||
background:var(--surface-2);color:var(--text-2);border:1px solid var(--border)}
|
||||
.pill-accent{background:color-mix(in srgb,var(--accent) 12%,transparent);color:var(--accent);border-color:color-mix(in srgb,var(--accent) 28%,transparent)}
|
||||
|
||||
/* ================= BARS / DIVIDERS ================= */
|
||||
.divider{height:1px;background:var(--border);width:100%}
|
||||
.divider-accent{height:3px;width:72px;background:var(--accent);border-radius:2px}
|
||||
|
||||
/* ================= CHROME (header/footer/progress) ================= */
|
||||
.deck-header{position:absolute;top:24px;left:40px;right:40px;display:flex;align-items:center;justify-content:space-between;
|
||||
font-size:12px;color:var(--text-3);letter-spacing:.12em;text-transform:uppercase;z-index:10;pointer-events:none}
|
||||
.deck-footer{position:absolute;bottom:24px;left:40px;right:40px;display:flex;align-items:center;justify-content:space-between;
|
||||
font-size:12px;color:var(--text-3);z-index:10;pointer-events:none}
|
||||
.slide-number::before{content:attr(data-current)}
|
||||
.slide-number::after{content:" / " attr(data-total)}
|
||||
.progress-bar{position:fixed;left:0;right:0;bottom:0;height:3px;background:transparent;z-index:20}
|
||||
.progress-bar > span{display:block;height:100%;width:0;background:var(--accent);transition:width .3s var(--ease)}
|
||||
|
||||
/* ================= PRESENTER / OVERVIEW ================= */
|
||||
.notes{display:none!important}
|
||||
.notes-overlay{position:fixed;inset:auto 0 0 0;max-height:42vh;background:rgba(20,22,30,.95);color:#e8ebf4;
|
||||
padding:20px 32px;font-size:16px;line-height:1.6;border-top:1px solid rgba(255,255,255,.1);transform:translateY(100%);
|
||||
transition:transform .3s var(--ease);z-index:40;overflow:auto;font-family:var(--font-sans)}
|
||||
.notes-overlay.open{transform:translateY(0)}
|
||||
.overview{position:fixed;inset:0;background:rgba(10,12,18,.92);backdrop-filter:blur(12px);z-index:50;
|
||||
display:none;padding:40px;overflow:auto}
|
||||
.overview.open{display:grid;grid-template-columns:repeat(4,1fr);gap:20px;align-content:start}
|
||||
.overview .thumb{background:var(--surface);border:1px solid var(--border);border-radius:12px;
|
||||
aspect-ratio:16/9;overflow:hidden;cursor:pointer;position:relative;color:var(--text-1);padding:16px;
|
||||
font-size:11px;transition:transform .2s var(--ease)}
|
||||
.overview .thumb:hover{transform:scale(1.04)}
|
||||
.overview .thumb .n{position:absolute;top:8px;left:10px;font-weight:700;font-size:14px;color:var(--text-3)}
|
||||
.overview .thumb .t{position:absolute;bottom:10px;left:14px;right:14px;font-weight:600;color:var(--text-1)}
|
||||
|
||||
/* ================= PRESENTER VIEW ================= */
|
||||
/* Presenter view opens in a separate popup window (S key).
|
||||
* All presenter styles are self-contained in the popup HTML generated by runtime.js.
|
||||
* The audience window (this file) is NOT affected — it stays as normal deck view.
|
||||
* Only the .notes class below is needed to hide speaker notes from audience. */
|
||||
|
||||
/* ================= UTILITY ================= */
|
||||
.hidden{display:none!important}
|
||||
.nowrap{white-space:nowrap}
|
||||
.tr{text-align:right}.tc{text-align:center}.tl{text-align:left}
|
||||
.uppercase{text-transform:uppercase;letter-spacing:.12em}
|
||||
|
||||
/* ================= PRINT ================= */
|
||||
@media print{
|
||||
.slide{position:relative;opacity:1!important;transform:none!important;page-break-after:always;height:100vh}
|
||||
.deck-header,.deck-footer,.progress-bar,.notes-overlay,.overview{display:none!important}
|
||||
}
|
||||
BIN
skills/assets/bgm-ad.mp3
Normal file
BIN
skills/assets/bgm-ad.mp3
Normal file
Binary file not shown.
BIN
skills/assets/bgm-educational-alt.mp3
Normal file
BIN
skills/assets/bgm-educational-alt.mp3
Normal file
Binary file not shown.
BIN
skills/assets/bgm-educational.mp3
Normal file
BIN
skills/assets/bgm-educational.mp3
Normal file
Binary file not shown.
BIN
skills/assets/bgm-tech.mp3
Normal file
BIN
skills/assets/bgm-tech.mp3
Normal file
Binary file not shown.
BIN
skills/assets/bgm-tutorial-alt.mp3
Normal file
BIN
skills/assets/bgm-tutorial-alt.mp3
Normal file
Binary file not shown.
BIN
skills/assets/bgm-tutorial.mp3
Normal file
BIN
skills/assets/bgm-tutorial.mp3
Normal file
Binary file not shown.
166
skills/assets/browser_window.jsx
Normal file
166
skills/assets/browser_window.jsx
Normal file
@@ -0,0 +1,166 @@
|
||||
/**
|
||||
* BrowserWindow — 浏览器窗口边框(Chrome风格)
|
||||
*
|
||||
* 含:traffic lights + tab bar + URL bar
|
||||
*
|
||||
* 用法:
|
||||
* <BrowserWindow url="https://example.com" title="Example">
|
||||
* <YourWebPage />
|
||||
* </BrowserWindow>
|
||||
*/
|
||||
|
||||
const browserWindowStyles = {
|
||||
window: {
|
||||
display: 'inline-block',
|
||||
background: '#fff',
|
||||
borderRadius: 10,
|
||||
overflow: 'hidden',
|
||||
boxShadow: '0 30px 80px rgba(0,0,0,0.25), 0 0 0 0.5px rgba(0,0,0,0.15)',
|
||||
},
|
||||
chrome: {
|
||||
background: '#dee1e6',
|
||||
paddingTop: 10,
|
||||
paddingLeft: 10,
|
||||
paddingRight: 10,
|
||||
userSelect: 'none',
|
||||
},
|
||||
tabRow: {
|
||||
display: 'flex',
|
||||
alignItems: 'flex-end',
|
||||
gap: 6,
|
||||
position: 'relative',
|
||||
},
|
||||
trafficLights: {
|
||||
display: 'flex',
|
||||
gap: 8,
|
||||
alignItems: 'center',
|
||||
paddingBottom: 10,
|
||||
marginRight: 8,
|
||||
},
|
||||
light: {
|
||||
width: 12,
|
||||
height: 12,
|
||||
borderRadius: '50%',
|
||||
border: '0.5px solid rgba(0,0,0,0.15)',
|
||||
},
|
||||
close: { background: '#ff5f57' },
|
||||
minimize: { background: '#febc2e' },
|
||||
maximize: { background: '#28c840' },
|
||||
tab: {
|
||||
background: '#fff',
|
||||
padding: '8px 30px 8px 14px',
|
||||
borderTopLeftRadius: 10,
|
||||
borderTopRightRadius: 10,
|
||||
fontSize: 12,
|
||||
color: '#222',
|
||||
fontFamily: '-apple-system, sans-serif',
|
||||
maxWidth: 220,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
position: 'relative',
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
},
|
||||
favicon: {
|
||||
width: 14,
|
||||
height: 14,
|
||||
borderRadius: 2,
|
||||
background: '#999',
|
||||
flexShrink: 0,
|
||||
},
|
||||
navBar: {
|
||||
background: '#fff',
|
||||
padding: '8px 14px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 10,
|
||||
borderBottom: '1px solid #e5e7eb',
|
||||
},
|
||||
navButtons: {
|
||||
display: 'flex',
|
||||
gap: 4,
|
||||
color: '#5f6368',
|
||||
fontSize: 16,
|
||||
},
|
||||
navButton: {
|
||||
width: 28,
|
||||
height: 28,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderRadius: '50%',
|
||||
cursor: 'pointer',
|
||||
},
|
||||
urlBar: {
|
||||
flex: 1,
|
||||
background: '#f1f3f4',
|
||||
borderRadius: 999,
|
||||
padding: '7px 14px',
|
||||
fontSize: 13,
|
||||
color: '#333',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
fontFamily: '-apple-system, sans-serif',
|
||||
},
|
||||
lockIcon: {
|
||||
color: '#5f6368',
|
||||
fontSize: 12,
|
||||
},
|
||||
content: {
|
||||
position: 'relative',
|
||||
overflow: 'auto',
|
||||
background: '#fff',
|
||||
},
|
||||
};
|
||||
|
||||
function BrowserWindow({
|
||||
title = 'New Tab',
|
||||
url = 'https://example.com',
|
||||
width = 1200,
|
||||
height = 800,
|
||||
showTrafficLights = true,
|
||||
children,
|
||||
}) {
|
||||
return (
|
||||
<div style={browserWindowStyles.window}>
|
||||
<div style={browserWindowStyles.chrome}>
|
||||
<div style={browserWindowStyles.tabRow}>
|
||||
{showTrafficLights && (
|
||||
<div style={browserWindowStyles.trafficLights}>
|
||||
<div style={{ ...browserWindowStyles.light, ...browserWindowStyles.close }} />
|
||||
<div style={{ ...browserWindowStyles.light, ...browserWindowStyles.minimize }} />
|
||||
<div style={{ ...browserWindowStyles.light, ...browserWindowStyles.maximize }} />
|
||||
</div>
|
||||
)}
|
||||
<div style={browserWindowStyles.tab}>
|
||||
<div style={browserWindowStyles.favicon} />
|
||||
<span>{title}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={browserWindowStyles.navBar}>
|
||||
<div style={browserWindowStyles.navButtons}>
|
||||
<div style={browserWindowStyles.navButton}>←</div>
|
||||
<div style={browserWindowStyles.navButton}>→</div>
|
||||
<div style={browserWindowStyles.navButton}>↻</div>
|
||||
</div>
|
||||
<div style={browserWindowStyles.urlBar}>
|
||||
<span style={browserWindowStyles.lockIcon}>🔒</span>
|
||||
<span>{url}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ ...browserWindowStyles.content, width, height }}>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
window.BrowserWindow = BrowserWindow;
|
||||
}
|
||||
237
skills/assets/deck_index.html
Normal file
237
skills/assets/deck_index.html
Normal file
@@ -0,0 +1,237 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Deck · Multi-file Slide Index</title>
|
||||
<!--
|
||||
deck_index.html — 多文件 slide deck 的拼接器
|
||||
|
||||
配合「每页一个独立 HTML」架构使用。与单文件 deck_stage.js 对比:
|
||||
· 每页独立作用域(CSS/JS 都隔离),一页出 bug 不影响其他页
|
||||
· 单页可直接在浏览器打开验证,不依赖 JS goTo()
|
||||
· 多 agent 可并行做不同页,merge 时零冲突
|
||||
· 适合 ≥15 页的讲座/课件/长 deck
|
||||
|
||||
用法:
|
||||
1. 把本文件复制到 deck 根目录,重命名 index.html
|
||||
2. 在同目录建 slides/ 子目录,放每一页独立 HTML
|
||||
3. 编辑下方 MANIFEST 数组,按顺序列出文件名和人类可读标签
|
||||
4. 每张 slide HTML 建议尺寸 1920×1080,自带背景/字体;不要依赖外层 CSS
|
||||
|
||||
共享资源(如果需要):
|
||||
· shared/tokens.css — 跨页 CSS 变量(色板/字号)
|
||||
· shared/chrome.html — 页眉页脚可复用片段
|
||||
· 每页 HTML 自己 <link> 进去即可
|
||||
|
||||
键盘:← / → / Space / PgUp / PgDown / Home / End / 1-9 跳页 / P 打印
|
||||
-->
|
||||
|
||||
<!-- ═══════════════════════════════════════════════════════ -->
|
||||
<!-- EDIT THIS — deck 所有页按顺序列出 -->
|
||||
<!-- ═══════════════════════════════════════════════════════ -->
|
||||
<script>
|
||||
window.DECK_MANIFEST = [
|
||||
{ file: "slides/01-cover.html", label: "Cover" },
|
||||
{ file: "slides/02-quote.html", label: "Opening Quote" },
|
||||
{ file: "slides/03-intro.html", label: "Self-intro" },
|
||||
// 继续往下加。file 是相对本文件的路径,label 用于计数器
|
||||
];
|
||||
|
||||
// 固定 canvas 尺寸。每页 HTML 都应该按这个尺寸设计。
|
||||
window.DECK_WIDTH = 1920;
|
||||
window.DECK_HEIGHT = 1080;
|
||||
</script>
|
||||
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
html, body {
|
||||
height: 100%;
|
||||
background: #0a0a0a;
|
||||
overflow: hidden;
|
||||
font-family: -apple-system, "PingFang SC", sans-serif;
|
||||
}
|
||||
#stage {
|
||||
position: fixed;
|
||||
top: 50%; left: 50%;
|
||||
transform-origin: top left;
|
||||
will-change: transform;
|
||||
background: #fff;
|
||||
box-shadow: 0 10px 60px rgba(0,0,0,0.4);
|
||||
/* size set by JS from DECK_WIDTH/HEIGHT */
|
||||
}
|
||||
iframe {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 0;
|
||||
display: block;
|
||||
background: #fff;
|
||||
}
|
||||
.counter {
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
background: rgba(0,0,0,0.65);
|
||||
color: #fff;
|
||||
padding: 6px 14px;
|
||||
border-radius: 999px;
|
||||
font-size: 13px;
|
||||
letter-spacing: 0.05em;
|
||||
font-variant-numeric: tabular-nums;
|
||||
z-index: 100;
|
||||
user-select: none;
|
||||
opacity: 0.7;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
.counter:hover { opacity: 1; }
|
||||
.counter .label { color: rgba(255,255,255,0.7); margin-left: 8px; }
|
||||
.nav-zone {
|
||||
position: fixed;
|
||||
top: 0; bottom: 0;
|
||||
width: 15%;
|
||||
cursor: pointer;
|
||||
z-index: 50;
|
||||
}
|
||||
.nav-zone.left { left: 0; }
|
||||
.nav-zone.right { right: 0; }
|
||||
.nav-hint {
|
||||
position: absolute;
|
||||
top: 50%; transform: translateY(-50%);
|
||||
width: 44px; height: 44px;
|
||||
border-radius: 999px;
|
||||
background: rgba(255,255,255,0.08);
|
||||
color: rgba(255,255,255,0.6);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 22px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
.nav-zone.left .nav-hint { left: 20px; }
|
||||
.nav-zone.right .nav-hint { right: 20px; }
|
||||
.nav-zone:hover .nav-hint { opacity: 1; }
|
||||
|
||||
/* Print: one slide per page, no navigation UI */
|
||||
@media print {
|
||||
@page { size: 1920px 1080px; margin: 0; }
|
||||
html, body { background: #fff; overflow: visible; height: auto; }
|
||||
#stage { position: static; transform: none !important; box-shadow: none; }
|
||||
.counter, .nav-zone { display: none !important; }
|
||||
/* In print mode we render all slides sequentially — see JS */
|
||||
.print-stack { display: block; }
|
||||
.print-stack iframe {
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
page-break-after: always;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div id="stage">
|
||||
<iframe id="frame" src="about:blank"></iframe>
|
||||
</div>
|
||||
|
||||
<div class="nav-zone left" id="navL"><div class="nav-hint">‹</div></div>
|
||||
<div class="nav-zone right" id="navR"><div class="nav-hint">›</div></div>
|
||||
<div class="counter" id="counter">1 / 1</div>
|
||||
|
||||
<!-- Print-only stack: populated on beforeprint, stripped on afterprint -->
|
||||
<div class="print-stack" id="printStack" style="display:none;"></div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
const W = window.DECK_WIDTH || 1920;
|
||||
const H = window.DECK_HEIGHT || 1080;
|
||||
const deck = window.DECK_MANIFEST || [];
|
||||
const stage = document.getElementById('stage');
|
||||
const frame = document.getElementById('frame');
|
||||
const counter = document.getElementById('counter');
|
||||
const printStack = document.getElementById('printStack');
|
||||
const storageKey = 'deck-index-' + location.pathname;
|
||||
let current = 0;
|
||||
|
||||
stage.style.width = W + 'px';
|
||||
stage.style.height = H + 'px';
|
||||
|
||||
function fit() {
|
||||
const s = Math.min(window.innerWidth / W, window.innerHeight / H);
|
||||
const x = (window.innerWidth - W * s) / 2;
|
||||
const y = (window.innerHeight - H * s) / 2;
|
||||
stage.style.transform = `translate(${x}px, ${y}px) scale(${s})`;
|
||||
stage.style.top = '0';
|
||||
stage.style.left = '0';
|
||||
}
|
||||
|
||||
function show(idx) {
|
||||
if (idx < 0 || idx >= deck.length) return;
|
||||
current = idx;
|
||||
frame.src = deck[idx].file;
|
||||
counter.innerHTML = `${idx + 1} / ${deck.length} <span class="label">${deck[idx].label || ''}</span>`;
|
||||
try { localStorage.setItem(storageKey, String(idx)); } catch (_) {}
|
||||
if (location.hash !== '#' + (idx + 1)) {
|
||||
history.replaceState(null, '', '#' + (idx + 1));
|
||||
}
|
||||
}
|
||||
|
||||
function next() { show(Math.min(current + 1, deck.length - 1)); }
|
||||
function prev() { show(Math.max(current - 1, 0)); }
|
||||
|
||||
// Keyboard
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
|
||||
switch (e.key) {
|
||||
case 'ArrowRight': case ' ': case 'PageDown': e.preventDefault(); next(); break;
|
||||
case 'ArrowLeft': case 'PageUp': e.preventDefault(); prev(); break;
|
||||
case 'Home': e.preventDefault(); show(0); break;
|
||||
case 'End': e.preventDefault(); show(deck.length - 1); break;
|
||||
case 'p': case 'P': window.print(); break;
|
||||
default:
|
||||
if (e.key >= '1' && e.key <= '9') {
|
||||
const i = parseInt(e.key, 10) - 1;
|
||||
if (i < deck.length) { e.preventDefault(); show(i); }
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('navL').addEventListener('click', prev);
|
||||
document.getElementById('navR').addEventListener('click', next);
|
||||
window.addEventListener('resize', fit);
|
||||
window.addEventListener('hashchange', () => {
|
||||
const m = location.hash.match(/^#(\d+)$/);
|
||||
if (m) show(parseInt(m[1], 10) - 1);
|
||||
});
|
||||
|
||||
// Initial: hash > localStorage > 0
|
||||
const hashMatch = location.hash.match(/^#(\d+)$/);
|
||||
if (hashMatch) current = Math.min(parseInt(hashMatch[1], 10) - 1, deck.length - 1);
|
||||
else try {
|
||||
const v = parseInt(localStorage.getItem(storageKey), 10);
|
||||
if (!isNaN(v) && v >= 0 && v < deck.length) current = v;
|
||||
} catch (_) {}
|
||||
fit();
|
||||
show(current);
|
||||
|
||||
// Print: build a stack of all iframes so browser prints every slide
|
||||
window.addEventListener('beforeprint', () => {
|
||||
printStack.innerHTML = '';
|
||||
deck.forEach(item => {
|
||||
const f = document.createElement('iframe');
|
||||
f.src = item.file;
|
||||
printStack.appendChild(f);
|
||||
});
|
||||
printStack.style.display = 'block';
|
||||
document.getElementById('stage').style.display = 'none';
|
||||
});
|
||||
window.addEventListener('afterprint', () => {
|
||||
printStack.innerHTML = '';
|
||||
printStack.style.display = 'none';
|
||||
document.getElementById('stage').style.display = '';
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
420
skills/assets/deck_stage.js
Normal file
420
skills/assets/deck_stage.js
Normal file
@@ -0,0 +1,420 @@
|
||||
/**
|
||||
* <deck-stage> — HTML幻灯片外壳web component
|
||||
*
|
||||
* 提供功能:
|
||||
* - 固定尺寸canvas(默认1920×1080)+ auto-scale + letterbox
|
||||
* - 键盘导航(←/→/Space/Home/End/Esc)
|
||||
* - 左右点击区域导航
|
||||
* - slide counter (当前/总数)
|
||||
* - localStorage持久化当前slide
|
||||
* - Speaker notes postMessage (支持外层渲染)
|
||||
* - Hash导航 (#slide-5 跳到第5张)
|
||||
* - Print-to-PDF支持 (Cmd+P / Ctrl+P 一页一slide)
|
||||
* - 自动给每个slide添加 data-screen-label
|
||||
*
|
||||
* 用法:
|
||||
* <deck-stage>
|
||||
* <section>Slide 1</section>
|
||||
* <section>Slide 2</section>
|
||||
* </deck-stage>
|
||||
*
|
||||
* 自定义尺寸:
|
||||
* <deck-stage width="1080" height="1920">...</deck-stage>
|
||||
*
|
||||
* Speaker notes:在<head>加
|
||||
* <script type="application/json" id="speaker-notes">
|
||||
* ["slide 1 notes", "slide 2 notes"]
|
||||
* </script>
|
||||
*/
|
||||
|
||||
(function() {
|
||||
const STORAGE_KEY_PREFIX = 'deck-stage-slide-';
|
||||
|
||||
class DeckStage extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
this.attachShadow({ mode: 'open' });
|
||||
this._currentSlide = 0;
|
||||
this._slides = [];
|
||||
this._storageKey = STORAGE_KEY_PREFIX + (location.pathname || 'default');
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this._width = parseInt(this.getAttribute('width')) || 1920;
|
||||
this._height = parseInt(this.getAttribute('height')) || 1080;
|
||||
|
||||
// Shadow DOM 先渲染(独立于子节点,不受 parser 时机影响)
|
||||
this._render();
|
||||
|
||||
// 防御:若 script 放在 <head> 里(而非 </deck-stage> 之后),
|
||||
// parser 此刻可能还没处理完子 <section>,querySelectorAll 会返回空。
|
||||
// 延迟到下一个事件循环,确保子节点都已 parse 完毕。
|
||||
const init = () => {
|
||||
this._collectSlides();
|
||||
this._setupEventListeners();
|
||||
this._restoreSlide();
|
||||
this._updateDisplay();
|
||||
this._setupPrintStyles();
|
||||
};
|
||||
|
||||
if (this.ownerDocument.readyState === 'loading') {
|
||||
// 文档还在 parse,等 DOMContentLoaded 一次搞定所有 section
|
||||
this.ownerDocument.addEventListener('DOMContentLoaded', init, { once: true });
|
||||
} else {
|
||||
// 文档已 parse 完(script 在 body 底部或 defer),下一帧收集即可
|
||||
requestAnimationFrame(init);
|
||||
}
|
||||
}
|
||||
|
||||
_render() {
|
||||
this.shadowRoot.innerHTML = `
|
||||
<style>
|
||||
:host {
|
||||
display: block;
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: #000;
|
||||
overflow: hidden;
|
||||
font-family: -apple-system, 'SF Pro Text', 'PingFang SC', sans-serif;
|
||||
}
|
||||
|
||||
:host([noscale]) .stage {
|
||||
transform: none !important;
|
||||
top: 0 !important;
|
||||
left: 0 !important;
|
||||
}
|
||||
|
||||
.stage {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform-origin: top left;
|
||||
will-change: transform;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.slide-wrapper {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
::slotted(section) {
|
||||
display: none;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
::slotted(section.active) {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.counter {
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
color: #fff;
|
||||
padding: 6px 14px;
|
||||
border-radius: 999px;
|
||||
font-size: 13px;
|
||||
font-variant-numeric: tabular-nums;
|
||||
z-index: 100;
|
||||
user-select: none;
|
||||
opacity: 0.6;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.counter:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.nav-zone {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 15%;
|
||||
cursor: pointer;
|
||||
z-index: 50;
|
||||
}
|
||||
|
||||
.nav-zone.left { left: 0; }
|
||||
.nav-zone.right { right: 0; }
|
||||
|
||||
.nav-hint {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 24px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.nav-zone.left .nav-hint { left: 20px; }
|
||||
.nav-zone.right .nav-hint { right: 20px; }
|
||||
|
||||
.nav-zone:hover .nav-hint {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@media print {
|
||||
:host {
|
||||
position: static;
|
||||
background: #fff;
|
||||
}
|
||||
.counter, .nav-zone {
|
||||
display: none !important;
|
||||
}
|
||||
.stage {
|
||||
position: static;
|
||||
transform: none !important;
|
||||
page-break-after: always;
|
||||
}
|
||||
::slotted(section) {
|
||||
display: block !important;
|
||||
position: relative !important;
|
||||
page-break-after: always;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="stage" id="stage" style="width: ${this._width}px; height: ${this._height}px;">
|
||||
<div class="slide-wrapper">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="nav-zone left" id="navLeft">
|
||||
<div class="nav-hint">‹</div>
|
||||
</div>
|
||||
<div class="nav-zone right" id="navRight">
|
||||
<div class="nav-hint">›</div>
|
||||
</div>
|
||||
|
||||
<div class="counter" id="counter">1 / 1</div>
|
||||
`;
|
||||
}
|
||||
|
||||
_collectSlides() {
|
||||
this._slides = Array.from(this.querySelectorAll(':scope > section'));
|
||||
|
||||
this._slides.forEach((slide, idx) => {
|
||||
if (!slide.hasAttribute('data-screen-label')) {
|
||||
const num = String(idx + 1).padStart(2, '0');
|
||||
slide.setAttribute('data-screen-label', num);
|
||||
}
|
||||
if (!slide.hasAttribute('data-om-validate')) {
|
||||
slide.setAttribute('data-om-validate', '');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_setupEventListeners() {
|
||||
window.addEventListener('resize', () => this._updateScale());
|
||||
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.target.matches('input, textarea, [contenteditable]')) return;
|
||||
|
||||
switch (e.key) {
|
||||
case 'ArrowRight':
|
||||
case ' ':
|
||||
case 'PageDown':
|
||||
e.preventDefault();
|
||||
this.next();
|
||||
break;
|
||||
case 'ArrowLeft':
|
||||
case 'PageUp':
|
||||
e.preventDefault();
|
||||
this.prev();
|
||||
break;
|
||||
case 'Home':
|
||||
e.preventDefault();
|
||||
this.goTo(0);
|
||||
break;
|
||||
case 'End':
|
||||
e.preventDefault();
|
||||
this.goTo(this._slides.length - 1);
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
this.shadowRoot.getElementById('navLeft').addEventListener('click', () => this.prev());
|
||||
this.shadowRoot.getElementById('navRight').addEventListener('click', () => this.next());
|
||||
|
||||
window.addEventListener('hashchange', () => this._handleHash());
|
||||
if (location.hash) {
|
||||
setTimeout(() => this._handleHash(), 0);
|
||||
}
|
||||
|
||||
const observer = new MutationObserver(() => {
|
||||
if (this.hasAttribute('noscale')) {
|
||||
this._updateScale();
|
||||
}
|
||||
});
|
||||
observer.observe(this, { attributes: true, attributeFilter: ['noscale'] });
|
||||
}
|
||||
|
||||
_handleHash() {
|
||||
const match = location.hash.match(/^#slide-(\d+)$/);
|
||||
if (match) {
|
||||
const idx = parseInt(match[1]) - 1;
|
||||
if (idx >= 0 && idx < this._slides.length) {
|
||||
this.goTo(idx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_restoreSlide() {
|
||||
try {
|
||||
const stored = localStorage.getItem(this._storageKey);
|
||||
if (stored !== null) {
|
||||
const idx = parseInt(stored);
|
||||
if (idx >= 0 && idx < this._slides.length) {
|
||||
this._currentSlide = idx;
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
_saveSlide() {
|
||||
try {
|
||||
localStorage.setItem(this._storageKey, String(this._currentSlide));
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
_updateScale() {
|
||||
if (this.hasAttribute('noscale')) {
|
||||
const stage = this.shadowRoot.getElementById('stage');
|
||||
stage.style.transform = 'none';
|
||||
stage.style.top = '0';
|
||||
stage.style.left = '0';
|
||||
return;
|
||||
}
|
||||
|
||||
const stage = this.shadowRoot.getElementById('stage');
|
||||
if (!stage) return;
|
||||
|
||||
const viewportW = window.innerWidth;
|
||||
const viewportH = window.innerHeight;
|
||||
const scale = Math.min(viewportW / this._width, viewportH / this._height);
|
||||
const scaledW = this._width * scale;
|
||||
const scaledH = this._height * scale;
|
||||
const offsetX = (viewportW - scaledW) / 2;
|
||||
const offsetY = (viewportH - scaledH) / 2;
|
||||
|
||||
stage.style.transform = `translate(${offsetX}px, ${offsetY}px) scale(${scale})`;
|
||||
stage.style.top = '0';
|
||||
stage.style.left = '0';
|
||||
}
|
||||
|
||||
_updateDisplay() {
|
||||
this._slides.forEach((slide, idx) => {
|
||||
slide.classList.toggle('active', idx === this._currentSlide);
|
||||
});
|
||||
|
||||
const counter = this.shadowRoot.getElementById('counter');
|
||||
if (counter) {
|
||||
counter.textContent = `${this._currentSlide + 1} / ${this._slides.length}`;
|
||||
}
|
||||
|
||||
this._updateScale();
|
||||
|
||||
try {
|
||||
window.postMessage({
|
||||
slideIndexChanged: this._currentSlide,
|
||||
totalSlides: this._slides.length
|
||||
}, '*');
|
||||
} catch (e) {}
|
||||
|
||||
try {
|
||||
if (window.parent && window.parent !== window) {
|
||||
window.parent.postMessage({
|
||||
slideIndexChanged: this._currentSlide,
|
||||
totalSlides: this._slides.length
|
||||
}, '*');
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
_setupPrintStyles() {
|
||||
const printStyle = document.createElement('style');
|
||||
printStyle.textContent = `
|
||||
@media print {
|
||||
@page {
|
||||
size: ${this._width}px ${this._height}px;
|
||||
margin: 0;
|
||||
}
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
deck-stage {
|
||||
position: static !important;
|
||||
}
|
||||
deck-stage > section {
|
||||
display: block !important;
|
||||
position: relative !important;
|
||||
width: ${this._width}px !important;
|
||||
height: ${this._height}px !important;
|
||||
page-break-after: always;
|
||||
overflow: hidden;
|
||||
}
|
||||
deck-stage > section:last-child {
|
||||
page-break-after: auto;
|
||||
}
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(printStyle);
|
||||
}
|
||||
|
||||
next() {
|
||||
if (this._currentSlide < this._slides.length - 1) {
|
||||
this._currentSlide++;
|
||||
this._saveSlide();
|
||||
this._updateDisplay();
|
||||
}
|
||||
}
|
||||
|
||||
prev() {
|
||||
if (this._currentSlide > 0) {
|
||||
this._currentSlide--;
|
||||
this._saveSlide();
|
||||
this._updateDisplay();
|
||||
}
|
||||
}
|
||||
|
||||
goTo(idx) {
|
||||
if (idx >= 0 && idx < this._slides.length) {
|
||||
this._currentSlide = idx;
|
||||
this._saveSlide();
|
||||
this._updateDisplay();
|
||||
}
|
||||
}
|
||||
|
||||
get currentSlide() {
|
||||
return this._currentSlide;
|
||||
}
|
||||
|
||||
get totalSlides() {
|
||||
return this._slides.length;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('deck-stage', DeckStage);
|
||||
|
||||
window.DeckStage = DeckStage;
|
||||
})();
|
||||
205
skills/assets/design_canvas.jsx
Normal file
205
skills/assets/design_canvas.jsx
Normal file
@@ -0,0 +1,205 @@
|
||||
/**
|
||||
* DesignCanvas — 变体并排网格布局
|
||||
*
|
||||
* 用于展示2+个静态设计variations让用户对比选择。
|
||||
* 每个variation有label,可hover放大。
|
||||
*
|
||||
* 用法:
|
||||
* <DesignCanvas
|
||||
* title="Hero区设计探索"
|
||||
* subtitle="3个方向对比"
|
||||
* columns={3}
|
||||
* >
|
||||
* <Variation label="Minimal" description="极简克制版">
|
||||
* <div>...你的设计1...</div>
|
||||
* </Variation>
|
||||
* <Variation label="Editorial" description="杂志编辑风">
|
||||
* <div>...你的设计2...</div>
|
||||
* </Variation>
|
||||
* <Variation label="Brutalist" description="粗粝原始">
|
||||
* <div>...你的设计3...</div>
|
||||
* </Variation>
|
||||
* </DesignCanvas>
|
||||
*
|
||||
* 配合React+Babel使用。放在合适的script里,然后window.DesignCanvas/window.Variation可用。
|
||||
*/
|
||||
|
||||
const canvasStyles = {
|
||||
container: {
|
||||
minHeight: '100vh',
|
||||
background: '#F5F5F0',
|
||||
padding: '40px 60px',
|
||||
fontFamily: '-apple-system, "SF Pro Text", "PingFang SC", sans-serif',
|
||||
},
|
||||
header: {
|
||||
marginBottom: 48,
|
||||
maxWidth: 900,
|
||||
},
|
||||
title: {
|
||||
fontSize: 36,
|
||||
fontWeight: 600,
|
||||
marginBottom: 12,
|
||||
color: '#1A1A1A',
|
||||
letterSpacing: '-0.02em',
|
||||
},
|
||||
subtitle: {
|
||||
fontSize: 16,
|
||||
color: '#666',
|
||||
lineHeight: 1.5,
|
||||
},
|
||||
grid: {
|
||||
display: 'grid',
|
||||
gap: 32,
|
||||
},
|
||||
cell: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 12,
|
||||
},
|
||||
cellHeader: {
|
||||
display: 'flex',
|
||||
alignItems: 'baseline',
|
||||
gap: 12,
|
||||
paddingBottom: 8,
|
||||
borderBottom: '1px solid #E0E0DA',
|
||||
},
|
||||
label: {
|
||||
fontSize: 14,
|
||||
fontWeight: 600,
|
||||
color: '#1A1A1A',
|
||||
letterSpacing: '-0.01em',
|
||||
},
|
||||
description: {
|
||||
fontSize: 13,
|
||||
color: '#888',
|
||||
},
|
||||
frame: {
|
||||
background: '#fff',
|
||||
borderRadius: 4,
|
||||
border: '1px solid #E0E0DA',
|
||||
overflow: 'hidden',
|
||||
position: 'relative',
|
||||
transition: 'transform 0.2s ease, box-shadow 0.2s ease',
|
||||
cursor: 'pointer',
|
||||
},
|
||||
frameInner: {
|
||||
position: 'relative',
|
||||
width: '100%',
|
||||
},
|
||||
badge: {
|
||||
position: 'absolute',
|
||||
top: 12,
|
||||
left: 12,
|
||||
background: 'rgba(0, 0, 0, 0.7)',
|
||||
color: '#fff',
|
||||
padding: '3px 8px',
|
||||
borderRadius: 4,
|
||||
fontSize: 11,
|
||||
fontWeight: 500,
|
||||
letterSpacing: '0.5px',
|
||||
textTransform: 'uppercase',
|
||||
zIndex: 10,
|
||||
pointerEvents: 'none',
|
||||
},
|
||||
};
|
||||
|
||||
function DesignCanvas({ title, subtitle, columns = 3, children }) {
|
||||
const [expanded, setExpanded] = React.useState(null);
|
||||
|
||||
const gridStyle = {
|
||||
...canvasStyles.grid,
|
||||
gridTemplateColumns: `repeat(${columns}, 1fr)`,
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={canvasStyles.container}>
|
||||
{(title || subtitle) && (
|
||||
<div style={canvasStyles.header}>
|
||||
{title && <h1 style={canvasStyles.title}>{title}</h1>}
|
||||
{subtitle && <p style={canvasStyles.subtitle}>{subtitle}</p>}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div style={gridStyle}>
|
||||
{React.Children.map(children, (child, idx) =>
|
||||
React.isValidElement(child)
|
||||
? React.cloneElement(child, {
|
||||
_index: idx,
|
||||
_expanded: expanded === idx,
|
||||
_onToggle: () => setExpanded(expanded === idx ? null : idx),
|
||||
})
|
||||
: child
|
||||
)}
|
||||
</div>
|
||||
|
||||
{expanded !== null && (
|
||||
<div
|
||||
onClick={() => setExpanded(null)}
|
||||
style={{
|
||||
position: 'fixed',
|
||||
inset: 0,
|
||||
background: 'rgba(0, 0, 0, 0.75)',
|
||||
zIndex: 1000,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: 40,
|
||||
cursor: 'zoom-out',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
onClick={e => e.stopPropagation()}
|
||||
style={{
|
||||
background: '#fff',
|
||||
borderRadius: 8,
|
||||
overflow: 'hidden',
|
||||
maxWidth: '90vw',
|
||||
maxHeight: '90vh',
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
{React.Children.toArray(children)[expanded]}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Variation({ label, description, number, children, _index, _expanded, _onToggle, aspectRatio = '4 / 3' }) {
|
||||
const displayNumber = number || String(_index + 1).padStart(2, '0');
|
||||
|
||||
return (
|
||||
<div style={canvasStyles.cell}>
|
||||
<div style={canvasStyles.cellHeader}>
|
||||
<span style={{ ...canvasStyles.label, color: '#999', fontFamily: 'ui-monospace, monospace', fontSize: 12 }}>
|
||||
{displayNumber}
|
||||
</span>
|
||||
<span style={canvasStyles.label}>{label}</span>
|
||||
{description && <span style={canvasStyles.description}>— {description}</span>}
|
||||
</div>
|
||||
|
||||
<div
|
||||
onClick={_onToggle}
|
||||
style={{
|
||||
...canvasStyles.frame,
|
||||
aspectRatio,
|
||||
}}
|
||||
onMouseEnter={e => {
|
||||
e.currentTarget.style.boxShadow = '0 8px 24px rgba(0,0,0,0.08)';
|
||||
}}
|
||||
onMouseLeave={e => {
|
||||
e.currentTarget.style.boxShadow = 'none';
|
||||
}}
|
||||
>
|
||||
<div style={canvasStyles.frameInner}>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
Object.assign(window, { DesignCanvas, Variation });
|
||||
}
|
||||
167
skills/assets/ecommerce.md
Normal file
167
skills/assets/ecommerce.md
Normal file
@@ -0,0 +1,167 @@
|
||||
<!-- Updated: 2026-02-07 -->
|
||||
# E-commerce SEO Strategy Template
|
||||
|
||||
## Industry Characteristics
|
||||
|
||||
- High transaction intent
|
||||
- Product comparison behavior
|
||||
- Price sensitivity
|
||||
- Visual-first decision making
|
||||
- Seasonal demand patterns
|
||||
- Competitive marketplace listings
|
||||
|
||||
## Recommended Site Architecture
|
||||
|
||||
```
|
||||
/
|
||||
├── Home
|
||||
├── /collections (or /categories)
|
||||
│ ├── /category-1
|
||||
│ │ ├── /subcategory-1
|
||||
│ │ └── ...
|
||||
│ ├── /category-2
|
||||
│ └── ...
|
||||
├── /products
|
||||
│ ├── /product-1
|
||||
│ ├── /product-2
|
||||
│ └── ...
|
||||
├── /brands
|
||||
│ ├── /brand-1
|
||||
│ └── ...
|
||||
├── /sale (or /deals)
|
||||
├── /new-arrivals
|
||||
├── /best-sellers
|
||||
├── /gift-guide
|
||||
├── /blog
|
||||
│ ├── /buying-guides
|
||||
│ ├── /how-to
|
||||
│ └── /trends
|
||||
├── /about
|
||||
├── /contact
|
||||
├── /shipping
|
||||
├── /returns
|
||||
└── /faq
|
||||
```
|
||||
|
||||
## Schema Recommendations
|
||||
|
||||
| Page Type | Schema Types |
|
||||
|-----------|-------------|
|
||||
| Product Page | Product, Offer, AggregateRating, Review, BreadcrumbList |
|
||||
| Category Page | CollectionPage, ItemList, BreadcrumbList |
|
||||
| Brand Page | Brand, Organization |
|
||||
| Blog | Article, BlogPosting |
|
||||
|
||||
### Additional E-commerce Schema (2025)
|
||||
|
||||
- **ProductGroup**: Use for products with variants (size, color). Wraps individual Product entries with `variesBy` and `hasVariant` properties. See `schema/templates.json`.
|
||||
- **Certification**: For product certifications (Energy Star, safety, organic). Replaced EnergyConsumptionDetails (April 2025). Use `hasCertification` on Product.
|
||||
- **OfferShippingDetails**: Include shipping rate, handling time, and transit time. Critical for Merchant Center eligibility.
|
||||
|
||||
> **Google Merchant Center Free Listings:** Products can appear in Google Shopping for free. Ensure Product structured data is in the initial server-rendered HTML (not JavaScript-injected) with required properties: `name`, `image`, `price`, `priceCurrency`, `availability`.
|
||||
|
||||
> **JS Rendering Note:** Product structured data should be in initial server-rendered HTML: not dynamically injected via JavaScript (per December 2025 Google JS SEO guidance).
|
||||
|
||||
### Product Schema Example
|
||||
```json
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Product",
|
||||
"name": "Product Name",
|
||||
"image": ["https://example.com/product.jpg"],
|
||||
"description": "Product description",
|
||||
"sku": "SKU123",
|
||||
"brand": {
|
||||
"@type": "Brand",
|
||||
"name": "Brand Name"
|
||||
},
|
||||
"offers": {
|
||||
"@type": "Offer",
|
||||
"price": "99.99",
|
||||
"priceCurrency": "USD",
|
||||
"availability": "https://schema.org/InStock",
|
||||
"url": "https://example.com/product"
|
||||
},
|
||||
"aggregateRating": {
|
||||
"@type": "AggregateRating",
|
||||
"ratingValue": "4.5",
|
||||
"reviewCount": "42"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Content Requirements
|
||||
|
||||
### Product Pages (min 400 words)
|
||||
- Unique product descriptions (not manufacturer copy)
|
||||
- Feature highlights
|
||||
- Use cases / who it's for
|
||||
- Specifications table
|
||||
- Size/fit guide (for apparel)
|
||||
- Care instructions
|
||||
- Customer reviews
|
||||
|
||||
### Category Pages (min 400 words)
|
||||
- Category introduction
|
||||
- Buying guide excerpt
|
||||
- Featured products
|
||||
- Subcategory links
|
||||
- Filter/sort options
|
||||
|
||||
## Technical Considerations
|
||||
|
||||
### Pagination
|
||||
- Use rel="next"/rel="prev" or load-more
|
||||
- Ensure all products are crawlable
|
||||
- Canonical to main category page
|
||||
|
||||
### Faceted Navigation
|
||||
- Noindex filter combinations that create duplicate content
|
||||
- Use canonical tags appropriately
|
||||
- Ensure popular filters are indexable
|
||||
|
||||
### Product Variations
|
||||
- Single URL for parent product with variants
|
||||
- Or separate URLs with canonical to parent
|
||||
- Structured data for all variants
|
||||
|
||||
## Content Priorities
|
||||
|
||||
### High Priority
|
||||
1. Category pages (top level)
|
||||
2. Best-selling product pages
|
||||
3. Homepage
|
||||
4. Buying guides for main categories
|
||||
|
||||
### Medium Priority
|
||||
1. Subcategory pages
|
||||
2. Brand pages
|
||||
3. Comparison content
|
||||
4. Seasonal landing pages
|
||||
|
||||
### Blog Topics
|
||||
- Buying guides ("How to Choose...")
|
||||
- Product comparisons
|
||||
- Trend reports
|
||||
- Use cases and inspiration
|
||||
- Care and maintenance guides
|
||||
|
||||
## Key Metrics to Track
|
||||
|
||||
- Revenue from organic search
|
||||
- Product page rankings
|
||||
- Category page rankings
|
||||
- Click-through rate (rich results)
|
||||
- Average order value from organic
|
||||
|
||||
## Generative Engine Optimization (GEO) for E-commerce
|
||||
|
||||
AI search platforms increasingly answer product queries directly. Optimize for AI citation:
|
||||
|
||||
- [ ] Include clear product specifications, dimensions, materials in structured format
|
||||
- [ ] Use ProductGroup schema for variant products
|
||||
- [ ] Provide original product photography with descriptive alt text
|
||||
- [ ] Include genuine customer review content (AggregateRating schema)
|
||||
- [ ] Maintain consistent product entity data across all platforms (site, Amazon, Merchant Center)
|
||||
- [ ] Structure comparison content with clear feature tables AI can parse
|
||||
- [ ] Add detailed FAQ content for common product questions
|
||||
162
skills/assets/example-architecture-dark.html
Normal file
162
skills/assets/example-architecture-dark.html
Normal file
@@ -0,0 +1,162 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>littlemight.com · Architecture</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
:root {
|
||||
--color-paper: #1c1a17;
|
||||
--color-ink: #f1efe7;
|
||||
--color-muted: #a8a69d;
|
||||
--color-accent: #ff6a30;
|
||||
--font-sans: 'Geist', system-ui, sans-serif;
|
||||
--font-serif: 'Instrument Serif', serif;
|
||||
--font-mono: 'Geist Mono', ui-monospace, monospace;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-sans);
|
||||
background: var(--color-paper);
|
||||
color: var(--color-ink);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 3rem 2rem;
|
||||
}
|
||||
|
||||
.frame { max-width: 1200px; width: 100%; }
|
||||
|
||||
.eyebrow {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.66rem;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.18em;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-muted);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-family: var(--font-serif);
|
||||
font-size: clamp(1.5rem, 2.4vw + 0.75rem, 2rem);
|
||||
font-weight: 400;
|
||||
letter-spacing: -0.02em;
|
||||
line-height: 1.15;
|
||||
color: var(--color-ink);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
svg { width: 100%; min-width: 900px; display: block; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="frame">
|
||||
<p class="eyebrow">Architecture · Diagram Design</p>
|
||||
<h1>littlemight.com in production</h1>
|
||||
|
||||
<svg viewBox="0 0 1000 480" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<pattern id="dots" width="22" height="22" patternUnits="userSpaceOnUse">
|
||||
<circle cx="1" cy="1" r="0.9" fill="rgba(241,239,231,0.10)"/>
|
||||
</pattern>
|
||||
<marker id="arrow" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0, 8 3, 0 6" fill="#a8a69d"/></marker>
|
||||
<marker id="arrow-accent" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0, 8 3, 0 6" fill="#ff6a30"/></marker>
|
||||
<marker id="arrow-link" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0, 8 3, 0 6" fill="#5ba8eb"/></marker>
|
||||
</defs>
|
||||
|
||||
<rect width="100%" height="100%" fill="#1c1a17"/>
|
||||
<rect width="100%" height="100%" fill="url(#dots)" opacity="0.55"/>
|
||||
|
||||
<!-- Arrows first (behind boxes) -->
|
||||
<line x1="168" y1="272" x2="220" y2="272" stroke="#5ba8eb" stroke-width="1.2" marker-end="url(#arrow-link)"/>
|
||||
<line x1="364" y1="272" x2="416" y2="272" stroke="#ff6a30" stroke-width="1.4" marker-end="url(#arrow-accent)"/>
|
||||
<line x1="576" y1="256" x2="628" y2="212" stroke="#a8a69d" stroke-width="1.2" marker-end="url(#arrow)"/>
|
||||
<line x1="576" y1="288" x2="628" y2="332" stroke="#a8a69d" stroke-width="1.2" marker-end="url(#arrow)"/>
|
||||
|
||||
<!-- Arrow labels -->
|
||||
<rect x="172" y="252" width="48" height="12" rx="2" fill="#1c1a17"/>
|
||||
<text x="196" y="262" fill="#5ba8eb" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">HTTPS</text>
|
||||
|
||||
<rect x="368" y="252" width="48" height="12" rx="2" fill="#1c1a17"/>
|
||||
<text x="392" y="262" fill="#ff6a30" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">SSR</text>
|
||||
|
||||
<rect x="560" y="212" width="56" height="12" rx="2" fill="#1c1a17"/>
|
||||
<text x="588" y="222" fill="#a8a69d" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">READ MDX</text>
|
||||
|
||||
<rect x="560" y="320" width="52" height="12" rx="2" fill="#1c1a17"/>
|
||||
<text x="586" y="330" fill="#a8a69d" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">QUERY</text>
|
||||
|
||||
<!-- Node: Reader -->
|
||||
<rect x="40" y="240" width="128" height="64" rx="6" fill="#1c1a17"/>
|
||||
<rect x="40" y="240" width="128" height="64" rx="6" fill="rgba(168,166,157,0.10)" stroke="#8e8c83" stroke-width="1"/>
|
||||
<rect x="48" y="248" width="28" height="12" rx="2" fill="transparent" stroke="rgba(142,140,131,0.40)" stroke-width="0.8"/>
|
||||
<text x="62" y="257" fill="#8e8c83" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">EXT</text>
|
||||
<text x="104" y="276" fill="#f1efe7" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Reader</text>
|
||||
<text x="104" y="292" fill="#a8a69d" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">Browser</text>
|
||||
|
||||
<!-- Node: Cloudflare -->
|
||||
<rect x="220" y="240" width="144" height="64" rx="6" fill="#1c1a17"/>
|
||||
<rect x="220" y="240" width="144" height="64" rx="6" fill="rgba(241,239,231,0.03)" stroke="rgba(241,239,231,0.30)" stroke-width="1"/>
|
||||
<rect x="228" y="248" width="32" height="12" rx="2" fill="transparent" stroke="rgba(241,239,231,0.22)" stroke-width="0.8"/>
|
||||
<text x="244" y="257" fill="#8e8c83" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">EDGE</text>
|
||||
<text x="356" y="300" fill="rgba(241,239,231,0.06)" font-size="32" font-weight="600" font-family="'Geist Mono', monospace" text-anchor="end">01</text>
|
||||
<text x="292" y="276" fill="#f1efe7" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Cloudflare</text>
|
||||
<text x="292" y="292" fill="#a8a69d" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">Pages · cache</text>
|
||||
|
||||
<!-- Node: Astro (focal coral) -->
|
||||
<rect x="416" y="240" width="160" height="64" rx="6" fill="#1c1a17"/>
|
||||
<rect x="416" y="240" width="160" height="64" rx="6" fill="rgba(255,106,48,0.08)" stroke="#ff6a30" stroke-width="1"/>
|
||||
<rect x="424" y="248" width="32" height="12" rx="2" fill="transparent" stroke="rgba(255,106,48,0.50)" stroke-width="0.8"/>
|
||||
<text x="440" y="257" fill="#ff6a30" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">ORIG</text>
|
||||
<text x="568" y="300" fill="rgba(255,106,48,0.10)" font-size="32" font-weight="600" font-family="'Geist Mono', monospace" text-anchor="end">02</text>
|
||||
<text x="496" y="276" fill="#f1efe7" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Astro Origin</text>
|
||||
<text x="496" y="292" fill="#a8a69d" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">SSR + MDX</text>
|
||||
|
||||
<!-- Node: MDX Bundle -->
|
||||
<rect x="628" y="160" width="144" height="64" rx="6" fill="#1c1a17"/>
|
||||
<rect x="628" y="160" width="144" height="64" rx="6" fill="#2a2723" stroke="#f1efe7" stroke-width="1"/>
|
||||
<rect x="636" y="168" width="32" height="12" rx="2" fill="transparent" stroke="rgba(241,239,231,0.40)" stroke-width="0.8"/>
|
||||
<text x="652" y="177" fill="#f1efe7" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">BUN</text>
|
||||
<text x="700" y="196" fill="#f1efe7" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">MDX Bundle</text>
|
||||
<text x="700" y="212" fill="#a8a69d" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">src/content/*.mdx</text>
|
||||
|
||||
<!-- Node: Content CMS -->
|
||||
<rect x="628" y="320" width="144" height="64" rx="6" fill="#1c1a17"/>
|
||||
<rect x="628" y="320" width="144" height="64" rx="6" fill="rgba(241,239,231,0.05)" stroke="#a8a69d" stroke-width="1"/>
|
||||
<rect x="636" y="328" width="28" height="12" rx="2" fill="transparent" stroke="rgba(168,166,157,0.50)" stroke-width="0.8"/>
|
||||
<text x="650" y="337" fill="#a8a69d" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">CMS</text>
|
||||
<text x="700" y="356" fill="#f1efe7" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Content CMS</text>
|
||||
<text x="700" y="372" fill="#a8a69d" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">assets · og images</text>
|
||||
|
||||
<!-- Legend strip -->
|
||||
<line x1="40" y1="404" x2="960" y2="404" stroke="rgba(241,239,231,0.10)" stroke-width="0.8"/>
|
||||
<text x="40" y="420" fill="#a8a69d" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">LEGEND</text>
|
||||
|
||||
<rect x="40" y="436" width="14" height="10" rx="2" fill="rgba(255,106,48,0.08)" stroke="#ff6a30" stroke-width="1"/>
|
||||
<text x="60" y="444" fill="#a8a69d" font-size="8.5" font-family="'Geist', sans-serif">Focal / origin</text>
|
||||
|
||||
<rect x="180" y="436" width="14" height="10" rx="2" fill="#2a2723" stroke="#f1efe7" stroke-width="1"/>
|
||||
<text x="200" y="444" fill="#a8a69d" font-size="8.5" font-family="'Geist', sans-serif">Backend / bundle</text>
|
||||
|
||||
<rect x="340" y="436" width="14" height="10" rx="2" fill="rgba(241,239,231,0.05)" stroke="#a8a69d" stroke-width="1"/>
|
||||
<text x="360" y="444" fill="#a8a69d" font-size="8.5" font-family="'Geist', sans-serif">Store</text>
|
||||
|
||||
<rect x="436" y="436" width="14" height="10" rx="2" fill="rgba(241,239,231,0.03)" stroke="rgba(241,239,231,0.30)" stroke-width="1"/>
|
||||
<text x="456" y="444" fill="#a8a69d" font-size="8.5" font-family="'Geist', sans-serif">Cloud</text>
|
||||
|
||||
<rect x="528" y="436" width="14" height="10" rx="2" fill="rgba(168,166,157,0.10)" stroke="#8e8c83" stroke-width="1"/>
|
||||
<text x="548" y="444" fill="#a8a69d" font-size="8.5" font-family="'Geist', sans-serif">External</text>
|
||||
|
||||
<line x1="636" y1="442" x2="664" y2="442" stroke="#5ba8eb" stroke-width="1.2" marker-end="url(#arrow-link)"/>
|
||||
<text x="672" y="444" fill="#a8a69d" font-size="8.5" font-family="'Geist', sans-serif">HTTP request</text>
|
||||
|
||||
<line x1="784" y1="442" x2="812" y2="442" stroke="#ff6a30" stroke-width="1.4" marker-end="url(#arrow-accent)"/>
|
||||
<text x="820" y="444" fill="#a8a69d" font-size="8.5" font-family="'Geist', sans-serif">Primary flow</text>
|
||||
</svg>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
174
skills/assets/example-architecture-full.html
Normal file
174
skills/assets/example-architecture-full.html
Normal file
@@ -0,0 +1,174 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>littlemight.com · Architecture</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
:root {
|
||||
--color-paper: #f5f4ed; --color-paper-2: #efeee5;
|
||||
--color-ink: #0b0d0b; --color-muted: #52534e; --color-soft: #65655c;
|
||||
--color-rule: rgba(11,13,11,0.12);
|
||||
--color-accent: #f7591f; --color-accent-tint: rgba(247,89,31,0.08);
|
||||
--color-link: #1a70c7;
|
||||
--font-sans: 'Geist', system-ui, sans-serif;
|
||||
--font-serif: 'Instrument Serif', serif;
|
||||
--font-mono: 'Geist Mono', ui-monospace, monospace;
|
||||
}
|
||||
body { font-family: var(--font-sans); background: var(--color-paper); min-height: 100vh; padding: 3rem 2rem; color: var(--color-ink); }
|
||||
.container { max-width: 1200px; margin: 0 auto; }
|
||||
.header { margin-bottom: 2.5rem; }
|
||||
.header-eyebrow { font-family: var(--font-mono); font-size: 0.66rem; font-weight: 500; letter-spacing: 0.18em; text-transform: uppercase; color: var(--color-muted); margin-bottom: 0.75rem; }
|
||||
h1 { font-family: var(--font-serif); font-size: clamp(1.75rem, 3vw + 1rem, 2.5rem); font-weight: 400; letter-spacing: -0.02em; line-height: 1.1; margin-bottom: 0.5rem; }
|
||||
.subtitle { font-size: 1rem; line-height: 1.55; color: var(--color-muted); max-width: 58ch; }
|
||||
.diagram-container { background: var(--color-paper-2); border-radius: 8px; border: 1px solid var(--color-rule); padding: 1.5rem; overflow-x: auto; }
|
||||
svg { width: 100%; min-width: 900px; display: block; }
|
||||
.cards { display: grid; grid-template-columns: 1.1fr 1fr 0.9fr; gap: 1rem; margin-top: 1.5rem; }
|
||||
@media (max-width: 820px) { .cards { grid-template-columns: 1fr; } }
|
||||
.card { background: #fff; border-radius: 6px; border: 1px solid var(--color-rule); padding: 1.25rem; }
|
||||
.card .eyebrow { font-family: var(--font-mono); font-size: 0.5rem; letter-spacing: 0.18em; text-transform: uppercase; color: var(--color-muted); margin-bottom: 0.5rem; }
|
||||
.card-header { display: flex; align-items: center; gap: 0.6rem; margin-bottom: 0.875rem; padding-bottom: 0.875rem; border-bottom: 1px solid rgba(11,13,11,0.08); }
|
||||
.card-dot { width: 7px; height: 7px; border-radius: 50%; }
|
||||
.card-dot.ink { background: var(--color-ink); } .card-dot.muted { background: var(--color-muted); } .card-dot.coral { background: var(--color-accent); }
|
||||
.card h3 { font-size: 0.875rem; font-weight: 600; letter-spacing: -0.005em; }
|
||||
.card p, .card ul { color: var(--color-muted); font-size: 0.8125rem; line-height: 1.55; list-style: none; }
|
||||
.card li { margin-bottom: 0.3rem; padding-left: 0.875rem; position: relative; }
|
||||
.card li::before { content: '—'; position: absolute; left: 0; color: rgba(11,13,11,0.25); font-size: 0.75rem; }
|
||||
.footer { margin-top: 2rem; padding-top: 1.5rem; border-top: 1px solid rgba(11,13,11,0.10); font-family: var(--font-mono); font-size: 0.72rem; letter-spacing: 0.06em; color: var(--color-soft); display: flex; justify-content: space-between; flex-wrap: wrap; gap: 0.5rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<p class="header-eyebrow">Architecture · Diagram Design</p>
|
||||
<h1>littlemight.com in production</h1>
|
||||
<p class="subtitle">The static-first stack: Cloudflare's edge absorbs most reads, Astro renders MDX on misses, content is checked into the repo.</p>
|
||||
</div>
|
||||
|
||||
<div class="diagram-container">
|
||||
<svg viewBox="0 0 1000 480" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<pattern id="dots" width="22" height="22" patternUnits="userSpaceOnUse">
|
||||
<circle cx="1" cy="1" r="0.9" fill="rgba(11,13,11,0.10)"/>
|
||||
</pattern>
|
||||
<marker id="arrow" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0, 8 3, 0 6" fill="#52534e"/></marker>
|
||||
<marker id="arrow-accent" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0, 8 3, 0 6" fill="#f7591f"/></marker>
|
||||
<marker id="arrow-link" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0, 8 3, 0 6" fill="#1a70c7"/></marker>
|
||||
</defs>
|
||||
|
||||
<rect width="100%" height="100%" fill="#f5f4ed"/>
|
||||
<rect width="100%" height="100%" fill="url(#dots)" opacity="0.55"/>
|
||||
|
||||
<!-- Arrows first (behind boxes) -->
|
||||
<line x1="168" y1="272" x2="220" y2="272" stroke="#1a70c7" stroke-width="1.2" marker-end="url(#arrow-link)"/>
|
||||
<line x1="364" y1="272" x2="416" y2="272" stroke="#f7591f" stroke-width="1.4" marker-end="url(#arrow-accent)"/>
|
||||
<line x1="576" y1="256" x2="628" y2="212" stroke="#52534e" stroke-width="1.2" marker-end="url(#arrow)"/>
|
||||
<line x1="576" y1="288" x2="628" y2="332" stroke="#52534e" stroke-width="1.2" marker-end="url(#arrow)"/>
|
||||
|
||||
<!-- Arrow labels -->
|
||||
<rect x="172" y="252" width="48" height="12" rx="2" fill="#f5f4ed"/>
|
||||
<text x="196" y="262" fill="#1a70c7" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">HTTPS</text>
|
||||
|
||||
<rect x="368" y="252" width="48" height="12" rx="2" fill="#f5f4ed"/>
|
||||
<text x="392" y="262" fill="#f7591f" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">SSR</text>
|
||||
|
||||
<rect x="560" y="212" width="56" height="12" rx="2" fill="#f5f4ed"/>
|
||||
<text x="588" y="222" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">READ MDX</text>
|
||||
|
||||
<rect x="560" y="320" width="52" height="12" rx="2" fill="#f5f4ed"/>
|
||||
<text x="586" y="330" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">QUERY</text>
|
||||
|
||||
<!-- Node: Reader -->
|
||||
<rect x="40" y="240" width="128" height="64" rx="6" fill="#f5f4ed"/>
|
||||
<rect x="40" y="240" width="128" height="64" rx="6" fill="rgba(82,83,78,0.10)" stroke="#65655c" stroke-width="1"/>
|
||||
<rect x="48" y="248" width="28" height="12" rx="2" fill="transparent" stroke="rgba(101,101,92,0.40)" stroke-width="0.8"/>
|
||||
<text x="62" y="257" fill="#65655c" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">EXT</text>
|
||||
<text x="104" y="276" fill="#0b0d0b" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Reader</text>
|
||||
<text x="104" y="292" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">Browser</text>
|
||||
|
||||
<!-- Node: Cloudflare -->
|
||||
<rect x="220" y="240" width="144" height="64" rx="6" fill="#f5f4ed"/>
|
||||
<rect x="220" y="240" width="144" height="64" rx="6" fill="rgba(11,13,11,0.03)" stroke="rgba(11,13,11,0.30)" stroke-width="1"/>
|
||||
<rect x="228" y="248" width="32" height="12" rx="2" fill="transparent" stroke="rgba(11,13,11,0.22)" stroke-width="0.8"/>
|
||||
<text x="244" y="257" fill="#65655c" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">EDGE</text>
|
||||
<text x="356" y="300" fill="rgba(11,13,11,0.06)" font-size="32" font-weight="600" font-family="'Geist Mono', monospace" text-anchor="end">01</text>
|
||||
<text x="292" y="276" fill="#0b0d0b" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Cloudflare</text>
|
||||
<text x="292" y="292" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">Pages · cache</text>
|
||||
|
||||
<!-- Node: Astro (focal coral) -->
|
||||
<rect x="416" y="240" width="160" height="64" rx="6" fill="#f5f4ed"/>
|
||||
<rect x="416" y="240" width="160" height="64" rx="6" fill="rgba(247,89,31,0.08)" stroke="#f7591f" stroke-width="1"/>
|
||||
<rect x="424" y="248" width="32" height="12" rx="2" fill="transparent" stroke="rgba(247,89,31,0.50)" stroke-width="0.8"/>
|
||||
<text x="440" y="257" fill="#f7591f" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">ORIG</text>
|
||||
<text x="568" y="300" fill="rgba(247,89,31,0.10)" font-size="32" font-weight="600" font-family="'Geist Mono', monospace" text-anchor="end">02</text>
|
||||
<text x="496" y="276" fill="#0b0d0b" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Astro Origin</text>
|
||||
<text x="496" y="292" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">SSR + MDX</text>
|
||||
|
||||
<!-- Node: MDX Bundle -->
|
||||
<rect x="628" y="160" width="144" height="64" rx="6" fill="#f5f4ed"/>
|
||||
<rect x="628" y="160" width="144" height="64" rx="6" fill="#ffffff" stroke="#0b0d0b" stroke-width="1"/>
|
||||
<rect x="636" y="168" width="32" height="12" rx="2" fill="transparent" stroke="rgba(11,13,11,0.40)" stroke-width="0.8"/>
|
||||
<text x="652" y="177" fill="#0b0d0b" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">BUN</text>
|
||||
<text x="700" y="196" fill="#0b0d0b" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">MDX Bundle</text>
|
||||
<text x="700" y="212" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">src/content/*.mdx</text>
|
||||
|
||||
<!-- Node: Content CMS -->
|
||||
<rect x="628" y="320" width="144" height="64" rx="6" fill="#f5f4ed"/>
|
||||
<rect x="628" y="320" width="144" height="64" rx="6" fill="rgba(11,13,11,0.05)" stroke="#52534e" stroke-width="1"/>
|
||||
<rect x="636" y="328" width="28" height="12" rx="2" fill="transparent" stroke="rgba(82,83,78,0.50)" stroke-width="0.8"/>
|
||||
<text x="650" y="337" fill="#52534e" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">CMS</text>
|
||||
<text x="700" y="356" fill="#0b0d0b" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Content CMS</text>
|
||||
<text x="700" y="372" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">assets · og images</text>
|
||||
|
||||
<!-- Legend strip -->
|
||||
<line x1="40" y1="404" x2="960" y2="404" stroke="rgba(11,13,11,0.10)" stroke-width="0.8"/>
|
||||
<text x="40" y="420" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">LEGEND</text>
|
||||
|
||||
<rect x="40" y="436" width="14" height="10" rx="2" fill="rgba(247,89,31,0.08)" stroke="#f7591f" stroke-width="1"/>
|
||||
<text x="60" y="444" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Focal / origin</text>
|
||||
|
||||
<rect x="180" y="436" width="14" height="10" rx="2" fill="#ffffff" stroke="#0b0d0b" stroke-width="1"/>
|
||||
<text x="200" y="444" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Backend / bundle</text>
|
||||
|
||||
<rect x="340" y="436" width="14" height="10" rx="2" fill="rgba(11,13,11,0.05)" stroke="#52534e" stroke-width="1"/>
|
||||
<text x="360" y="444" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Store</text>
|
||||
|
||||
<rect x="436" y="436" width="14" height="10" rx="2" fill="rgba(11,13,11,0.03)" stroke="rgba(11,13,11,0.30)" stroke-width="1"/>
|
||||
<text x="456" y="444" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Cloud</text>
|
||||
|
||||
<rect x="528" y="436" width="14" height="10" rx="2" fill="rgba(82,83,78,0.10)" stroke="#65655c" stroke-width="1"/>
|
||||
<text x="548" y="444" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">External</text>
|
||||
|
||||
<line x1="636" y1="442" x2="664" y2="442" stroke="#1a70c7" stroke-width="1.2" marker-end="url(#arrow-link)"/>
|
||||
<text x="672" y="444" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">HTTP request</text>
|
||||
|
||||
<line x1="784" y1="442" x2="812" y2="442" stroke="#f7591f" stroke-width="1.4" marker-end="url(#arrow-accent)"/>
|
||||
<text x="820" y="444" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Primary flow</text>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div class="cards">
|
||||
<div class="card">
|
||||
<p class="eyebrow">THE HEADLINE</p>
|
||||
<div class="card-header"><span class="card-dot coral"></span><h3>Edge absorbs the reads</h3></div>
|
||||
<p>Nearly every reader is served by Cloudflare's edge cache. The Astro origin only wakes on a cold slug or a revalidation.</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-header"><span class="card-dot ink"></span><h3>Content lives in the repo</h3></div>
|
||||
<ul><li>Posts are MDX files</li><li>Checked in, reviewed in PRs</li><li>No runtime database</li></ul>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-header"><span class="card-dot muted"></span><h3>CMS holds the big stuff</h3></div>
|
||||
<p>Images, OG art, and downloadable assets live in a separate bucket keyed by slug. Astro links them at render time.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<span>littlemight.com · architecture</span>
|
||||
<span>example · diagram-design</span>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
162
skills/assets/example-architecture.html
Normal file
162
skills/assets/example-architecture.html
Normal file
@@ -0,0 +1,162 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>littlemight.com · Architecture</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
:root {
|
||||
--color-paper: #f5f4ed;
|
||||
--color-ink: #0b0d0b;
|
||||
--color-muted: #52534e;
|
||||
--color-accent: #f7591f;
|
||||
--font-sans: 'Geist', system-ui, sans-serif;
|
||||
--font-serif: 'Instrument Serif', serif;
|
||||
--font-mono: 'Geist Mono', ui-monospace, monospace;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-sans);
|
||||
background: var(--color-paper);
|
||||
color: var(--color-ink);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 3rem 2rem;
|
||||
}
|
||||
|
||||
.frame { max-width: 1200px; width: 100%; }
|
||||
|
||||
.eyebrow {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.66rem;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.18em;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-muted);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-family: var(--font-serif);
|
||||
font-size: clamp(1.5rem, 2.4vw + 0.75rem, 2rem);
|
||||
font-weight: 400;
|
||||
letter-spacing: -0.02em;
|
||||
line-height: 1.15;
|
||||
color: var(--color-ink);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
svg { width: 100%; min-width: 900px; display: block; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="frame">
|
||||
<p class="eyebrow">Architecture · Diagram Design</p>
|
||||
<h1>littlemight.com in production</h1>
|
||||
|
||||
<svg viewBox="0 0 1000 480" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<pattern id="dots" width="22" height="22" patternUnits="userSpaceOnUse">
|
||||
<circle cx="1" cy="1" r="0.9" fill="rgba(11,13,11,0.10)"/>
|
||||
</pattern>
|
||||
<marker id="arrow" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0, 8 3, 0 6" fill="#52534e"/></marker>
|
||||
<marker id="arrow-accent" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0, 8 3, 0 6" fill="#f7591f"/></marker>
|
||||
<marker id="arrow-link" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0, 8 3, 0 6" fill="#1a70c7"/></marker>
|
||||
</defs>
|
||||
|
||||
<rect width="100%" height="100%" fill="#f5f4ed"/>
|
||||
<rect width="100%" height="100%" fill="url(#dots)" opacity="0.55"/>
|
||||
|
||||
<!-- Arrows first (behind boxes) -->
|
||||
<line x1="168" y1="272" x2="220" y2="272" stroke="#1a70c7" stroke-width="1.2" marker-end="url(#arrow-link)"/>
|
||||
<line x1="364" y1="272" x2="416" y2="272" stroke="#f7591f" stroke-width="1.4" marker-end="url(#arrow-accent)"/>
|
||||
<line x1="576" y1="256" x2="628" y2="212" stroke="#52534e" stroke-width="1.2" marker-end="url(#arrow)"/>
|
||||
<line x1="576" y1="288" x2="628" y2="332" stroke="#52534e" stroke-width="1.2" marker-end="url(#arrow)"/>
|
||||
|
||||
<!-- Arrow labels -->
|
||||
<rect x="172" y="252" width="48" height="12" rx="2" fill="#f5f4ed"/>
|
||||
<text x="196" y="262" fill="#1a70c7" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">HTTPS</text>
|
||||
|
||||
<rect x="368" y="252" width="48" height="12" rx="2" fill="#f5f4ed"/>
|
||||
<text x="392" y="262" fill="#f7591f" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">SSR</text>
|
||||
|
||||
<rect x="560" y="212" width="56" height="12" rx="2" fill="#f5f4ed"/>
|
||||
<text x="588" y="222" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">READ MDX</text>
|
||||
|
||||
<rect x="560" y="320" width="52" height="12" rx="2" fill="#f5f4ed"/>
|
||||
<text x="586" y="330" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">QUERY</text>
|
||||
|
||||
<!-- Node: Reader -->
|
||||
<rect x="40" y="240" width="128" height="64" rx="6" fill="#f5f4ed"/>
|
||||
<rect x="40" y="240" width="128" height="64" rx="6" fill="rgba(82,83,78,0.10)" stroke="#65655c" stroke-width="1"/>
|
||||
<rect x="48" y="248" width="28" height="12" rx="2" fill="transparent" stroke="rgba(101,101,92,0.40)" stroke-width="0.8"/>
|
||||
<text x="62" y="257" fill="#65655c" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">EXT</text>
|
||||
<text x="104" y="276" fill="#0b0d0b" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Reader</text>
|
||||
<text x="104" y="292" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">Browser</text>
|
||||
|
||||
<!-- Node: Cloudflare -->
|
||||
<rect x="220" y="240" width="144" height="64" rx="6" fill="#f5f4ed"/>
|
||||
<rect x="220" y="240" width="144" height="64" rx="6" fill="rgba(11,13,11,0.03)" stroke="rgba(11,13,11,0.30)" stroke-width="1"/>
|
||||
<rect x="228" y="248" width="32" height="12" rx="2" fill="transparent" stroke="rgba(11,13,11,0.22)" stroke-width="0.8"/>
|
||||
<text x="244" y="257" fill="#65655c" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">EDGE</text>
|
||||
<text x="356" y="300" fill="rgba(11,13,11,0.06)" font-size="32" font-weight="600" font-family="'Geist Mono', monospace" text-anchor="end">01</text>
|
||||
<text x="292" y="276" fill="#0b0d0b" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Cloudflare</text>
|
||||
<text x="292" y="292" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">Pages · cache</text>
|
||||
|
||||
<!-- Node: Astro (focal coral) -->
|
||||
<rect x="416" y="240" width="160" height="64" rx="6" fill="#f5f4ed"/>
|
||||
<rect x="416" y="240" width="160" height="64" rx="6" fill="rgba(247,89,31,0.08)" stroke="#f7591f" stroke-width="1"/>
|
||||
<rect x="424" y="248" width="32" height="12" rx="2" fill="transparent" stroke="rgba(247,89,31,0.50)" stroke-width="0.8"/>
|
||||
<text x="440" y="257" fill="#f7591f" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">ORIG</text>
|
||||
<text x="568" y="300" fill="rgba(247,89,31,0.10)" font-size="32" font-weight="600" font-family="'Geist Mono', monospace" text-anchor="end">02</text>
|
||||
<text x="496" y="276" fill="#0b0d0b" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Astro Origin</text>
|
||||
<text x="496" y="292" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">SSR + MDX</text>
|
||||
|
||||
<!-- Node: MDX Bundle -->
|
||||
<rect x="628" y="160" width="144" height="64" rx="6" fill="#f5f4ed"/>
|
||||
<rect x="628" y="160" width="144" height="64" rx="6" fill="#ffffff" stroke="#0b0d0b" stroke-width="1"/>
|
||||
<rect x="636" y="168" width="32" height="12" rx="2" fill="transparent" stroke="rgba(11,13,11,0.40)" stroke-width="0.8"/>
|
||||
<text x="652" y="177" fill="#0b0d0b" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">BUN</text>
|
||||
<text x="700" y="196" fill="#0b0d0b" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">MDX Bundle</text>
|
||||
<text x="700" y="212" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">src/content/*.mdx</text>
|
||||
|
||||
<!-- Node: Content CMS -->
|
||||
<rect x="628" y="320" width="144" height="64" rx="6" fill="#f5f4ed"/>
|
||||
<rect x="628" y="320" width="144" height="64" rx="6" fill="rgba(11,13,11,0.05)" stroke="#52534e" stroke-width="1"/>
|
||||
<rect x="636" y="328" width="28" height="12" rx="2" fill="transparent" stroke="rgba(82,83,78,0.50)" stroke-width="0.8"/>
|
||||
<text x="650" y="337" fill="#52534e" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">CMS</text>
|
||||
<text x="700" y="356" fill="#0b0d0b" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Content CMS</text>
|
||||
<text x="700" y="372" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">assets · og images</text>
|
||||
|
||||
<!-- Legend strip -->
|
||||
<line x1="40" y1="404" x2="960" y2="404" stroke="rgba(11,13,11,0.10)" stroke-width="0.8"/>
|
||||
<text x="40" y="420" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">LEGEND</text>
|
||||
|
||||
<rect x="40" y="436" width="14" height="10" rx="2" fill="rgba(247,89,31,0.08)" stroke="#f7591f" stroke-width="1"/>
|
||||
<text x="60" y="444" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Focal / origin</text>
|
||||
|
||||
<rect x="180" y="436" width="14" height="10" rx="2" fill="#ffffff" stroke="#0b0d0b" stroke-width="1"/>
|
||||
<text x="200" y="444" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Backend / bundle</text>
|
||||
|
||||
<rect x="340" y="436" width="14" height="10" rx="2" fill="rgba(11,13,11,0.05)" stroke="#52534e" stroke-width="1"/>
|
||||
<text x="360" y="444" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Store</text>
|
||||
|
||||
<rect x="436" y="436" width="14" height="10" rx="2" fill="rgba(11,13,11,0.03)" stroke="rgba(11,13,11,0.30)" stroke-width="1"/>
|
||||
<text x="456" y="444" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Cloud</text>
|
||||
|
||||
<rect x="528" y="436" width="14" height="10" rx="2" fill="rgba(82,83,78,0.10)" stroke="#65655c" stroke-width="1"/>
|
||||
<text x="548" y="444" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">External</text>
|
||||
|
||||
<line x1="636" y1="442" x2="664" y2="442" stroke="#1a70c7" stroke-width="1.2" marker-end="url(#arrow-link)"/>
|
||||
<text x="672" y="444" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">HTTP request</text>
|
||||
|
||||
<line x1="784" y1="442" x2="812" y2="442" stroke="#f7591f" stroke-width="1.4" marker-end="url(#arrow-accent)"/>
|
||||
<text x="820" y="444" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Primary flow</text>
|
||||
</svg>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
200
skills/assets/example-er-dark.html
Normal file
200
skills/assets/example-er-dark.html
Normal file
@@ -0,0 +1,200 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>littlemight content model · ER</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
:root {
|
||||
--color-paper: #1c1a17;
|
||||
--color-ink: #f1efe7;
|
||||
--color-muted: #a8a69d;
|
||||
--color-accent: #ff6a30;
|
||||
--font-sans: 'Geist', system-ui, sans-serif;
|
||||
--font-serif: 'Instrument Serif', serif;
|
||||
--font-mono: 'Geist Mono', ui-monospace, monospace;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-sans);
|
||||
background: var(--color-paper);
|
||||
color: var(--color-ink);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 3rem 2rem;
|
||||
}
|
||||
|
||||
.frame { max-width: 1200px; width: 100%; }
|
||||
|
||||
.eyebrow {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.66rem;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.18em;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-muted);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-family: var(--font-serif);
|
||||
font-size: clamp(1.5rem, 2.4vw + 0.75rem, 2rem);
|
||||
font-weight: 400;
|
||||
letter-spacing: -0.02em;
|
||||
line-height: 1.15;
|
||||
color: var(--color-ink);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
svg { width: 100%; min-width: 900px; display: block; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="frame">
|
||||
<p class="eyebrow">ER · Diagram Design</p>
|
||||
<h1>littlemight content model</h1>
|
||||
|
||||
<svg viewBox="0 0 1000 480" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<pattern id="dots" width="22" height="22" patternUnits="userSpaceOnUse">
|
||||
<circle cx="1" cy="1" r="0.9" fill="rgba(241,239,231,0.10)"/>
|
||||
</pattern>
|
||||
<marker id="arrow" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0, 8 3, 0 6" fill="#a8a69d"/></marker>
|
||||
</defs>
|
||||
|
||||
<rect width="100%" height="100%" fill="#1c1a17"/>
|
||||
<rect width="100%" height="100%" fill="url(#dots)" opacity="0.55"/>
|
||||
|
||||
<!-- Relationship lines (drawn first) -->
|
||||
<!-- 1. Author — Article (1:N "writes") -->
|
||||
<line x1="260" y1="240" x2="400" y2="240" stroke="#a8a69d" stroke-width="1" />
|
||||
<!-- 2. Article — ArticleTag (1:N) -->
|
||||
<line x1="640" y1="320" x2="780" y2="328" stroke="#a8a69d" stroke-width="1" />
|
||||
<!-- 3. Tag — ArticleTag (1:N, vertical) -->
|
||||
<line x1="880" y1="248" x2="880" y2="280" stroke="#a8a69d" stroke-width="1" />
|
||||
|
||||
<!-- Cardinality labels -->
|
||||
<rect x="266" y="232" width="12" height="12" rx="2" fill="#1c1a17"/>
|
||||
<text x="272" y="242" fill="#a8a69d" font-size="10" font-family="'Geist Mono', monospace" text-anchor="middle" font-weight="600">1</text>
|
||||
|
||||
<rect x="378" y="232" width="16" height="12" rx="2" fill="#1c1a17"/>
|
||||
<text x="386" y="242" fill="#a8a69d" font-size="10" font-family="'Geist Mono', monospace" text-anchor="middle" font-weight="600">N</text>
|
||||
|
||||
<rect x="646" y="316" width="12" height="12" rx="2" fill="#1c1a17"/>
|
||||
<text x="652" y="326" fill="#a8a69d" font-size="10" font-family="'Geist Mono', monospace" text-anchor="middle" font-weight="600">1</text>
|
||||
|
||||
<rect x="760" y="324" width="16" height="12" rx="2" fill="#1c1a17"/>
|
||||
<text x="768" y="334" fill="#a8a69d" font-size="10" font-family="'Geist Mono', monospace" text-anchor="middle" font-weight="600">N</text>
|
||||
|
||||
<rect x="872" y="252" width="16" height="12" rx="2" fill="#1c1a17"/>
|
||||
<text x="880" y="262" fill="#a8a69d" font-size="10" font-family="'Geist Mono', monospace" text-anchor="middle" font-weight="600">1</text>
|
||||
|
||||
<rect x="872" y="268" width="16" height="12" rx="2" fill="#1c1a17"/>
|
||||
<text x="880" y="278" fill="#a8a69d" font-size="10" font-family="'Geist Mono', monospace" text-anchor="middle" font-weight="600">N</text>
|
||||
|
||||
<!-- Relationship labels -->
|
||||
<rect x="304" y="220" width="56" height="14" rx="2" fill="#1c1a17"/>
|
||||
<text x="332" y="230" fill="#a8a69d" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.12em">WRITES</text>
|
||||
|
||||
<rect x="688" y="300" width="56" height="14" rx="2" fill="#1c1a17"/>
|
||||
<text x="716" y="310" fill="#a8a69d" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.12em">TAGGED</text>
|
||||
|
||||
<!-- Entity: Author -->
|
||||
<rect x="60" y="160" width="200" height="160" rx="6" fill="#2a2723" stroke="#f1efe7" stroke-width="1"/>
|
||||
<rect x="60" y="160" width="200" height="40" rx="6" fill="rgba(241,239,231,0.04)" stroke="none"/>
|
||||
<rect x="60" y="192" width="200" height="8" fill="rgba(241,239,231,0.04)"/>
|
||||
<line x1="60" y1="200" x2="260" y2="200" stroke="rgba(241,239,231,0.22)" stroke-width="1"/>
|
||||
<text x="76" y="176" fill="#a8a69d" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.14em">ENTITY</text>
|
||||
<text x="76" y="192" fill="#f1efe7" font-size="14" font-weight="600" font-family="'Geist', sans-serif">Author</text>
|
||||
<text x="76" y="220" fill="#f1efe7" font-size="10" font-family="'Geist Mono', monospace"># id</text>
|
||||
<text x="220" y="220" fill="#a8a69d" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end">uuid</text>
|
||||
<text x="76" y="240" fill="#f1efe7" font-size="10" font-family="'Geist Mono', monospace">handle</text>
|
||||
<text x="220" y="240" fill="#a8a69d" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end">text</text>
|
||||
<text x="76" y="260" fill="#f1efe7" font-size="10" font-family="'Geist Mono', monospace">name</text>
|
||||
<text x="220" y="260" fill="#a8a69d" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end">text</text>
|
||||
<text x="76" y="280" fill="#f1efe7" font-size="10" font-family="'Geist Mono', monospace">bio</text>
|
||||
<text x="220" y="280" fill="#a8a69d" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end">text</text>
|
||||
<text x="76" y="300" fill="#f1efe7" font-size="10" font-family="'Geist Mono', monospace">site_url</text>
|
||||
<text x="220" y="300" fill="#a8a69d" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end">text</text>
|
||||
|
||||
<!-- Entity: Article (focal coral) -->
|
||||
<rect x="400" y="120" width="240" height="240" rx="6" fill="rgba(255,106,48,0.04)" stroke="#ff6a30" stroke-width="1"/>
|
||||
<rect x="400" y="120" width="240" height="40" rx="6" fill="rgba(255,106,48,0.10)" stroke="none"/>
|
||||
<rect x="400" y="152" width="240" height="8" fill="rgba(255,106,48,0.10)"/>
|
||||
<line x1="400" y1="160" x2="640" y2="160" stroke="rgba(255,106,48,0.40)" stroke-width="1"/>
|
||||
<text x="416" y="136" fill="#ff6a30" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.14em">ENTITY · AGGREGATE ROOT</text>
|
||||
<text x="416" y="152" fill="#f1efe7" font-size="14" font-weight="600" font-family="'Geist', sans-serif">Article</text>
|
||||
<text x="416" y="180" fill="#f1efe7" font-size="10" font-family="'Geist Mono', monospace"># id</text>
|
||||
<text x="600" y="180" fill="#a8a69d" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end">uuid</text>
|
||||
<text x="416" y="200" fill="#f1efe7" font-size="10" font-family="'Geist Mono', monospace">title</text>
|
||||
<text x="600" y="200" fill="#a8a69d" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end">text</text>
|
||||
<text x="416" y="220" fill="#f1efe7" font-size="10" font-family="'Geist Mono', monospace">slug</text>
|
||||
<text x="600" y="220" fill="#a8a69d" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end">text · unique</text>
|
||||
<text x="416" y="240" fill="#f1efe7" font-size="10" font-family="'Geist Mono', monospace">body_mdx</text>
|
||||
<text x="600" y="240" fill="#a8a69d" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end">text</text>
|
||||
<text x="416" y="260" fill="#f1efe7" font-size="10" font-family="'Geist Mono', monospace">published_at</text>
|
||||
<text x="600" y="260" fill="#a8a69d" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end">timestamp</text>
|
||||
<text x="416" y="280" fill="#f1efe7" font-size="10" font-family="'Geist Mono', monospace">→ author_id</text>
|
||||
<text x="600" y="280" fill="#a8a69d" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end">uuid</text>
|
||||
<text x="416" y="300" fill="#f1efe7" font-size="10" font-family="'Geist Mono', monospace">status</text>
|
||||
<text x="600" y="300" fill="#a8a69d" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end">enum</text>
|
||||
<text x="416" y="320" fill="#f1efe7" font-size="10" font-family="'Geist Mono', monospace">og_image</text>
|
||||
<text x="600" y="320" fill="#a8a69d" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end">text · url</text>
|
||||
|
||||
<!-- Entity: Tag -->
|
||||
<rect x="780" y="120" width="200" height="128" rx="6" fill="#2a2723" stroke="#f1efe7" stroke-width="1"/>
|
||||
<rect x="780" y="120" width="200" height="40" rx="6" fill="rgba(241,239,231,0.04)" stroke="none"/>
|
||||
<rect x="780" y="152" width="200" height="8" fill="rgba(241,239,231,0.04)"/>
|
||||
<line x1="780" y1="160" x2="980" y2="160" stroke="rgba(241,239,231,0.22)" stroke-width="1"/>
|
||||
<text x="796" y="136" fill="#a8a69d" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.14em">ENTITY</text>
|
||||
<text x="796" y="152" fill="#f1efe7" font-size="14" font-weight="600" font-family="'Geist', sans-serif">Tag</text>
|
||||
<text x="796" y="180" fill="#f1efe7" font-size="10" font-family="'Geist Mono', monospace"># id</text>
|
||||
<text x="940" y="180" fill="#a8a69d" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end">uuid</text>
|
||||
<text x="796" y="200" fill="#f1efe7" font-size="10" font-family="'Geist Mono', monospace">slug</text>
|
||||
<text x="940" y="200" fill="#a8a69d" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end">text · unique</text>
|
||||
<text x="796" y="220" fill="#f1efe7" font-size="10" font-family="'Geist Mono', monospace">name</text>
|
||||
<text x="940" y="220" fill="#a8a69d" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end">text</text>
|
||||
<text x="796" y="240" fill="#f1efe7" font-size="10" font-family="'Geist Mono', monospace">description</text>
|
||||
<text x="940" y="240" fill="#a8a69d" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end">text</text>
|
||||
|
||||
<!-- Entity: ArticleTag (join) -->
|
||||
<rect x="780" y="280" width="200" height="96" rx="6" fill="rgba(241,239,231,0.04)" stroke="#a8a69d" stroke-width="1" stroke-dasharray="4,3"/>
|
||||
<rect x="780" y="280" width="200" height="40" rx="6" fill="rgba(241,239,231,0.06)" stroke="none"/>
|
||||
<rect x="780" y="312" width="200" height="8" fill="rgba(241,239,231,0.06)"/>
|
||||
<line x1="780" y1="320" x2="980" y2="320" stroke="rgba(241,239,231,0.22)" stroke-width="1"/>
|
||||
<text x="796" y="296" fill="#a8a69d" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.14em">JOIN</text>
|
||||
<text x="796" y="312" fill="#f1efe7" font-size="14" font-weight="600" font-family="'Geist', sans-serif">ArticleTag</text>
|
||||
<text x="796" y="340" fill="#f1efe7" font-size="10" font-family="'Geist Mono', monospace">→ article_id</text>
|
||||
<text x="940" y="340" fill="#a8a69d" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end">uuid</text>
|
||||
<text x="796" y="360" fill="#f1efe7" font-size="10" font-family="'Geist Mono', monospace">→ tag_id</text>
|
||||
<text x="940" y="360" fill="#a8a69d" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end">uuid</text>
|
||||
|
||||
<!-- Legend -->
|
||||
<line x1="40" y1="404" x2="960" y2="404" stroke="rgba(241,239,231,0.10)" stroke-width="0.8"/>
|
||||
<text x="40" y="420" fill="#a8a69d" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">LEGEND</text>
|
||||
|
||||
<rect x="40" y="436" width="14" height="10" rx="2" fill="rgba(255,106,48,0.04)" stroke="#ff6a30" stroke-width="1"/>
|
||||
<text x="60" y="444" fill="#a8a69d" font-size="8.5" font-family="'Geist', sans-serif">Aggregate root</text>
|
||||
|
||||
<rect x="180" y="436" width="14" height="10" rx="2" fill="#2a2723" stroke="#f1efe7" stroke-width="1"/>
|
||||
<text x="200" y="444" fill="#a8a69d" font-size="8.5" font-family="'Geist', sans-serif">Entity</text>
|
||||
|
||||
<rect x="268" y="436" width="14" height="10" rx="2" fill="rgba(241,239,231,0.04)" stroke="#a8a69d" stroke-width="1" stroke-dasharray="3,2"/>
|
||||
<text x="288" y="444" fill="#a8a69d" font-size="8.5" font-family="'Geist', sans-serif">Join table</text>
|
||||
|
||||
<text x="372" y="444" fill="#f1efe7" font-size="10" font-family="'Geist Mono', monospace" font-weight="600">#</text>
|
||||
<text x="388" y="444" fill="#a8a69d" font-size="8.5" font-family="'Geist', sans-serif">Primary key</text>
|
||||
|
||||
<text x="476" y="444" fill="#f1efe7" font-size="10" font-family="'Geist Mono', monospace" font-weight="600">→</text>
|
||||
<text x="492" y="444" fill="#a8a69d" font-size="8.5" font-family="'Geist', sans-serif">Foreign key</text>
|
||||
|
||||
<text x="584" y="444" fill="#a8a69d" font-size="10" font-family="'Geist Mono', monospace" font-weight="600">1 / N</text>
|
||||
<text x="616" y="444" fill="#a8a69d" font-size="8.5" font-family="'Geist', sans-serif">Cardinality</text>
|
||||
</svg>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
203
skills/assets/example-er-full.html
Normal file
203
skills/assets/example-er-full.html
Normal file
@@ -0,0 +1,203 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>littlemight content model · ER</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
:root { --color-paper:#f5f4ed; --color-paper-2:#efeee5; --color-ink:#0b0d0b; --color-muted:#52534e; --color-soft:#65655c; --color-rule:rgba(11,13,11,0.12); --color-accent:#f7591f; --color-link:#1a70c7; --font-sans:'Geist',system-ui,sans-serif; --font-serif:'Instrument Serif',serif; --font-mono:'Geist Mono',ui-monospace,monospace; }
|
||||
body { font-family: var(--font-sans); background: var(--color-paper); min-height: 100vh; padding: 3rem 2rem; color: var(--color-ink); }
|
||||
.container { max-width: 1200px; margin: 0 auto; }
|
||||
.header { margin-bottom: 2.5rem; }
|
||||
.header-eyebrow { font-family: var(--font-mono); font-size: 0.66rem; font-weight: 500; letter-spacing: 0.18em; text-transform: uppercase; color: var(--color-muted); margin-bottom: 0.75rem; }
|
||||
h1 { font-family: var(--font-serif); font-size: clamp(1.75rem, 3vw + 1rem, 2.5rem); font-weight: 400; letter-spacing: -0.02em; line-height: 1.1; margin-bottom: 0.5rem; }
|
||||
.subtitle { font-size: 1rem; line-height: 1.55; color: var(--color-muted); max-width: 58ch; }
|
||||
.diagram-container { background: var(--color-paper-2); border-radius: 8px; border: 1px solid var(--color-rule); padding: 1.5rem; overflow-x: auto; }
|
||||
svg { width: 100%; min-width: 900px; display: block; }
|
||||
.cards { display: grid; grid-template-columns: 1.1fr 1fr 0.9fr; gap: 1rem; margin-top: 1.5rem; }
|
||||
@media (max-width: 820px) { .cards { grid-template-columns: 1fr; } }
|
||||
.card { background: #fff; border-radius: 6px; border: 1px solid var(--color-rule); padding: 1.25rem; }
|
||||
.card .eyebrow { font-family: var(--font-mono); font-size: 0.5rem; letter-spacing: 0.18em; text-transform: uppercase; color: var(--color-muted); margin-bottom: 0.5rem; }
|
||||
.card-header { display: flex; align-items: center; gap: 0.6rem; margin-bottom: 0.875rem; padding-bottom: 0.875rem; border-bottom: 1px solid rgba(11,13,11,0.08); }
|
||||
.card-dot { width: 7px; height: 7px; border-radius: 50%; }
|
||||
.card-dot.ink { background: var(--color-ink); } .card-dot.muted { background: var(--color-muted); } .card-dot.coral { background: var(--color-accent); }
|
||||
.card h3 { font-size: 0.875rem; font-weight: 600; }
|
||||
.card p, .card ul { color: var(--color-muted); font-size: 0.8125rem; line-height: 1.55; list-style: none; }
|
||||
.card li { margin-bottom: 0.3rem; padding-left: 0.875rem; position: relative; }
|
||||
.card li::before { content: '—'; position: absolute; left: 0; color: rgba(11,13,11,0.25); font-size: 0.75rem; }
|
||||
.footer { margin-top: 2rem; padding-top: 1.5rem; border-top: 1px solid rgba(11,13,11,0.10); font-family: var(--font-mono); font-size: 0.72rem; letter-spacing: 0.06em; color: var(--color-soft); display: flex; justify-content: space-between; flex-wrap: wrap; gap: 0.5rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<p class="header-eyebrow">ER · Diagram Design</p>
|
||||
<h1>littlemight content model</h1>
|
||||
<p class="subtitle">The four entities behind the site. Article is the aggregate root — everything else exists to describe or classify it. `#` marks primary keys, `→` marks foreign keys.</p>
|
||||
</div>
|
||||
|
||||
<div class="diagram-container">
|
||||
<svg viewBox="0 0 1000 480" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<pattern id="dots" width="22" height="22" patternUnits="userSpaceOnUse">
|
||||
<circle cx="1" cy="1" r="0.9" fill="rgba(11,13,11,0.10)"/>
|
||||
</pattern>
|
||||
<marker id="arrow" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0, 8 3, 0 6" fill="#52534e"/></marker>
|
||||
</defs>
|
||||
|
||||
<rect width="100%" height="100%" fill="#f5f4ed"/>
|
||||
<rect width="100%" height="100%" fill="url(#dots)" opacity="0.55"/>
|
||||
|
||||
<!-- Relationship lines (drawn first) -->
|
||||
<!-- 1. Author — Article (1:N "writes") -->
|
||||
<line x1="260" y1="240" x2="400" y2="240" stroke="#52534e" stroke-width="1" />
|
||||
<!-- 2. Article — ArticleTag (1:N) -->
|
||||
<line x1="640" y1="320" x2="780" y2="328" stroke="#52534e" stroke-width="1" />
|
||||
<!-- 3. Tag — ArticleTag (1:N, vertical) -->
|
||||
<line x1="880" y1="248" x2="880" y2="280" stroke="#52534e" stroke-width="1" />
|
||||
|
||||
<!-- Cardinality labels -->
|
||||
<rect x="266" y="232" width="12" height="12" rx="2" fill="#f5f4ed"/>
|
||||
<text x="272" y="242" fill="#52534e" font-size="10" font-family="'Geist Mono', monospace" text-anchor="middle" font-weight="600">1</text>
|
||||
|
||||
<rect x="378" y="232" width="16" height="12" rx="2" fill="#f5f4ed"/>
|
||||
<text x="386" y="242" fill="#52534e" font-size="10" font-family="'Geist Mono', monospace" text-anchor="middle" font-weight="600">N</text>
|
||||
|
||||
<rect x="646" y="316" width="12" height="12" rx="2" fill="#f5f4ed"/>
|
||||
<text x="652" y="326" fill="#52534e" font-size="10" font-family="'Geist Mono', monospace" text-anchor="middle" font-weight="600">1</text>
|
||||
|
||||
<rect x="760" y="324" width="16" height="12" rx="2" fill="#f5f4ed"/>
|
||||
<text x="768" y="334" fill="#52534e" font-size="10" font-family="'Geist Mono', monospace" text-anchor="middle" font-weight="600">N</text>
|
||||
|
||||
<rect x="872" y="252" width="16" height="12" rx="2" fill="#f5f4ed"/>
|
||||
<text x="880" y="262" fill="#52534e" font-size="10" font-family="'Geist Mono', monospace" text-anchor="middle" font-weight="600">1</text>
|
||||
|
||||
<rect x="872" y="268" width="16" height="12" rx="2" fill="#f5f4ed"/>
|
||||
<text x="880" y="278" fill="#52534e" font-size="10" font-family="'Geist Mono', monospace" text-anchor="middle" font-weight="600">N</text>
|
||||
|
||||
<!-- Relationship labels -->
|
||||
<rect x="304" y="220" width="56" height="14" rx="2" fill="#f5f4ed"/>
|
||||
<text x="332" y="230" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.12em">WRITES</text>
|
||||
|
||||
<rect x="688" y="300" width="56" height="14" rx="2" fill="#f5f4ed"/>
|
||||
<text x="716" y="310" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.12em">TAGGED</text>
|
||||
|
||||
<!-- Entity: Author -->
|
||||
<rect x="60" y="160" width="200" height="160" rx="6" fill="#ffffff" stroke="#0b0d0b" stroke-width="1"/>
|
||||
<rect x="60" y="160" width="200" height="40" rx="6" fill="rgba(11,13,11,0.04)" stroke="none"/>
|
||||
<rect x="60" y="192" width="200" height="8" fill="rgba(11,13,11,0.04)"/>
|
||||
<line x1="60" y1="200" x2="260" y2="200" stroke="rgba(11,13,11,0.22)" stroke-width="1"/>
|
||||
<text x="76" y="176" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.14em">ENTITY</text>
|
||||
<text x="76" y="192" fill="#0b0d0b" font-size="14" font-weight="600" font-family="'Geist', sans-serif">Author</text>
|
||||
<text x="76" y="220" fill="#0b0d0b" font-size="10" font-family="'Geist Mono', monospace"># id</text>
|
||||
<text x="220" y="220" fill="#52534e" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end">uuid</text>
|
||||
<text x="76" y="240" fill="#0b0d0b" font-size="10" font-family="'Geist Mono', monospace">handle</text>
|
||||
<text x="220" y="240" fill="#52534e" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end">text</text>
|
||||
<text x="76" y="260" fill="#0b0d0b" font-size="10" font-family="'Geist Mono', monospace">name</text>
|
||||
<text x="220" y="260" fill="#52534e" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end">text</text>
|
||||
<text x="76" y="280" fill="#0b0d0b" font-size="10" font-family="'Geist Mono', monospace">bio</text>
|
||||
<text x="220" y="280" fill="#52534e" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end">text</text>
|
||||
<text x="76" y="300" fill="#0b0d0b" font-size="10" font-family="'Geist Mono', monospace">site_url</text>
|
||||
<text x="220" y="300" fill="#52534e" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end">text</text>
|
||||
|
||||
<!-- Entity: Article (focal coral) -->
|
||||
<rect x="400" y="120" width="240" height="240" rx="6" fill="rgba(247,89,31,0.04)" stroke="#f7591f" stroke-width="1"/>
|
||||
<rect x="400" y="120" width="240" height="40" rx="6" fill="rgba(247,89,31,0.10)" stroke="none"/>
|
||||
<rect x="400" y="152" width="240" height="8" fill="rgba(247,89,31,0.10)"/>
|
||||
<line x1="400" y1="160" x2="640" y2="160" stroke="rgba(247,89,31,0.40)" stroke-width="1"/>
|
||||
<text x="416" y="136" fill="#f7591f" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.14em">ENTITY · AGGREGATE ROOT</text>
|
||||
<text x="416" y="152" fill="#0b0d0b" font-size="14" font-weight="600" font-family="'Geist', sans-serif">Article</text>
|
||||
<text x="416" y="180" fill="#0b0d0b" font-size="10" font-family="'Geist Mono', monospace"># id</text>
|
||||
<text x="600" y="180" fill="#52534e" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end">uuid</text>
|
||||
<text x="416" y="200" fill="#0b0d0b" font-size="10" font-family="'Geist Mono', monospace">title</text>
|
||||
<text x="600" y="200" fill="#52534e" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end">text</text>
|
||||
<text x="416" y="220" fill="#0b0d0b" font-size="10" font-family="'Geist Mono', monospace">slug</text>
|
||||
<text x="600" y="220" fill="#52534e" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end">text · unique</text>
|
||||
<text x="416" y="240" fill="#0b0d0b" font-size="10" font-family="'Geist Mono', monospace">body_mdx</text>
|
||||
<text x="600" y="240" fill="#52534e" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end">text</text>
|
||||
<text x="416" y="260" fill="#0b0d0b" font-size="10" font-family="'Geist Mono', monospace">published_at</text>
|
||||
<text x="600" y="260" fill="#52534e" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end">timestamp</text>
|
||||
<text x="416" y="280" fill="#0b0d0b" font-size="10" font-family="'Geist Mono', monospace">→ author_id</text>
|
||||
<text x="600" y="280" fill="#52534e" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end">uuid</text>
|
||||
<text x="416" y="300" fill="#0b0d0b" font-size="10" font-family="'Geist Mono', monospace">status</text>
|
||||
<text x="600" y="300" fill="#52534e" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end">enum</text>
|
||||
<text x="416" y="320" fill="#0b0d0b" font-size="10" font-family="'Geist Mono', monospace">og_image</text>
|
||||
<text x="600" y="320" fill="#52534e" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end">text · url</text>
|
||||
|
||||
<!-- Entity: Tag -->
|
||||
<rect x="780" y="120" width="200" height="128" rx="6" fill="#ffffff" stroke="#0b0d0b" stroke-width="1"/>
|
||||
<rect x="780" y="120" width="200" height="40" rx="6" fill="rgba(11,13,11,0.04)" stroke="none"/>
|
||||
<rect x="780" y="152" width="200" height="8" fill="rgba(11,13,11,0.04)"/>
|
||||
<line x1="780" y1="160" x2="980" y2="160" stroke="rgba(11,13,11,0.22)" stroke-width="1"/>
|
||||
<text x="796" y="136" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.14em">ENTITY</text>
|
||||
<text x="796" y="152" fill="#0b0d0b" font-size="14" font-weight="600" font-family="'Geist', sans-serif">Tag</text>
|
||||
<text x="796" y="180" fill="#0b0d0b" font-size="10" font-family="'Geist Mono', monospace"># id</text>
|
||||
<text x="940" y="180" fill="#52534e" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end">uuid</text>
|
||||
<text x="796" y="200" fill="#0b0d0b" font-size="10" font-family="'Geist Mono', monospace">slug</text>
|
||||
<text x="940" y="200" fill="#52534e" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end">text · unique</text>
|
||||
<text x="796" y="220" fill="#0b0d0b" font-size="10" font-family="'Geist Mono', monospace">name</text>
|
||||
<text x="940" y="220" fill="#52534e" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end">text</text>
|
||||
<text x="796" y="240" fill="#0b0d0b" font-size="10" font-family="'Geist Mono', monospace">description</text>
|
||||
<text x="940" y="240" fill="#52534e" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end">text</text>
|
||||
|
||||
<!-- Entity: ArticleTag (join) -->
|
||||
<rect x="780" y="280" width="200" height="96" rx="6" fill="rgba(11,13,11,0.04)" stroke="#52534e" stroke-width="1" stroke-dasharray="4,3"/>
|
||||
<rect x="780" y="280" width="200" height="40" rx="6" fill="rgba(11,13,11,0.06)" stroke="none"/>
|
||||
<rect x="780" y="312" width="200" height="8" fill="rgba(11,13,11,0.06)"/>
|
||||
<line x1="780" y1="320" x2="980" y2="320" stroke="rgba(11,13,11,0.22)" stroke-width="1"/>
|
||||
<text x="796" y="296" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.14em">JOIN</text>
|
||||
<text x="796" y="312" fill="#0b0d0b" font-size="14" font-weight="600" font-family="'Geist', sans-serif">ArticleTag</text>
|
||||
<text x="796" y="340" fill="#0b0d0b" font-size="10" font-family="'Geist Mono', monospace">→ article_id</text>
|
||||
<text x="940" y="340" fill="#52534e" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end">uuid</text>
|
||||
<text x="796" y="360" fill="#0b0d0b" font-size="10" font-family="'Geist Mono', monospace">→ tag_id</text>
|
||||
<text x="940" y="360" fill="#52534e" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end">uuid</text>
|
||||
|
||||
<!-- Legend -->
|
||||
<line x1="40" y1="404" x2="960" y2="404" stroke="rgba(11,13,11,0.10)" stroke-width="0.8"/>
|
||||
<text x="40" y="420" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">LEGEND</text>
|
||||
|
||||
<rect x="40" y="436" width="14" height="10" rx="2" fill="rgba(247,89,31,0.04)" stroke="#f7591f" stroke-width="1"/>
|
||||
<text x="60" y="444" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Aggregate root</text>
|
||||
|
||||
<rect x="180" y="436" width="14" height="10" rx="2" fill="#ffffff" stroke="#0b0d0b" stroke-width="1"/>
|
||||
<text x="200" y="444" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Entity</text>
|
||||
|
||||
<rect x="268" y="436" width="14" height="10" rx="2" fill="rgba(11,13,11,0.04)" stroke="#52534e" stroke-width="1" stroke-dasharray="3,2"/>
|
||||
<text x="288" y="444" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Join table</text>
|
||||
|
||||
<text x="372" y="444" fill="#0b0d0b" font-size="10" font-family="'Geist Mono', monospace" font-weight="600">#</text>
|
||||
<text x="388" y="444" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Primary key</text>
|
||||
|
||||
<text x="476" y="444" fill="#0b0d0b" font-size="10" font-family="'Geist Mono', monospace" font-weight="600">→</text>
|
||||
<text x="492" y="444" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Foreign key</text>
|
||||
|
||||
<text x="584" y="444" fill="#52534e" font-size="10" font-family="'Geist Mono', monospace" font-weight="600">1 / N</text>
|
||||
<text x="616" y="444" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Cardinality</text>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div class="cards">
|
||||
<div class="card">
|
||||
<p class="eyebrow">THE HEADLINE</p>
|
||||
<div class="card-header"><span class="card-dot coral"></span><h3>Article is the root</h3></div>
|
||||
<p>Author, Tag, and the join table only exist to describe Article. If you're thinking about a feature, start here and trace outward.</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-header"><span class="card-dot ink"></span><h3>Many-to-many via a join</h3></div>
|
||||
<ul><li>Tags aren't embedded on Article</li><li>ArticleTag is a pure join — no metadata</li><li>Dashed border signals it's not a primary entity</li></ul>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-header"><span class="card-dot muted"></span><h3>Cardinality over arrows</h3></div>
|
||||
<p>Plain lines with 1/N at the ends read cleaner than crow's feet at this size. Every relationship carries both numbers.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<span>littlemight content model · ER</span>
|
||||
<span>example · diagram-design</span>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
200
skills/assets/example-er.html
Normal file
200
skills/assets/example-er.html
Normal file
@@ -0,0 +1,200 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>littlemight content model · ER</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
:root {
|
||||
--color-paper: #f5f4ed;
|
||||
--color-ink: #0b0d0b;
|
||||
--color-muted: #52534e;
|
||||
--color-accent: #f7591f;
|
||||
--font-sans: 'Geist', system-ui, sans-serif;
|
||||
--font-serif: 'Instrument Serif', serif;
|
||||
--font-mono: 'Geist Mono', ui-monospace, monospace;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-sans);
|
||||
background: var(--color-paper);
|
||||
color: var(--color-ink);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 3rem 2rem;
|
||||
}
|
||||
|
||||
.frame { max-width: 1200px; width: 100%; }
|
||||
|
||||
.eyebrow {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.66rem;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.18em;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-muted);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-family: var(--font-serif);
|
||||
font-size: clamp(1.5rem, 2.4vw + 0.75rem, 2rem);
|
||||
font-weight: 400;
|
||||
letter-spacing: -0.02em;
|
||||
line-height: 1.15;
|
||||
color: var(--color-ink);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
svg { width: 100%; min-width: 900px; display: block; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="frame">
|
||||
<p class="eyebrow">ER · Diagram Design</p>
|
||||
<h1>littlemight content model</h1>
|
||||
|
||||
<svg viewBox="0 0 1000 480" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<pattern id="dots" width="22" height="22" patternUnits="userSpaceOnUse">
|
||||
<circle cx="1" cy="1" r="0.9" fill="rgba(11,13,11,0.10)"/>
|
||||
</pattern>
|
||||
<marker id="arrow" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0, 8 3, 0 6" fill="#52534e"/></marker>
|
||||
</defs>
|
||||
|
||||
<rect width="100%" height="100%" fill="#f5f4ed"/>
|
||||
<rect width="100%" height="100%" fill="url(#dots)" opacity="0.55"/>
|
||||
|
||||
<!-- Relationship lines (drawn first) -->
|
||||
<!-- 1. Author — Article (1:N "writes") -->
|
||||
<line x1="260" y1="240" x2="400" y2="240" stroke="#52534e" stroke-width="1" />
|
||||
<!-- 2. Article — ArticleTag (1:N) -->
|
||||
<line x1="640" y1="320" x2="780" y2="328" stroke="#52534e" stroke-width="1" />
|
||||
<!-- 3. Tag — ArticleTag (1:N, vertical) -->
|
||||
<line x1="880" y1="248" x2="880" y2="280" stroke="#52534e" stroke-width="1" />
|
||||
|
||||
<!-- Cardinality labels -->
|
||||
<rect x="266" y="232" width="12" height="12" rx="2" fill="#f5f4ed"/>
|
||||
<text x="272" y="242" fill="#52534e" font-size="10" font-family="'Geist Mono', monospace" text-anchor="middle" font-weight="600">1</text>
|
||||
|
||||
<rect x="378" y="232" width="16" height="12" rx="2" fill="#f5f4ed"/>
|
||||
<text x="386" y="242" fill="#52534e" font-size="10" font-family="'Geist Mono', monospace" text-anchor="middle" font-weight="600">N</text>
|
||||
|
||||
<rect x="646" y="316" width="12" height="12" rx="2" fill="#f5f4ed"/>
|
||||
<text x="652" y="326" fill="#52534e" font-size="10" font-family="'Geist Mono', monospace" text-anchor="middle" font-weight="600">1</text>
|
||||
|
||||
<rect x="760" y="324" width="16" height="12" rx="2" fill="#f5f4ed"/>
|
||||
<text x="768" y="334" fill="#52534e" font-size="10" font-family="'Geist Mono', monospace" text-anchor="middle" font-weight="600">N</text>
|
||||
|
||||
<rect x="872" y="252" width="16" height="12" rx="2" fill="#f5f4ed"/>
|
||||
<text x="880" y="262" fill="#52534e" font-size="10" font-family="'Geist Mono', monospace" text-anchor="middle" font-weight="600">1</text>
|
||||
|
||||
<rect x="872" y="268" width="16" height="12" rx="2" fill="#f5f4ed"/>
|
||||
<text x="880" y="278" fill="#52534e" font-size="10" font-family="'Geist Mono', monospace" text-anchor="middle" font-weight="600">N</text>
|
||||
|
||||
<!-- Relationship labels -->
|
||||
<rect x="304" y="220" width="56" height="14" rx="2" fill="#f5f4ed"/>
|
||||
<text x="332" y="230" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.12em">WRITES</text>
|
||||
|
||||
<rect x="688" y="300" width="56" height="14" rx="2" fill="#f5f4ed"/>
|
||||
<text x="716" y="310" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.12em">TAGGED</text>
|
||||
|
||||
<!-- Entity: Author -->
|
||||
<rect x="60" y="160" width="200" height="160" rx="6" fill="#ffffff" stroke="#0b0d0b" stroke-width="1"/>
|
||||
<rect x="60" y="160" width="200" height="40" rx="6" fill="rgba(11,13,11,0.04)" stroke="none"/>
|
||||
<rect x="60" y="192" width="200" height="8" fill="rgba(11,13,11,0.04)"/>
|
||||
<line x1="60" y1="200" x2="260" y2="200" stroke="rgba(11,13,11,0.22)" stroke-width="1"/>
|
||||
<text x="76" y="176" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.14em">ENTITY</text>
|
||||
<text x="76" y="192" fill="#0b0d0b" font-size="14" font-weight="600" font-family="'Geist', sans-serif">Author</text>
|
||||
<text x="76" y="220" fill="#0b0d0b" font-size="10" font-family="'Geist Mono', monospace"># id</text>
|
||||
<text x="220" y="220" fill="#52534e" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end">uuid</text>
|
||||
<text x="76" y="240" fill="#0b0d0b" font-size="10" font-family="'Geist Mono', monospace">handle</text>
|
||||
<text x="220" y="240" fill="#52534e" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end">text</text>
|
||||
<text x="76" y="260" fill="#0b0d0b" font-size="10" font-family="'Geist Mono', monospace">name</text>
|
||||
<text x="220" y="260" fill="#52534e" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end">text</text>
|
||||
<text x="76" y="280" fill="#0b0d0b" font-size="10" font-family="'Geist Mono', monospace">bio</text>
|
||||
<text x="220" y="280" fill="#52534e" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end">text</text>
|
||||
<text x="76" y="300" fill="#0b0d0b" font-size="10" font-family="'Geist Mono', monospace">site_url</text>
|
||||
<text x="220" y="300" fill="#52534e" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end">text</text>
|
||||
|
||||
<!-- Entity: Article (focal coral) -->
|
||||
<rect x="400" y="120" width="240" height="240" rx="6" fill="rgba(247,89,31,0.04)" stroke="#f7591f" stroke-width="1"/>
|
||||
<rect x="400" y="120" width="240" height="40" rx="6" fill="rgba(247,89,31,0.10)" stroke="none"/>
|
||||
<rect x="400" y="152" width="240" height="8" fill="rgba(247,89,31,0.10)"/>
|
||||
<line x1="400" y1="160" x2="640" y2="160" stroke="rgba(247,89,31,0.40)" stroke-width="1"/>
|
||||
<text x="416" y="136" fill="#f7591f" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.14em">ENTITY · AGGREGATE ROOT</text>
|
||||
<text x="416" y="152" fill="#0b0d0b" font-size="14" font-weight="600" font-family="'Geist', sans-serif">Article</text>
|
||||
<text x="416" y="180" fill="#0b0d0b" font-size="10" font-family="'Geist Mono', monospace"># id</text>
|
||||
<text x="600" y="180" fill="#52534e" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end">uuid</text>
|
||||
<text x="416" y="200" fill="#0b0d0b" font-size="10" font-family="'Geist Mono', monospace">title</text>
|
||||
<text x="600" y="200" fill="#52534e" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end">text</text>
|
||||
<text x="416" y="220" fill="#0b0d0b" font-size="10" font-family="'Geist Mono', monospace">slug</text>
|
||||
<text x="600" y="220" fill="#52534e" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end">text · unique</text>
|
||||
<text x="416" y="240" fill="#0b0d0b" font-size="10" font-family="'Geist Mono', monospace">body_mdx</text>
|
||||
<text x="600" y="240" fill="#52534e" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end">text</text>
|
||||
<text x="416" y="260" fill="#0b0d0b" font-size="10" font-family="'Geist Mono', monospace">published_at</text>
|
||||
<text x="600" y="260" fill="#52534e" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end">timestamp</text>
|
||||
<text x="416" y="280" fill="#0b0d0b" font-size="10" font-family="'Geist Mono', monospace">→ author_id</text>
|
||||
<text x="600" y="280" fill="#52534e" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end">uuid</text>
|
||||
<text x="416" y="300" fill="#0b0d0b" font-size="10" font-family="'Geist Mono', monospace">status</text>
|
||||
<text x="600" y="300" fill="#52534e" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end">enum</text>
|
||||
<text x="416" y="320" fill="#0b0d0b" font-size="10" font-family="'Geist Mono', monospace">og_image</text>
|
||||
<text x="600" y="320" fill="#52534e" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end">text · url</text>
|
||||
|
||||
<!-- Entity: Tag -->
|
||||
<rect x="780" y="120" width="200" height="128" rx="6" fill="#ffffff" stroke="#0b0d0b" stroke-width="1"/>
|
||||
<rect x="780" y="120" width="200" height="40" rx="6" fill="rgba(11,13,11,0.04)" stroke="none"/>
|
||||
<rect x="780" y="152" width="200" height="8" fill="rgba(11,13,11,0.04)"/>
|
||||
<line x1="780" y1="160" x2="980" y2="160" stroke="rgba(11,13,11,0.22)" stroke-width="1"/>
|
||||
<text x="796" y="136" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.14em">ENTITY</text>
|
||||
<text x="796" y="152" fill="#0b0d0b" font-size="14" font-weight="600" font-family="'Geist', sans-serif">Tag</text>
|
||||
<text x="796" y="180" fill="#0b0d0b" font-size="10" font-family="'Geist Mono', monospace"># id</text>
|
||||
<text x="940" y="180" fill="#52534e" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end">uuid</text>
|
||||
<text x="796" y="200" fill="#0b0d0b" font-size="10" font-family="'Geist Mono', monospace">slug</text>
|
||||
<text x="940" y="200" fill="#52534e" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end">text · unique</text>
|
||||
<text x="796" y="220" fill="#0b0d0b" font-size="10" font-family="'Geist Mono', monospace">name</text>
|
||||
<text x="940" y="220" fill="#52534e" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end">text</text>
|
||||
<text x="796" y="240" fill="#0b0d0b" font-size="10" font-family="'Geist Mono', monospace">description</text>
|
||||
<text x="940" y="240" fill="#52534e" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end">text</text>
|
||||
|
||||
<!-- Entity: ArticleTag (join) -->
|
||||
<rect x="780" y="280" width="200" height="96" rx="6" fill="rgba(11,13,11,0.04)" stroke="#52534e" stroke-width="1" stroke-dasharray="4,3"/>
|
||||
<rect x="780" y="280" width="200" height="40" rx="6" fill="rgba(11,13,11,0.06)" stroke="none"/>
|
||||
<rect x="780" y="312" width="200" height="8" fill="rgba(11,13,11,0.06)"/>
|
||||
<line x1="780" y1="320" x2="980" y2="320" stroke="rgba(11,13,11,0.22)" stroke-width="1"/>
|
||||
<text x="796" y="296" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.14em">JOIN</text>
|
||||
<text x="796" y="312" fill="#0b0d0b" font-size="14" font-weight="600" font-family="'Geist', sans-serif">ArticleTag</text>
|
||||
<text x="796" y="340" fill="#0b0d0b" font-size="10" font-family="'Geist Mono', monospace">→ article_id</text>
|
||||
<text x="940" y="340" fill="#52534e" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end">uuid</text>
|
||||
<text x="796" y="360" fill="#0b0d0b" font-size="10" font-family="'Geist Mono', monospace">→ tag_id</text>
|
||||
<text x="940" y="360" fill="#52534e" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end">uuid</text>
|
||||
|
||||
<!-- Legend -->
|
||||
<line x1="40" y1="404" x2="960" y2="404" stroke="rgba(11,13,11,0.10)" stroke-width="0.8"/>
|
||||
<text x="40" y="420" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">LEGEND</text>
|
||||
|
||||
<rect x="40" y="436" width="14" height="10" rx="2" fill="rgba(247,89,31,0.04)" stroke="#f7591f" stroke-width="1"/>
|
||||
<text x="60" y="444" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Aggregate root</text>
|
||||
|
||||
<rect x="180" y="436" width="14" height="10" rx="2" fill="#ffffff" stroke="#0b0d0b" stroke-width="1"/>
|
||||
<text x="200" y="444" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Entity</text>
|
||||
|
||||
<rect x="268" y="436" width="14" height="10" rx="2" fill="rgba(11,13,11,0.04)" stroke="#52534e" stroke-width="1" stroke-dasharray="3,2"/>
|
||||
<text x="288" y="444" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Join table</text>
|
||||
|
||||
<text x="372" y="444" fill="#0b0d0b" font-size="10" font-family="'Geist Mono', monospace" font-weight="600">#</text>
|
||||
<text x="388" y="444" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Primary key</text>
|
||||
|
||||
<text x="476" y="444" fill="#0b0d0b" font-size="10" font-family="'Geist Mono', monospace" font-weight="600">→</text>
|
||||
<text x="492" y="444" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Foreign key</text>
|
||||
|
||||
<text x="584" y="444" fill="#52534e" font-size="10" font-family="'Geist Mono', monospace" font-weight="600">1 / N</text>
|
||||
<text x="616" y="444" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Cardinality</text>
|
||||
</svg>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
154
skills/assets/example-flowchart-dark.html
Normal file
154
skills/assets/example-flowchart-dark.html
Normal file
@@ -0,0 +1,154 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Should you write this as a skill?</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
:root {
|
||||
--color-paper: #1c1a17;
|
||||
--color-ink: #f1efe7;
|
||||
--color-muted: #a8a69d;
|
||||
--color-accent: #ff6a30;
|
||||
--font-sans: 'Geist', system-ui, sans-serif;
|
||||
--font-serif: 'Instrument Serif', serif;
|
||||
--font-mono: 'Geist Mono', ui-monospace, monospace;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-sans);
|
||||
background: var(--color-paper);
|
||||
color: var(--color-ink);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 3rem 2rem;
|
||||
}
|
||||
|
||||
.frame { max-width: 1200px; width: 100%; }
|
||||
|
||||
.eyebrow {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.66rem;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.18em;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-muted);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-family: var(--font-serif);
|
||||
font-size: clamp(1.5rem, 2.4vw + 0.75rem, 2rem);
|
||||
font-weight: 400;
|
||||
letter-spacing: -0.02em;
|
||||
line-height: 1.15;
|
||||
color: var(--color-ink);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
svg { width: 100%; min-width: 900px; display: block; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="frame">
|
||||
<p class="eyebrow">Flowchart · Diagram Design</p>
|
||||
<h1>Should you write this as a skill?</h1>
|
||||
|
||||
<svg viewBox="0 0 1000 600" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<pattern id="dots" width="22" height="22" patternUnits="userSpaceOnUse">
|
||||
<circle cx="1" cy="1" r="0.9" fill="rgba(241,239,231,0.10)"/>
|
||||
</pattern>
|
||||
<marker id="arrow" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0, 8 3, 0 6" fill="#a8a69d"/></marker>
|
||||
<marker id="arrow-accent" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0, 8 3, 0 6" fill="#ff6a30"/></marker>
|
||||
</defs>
|
||||
|
||||
<rect width="100%" height="100%" fill="#1c1a17"/>
|
||||
<rect width="100%" height="100%" fill="url(#dots)" opacity="0.55"/>
|
||||
|
||||
<!-- Arrows (drawn first, behind nodes) -->
|
||||
<!-- Start → Step -->
|
||||
<line x1="500" y1="88" x2="500" y2="120" stroke="#a8a69d" stroke-width="1.2" marker-end="url(#arrow)"/>
|
||||
<!-- Step → Diamond 1 -->
|
||||
<line x1="500" y1="168" x2="500" y2="192" stroke="#a8a69d" stroke-width="1.2" marker-end="url(#arrow)"/>
|
||||
<!-- Diamond 1 "NO" right -->
|
||||
<line x1="600" y1="240" x2="720" y2="240" stroke="#a8a69d" stroke-width="1.2" marker-end="url(#arrow)"/>
|
||||
<!-- Diamond 1 "YES" down -->
|
||||
<line x1="500" y1="288" x2="500" y2="328" stroke="#a8a69d" stroke-width="1.2" marker-end="url(#arrow)"/>
|
||||
<!-- Diamond 2 "NO" right -->
|
||||
<line x1="600" y1="376" x2="720" y2="376" stroke="#a8a69d" stroke-width="1.2" marker-end="url(#arrow)"/>
|
||||
<!-- Diamond 2 "YES" down (coral — happy path) -->
|
||||
<line x1="500" y1="424" x2="500" y2="464" stroke="#ff6a30" stroke-width="1.4" marker-end="url(#arrow-accent)"/>
|
||||
|
||||
<!-- Arrow labels -->
|
||||
<rect x="644" y="230" width="24" height="12" rx="2" fill="#1c1a17"/>
|
||||
<text x="656" y="239" fill="#a8a69d" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.12em">NO</text>
|
||||
|
||||
<rect x="484" y="298" width="32" height="12" rx="2" fill="#1c1a17"/>
|
||||
<text x="500" y="307" fill="#a8a69d" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.12em">YES</text>
|
||||
|
||||
<rect x="644" y="366" width="24" height="12" rx="2" fill="#1c1a17"/>
|
||||
<text x="656" y="375" fill="#a8a69d" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.12em">NO</text>
|
||||
|
||||
<rect x="484" y="434" width="32" height="12" rx="2" fill="#1c1a17"/>
|
||||
<text x="500" y="443" fill="#ff6a30" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.12em">YES</text>
|
||||
|
||||
<!-- Start oval -->
|
||||
<rect x="420" y="40" width="160" height="48" rx="24" fill="rgba(241,239,231,0.03)" stroke="rgba(241,239,231,0.30)" stroke-width="1"/>
|
||||
<text x="500" y="68" fill="#f1efe7" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">New workflow</text>
|
||||
|
||||
<!-- Rectangle: Step -->
|
||||
<rect x="420" y="120" width="160" height="48" rx="6" fill="#2a2723" stroke="#f1efe7" stroke-width="1"/>
|
||||
<text x="500" y="148" fill="#f1efe7" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Do it manually once</text>
|
||||
|
||||
<!-- Diamond 1: Repeated >3 times? -->
|
||||
<polygon points="500,192 600,240 500,288 400,240" fill="#2a2723" stroke="#f1efe7" stroke-width="1"/>
|
||||
<text x="500" y="238" fill="#f1efe7" font-size="11" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Will you repeat</text>
|
||||
<text x="500" y="252" fill="#f1efe7" font-size="11" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">it >3 times?</text>
|
||||
|
||||
<!-- End oval: One-off -->
|
||||
<rect x="720" y="216" width="160" height="48" rx="24" fill="rgba(241,239,231,0.03)" stroke="rgba(241,239,231,0.30)" stroke-width="1"/>
|
||||
<text x="800" y="240" fill="#f1efe7" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">One-off</text>
|
||||
<text x="800" y="254" fill="#a8a69d" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">keep manual</text>
|
||||
|
||||
<!-- Diamond 2: Reusable across projects? -->
|
||||
<polygon points="500,328 600,376 500,424 400,376" fill="#2a2723" stroke="#f1efe7" stroke-width="1"/>
|
||||
<text x="500" y="374" fill="#f1efe7" font-size="11" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Reusable across</text>
|
||||
<text x="500" y="388" fill="#f1efe7" font-size="11" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">projects?</text>
|
||||
|
||||
<!-- End oval: CLAUDE.md note -->
|
||||
<rect x="720" y="352" width="160" height="48" rx="24" fill="rgba(241,239,231,0.03)" stroke="rgba(241,239,231,0.30)" stroke-width="1"/>
|
||||
<text x="800" y="376" fill="#f1efe7" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">CLAUDE.md note</text>
|
||||
<text x="800" y="390" fill="#a8a69d" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">project-scoped</text>
|
||||
|
||||
<!-- End oval: Write a skill (coral focal) -->
|
||||
<rect x="420" y="464" width="160" height="56" rx="28" fill="rgba(255,106,48,0.08)" stroke="#ff6a30" stroke-width="1"/>
|
||||
<text x="500" y="492" fill="#f1efe7" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Write a skill</text>
|
||||
<text x="500" y="508" fill="#a8a69d" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">reusable + assets</text>
|
||||
|
||||
<!-- Legend -->
|
||||
<line x1="40" y1="540" x2="960" y2="540" stroke="rgba(241,239,231,0.10)" stroke-width="0.8"/>
|
||||
<text x="40" y="556" fill="#a8a69d" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">LEGEND · SHAPE CARRIES TYPE</text>
|
||||
|
||||
<rect x="40" y="572" width="24" height="12" rx="6" fill="rgba(241,239,231,0.03)" stroke="rgba(241,239,231,0.30)" stroke-width="1"/>
|
||||
<text x="72" y="582" fill="#a8a69d" font-size="8.5" font-family="'Geist', sans-serif">Start / end (oval)</text>
|
||||
|
||||
<rect x="220" y="572" width="24" height="12" rx="2" fill="#2a2723" stroke="#f1efe7" stroke-width="1"/>
|
||||
<text x="252" y="582" fill="#a8a69d" font-size="8.5" font-family="'Geist', sans-serif">Step (rectangle)</text>
|
||||
|
||||
<polygon points="412,578 424,572 436,578 424,584" fill="#2a2723" stroke="#f1efe7" stroke-width="1"/>
|
||||
<text x="448" y="582" fill="#a8a69d" font-size="8.5" font-family="'Geist', sans-serif">Decision (diamond)</text>
|
||||
|
||||
<line x1="604" y1="580" x2="632" y2="580" stroke="#ff6a30" stroke-width="1.4" marker-end="url(#arrow-accent)"/>
|
||||
<text x="640" y="582" fill="#a8a69d" font-size="8.5" font-family="'Geist', sans-serif">Happy path</text>
|
||||
|
||||
<line x1="768" y1="580" x2="796" y2="580" stroke="#a8a69d" stroke-width="1.2" marker-end="url(#arrow)"/>
|
||||
<text x="804" y="582" fill="#a8a69d" font-size="8.5" font-family="'Geist', sans-serif">Branch</text>
|
||||
</svg>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
157
skills/assets/example-flowchart-full.html
Normal file
157
skills/assets/example-flowchart-full.html
Normal file
@@ -0,0 +1,157 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Should you write this as a skill?</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
:root { --color-paper:#f5f4ed; --color-paper-2:#efeee5; --color-ink:#0b0d0b; --color-muted:#52534e; --color-soft:#65655c; --color-rule:rgba(11,13,11,0.12); --color-accent:#f7591f; --color-link:#1a70c7; --font-sans:'Geist',system-ui,sans-serif; --font-serif:'Instrument Serif',serif; --font-mono:'Geist Mono',ui-monospace,monospace; }
|
||||
body { font-family: var(--font-sans); background: var(--color-paper); min-height: 100vh; padding: 3rem 2rem; color: var(--color-ink); }
|
||||
.container { max-width: 1200px; margin: 0 auto; }
|
||||
.header { margin-bottom: 2.5rem; }
|
||||
.header-eyebrow { font-family: var(--font-mono); font-size: 0.66rem; font-weight: 500; letter-spacing: 0.18em; text-transform: uppercase; color: var(--color-muted); margin-bottom: 0.75rem; }
|
||||
h1 { font-family: var(--font-serif); font-size: clamp(1.75rem, 3vw + 1rem, 2.5rem); font-weight: 400; letter-spacing: -0.02em; line-height: 1.1; margin-bottom: 0.5rem; }
|
||||
.subtitle { font-size: 1rem; line-height: 1.55; color: var(--color-muted); max-width: 58ch; }
|
||||
.diagram-container { background: var(--color-paper-2); border-radius: 8px; border: 1px solid var(--color-rule); padding: 1.5rem; overflow-x: auto; }
|
||||
svg { width: 100%; min-width: 900px; display: block; }
|
||||
.cards { display: grid; grid-template-columns: 1.1fr 1fr 0.9fr; gap: 1rem; margin-top: 1.5rem; }
|
||||
@media (max-width: 820px) { .cards { grid-template-columns: 1fr; } }
|
||||
.card { background: #fff; border-radius: 6px; border: 1px solid var(--color-rule); padding: 1.25rem; }
|
||||
.card .eyebrow { font-family: var(--font-mono); font-size: 0.5rem; letter-spacing: 0.18em; text-transform: uppercase; color: var(--color-muted); margin-bottom: 0.5rem; }
|
||||
.card-header { display: flex; align-items: center; gap: 0.6rem; margin-bottom: 0.875rem; padding-bottom: 0.875rem; border-bottom: 1px solid rgba(11,13,11,0.08); }
|
||||
.card-dot { width: 7px; height: 7px; border-radius: 50%; }
|
||||
.card-dot.ink { background: var(--color-ink); } .card-dot.muted { background: var(--color-muted); } .card-dot.coral { background: var(--color-accent); }
|
||||
.card h3 { font-size: 0.875rem; font-weight: 600; }
|
||||
.card p, .card ul { color: var(--color-muted); font-size: 0.8125rem; line-height: 1.55; list-style: none; }
|
||||
.card li { margin-bottom: 0.3rem; padding-left: 0.875rem; position: relative; }
|
||||
.card li::before { content: '—'; position: absolute; left: 0; color: rgba(11,13,11,0.25); font-size: 0.75rem; }
|
||||
.footer { margin-top: 2rem; padding-top: 1.5rem; border-top: 1px solid rgba(11,13,11,0.10); font-family: var(--font-mono); font-size: 0.72rem; letter-spacing: 0.06em; color: var(--color-soft); display: flex; justify-content: space-between; flex-wrap: wrap; gap: 0.5rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<p class="header-eyebrow">Flowchart · Diagram Design</p>
|
||||
<h1>Should you write this as a skill?</h1>
|
||||
<p class="subtitle">A three-decision triage for turning a one-off workflow into something reusable. Shape carries type — ovals bracket the flow, rectangles are steps, diamonds are decisions.</p>
|
||||
</div>
|
||||
|
||||
<div class="diagram-container">
|
||||
<svg viewBox="0 0 1000 600" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<pattern id="dots" width="22" height="22" patternUnits="userSpaceOnUse">
|
||||
<circle cx="1" cy="1" r="0.9" fill="rgba(11,13,11,0.10)"/>
|
||||
</pattern>
|
||||
<marker id="arrow" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0, 8 3, 0 6" fill="#52534e"/></marker>
|
||||
<marker id="arrow-accent" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0, 8 3, 0 6" fill="#f7591f"/></marker>
|
||||
</defs>
|
||||
|
||||
<rect width="100%" height="100%" fill="#f5f4ed"/>
|
||||
<rect width="100%" height="100%" fill="url(#dots)" opacity="0.55"/>
|
||||
|
||||
<!-- Arrows (drawn first, behind nodes) -->
|
||||
<!-- Start → Step -->
|
||||
<line x1="500" y1="88" x2="500" y2="120" stroke="#52534e" stroke-width="1.2" marker-end="url(#arrow)"/>
|
||||
<!-- Step → Diamond 1 -->
|
||||
<line x1="500" y1="168" x2="500" y2="192" stroke="#52534e" stroke-width="1.2" marker-end="url(#arrow)"/>
|
||||
<!-- Diamond 1 "NO" right -->
|
||||
<line x1="600" y1="240" x2="720" y2="240" stroke="#52534e" stroke-width="1.2" marker-end="url(#arrow)"/>
|
||||
<!-- Diamond 1 "YES" down -->
|
||||
<line x1="500" y1="288" x2="500" y2="328" stroke="#52534e" stroke-width="1.2" marker-end="url(#arrow)"/>
|
||||
<!-- Diamond 2 "NO" right -->
|
||||
<line x1="600" y1="376" x2="720" y2="376" stroke="#52534e" stroke-width="1.2" marker-end="url(#arrow)"/>
|
||||
<!-- Diamond 2 "YES" down (coral — happy path) -->
|
||||
<line x1="500" y1="424" x2="500" y2="464" stroke="#f7591f" stroke-width="1.4" marker-end="url(#arrow-accent)"/>
|
||||
|
||||
<!-- Arrow labels -->
|
||||
<rect x="644" y="230" width="24" height="12" rx="2" fill="#f5f4ed"/>
|
||||
<text x="656" y="239" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.12em">NO</text>
|
||||
|
||||
<rect x="484" y="298" width="32" height="12" rx="2" fill="#f5f4ed"/>
|
||||
<text x="500" y="307" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.12em">YES</text>
|
||||
|
||||
<rect x="644" y="366" width="24" height="12" rx="2" fill="#f5f4ed"/>
|
||||
<text x="656" y="375" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.12em">NO</text>
|
||||
|
||||
<rect x="484" y="434" width="32" height="12" rx="2" fill="#f5f4ed"/>
|
||||
<text x="500" y="443" fill="#f7591f" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.12em">YES</text>
|
||||
|
||||
<!-- Start oval -->
|
||||
<rect x="420" y="40" width="160" height="48" rx="24" fill="rgba(11,13,11,0.03)" stroke="rgba(11,13,11,0.30)" stroke-width="1"/>
|
||||
<text x="500" y="68" fill="#0b0d0b" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">New workflow</text>
|
||||
|
||||
<!-- Rectangle: Step -->
|
||||
<rect x="420" y="120" width="160" height="48" rx="6" fill="#ffffff" stroke="#0b0d0b" stroke-width="1"/>
|
||||
<text x="500" y="148" fill="#0b0d0b" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Do it manually once</text>
|
||||
|
||||
<!-- Diamond 1: Repeated >3 times? -->
|
||||
<polygon points="500,192 600,240 500,288 400,240" fill="#ffffff" stroke="#0b0d0b" stroke-width="1"/>
|
||||
<text x="500" y="238" fill="#0b0d0b" font-size="11" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Will you repeat</text>
|
||||
<text x="500" y="252" fill="#0b0d0b" font-size="11" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">it >3 times?</text>
|
||||
|
||||
<!-- End oval: One-off -->
|
||||
<rect x="720" y="216" width="160" height="48" rx="24" fill="rgba(11,13,11,0.03)" stroke="rgba(11,13,11,0.30)" stroke-width="1"/>
|
||||
<text x="800" y="240" fill="#0b0d0b" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">One-off</text>
|
||||
<text x="800" y="254" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">keep manual</text>
|
||||
|
||||
<!-- Diamond 2: Reusable across projects? -->
|
||||
<polygon points="500,328 600,376 500,424 400,376" fill="#ffffff" stroke="#0b0d0b" stroke-width="1"/>
|
||||
<text x="500" y="374" fill="#0b0d0b" font-size="11" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Reusable across</text>
|
||||
<text x="500" y="388" fill="#0b0d0b" font-size="11" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">projects?</text>
|
||||
|
||||
<!-- End oval: CLAUDE.md note -->
|
||||
<rect x="720" y="352" width="160" height="48" rx="24" fill="rgba(11,13,11,0.03)" stroke="rgba(11,13,11,0.30)" stroke-width="1"/>
|
||||
<text x="800" y="376" fill="#0b0d0b" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">CLAUDE.md note</text>
|
||||
<text x="800" y="390" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">project-scoped</text>
|
||||
|
||||
<!-- End oval: Write a skill (coral focal) -->
|
||||
<rect x="420" y="464" width="160" height="56" rx="28" fill="rgba(247,89,31,0.08)" stroke="#f7591f" stroke-width="1"/>
|
||||
<text x="500" y="492" fill="#0b0d0b" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Write a skill</text>
|
||||
<text x="500" y="508" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">reusable + assets</text>
|
||||
|
||||
<!-- Legend -->
|
||||
<line x1="40" y1="540" x2="960" y2="540" stroke="rgba(11,13,11,0.10)" stroke-width="0.8"/>
|
||||
<text x="40" y="556" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">LEGEND · SHAPE CARRIES TYPE</text>
|
||||
|
||||
<rect x="40" y="572" width="24" height="12" rx="6" fill="rgba(11,13,11,0.03)" stroke="rgba(11,13,11,0.30)" stroke-width="1"/>
|
||||
<text x="72" y="582" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Start / end (oval)</text>
|
||||
|
||||
<rect x="220" y="572" width="24" height="12" rx="2" fill="#ffffff" stroke="#0b0d0b" stroke-width="1"/>
|
||||
<text x="252" y="582" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Step (rectangle)</text>
|
||||
|
||||
<polygon points="412,578 424,572 436,578 424,584" fill="#ffffff" stroke="#0b0d0b" stroke-width="1"/>
|
||||
<text x="448" y="582" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Decision (diamond)</text>
|
||||
|
||||
<line x1="604" y1="580" x2="632" y2="580" stroke="#f7591f" stroke-width="1.4" marker-end="url(#arrow-accent)"/>
|
||||
<text x="640" y="582" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Happy path</text>
|
||||
|
||||
<line x1="768" y1="580" x2="796" y2="580" stroke="#52534e" stroke-width="1.2" marker-end="url(#arrow)"/>
|
||||
<text x="804" y="582" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Branch</text>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div class="cards">
|
||||
<div class="card">
|
||||
<p class="eyebrow">WHY THIS FLOWCHART</p>
|
||||
<div class="card-header"><span class="card-dot coral"></span><h3>Most things aren't skills</h3></div>
|
||||
<p>The happy path is narrow on purpose. Only workflows that clear all three gates earn the overhead of a reusable skill — everything else is better as a project note.</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-header"><span class="card-dot ink"></span><h3>Shape, not color</h3></div>
|
||||
<ul><li>Oval bookends the flow</li><li>Rectangle is a step</li><li>Diamond is a decision</li><li>Color reserved for the happy path</li></ul>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-header"><span class="card-dot muted"></span><h3>Every branch gets a label</h3></div>
|
||||
<p>Unlabeled branches turn a flowchart into a maze. Yes/No is fine; conditions in mono when the logic is richer.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<span>flowchart · should I write this as a skill?</span>
|
||||
<span>example · diagram-design</span>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
154
skills/assets/example-flowchart.html
Normal file
154
skills/assets/example-flowchart.html
Normal file
@@ -0,0 +1,154 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Should you write this as a skill?</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
:root {
|
||||
--color-paper: #f5f4ed;
|
||||
--color-ink: #0b0d0b;
|
||||
--color-muted: #52534e;
|
||||
--color-accent: #f7591f;
|
||||
--font-sans: 'Geist', system-ui, sans-serif;
|
||||
--font-serif: 'Instrument Serif', serif;
|
||||
--font-mono: 'Geist Mono', ui-monospace, monospace;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-sans);
|
||||
background: var(--color-paper);
|
||||
color: var(--color-ink);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 3rem 2rem;
|
||||
}
|
||||
|
||||
.frame { max-width: 1200px; width: 100%; }
|
||||
|
||||
.eyebrow {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.66rem;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.18em;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-muted);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-family: var(--font-serif);
|
||||
font-size: clamp(1.5rem, 2.4vw + 0.75rem, 2rem);
|
||||
font-weight: 400;
|
||||
letter-spacing: -0.02em;
|
||||
line-height: 1.15;
|
||||
color: var(--color-ink);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
svg { width: 100%; min-width: 900px; display: block; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="frame">
|
||||
<p class="eyebrow">Flowchart · Diagram Design</p>
|
||||
<h1>Should you write this as a skill?</h1>
|
||||
|
||||
<svg viewBox="0 0 1000 600" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<pattern id="dots" width="22" height="22" patternUnits="userSpaceOnUse">
|
||||
<circle cx="1" cy="1" r="0.9" fill="rgba(11,13,11,0.10)"/>
|
||||
</pattern>
|
||||
<marker id="arrow" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0, 8 3, 0 6" fill="#52534e"/></marker>
|
||||
<marker id="arrow-accent" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0, 8 3, 0 6" fill="#f7591f"/></marker>
|
||||
</defs>
|
||||
|
||||
<rect width="100%" height="100%" fill="#f5f4ed"/>
|
||||
<rect width="100%" height="100%" fill="url(#dots)" opacity="0.55"/>
|
||||
|
||||
<!-- Arrows (drawn first, behind nodes) -->
|
||||
<!-- Start → Step -->
|
||||
<line x1="500" y1="88" x2="500" y2="120" stroke="#52534e" stroke-width="1.2" marker-end="url(#arrow)"/>
|
||||
<!-- Step → Diamond 1 -->
|
||||
<line x1="500" y1="168" x2="500" y2="192" stroke="#52534e" stroke-width="1.2" marker-end="url(#arrow)"/>
|
||||
<!-- Diamond 1 "NO" right -->
|
||||
<line x1="600" y1="240" x2="720" y2="240" stroke="#52534e" stroke-width="1.2" marker-end="url(#arrow)"/>
|
||||
<!-- Diamond 1 "YES" down -->
|
||||
<line x1="500" y1="288" x2="500" y2="328" stroke="#52534e" stroke-width="1.2" marker-end="url(#arrow)"/>
|
||||
<!-- Diamond 2 "NO" right -->
|
||||
<line x1="600" y1="376" x2="720" y2="376" stroke="#52534e" stroke-width="1.2" marker-end="url(#arrow)"/>
|
||||
<!-- Diamond 2 "YES" down (coral — happy path) -->
|
||||
<line x1="500" y1="424" x2="500" y2="464" stroke="#f7591f" stroke-width="1.4" marker-end="url(#arrow-accent)"/>
|
||||
|
||||
<!-- Arrow labels -->
|
||||
<rect x="644" y="230" width="24" height="12" rx="2" fill="#f5f4ed"/>
|
||||
<text x="656" y="239" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.12em">NO</text>
|
||||
|
||||
<rect x="484" y="298" width="32" height="12" rx="2" fill="#f5f4ed"/>
|
||||
<text x="500" y="307" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.12em">YES</text>
|
||||
|
||||
<rect x="644" y="366" width="24" height="12" rx="2" fill="#f5f4ed"/>
|
||||
<text x="656" y="375" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.12em">NO</text>
|
||||
|
||||
<rect x="484" y="434" width="32" height="12" rx="2" fill="#f5f4ed"/>
|
||||
<text x="500" y="443" fill="#f7591f" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.12em">YES</text>
|
||||
|
||||
<!-- Start oval -->
|
||||
<rect x="420" y="40" width="160" height="48" rx="24" fill="rgba(11,13,11,0.03)" stroke="rgba(11,13,11,0.30)" stroke-width="1"/>
|
||||
<text x="500" y="68" fill="#0b0d0b" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">New workflow</text>
|
||||
|
||||
<!-- Rectangle: Step -->
|
||||
<rect x="420" y="120" width="160" height="48" rx="6" fill="#ffffff" stroke="#0b0d0b" stroke-width="1"/>
|
||||
<text x="500" y="148" fill="#0b0d0b" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Do it manually once</text>
|
||||
|
||||
<!-- Diamond 1: Repeated >3 times? -->
|
||||
<polygon points="500,192 600,240 500,288 400,240" fill="#ffffff" stroke="#0b0d0b" stroke-width="1"/>
|
||||
<text x="500" y="238" fill="#0b0d0b" font-size="11" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Will you repeat</text>
|
||||
<text x="500" y="252" fill="#0b0d0b" font-size="11" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">it >3 times?</text>
|
||||
|
||||
<!-- End oval: One-off -->
|
||||
<rect x="720" y="216" width="160" height="48" rx="24" fill="rgba(11,13,11,0.03)" stroke="rgba(11,13,11,0.30)" stroke-width="1"/>
|
||||
<text x="800" y="240" fill="#0b0d0b" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">One-off</text>
|
||||
<text x="800" y="254" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">keep manual</text>
|
||||
|
||||
<!-- Diamond 2: Reusable across projects? -->
|
||||
<polygon points="500,328 600,376 500,424 400,376" fill="#ffffff" stroke="#0b0d0b" stroke-width="1"/>
|
||||
<text x="500" y="374" fill="#0b0d0b" font-size="11" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Reusable across</text>
|
||||
<text x="500" y="388" fill="#0b0d0b" font-size="11" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">projects?</text>
|
||||
|
||||
<!-- End oval: CLAUDE.md note -->
|
||||
<rect x="720" y="352" width="160" height="48" rx="24" fill="rgba(11,13,11,0.03)" stroke="rgba(11,13,11,0.30)" stroke-width="1"/>
|
||||
<text x="800" y="376" fill="#0b0d0b" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">CLAUDE.md note</text>
|
||||
<text x="800" y="390" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">project-scoped</text>
|
||||
|
||||
<!-- End oval: Write a skill (coral focal) -->
|
||||
<rect x="420" y="464" width="160" height="56" rx="28" fill="rgba(247,89,31,0.08)" stroke="#f7591f" stroke-width="1"/>
|
||||
<text x="500" y="492" fill="#0b0d0b" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Write a skill</text>
|
||||
<text x="500" y="508" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">reusable + assets</text>
|
||||
|
||||
<!-- Legend -->
|
||||
<line x1="40" y1="540" x2="960" y2="540" stroke="rgba(11,13,11,0.10)" stroke-width="0.8"/>
|
||||
<text x="40" y="556" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">LEGEND · SHAPE CARRIES TYPE</text>
|
||||
|
||||
<rect x="40" y="572" width="24" height="12" rx="6" fill="rgba(11,13,11,0.03)" stroke="rgba(11,13,11,0.30)" stroke-width="1"/>
|
||||
<text x="72" y="582" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Start / end (oval)</text>
|
||||
|
||||
<rect x="220" y="572" width="24" height="12" rx="2" fill="#ffffff" stroke="#0b0d0b" stroke-width="1"/>
|
||||
<text x="252" y="582" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Step (rectangle)</text>
|
||||
|
||||
<polygon points="412,578 424,572 436,578 424,584" fill="#ffffff" stroke="#0b0d0b" stroke-width="1"/>
|
||||
<text x="448" y="582" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Decision (diamond)</text>
|
||||
|
||||
<line x1="604" y1="580" x2="632" y2="580" stroke="#f7591f" stroke-width="1.4" marker-end="url(#arrow-accent)"/>
|
||||
<text x="640" y="582" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Happy path</text>
|
||||
|
||||
<line x1="768" y1="580" x2="796" y2="580" stroke="#52534e" stroke-width="1.2" marker-end="url(#arrow)"/>
|
||||
<text x="804" y="582" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Branch</text>
|
||||
</svg>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
121
skills/assets/example-layers-dark.html
Normal file
121
skills/assets/example-layers-dark.html
Normal file
@@ -0,0 +1,121 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>AI app stack · Layer hierarchy</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
:root {
|
||||
--color-paper: #1c1a17;
|
||||
--color-ink: #f1efe7;
|
||||
--color-muted: #a8a69d;
|
||||
--color-accent: #ff6a30;
|
||||
--font-sans: 'Geist', system-ui, sans-serif;
|
||||
--font-serif: 'Instrument Serif', serif;
|
||||
--font-mono: 'Geist Mono', ui-monospace, monospace;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-sans);
|
||||
background: var(--color-paper);
|
||||
color: var(--color-ink);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 3rem 2rem;
|
||||
}
|
||||
|
||||
.frame { max-width: 1200px; width: 100%; }
|
||||
|
||||
.eyebrow {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.66rem;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.18em;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-muted);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-family: var(--font-serif);
|
||||
font-size: clamp(1.5rem, 2.4vw + 0.75rem, 2rem);
|
||||
font-weight: 400;
|
||||
letter-spacing: -0.02em;
|
||||
line-height: 1.15;
|
||||
color: var(--color-ink);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
svg { width: 100%; min-width: 900px; display: block; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="frame">
|
||||
<p class="eyebrow">Layer stack · Diagram Design</p>
|
||||
<h1>AI app stack · Where the work actually happens</h1>
|
||||
|
||||
<svg viewBox="0 0 1000 500" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<pattern id="dots" width="22" height="22" patternUnits="userSpaceOnUse">
|
||||
<circle cx="1" cy="1" r="0.9" fill="rgba(241,239,231,0.10)"/>
|
||||
</pattern>
|
||||
</defs>
|
||||
|
||||
<rect width="100%" height="100%" fill="#1c1a17"/>
|
||||
<rect width="100%" height="100%" fill="url(#dots)" opacity="0.55"/>
|
||||
|
||||
<!-- Direction column (left margin) -->
|
||||
<text x="60" y="68" fill="#a8a69d" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">ABSTRACTION</text>
|
||||
<line x1="80" y1="80" x2="80" y2="400" stroke="rgba(241,239,231,0.30)" stroke-width="1"/>
|
||||
<polygon points="76,80 84,80 80,72" fill="#a8a69d"/>
|
||||
<text x="60" y="416" fill="#a8a69d" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">SILICON</text>
|
||||
|
||||
<!-- Stack container hairlines (top + bottom edges) -->
|
||||
<line x1="120" y1="80" x2="960" y2="80" stroke="rgba(241,239,231,0.12)" stroke-width="1"/>
|
||||
<line x1="120" y1="400" x2="960" y2="400" stroke="rgba(241,239,231,0.12)" stroke-width="1"/>
|
||||
|
||||
<!-- L5 — UI (top layer, lifted fill) -->
|
||||
<rect x="120" y="80" width="840" height="64" fill="#2a2723"/>
|
||||
<line x1="120" y1="144" x2="960" y2="144" stroke="rgba(241,239,231,0.12)" stroke-width="1"/>
|
||||
<text x="140" y="116" fill="#a8a69d" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.14em">L5</text>
|
||||
<text x="260" y="118" fill="#f1efe7" font-size="16" font-weight="600" font-family="'Geist', sans-serif">UI surface</text>
|
||||
<text x="940" y="118" fill="#a8a69d" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end" letter-spacing="0.08em">chat, editor, canvas</text>
|
||||
|
||||
<!-- L4 — Agent harness (FOCAL, coral tint + coral stroke) -->
|
||||
<rect x="120" y="144" width="840" height="64" fill="rgba(255,106,48,0.12)"/>
|
||||
<rect x="120" y="144" width="840" height="64" fill="none" stroke="#ff6a30" stroke-width="1"/>
|
||||
<text x="140" y="180" fill="#ff6a30" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.14em" font-weight="600">L4</text>
|
||||
<text x="260" y="182" fill="#f1efe7" font-size="16" font-weight="600" font-family="'Geist', sans-serif">Agent harness</text>
|
||||
<text x="940" y="182" fill="#ff6a30" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end" letter-spacing="0.08em">tools, memory, loop</text>
|
||||
|
||||
<!-- L3 — Prompt layer -->
|
||||
<rect x="120" y="208" width="840" height="64" fill="#1c1a17"/>
|
||||
<line x1="120" y1="272" x2="960" y2="272" stroke="rgba(241,239,231,0.12)" stroke-width="1"/>
|
||||
<text x="140" y="244" fill="#a8a69d" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.14em">L3</text>
|
||||
<text x="260" y="246" fill="#f1efe7" font-size="16" font-weight="600" font-family="'Geist', sans-serif">Prompt layer</text>
|
||||
<text x="940" y="246" fill="#a8a69d" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end" letter-spacing="0.08em">system, few-shot, caching</text>
|
||||
|
||||
<!-- L2 — SDK -->
|
||||
<rect x="120" y="272" width="840" height="64" fill="#221f1c"/>
|
||||
<line x1="120" y1="336" x2="960" y2="336" stroke="rgba(241,239,231,0.12)" stroke-width="1"/>
|
||||
<text x="140" y="308" fill="#a8a69d" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.14em">L2</text>
|
||||
<text x="260" y="310" fill="#f1efe7" font-size="16" font-weight="600" font-family="'Geist', sans-serif">SDK / client</text>
|
||||
<text x="940" y="310" fill="#a8a69d" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end" letter-spacing="0.08em">auth, retries, streaming</text>
|
||||
|
||||
<!-- L1 — Model -->
|
||||
<rect x="120" y="336" width="840" height="64" fill="#221f1c"/>
|
||||
<text x="140" y="372" fill="#a8a69d" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.14em">L1</text>
|
||||
<text x="260" y="374" fill="#f1efe7" font-size="16" font-weight="600" font-family="'Geist', sans-serif">Model weights</text>
|
||||
<text x="940" y="374" fill="#a8a69d" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end" letter-spacing="0.08em">opus, sonnet, haiku</text>
|
||||
|
||||
<!-- Caption -->
|
||||
<text x="120" y="456" fill="#a8a69d" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">FOCAL LAYER</text>
|
||||
<text x="240" y="456" fill="#a8a69d" font-size="8.5" font-family="'Geist', sans-serif" font-style="italic">The harness is where most product differentiation actually lives — tools, memory, and the loop that stitches model calls into useful work.</text>
|
||||
</svg>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
124
skills/assets/example-layers-full.html
Normal file
124
skills/assets/example-layers-full.html
Normal file
@@ -0,0 +1,124 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>AI app stack · Layer hierarchy</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
:root { --color-paper:#f5f4ed; --color-paper-2:#efeee5; --color-ink:#0b0d0b; --color-muted:#52534e; --color-soft:#65655c; --color-rule:rgba(11,13,11,0.12); --color-accent:#f7591f; --color-link:#1a70c7; --font-sans:'Geist',system-ui,sans-serif; --font-serif:'Instrument Serif',serif; --font-mono:'Geist Mono',ui-monospace,monospace; }
|
||||
body { font-family: var(--font-sans); background: var(--color-paper); min-height: 100vh; padding: 3rem 2rem; color: var(--color-ink); }
|
||||
.container { max-width: 1200px; margin: 0 auto; }
|
||||
.header { margin-bottom: 2.5rem; }
|
||||
.header-eyebrow { font-family: var(--font-mono); font-size: 0.66rem; font-weight: 500; letter-spacing: 0.18em; text-transform: uppercase; color: var(--color-muted); margin-bottom: 0.75rem; }
|
||||
h1 { font-family: var(--font-serif); font-size: clamp(1.75rem, 3vw + 1rem, 2.5rem); font-weight: 400; letter-spacing: -0.02em; line-height: 1.1; margin-bottom: 0.5rem; }
|
||||
.subtitle { font-size: 1rem; line-height: 1.55; color: var(--color-muted); max-width: 58ch; }
|
||||
.diagram-container { background: var(--color-paper-2); border-radius: 8px; border: 1px solid var(--color-rule); padding: 1.5rem; overflow-x: auto; }
|
||||
svg { width: 100%; min-width: 900px; display: block; }
|
||||
.cards { display: grid; grid-template-columns: 1.1fr 1fr 0.9fr; gap: 1rem; margin-top: 1.5rem; }
|
||||
@media (max-width: 820px) { .cards { grid-template-columns: 1fr; } }
|
||||
.card { background: #fff; border-radius: 6px; border: 1px solid var(--color-rule); padding: 1.25rem; }
|
||||
.card .eyebrow { font-family: var(--font-mono); font-size: 0.5rem; letter-spacing: 0.18em; text-transform: uppercase; color: var(--color-muted); margin-bottom: 0.5rem; }
|
||||
.card-header { display: flex; align-items: center; gap: 0.6rem; margin-bottom: 0.875rem; padding-bottom: 0.875rem; border-bottom: 1px solid rgba(11,13,11,0.08); }
|
||||
.card-dot { width: 7px; height: 7px; border-radius: 50%; }
|
||||
.card-dot.ink { background: var(--color-ink); } .card-dot.muted { background: var(--color-muted); } .card-dot.coral { background: var(--color-accent); }
|
||||
.card h3 { font-size: 0.875rem; font-weight: 600; }
|
||||
.card p, .card ul { color: var(--color-muted); font-size: 0.8125rem; line-height: 1.55; list-style: none; }
|
||||
.card li { margin-bottom: 0.3rem; padding-left: 0.875rem; position: relative; }
|
||||
.card li::before { content: '—'; position: absolute; left: 0; color: rgba(11,13,11,0.25); font-size: 0.75rem; }
|
||||
.footer { margin-top: 2rem; padding-top: 1.5rem; border-top: 1px solid rgba(11,13,11,0.10); font-family: var(--font-mono); font-size: 0.72rem; letter-spacing: 0.06em; color: var(--color-soft); display: flex; justify-content: space-between; flex-wrap: wrap; gap: 0.5rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<p class="header-eyebrow">Layer stack · Diagram Design</p>
|
||||
<h1>AI app stack · Where the work actually happens</h1>
|
||||
<p class="subtitle">Five layers between silicon and the user. Most teams over-invest in the model and under-invest in the harness — which is the layer that decides whether the app feels magical or flaky.</p>
|
||||
</div>
|
||||
|
||||
<div class="diagram-container">
|
||||
<svg viewBox="0 0 1000 500" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<pattern id="dots" width="22" height="22" patternUnits="userSpaceOnUse">
|
||||
<circle cx="1" cy="1" r="0.9" fill="rgba(11,13,11,0.10)"/>
|
||||
</pattern>
|
||||
</defs>
|
||||
|
||||
<rect width="100%" height="100%" fill="#f5f4ed"/>
|
||||
<rect width="100%" height="100%" fill="url(#dots)" opacity="0.55"/>
|
||||
|
||||
<!-- Direction column (left margin) -->
|
||||
<text x="60" y="68" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">ABSTRACTION</text>
|
||||
<line x1="80" y1="80" x2="80" y2="400" stroke="rgba(11,13,11,0.30)" stroke-width="1"/>
|
||||
<polygon points="76,80 84,80 80,72" fill="#52534e"/>
|
||||
<text x="60" y="416" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">SILICON</text>
|
||||
|
||||
<!-- Stack container hairlines (top + bottom edges) -->
|
||||
<line x1="120" y1="80" x2="960" y2="80" stroke="rgba(11,13,11,0.12)" stroke-width="1"/>
|
||||
<line x1="120" y1="400" x2="960" y2="400" stroke="rgba(11,13,11,0.12)" stroke-width="1"/>
|
||||
|
||||
<!-- L5 — UI (top layer, near-white fill) -->
|
||||
<rect x="120" y="80" width="840" height="64" fill="#ffffff"/>
|
||||
<line x1="120" y1="144" x2="960" y2="144" stroke="rgba(11,13,11,0.12)" stroke-width="1"/>
|
||||
<text x="140" y="116" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.14em">L5</text>
|
||||
<text x="260" y="118" fill="#0b0d0b" font-size="16" font-weight="600" font-family="'Geist', sans-serif">UI surface</text>
|
||||
<text x="940" y="118" fill="#52534e" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end" letter-spacing="0.08em">chat, editor, canvas</text>
|
||||
|
||||
<!-- L4 — Agent harness (FOCAL, coral tint + coral stroke) -->
|
||||
<rect x="120" y="144" width="840" height="64" fill="rgba(247,89,31,0.08)"/>
|
||||
<rect x="120" y="144" width="840" height="64" fill="none" stroke="#f7591f" stroke-width="1"/>
|
||||
<text x="140" y="180" fill="#f7591f" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.14em" font-weight="600">L4</text>
|
||||
<text x="260" y="182" fill="#0b0d0b" font-size="16" font-weight="600" font-family="'Geist', sans-serif">Agent harness</text>
|
||||
<text x="940" y="182" fill="#f7591f" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end" letter-spacing="0.08em">tools, memory, loop</text>
|
||||
|
||||
<!-- L3 — Prompt layer -->
|
||||
<rect x="120" y="208" width="840" height="64" fill="#f5f4ed"/>
|
||||
<line x1="120" y1="272" x2="960" y2="272" stroke="rgba(11,13,11,0.12)" stroke-width="1"/>
|
||||
<text x="140" y="244" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.14em">L3</text>
|
||||
<text x="260" y="246" fill="#0b0d0b" font-size="16" font-weight="600" font-family="'Geist', sans-serif">Prompt layer</text>
|
||||
<text x="940" y="246" fill="#52534e" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end" letter-spacing="0.08em">system, few-shot, caching</text>
|
||||
|
||||
<!-- L2 — SDK -->
|
||||
<rect x="120" y="272" width="840" height="64" fill="#efeee5"/>
|
||||
<line x1="120" y1="336" x2="960" y2="336" stroke="rgba(11,13,11,0.12)" stroke-width="1"/>
|
||||
<text x="140" y="308" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.14em">L2</text>
|
||||
<text x="260" y="310" fill="#0b0d0b" font-size="16" font-weight="600" font-family="'Geist', sans-serif">SDK / client</text>
|
||||
<text x="940" y="310" fill="#52534e" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end" letter-spacing="0.08em">auth, retries, streaming</text>
|
||||
|
||||
<!-- L1 — Model -->
|
||||
<rect x="120" y="336" width="840" height="64" fill="#efeee5"/>
|
||||
<text x="140" y="372" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.14em">L1</text>
|
||||
<text x="260" y="374" fill="#0b0d0b" font-size="16" font-weight="600" font-family="'Geist', sans-serif">Model weights</text>
|
||||
<text x="940" y="374" fill="#52534e" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end" letter-spacing="0.08em">opus, sonnet, haiku</text>
|
||||
|
||||
<!-- Caption -->
|
||||
<text x="120" y="456" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">FOCAL LAYER</text>
|
||||
<text x="240" y="456" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif" font-style="italic">The harness is where most product differentiation actually lives — tools, memory, and the loop that stitches model calls into useful work.</text>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div class="cards">
|
||||
<div class="card">
|
||||
<p class="eyebrow">THE HEADLINE</p>
|
||||
<div class="card-header"><span class="card-dot coral"></span><h3>Coral marks the layer that pays rent</h3></div>
|
||||
<p>Swapping models is a knob. Rewriting the harness is a product decision. The coral band says: this is the layer where you out-execute the competition, not the one where you chase leaderboards.</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-header"><span class="card-dot ink"></span><h3>Reading the stack</h3></div>
|
||||
<ul><li>Abstraction rises up the left column</li><li>L-index on the left, note on the right</li><li>Hairlines between layers, no shadows</li><li>Fill shade shifts from paper-2 to white</li></ul>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-header"><span class="card-dot muted"></span><h3>Why only five</h3></div>
|
||||
<p>Six-plus layers become a legend, not a diagram. Five holds the whole thing on one screen — every band readable without squinting, every note a scan away.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<span>ai app stack · layer hierarchy</span>
|
||||
<span>example · diagram-design</span>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
121
skills/assets/example-layers.html
Normal file
121
skills/assets/example-layers.html
Normal file
@@ -0,0 +1,121 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>AI app stack · Layer hierarchy</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
:root {
|
||||
--color-paper: #f5f4ed;
|
||||
--color-ink: #0b0d0b;
|
||||
--color-muted: #52534e;
|
||||
--color-accent: #f7591f;
|
||||
--font-sans: 'Geist', system-ui, sans-serif;
|
||||
--font-serif: 'Instrument Serif', serif;
|
||||
--font-mono: 'Geist Mono', ui-monospace, monospace;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-sans);
|
||||
background: var(--color-paper);
|
||||
color: var(--color-ink);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 3rem 2rem;
|
||||
}
|
||||
|
||||
.frame { max-width: 1200px; width: 100%; }
|
||||
|
||||
.eyebrow {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.66rem;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.18em;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-muted);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-family: var(--font-serif);
|
||||
font-size: clamp(1.5rem, 2.4vw + 0.75rem, 2rem);
|
||||
font-weight: 400;
|
||||
letter-spacing: -0.02em;
|
||||
line-height: 1.15;
|
||||
color: var(--color-ink);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
svg { width: 100%; min-width: 900px; display: block; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="frame">
|
||||
<p class="eyebrow">Layer stack · Diagram Design</p>
|
||||
<h1>AI app stack · Where the work actually happens</h1>
|
||||
|
||||
<svg viewBox="0 0 1000 500" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<pattern id="dots" width="22" height="22" patternUnits="userSpaceOnUse">
|
||||
<circle cx="1" cy="1" r="0.9" fill="rgba(11,13,11,0.10)"/>
|
||||
</pattern>
|
||||
</defs>
|
||||
|
||||
<rect width="100%" height="100%" fill="#f5f4ed"/>
|
||||
<rect width="100%" height="100%" fill="url(#dots)" opacity="0.55"/>
|
||||
|
||||
<!-- Direction column (left margin) -->
|
||||
<text x="60" y="68" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">ABSTRACTION</text>
|
||||
<line x1="80" y1="80" x2="80" y2="400" stroke="rgba(11,13,11,0.30)" stroke-width="1"/>
|
||||
<polygon points="76,80 84,80 80,72" fill="#52534e"/>
|
||||
<text x="60" y="416" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">SILICON</text>
|
||||
|
||||
<!-- Stack container hairlines (top + bottom edges) -->
|
||||
<line x1="120" y1="80" x2="960" y2="80" stroke="rgba(11,13,11,0.12)" stroke-width="1"/>
|
||||
<line x1="120" y1="400" x2="960" y2="400" stroke="rgba(11,13,11,0.12)" stroke-width="1"/>
|
||||
|
||||
<!-- L5 — UI (top layer, near-white fill) -->
|
||||
<rect x="120" y="80" width="840" height="64" fill="#ffffff"/>
|
||||
<line x1="120" y1="144" x2="960" y2="144" stroke="rgba(11,13,11,0.12)" stroke-width="1"/>
|
||||
<text x="140" y="116" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.14em">L5</text>
|
||||
<text x="260" y="118" fill="#0b0d0b" font-size="16" font-weight="600" font-family="'Geist', sans-serif">UI surface</text>
|
||||
<text x="940" y="118" fill="#52534e" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end" letter-spacing="0.08em">chat, editor, canvas</text>
|
||||
|
||||
<!-- L4 — Agent harness (FOCAL, coral tint + coral stroke) -->
|
||||
<rect x="120" y="144" width="840" height="64" fill="rgba(247,89,31,0.08)"/>
|
||||
<rect x="120" y="144" width="840" height="64" fill="none" stroke="#f7591f" stroke-width="1"/>
|
||||
<text x="140" y="180" fill="#f7591f" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.14em" font-weight="600">L4</text>
|
||||
<text x="260" y="182" fill="#0b0d0b" font-size="16" font-weight="600" font-family="'Geist', sans-serif">Agent harness</text>
|
||||
<text x="940" y="182" fill="#f7591f" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end" letter-spacing="0.08em">tools, memory, loop</text>
|
||||
|
||||
<!-- L3 — Prompt layer -->
|
||||
<rect x="120" y="208" width="840" height="64" fill="#f5f4ed"/>
|
||||
<line x1="120" y1="272" x2="960" y2="272" stroke="rgba(11,13,11,0.12)" stroke-width="1"/>
|
||||
<text x="140" y="244" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.14em">L3</text>
|
||||
<text x="260" y="246" fill="#0b0d0b" font-size="16" font-weight="600" font-family="'Geist', sans-serif">Prompt layer</text>
|
||||
<text x="940" y="246" fill="#52534e" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end" letter-spacing="0.08em">system, few-shot, caching</text>
|
||||
|
||||
<!-- L2 — SDK -->
|
||||
<rect x="120" y="272" width="840" height="64" fill="#efeee5"/>
|
||||
<line x1="120" y1="336" x2="960" y2="336" stroke="rgba(11,13,11,0.12)" stroke-width="1"/>
|
||||
<text x="140" y="308" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.14em">L2</text>
|
||||
<text x="260" y="310" fill="#0b0d0b" font-size="16" font-weight="600" font-family="'Geist', sans-serif">SDK / client</text>
|
||||
<text x="940" y="310" fill="#52534e" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end" letter-spacing="0.08em">auth, retries, streaming</text>
|
||||
|
||||
<!-- L1 — Model -->
|
||||
<rect x="120" y="336" width="840" height="64" fill="#efeee5"/>
|
||||
<text x="140" y="372" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.14em">L1</text>
|
||||
<text x="260" y="374" fill="#0b0d0b" font-size="16" font-weight="600" font-family="'Geist', sans-serif">Model weights</text>
|
||||
<text x="940" y="374" fill="#52534e" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end" letter-spacing="0.08em">opus, sonnet, haiku</text>
|
||||
|
||||
<!-- Caption -->
|
||||
<text x="120" y="456" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">FOCAL LAYER</text>
|
||||
<text x="240" y="456" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif" font-style="italic">The harness is where most product differentiation actually lives — tools, memory, and the loop that stitches model calls into useful work.</text>
|
||||
</svg>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
130
skills/assets/example-nested-dark.html
Normal file
130
skills/assets/example-nested-dark.html
Normal file
@@ -0,0 +1,130 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>The CLAUDE.md Hierarchy</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
:root {
|
||||
--color-paper: #1c1a17;
|
||||
--color-ink: #f1efe7;
|
||||
--color-muted: #a8a69d;
|
||||
--color-accent: #ff6a30;
|
||||
--font-sans: 'Geist', system-ui, sans-serif;
|
||||
--font-serif: 'Instrument Serif', serif;
|
||||
--font-mono: 'Geist Mono', ui-monospace, monospace;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-sans);
|
||||
background: var(--color-paper);
|
||||
color: var(--color-ink);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 3rem 2rem;
|
||||
}
|
||||
|
||||
.frame { max-width: 1200px; width: 100%; }
|
||||
|
||||
.eyebrow {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.66rem;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.18em;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-muted);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-family: var(--font-serif);
|
||||
font-size: clamp(1.5rem, 2.4vw + 0.75rem, 2rem);
|
||||
font-weight: 400;
|
||||
letter-spacing: -0.02em;
|
||||
line-height: 1.15;
|
||||
color: var(--color-ink);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
svg { width: 100%; min-width: 900px; display: block; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="frame">
|
||||
<p class="eyebrow">Nested · Diagram Design</p>
|
||||
<h1>The CLAUDE.md Hierarchy</h1>
|
||||
|
||||
<svg viewBox="0 0 1000 500" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<pattern id="dots" width="22" height="22" patternUnits="userSpaceOnUse">
|
||||
<circle cx="1" cy="1" r="0.9" fill="rgba(241,239,231,0.10)"/>
|
||||
</pattern>
|
||||
</defs>
|
||||
|
||||
<rect width="100%" height="100%" fill="#1c1a17"/>
|
||||
<rect width="100%" height="100%" fill="url(#dots)" opacity="0.55"/>
|
||||
|
||||
<!-- Level 1: ~/.claude/ (global) — outermost -->
|
||||
<rect x="40" y="60" width="920" height="380" rx="8" fill="rgba(241,239,231,0.015)" stroke="rgba(241,239,231,0.30)" stroke-width="1"/>
|
||||
<!-- Level 2: ~/vault/ -->
|
||||
<rect x="72" y="96" width="856" height="308" rx="8" fill="rgba(241,239,231,0.02)" stroke="rgba(241,239,231,0.35)" stroke-width="1"/>
|
||||
<!-- Level 3: /business -->
|
||||
<rect x="104" y="132" width="792" height="236" rx="8" fill="rgba(241,239,231,0.025)" stroke="rgba(241,239,231,0.45)" stroke-width="1"/>
|
||||
<!-- Level 4: /marketing -->
|
||||
<rect x="136" y="168" width="728" height="164" rx="8" fill="rgba(241,239,231,0.03)" stroke="#a8a69d" stroke-width="1"/>
|
||||
<!-- Level 5: /project — innermost, coral focal -->
|
||||
<rect x="168" y="204" width="664" height="92" rx="8" fill="rgba(255,106,48,0.08)" stroke="#ff6a30" stroke-width="1"/>
|
||||
|
||||
<!-- Level labels on paper-colored masks -->
|
||||
<rect x="56" y="52" width="188" height="16" fill="#1c1a17"/>
|
||||
<text x="64" y="64" fill="#a8a69d" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.14em">~/.claude/ (global)</text>
|
||||
|
||||
<rect x="88" y="88" width="148" height="16" fill="#1c1a17"/>
|
||||
<text x="96" y="100" fill="#a8a69d" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.14em">~/vault/ (notes)</text>
|
||||
|
||||
<rect x="120" y="124" width="96" height="16" fill="#1c1a17"/>
|
||||
<text x="128" y="136" fill="#a8a69d" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.14em">/business</text>
|
||||
|
||||
<rect x="152" y="160" width="108" height="16" fill="#1c1a17"/>
|
||||
<text x="160" y="172" fill="#f1efe7" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.14em">/marketing</text>
|
||||
|
||||
<rect x="184" y="196" width="88" height="16" fill="#1c1a17"/>
|
||||
<text x="192" y="208" fill="#ff6a30" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.14em" font-weight="600">/project</text>
|
||||
|
||||
<!-- File-icon glyphs (simple rect with folded corner) inside each level -->
|
||||
<g transform="translate(908, 408)">
|
||||
<path d="M0 0 L16 0 L20 4 L20 20 L0 20 Z" fill="#1c1a17" stroke="rgba(241,239,231,0.35)" stroke-width="1"/>
|
||||
<path d="M16 0 L16 4 L20 4" fill="none" stroke="rgba(241,239,231,0.35)" stroke-width="1"/>
|
||||
</g>
|
||||
<g transform="translate(876, 372)">
|
||||
<path d="M0 0 L16 0 L20 4 L20 20 L0 20 Z" fill="#1c1a17" stroke="rgba(241,239,231,0.40)" stroke-width="1"/>
|
||||
<path d="M16 0 L16 4 L20 4" fill="none" stroke="rgba(241,239,231,0.40)" stroke-width="1"/>
|
||||
</g>
|
||||
<g transform="translate(844, 336)">
|
||||
<path d="M0 0 L16 0 L20 4 L20 20 L0 20 Z" fill="#1c1a17" stroke="rgba(241,239,231,0.50)" stroke-width="1"/>
|
||||
<path d="M16 0 L16 4 L20 4" fill="none" stroke="rgba(241,239,231,0.50)" stroke-width="1"/>
|
||||
</g>
|
||||
<g transform="translate(812, 300)">
|
||||
<path d="M0 0 L16 0 L20 4 L20 20 L0 20 Z" fill="#1c1a17" stroke="#a8a69d" stroke-width="1"/>
|
||||
<path d="M16 0 L16 4 L20 4" fill="none" stroke="#a8a69d" stroke-width="1"/>
|
||||
</g>
|
||||
|
||||
<!-- Innermost label — human readable -->
|
||||
<text x="500" y="248" fill="#f1efe7" font-size="16" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">CLAUDE.md</text>
|
||||
<text x="500" y="272" fill="#a8a69d" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">inherits every level above</text>
|
||||
|
||||
<!-- Annotation top-right: italic callout with curved arrow -->
|
||||
<text x="904" y="36" fill="#f1efe7" font-size="14" font-style="italic" font-family="'Instrument Serif', serif" text-anchor="end">no imports, no configuration</text>
|
||||
<path d="M 820 44 Q 700 84 520 216" fill="none" stroke="rgba(241,239,231,0.40)" stroke-width="1" stroke-dasharray="4,3"/>
|
||||
<circle cx="520" cy="216" r="2" fill="#f1efe7"/>
|
||||
|
||||
<!-- Annotation bottom-left: italic callout -->
|
||||
<text x="40" y="484" fill="#a8a69d" font-size="14" font-style="italic" font-family="'Instrument Serif', serif">structure IS the index</text>
|
||||
</svg>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
133
skills/assets/example-nested-full.html
Normal file
133
skills/assets/example-nested-full.html
Normal file
@@ -0,0 +1,133 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>The CLAUDE.md Hierarchy</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
:root { --color-paper:#f5f4ed; --color-paper-2:#efeee5; --color-ink:#0b0d0b; --color-muted:#52534e; --color-soft:#65655c; --color-rule:rgba(11,13,11,0.12); --color-accent:#f7591f; --color-link:#1a70c7; --font-sans:'Geist',system-ui,sans-serif; --font-serif:'Instrument Serif',serif; --font-mono:'Geist Mono',ui-monospace,monospace; }
|
||||
body { font-family: var(--font-sans); background: var(--color-paper); min-height: 100vh; padding: 3rem 2rem; color: var(--color-ink); }
|
||||
.container { max-width: 1200px; margin: 0 auto; }
|
||||
.header { margin-bottom: 2.5rem; }
|
||||
.header-eyebrow { font-family: var(--font-mono); font-size: 0.66rem; font-weight: 500; letter-spacing: 0.18em; text-transform: uppercase; color: var(--color-muted); margin-bottom: 0.75rem; }
|
||||
h1 { font-family: var(--font-serif); font-size: clamp(1.75rem, 3vw + 1rem, 2.5rem); font-weight: 400; letter-spacing: -0.02em; line-height: 1.1; margin-bottom: 0.5rem; }
|
||||
.subtitle { font-size: 1rem; line-height: 1.55; color: var(--color-muted); max-width: 58ch; }
|
||||
.diagram-container { background: var(--color-paper-2); border-radius: 8px; border: 1px solid var(--color-rule); padding: 1.5rem; overflow-x: auto; }
|
||||
svg { width: 100%; min-width: 900px; display: block; }
|
||||
.cards { display: grid; grid-template-columns: 1.1fr 1fr 0.9fr; gap: 1rem; margin-top: 1.5rem; }
|
||||
@media (max-width: 820px) { .cards { grid-template-columns: 1fr; } }
|
||||
.card { background: #fff; border-radius: 6px; border: 1px solid var(--color-rule); padding: 1.25rem; }
|
||||
.card .eyebrow { font-family: var(--font-mono); font-size: 0.5rem; letter-spacing: 0.18em; text-transform: uppercase; color: var(--color-muted); margin-bottom: 0.5rem; }
|
||||
.card-header { display: flex; align-items: center; gap: 0.6rem; margin-bottom: 0.875rem; padding-bottom: 0.875rem; border-bottom: 1px solid rgba(11,13,11,0.08); }
|
||||
.card-dot { width: 7px; height: 7px; border-radius: 50%; }
|
||||
.card-dot.ink { background: var(--color-ink); } .card-dot.muted { background: var(--color-muted); } .card-dot.coral { background: var(--color-accent); }
|
||||
.card h3 { font-size: 0.875rem; font-weight: 600; }
|
||||
.card p, .card ul { color: var(--color-muted); font-size: 0.8125rem; line-height: 1.55; list-style: none; }
|
||||
.card li { margin-bottom: 0.3rem; padding-left: 0.875rem; position: relative; }
|
||||
.card li::before { content: '—'; position: absolute; left: 0; color: rgba(11,13,11,0.25); font-size: 0.75rem; }
|
||||
.footer { margin-top: 2rem; padding-top: 1.5rem; border-top: 1px solid rgba(11,13,11,0.10); font-family: var(--font-mono); font-size: 0.72rem; letter-spacing: 0.06em; color: var(--color-soft); display: flex; justify-content: space-between; flex-wrap: wrap; gap: 0.5rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<p class="header-eyebrow">Nested · Diagram Design</p>
|
||||
<h1>The CLAUDE.md Hierarchy</h1>
|
||||
<p class="subtitle">How Claude Code composes context from every folder level above. Each outer ring is a broader scope; the innermost box is where the work happens — inheriting every instruction above it without a single import statement.</p>
|
||||
</div>
|
||||
|
||||
<div class="diagram-container">
|
||||
<svg viewBox="0 0 1000 500" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<pattern id="dots" width="22" height="22" patternUnits="userSpaceOnUse">
|
||||
<circle cx="1" cy="1" r="0.9" fill="rgba(11,13,11,0.10)"/>
|
||||
</pattern>
|
||||
</defs>
|
||||
|
||||
<rect width="100%" height="100%" fill="#f5f4ed"/>
|
||||
<rect width="100%" height="100%" fill="url(#dots)" opacity="0.55"/>
|
||||
|
||||
<!-- Level 1: ~/.claude/ (global) — outermost -->
|
||||
<rect x="40" y="60" width="920" height="380" rx="8" fill="rgba(11,13,11,0.015)" stroke="rgba(11,13,11,0.30)" stroke-width="1"/>
|
||||
<!-- Level 2: ~/vault/ -->
|
||||
<rect x="72" y="96" width="856" height="308" rx="8" fill="rgba(11,13,11,0.02)" stroke="rgba(11,13,11,0.35)" stroke-width="1"/>
|
||||
<!-- Level 3: /business -->
|
||||
<rect x="104" y="132" width="792" height="236" rx="8" fill="rgba(11,13,11,0.025)" stroke="rgba(11,13,11,0.45)" stroke-width="1"/>
|
||||
<!-- Level 4: /marketing -->
|
||||
<rect x="136" y="168" width="728" height="164" rx="8" fill="rgba(11,13,11,0.03)" stroke="#52534e" stroke-width="1"/>
|
||||
<!-- Level 5: /project — innermost, coral focal -->
|
||||
<rect x="168" y="204" width="664" height="92" rx="8" fill="rgba(247,89,31,0.06)" stroke="#f7591f" stroke-width="1"/>
|
||||
|
||||
<!-- Level labels on paper-2-colored masks (match diagram-container bg) -->
|
||||
<rect x="56" y="52" width="188" height="16" fill="#efeee5"/>
|
||||
<text x="64" y="64" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.14em">~/.claude/ (global)</text>
|
||||
|
||||
<rect x="88" y="88" width="148" height="16" fill="#efeee5"/>
|
||||
<text x="96" y="100" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.14em">~/vault/ (notes)</text>
|
||||
|
||||
<rect x="120" y="124" width="96" height="16" fill="#efeee5"/>
|
||||
<text x="128" y="136" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.14em">/business</text>
|
||||
|
||||
<rect x="152" y="160" width="108" height="16" fill="#efeee5"/>
|
||||
<text x="160" y="172" fill="#0b0d0b" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.14em">/marketing</text>
|
||||
|
||||
<rect x="184" y="196" width="88" height="16" fill="#efeee5"/>
|
||||
<text x="192" y="208" fill="#f7591f" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.14em" font-weight="600">/project</text>
|
||||
|
||||
<!-- File-icon glyphs inside each level -->
|
||||
<g transform="translate(908, 408)">
|
||||
<path d="M0 0 L16 0 L20 4 L20 20 L0 20 Z" fill="#efeee5" stroke="rgba(11,13,11,0.35)" stroke-width="1"/>
|
||||
<path d="M16 0 L16 4 L20 4" fill="none" stroke="rgba(11,13,11,0.35)" stroke-width="1"/>
|
||||
</g>
|
||||
<g transform="translate(876, 372)">
|
||||
<path d="M0 0 L16 0 L20 4 L20 20 L0 20 Z" fill="#efeee5" stroke="rgba(11,13,11,0.40)" stroke-width="1"/>
|
||||
<path d="M16 0 L16 4 L20 4" fill="none" stroke="rgba(11,13,11,0.40)" stroke-width="1"/>
|
||||
</g>
|
||||
<g transform="translate(844, 336)">
|
||||
<path d="M0 0 L16 0 L20 4 L20 20 L0 20 Z" fill="#efeee5" stroke="rgba(11,13,11,0.50)" stroke-width="1"/>
|
||||
<path d="M16 0 L16 4 L20 4" fill="none" stroke="rgba(11,13,11,0.50)" stroke-width="1"/>
|
||||
</g>
|
||||
<g transform="translate(812, 300)">
|
||||
<path d="M0 0 L16 0 L20 4 L20 20 L0 20 Z" fill="#efeee5" stroke="#52534e" stroke-width="1"/>
|
||||
<path d="M16 0 L16 4 L20 4" fill="none" stroke="#52534e" stroke-width="1"/>
|
||||
</g>
|
||||
|
||||
<!-- Innermost label — human readable -->
|
||||
<text x="500" y="248" fill="#0b0d0b" font-size="16" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">CLAUDE.md</text>
|
||||
<text x="500" y="272" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">inherits every level above</text>
|
||||
|
||||
<!-- Annotation top-right: italic callout with curved arrow -->
|
||||
<text x="904" y="36" fill="#0b0d0b" font-size="14" font-style="italic" font-family="'Instrument Serif', serif" text-anchor="end">no imports, no configuration</text>
|
||||
<path d="M 820 44 Q 700 84 520 216" fill="none" stroke="rgba(11,13,11,0.40)" stroke-width="1" stroke-dasharray="4,3"/>
|
||||
<circle cx="520" cy="216" r="2" fill="#0b0d0b"/>
|
||||
|
||||
<!-- Annotation bottom-left: italic callout -->
|
||||
<text x="40" y="484" fill="#52534e" font-size="14" font-style="italic" font-family="'Instrument Serif', serif">structure IS the index</text>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div class="cards">
|
||||
<div class="card">
|
||||
<p class="eyebrow">THE HEADLINE</p>
|
||||
<div class="card-header"><span class="card-dot coral"></span><h3>Containment is the config</h3></div>
|
||||
<p>Every CLAUDE.md inside a parent folder implicitly wraps the one below. No manifest, no import graph — the file tree itself declares scope, so renaming a folder is the only migration you'll ever run.</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-header"><span class="card-dot ink"></span><h3>Reads outside-in</h3></div>
|
||||
<ul><li>Global rules load first</li><li>Each parent narrows context</li><li>Innermost file gets the last word</li><li>Conflicts resolve by specificity</li></ul>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-header"><span class="card-dot muted"></span><h3>Five levels is the ceiling</h3></div>
|
||||
<p>Past five rings the diagram stops teaching — and so does the filesystem. If you need six levels of instruction, you probably need two projects instead.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<span>the claude.md hierarchy · nested containment</span>
|
||||
<span>example · diagram-design</span>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
134
skills/assets/example-nested.html
Normal file
134
skills/assets/example-nested.html
Normal file
@@ -0,0 +1,134 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>The CLAUDE.md Hierarchy</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
:root {
|
||||
--color-paper: #f5f4ed;
|
||||
--color-ink: #0b0d0b;
|
||||
--color-muted: #52534e;
|
||||
--color-accent: #f7591f;
|
||||
--font-sans: 'Geist', system-ui, sans-serif;
|
||||
--font-serif: 'Instrument Serif', serif;
|
||||
--font-mono: 'Geist Mono', ui-monospace, monospace;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-sans);
|
||||
background: var(--color-paper);
|
||||
color: var(--color-ink);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 3rem 2rem;
|
||||
}
|
||||
|
||||
.frame { max-width: 1200px; width: 100%; }
|
||||
|
||||
.eyebrow {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.66rem;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.18em;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-muted);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-family: var(--font-serif);
|
||||
font-size: clamp(1.5rem, 2.4vw + 0.75rem, 2rem);
|
||||
font-weight: 400;
|
||||
letter-spacing: -0.02em;
|
||||
line-height: 1.15;
|
||||
color: var(--color-ink);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
svg { width: 100%; min-width: 900px; display: block; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="frame">
|
||||
<p class="eyebrow">Nested · Diagram Design</p>
|
||||
<h1>The CLAUDE.md Hierarchy</h1>
|
||||
|
||||
<svg viewBox="0 0 1000 500" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<pattern id="dots" width="22" height="22" patternUnits="userSpaceOnUse">
|
||||
<circle cx="1" cy="1" r="0.9" fill="rgba(11,13,11,0.10)"/>
|
||||
</pattern>
|
||||
</defs>
|
||||
|
||||
<rect width="100%" height="100%" fill="#f5f4ed"/>
|
||||
<rect width="100%" height="100%" fill="url(#dots)" opacity="0.55"/>
|
||||
|
||||
<!-- Level 1: ~/.claude/ (global) — outermost -->
|
||||
<rect x="40" y="60" width="920" height="380" rx="8" fill="rgba(11,13,11,0.015)" stroke="rgba(11,13,11,0.30)" stroke-width="1"/>
|
||||
<!-- Level 2: ~/vault/ -->
|
||||
<rect x="72" y="96" width="856" height="308" rx="8" fill="rgba(11,13,11,0.02)" stroke="rgba(11,13,11,0.35)" stroke-width="1"/>
|
||||
<!-- Level 3: /business -->
|
||||
<rect x="104" y="132" width="792" height="236" rx="8" fill="rgba(11,13,11,0.025)" stroke="rgba(11,13,11,0.45)" stroke-width="1"/>
|
||||
<!-- Level 4: /marketing -->
|
||||
<rect x="136" y="168" width="728" height="164" rx="8" fill="rgba(11,13,11,0.03)" stroke="#52534e" stroke-width="1"/>
|
||||
<!-- Level 5: /project — innermost, coral focal -->
|
||||
<rect x="168" y="204" width="664" height="92" rx="8" fill="rgba(247,89,31,0.06)" stroke="#f7591f" stroke-width="1"/>
|
||||
|
||||
<!-- Level labels on paper-colored masks -->
|
||||
<rect x="56" y="52" width="188" height="16" fill="#f5f4ed"/>
|
||||
<text x="64" y="64" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.14em">~/.claude/ (global)</text>
|
||||
|
||||
<rect x="88" y="88" width="148" height="16" fill="#f5f4ed"/>
|
||||
<text x="96" y="100" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.14em">~/vault/ (notes)</text>
|
||||
|
||||
<rect x="120" y="124" width="96" height="16" fill="#f5f4ed"/>
|
||||
<text x="128" y="136" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.14em">/business</text>
|
||||
|
||||
<rect x="152" y="160" width="108" height="16" fill="#f5f4ed"/>
|
||||
<text x="160" y="172" fill="#0b0d0b" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.14em">/marketing</text>
|
||||
|
||||
<rect x="184" y="196" width="88" height="16" fill="#f5f4ed"/>
|
||||
<text x="192" y="208" fill="#f7591f" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.14em" font-weight="600">/project</text>
|
||||
|
||||
<!-- File-icon glyphs (simple rect with folded corner) inside each level -->
|
||||
<!-- Glyph in level 1 (bottom-right area of outer ring) -->
|
||||
<g transform="translate(908, 408)">
|
||||
<path d="M0 0 L16 0 L20 4 L20 20 L0 20 Z" fill="#f5f4ed" stroke="rgba(11,13,11,0.35)" stroke-width="1"/>
|
||||
<path d="M16 0 L16 4 L20 4" fill="none" stroke="rgba(11,13,11,0.35)" stroke-width="1"/>
|
||||
</g>
|
||||
<!-- Glyph in level 2 ring -->
|
||||
<g transform="translate(876, 372)">
|
||||
<path d="M0 0 L16 0 L20 4 L20 20 L0 20 Z" fill="#f5f4ed" stroke="rgba(11,13,11,0.40)" stroke-width="1"/>
|
||||
<path d="M16 0 L16 4 L20 4" fill="none" stroke="rgba(11,13,11,0.40)" stroke-width="1"/>
|
||||
</g>
|
||||
<!-- Glyph in level 3 ring -->
|
||||
<g transform="translate(844, 336)">
|
||||
<path d="M0 0 L16 0 L20 4 L20 20 L0 20 Z" fill="#f5f4ed" stroke="rgba(11,13,11,0.50)" stroke-width="1"/>
|
||||
<path d="M16 0 L16 4 L20 4" fill="none" stroke="rgba(11,13,11,0.50)" stroke-width="1"/>
|
||||
</g>
|
||||
<!-- Glyph in level 4 ring -->
|
||||
<g transform="translate(812, 300)">
|
||||
<path d="M0 0 L16 0 L20 4 L20 20 L0 20 Z" fill="#f5f4ed" stroke="#52534e" stroke-width="1"/>
|
||||
<path d="M16 0 L16 4 L20 4" fill="none" stroke="#52534e" stroke-width="1"/>
|
||||
</g>
|
||||
|
||||
<!-- Innermost label — human readable -->
|
||||
<text x="500" y="248" fill="#0b0d0b" font-size="16" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">CLAUDE.md</text>
|
||||
<text x="500" y="272" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">inherits every level above</text>
|
||||
|
||||
<!-- Annotation top-right: italic callout with curved arrow -->
|
||||
<text x="904" y="36" fill="#0b0d0b" font-size="14" font-style="italic" font-family="'Instrument Serif', serif" text-anchor="end">no imports, no configuration</text>
|
||||
<path d="M 820 44 Q 700 84 520 216" fill="none" stroke="rgba(11,13,11,0.40)" stroke-width="1" stroke-dasharray="4,3"/>
|
||||
<circle cx="520" cy="216" r="2" fill="#0b0d0b"/>
|
||||
|
||||
<!-- Annotation bottom-left: italic callout -->
|
||||
<text x="40" y="484" fill="#52534e" font-size="14" font-style="italic" font-family="'Instrument Serif', serif">structure IS the index</text>
|
||||
</svg>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
117
skills/assets/example-pyramid-dark.html
Normal file
117
skills/assets/example-pyramid-dark.html
Normal file
@@ -0,0 +1,117 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Content pyramid · what compounds</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
:root {
|
||||
--color-paper: #1c1a17;
|
||||
--color-ink: #f1efe7;
|
||||
--color-muted: #a8a69d;
|
||||
--color-accent: #ff6a30;
|
||||
--font-sans: 'Geist', system-ui, sans-serif;
|
||||
--font-serif: 'Instrument Serif', serif;
|
||||
--font-mono: 'Geist Mono', ui-monospace, monospace;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-sans);
|
||||
background: var(--color-paper);
|
||||
color: var(--color-ink);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 3rem 2rem;
|
||||
}
|
||||
|
||||
.frame { max-width: 1200px; width: 100%; }
|
||||
|
||||
.eyebrow {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.66rem;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.18em;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-muted);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-family: var(--font-serif);
|
||||
font-size: clamp(1.5rem, 2.4vw + 0.75rem, 2rem);
|
||||
font-weight: 400;
|
||||
letter-spacing: -0.02em;
|
||||
line-height: 1.15;
|
||||
color: var(--color-ink);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
svg { width: 100%; min-width: 900px; display: block; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="frame">
|
||||
<p class="eyebrow">Pyramid · Diagram Design</p>
|
||||
<h1>Content pyramid · what compounds</h1>
|
||||
|
||||
<svg viewBox="0 0 1000 500" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<pattern id="dots" width="22" height="22" patternUnits="userSpaceOnUse">
|
||||
<circle cx="1" cy="1" r="0.9" fill="rgba(241,239,231,0.10)"/>
|
||||
</pattern>
|
||||
</defs>
|
||||
|
||||
<rect width="100%" height="100%" fill="#1c1a17"/>
|
||||
<rect width="100%" height="100%" fill="url(#dots)" opacity="0.55"/>
|
||||
|
||||
<!-- Left axis: direction of rarity -->
|
||||
<line x1="100" y1="112" x2="100" y2="320" stroke="rgba(241,239,231,0.30)" stroke-width="1"/>
|
||||
<polygon points="96,112 104,112 100,100" fill="rgba(241,239,231,0.45)"/>
|
||||
<text x="80" y="216" fill="#a8a69d" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em" text-anchor="middle" transform="rotate(-90 80 216)">RARER · FEWER · COMPOUNDS ↑</text>
|
||||
|
||||
<!-- Base layer: Short posts -->
|
||||
<polygon points="240,280 760,280 820,344 180,344" fill="rgba(241,239,231,0.04)" stroke="rgba(241,239,231,0.12)" stroke-width="1"/>
|
||||
<text x="500" y="308" fill="#f1efe7" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Short posts</text>
|
||||
<text x="500" y="324" fill="#a8a69d" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">daily · ~200 words</text>
|
||||
<text x="836" y="316" fill="#a8a69d" font-size="9" font-family="'Geist Mono', monospace" letter-spacing="0.08em">~240/yr</text>
|
||||
|
||||
<!-- L3: Essays -->
|
||||
<polygon points="300,216 700,216 760,280 240,280" fill="rgba(241,239,231,0.04)" stroke="rgba(241,239,231,0.12)" stroke-width="1"/>
|
||||
<text x="500" y="244" fill="#f1efe7" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Essays</text>
|
||||
<text x="500" y="260" fill="#a8a69d" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">weekly · 800–1,500 words</text>
|
||||
<text x="776" y="252" fill="#a8a69d" font-size="9" font-family="'Geist Mono', monospace" letter-spacing="0.08em">~48/yr</text>
|
||||
|
||||
<!-- L2: Long-form guides -->
|
||||
<polygon points="360,152 640,152 700,216 300,216" fill="rgba(241,239,231,0.04)" stroke="rgba(241,239,231,0.12)" stroke-width="1"/>
|
||||
<text x="500" y="180" fill="#f1efe7" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Long-form guides</text>
|
||||
<text x="500" y="196" fill="#a8a69d" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">quarterly · 4,000+ words</text>
|
||||
<text x="716" y="188" fill="#a8a69d" font-size="9" font-family="'Geist Mono', monospace" letter-spacing="0.08em">~4/yr</text>
|
||||
|
||||
<!-- Apex: Flagship book — CORAL FOCAL (triangle, pointed) -->
|
||||
<polygon points="500,76 640,152 360,152" fill="rgba(255,106,48,0.10)" stroke="#ff6a30" stroke-width="1"/>
|
||||
<text x="500" y="120" fill="#f1efe7" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Flagship book</text>
|
||||
<text x="500" y="136" fill="#a8a69d" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">every 3–5 years</text>
|
||||
<text x="656" y="112" fill="#ff6a30" font-size="9" font-family="'Geist Mono', monospace" letter-spacing="0.08em">the apex</text>
|
||||
|
||||
<!-- Footnote under pyramid -->
|
||||
<text x="500" y="384" fill="#a8a69d" font-size="8.5" font-family="'Geist', sans-serif" text-anchor="middle" font-style="italic">The base funds the apex. The apex defines the base.</text>
|
||||
|
||||
<!-- Legend -->
|
||||
<line x1="40" y1="436" x2="960" y2="436" stroke="rgba(241,239,231,0.10)" stroke-width="0.8"/>
|
||||
<text x="40" y="452" fill="#a8a69d" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">LEGEND</text>
|
||||
|
||||
<rect x="40" y="464" width="16" height="12" fill="rgba(255,106,48,0.10)" stroke="#ff6a30" stroke-width="1"/>
|
||||
<text x="64" y="474" fill="#a8a69d" font-size="8.5" font-family="'Geist', sans-serif">Apex — rarest, highest leverage</text>
|
||||
|
||||
<rect x="280" y="464" width="16" height="12" fill="rgba(241,239,231,0.04)" stroke="rgba(241,239,231,0.25)" stroke-width="1"/>
|
||||
<text x="304" y="474" fill="#a8a69d" font-size="8.5" font-family="'Geist', sans-serif">Supporting layer — the volume work</text>
|
||||
|
||||
<text x="560" y="474" fill="#a8a69d" font-size="8.5" font-family="'Geist', sans-serif" font-style="italic">Layer width is honest: narrower = rarer shipping cadence.</text>
|
||||
</svg>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
120
skills/assets/example-pyramid-full.html
Normal file
120
skills/assets/example-pyramid-full.html
Normal file
@@ -0,0 +1,120 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Content pyramid · what compounds</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
:root { --color-paper:#f5f4ed; --color-paper-2:#efeee5; --color-ink:#0b0d0b; --color-muted:#52534e; --color-soft:#65655c; --color-rule:rgba(11,13,11,0.12); --color-accent:#f7591f; --color-link:#1a70c7; --font-sans:'Geist',system-ui,sans-serif; --font-serif:'Instrument Serif',serif; --font-mono:'Geist Mono',ui-monospace,monospace; }
|
||||
body { font-family: var(--font-sans); background: var(--color-paper); min-height: 100vh; padding: 3rem 2rem; color: var(--color-ink); }
|
||||
.container { max-width: 1200px; margin: 0 auto; }
|
||||
.header { margin-bottom: 2.5rem; }
|
||||
.header-eyebrow { font-family: var(--font-mono); font-size: 0.66rem; font-weight: 500; letter-spacing: 0.18em; text-transform: uppercase; color: var(--color-muted); margin-bottom: 0.75rem; }
|
||||
h1 { font-family: var(--font-serif); font-size: clamp(1.75rem, 3vw + 1rem, 2.5rem); font-weight: 400; letter-spacing: -0.02em; line-height: 1.1; margin-bottom: 0.5rem; }
|
||||
.subtitle { font-size: 1rem; line-height: 1.55; color: var(--color-muted); max-width: 58ch; }
|
||||
.diagram-container { background: var(--color-paper-2); border-radius: 8px; border: 1px solid var(--color-rule); padding: 1.5rem; overflow-x: auto; }
|
||||
svg { width: 100%; min-width: 900px; display: block; }
|
||||
.cards { display: grid; grid-template-columns: 1.1fr 1fr 0.9fr; gap: 1rem; margin-top: 1.5rem; }
|
||||
@media (max-width: 820px) { .cards { grid-template-columns: 1fr; } }
|
||||
.card { background: #fff; border-radius: 6px; border: 1px solid var(--color-rule); padding: 1.25rem; }
|
||||
.card .eyebrow { font-family: var(--font-mono); font-size: 0.5rem; letter-spacing: 0.18em; text-transform: uppercase; color: var(--color-muted); margin-bottom: 0.5rem; }
|
||||
.card-header { display: flex; align-items: center; gap: 0.6rem; margin-bottom: 0.875rem; padding-bottom: 0.875rem; border-bottom: 1px solid rgba(11,13,11,0.08); }
|
||||
.card-dot { width: 7px; height: 7px; border-radius: 50%; }
|
||||
.card-dot.ink { background: var(--color-ink); } .card-dot.muted { background: var(--color-muted); } .card-dot.coral { background: var(--color-accent); }
|
||||
.card h3 { font-size: 0.875rem; font-weight: 600; }
|
||||
.card p, .card ul { color: var(--color-muted); font-size: 0.8125rem; line-height: 1.55; list-style: none; }
|
||||
.card li { margin-bottom: 0.3rem; padding-left: 0.875rem; position: relative; }
|
||||
.card li::before { content: '—'; position: absolute; left: 0; color: rgba(11,13,11,0.25); font-size: 0.75rem; }
|
||||
.footer { margin-top: 2rem; padding-top: 1.5rem; border-top: 1px solid rgba(11,13,11,0.10); font-family: var(--font-mono); font-size: 0.72rem; letter-spacing: 0.06em; color: var(--color-soft); display: flex; justify-content: space-between; flex-wrap: wrap; gap: 0.5rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<p class="header-eyebrow">Pyramid · Diagram Design</p>
|
||||
<h1>Content pyramid · what compounds</h1>
|
||||
<p class="subtitle">Four layers of output, ordered by how rarely they ship and how far they travel. The base keeps you present. The apex defines the body of work — and it's the only layer anyone quotes back a decade later.</p>
|
||||
</div>
|
||||
|
||||
<div class="diagram-container">
|
||||
<svg viewBox="0 0 1000 500" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<pattern id="dots" width="22" height="22" patternUnits="userSpaceOnUse">
|
||||
<circle cx="1" cy="1" r="0.9" fill="rgba(11,13,11,0.10)"/>
|
||||
</pattern>
|
||||
</defs>
|
||||
|
||||
<rect width="100%" height="100%" fill="#f5f4ed"/>
|
||||
<rect width="100%" height="100%" fill="url(#dots)" opacity="0.55"/>
|
||||
|
||||
<!-- Left axis: direction of rarity -->
|
||||
<line x1="100" y1="112" x2="100" y2="320" stroke="rgba(11,13,11,0.30)" stroke-width="1"/>
|
||||
<polygon points="96,112 104,112 100,100" fill="rgba(11,13,11,0.45)"/>
|
||||
<text x="80" y="216" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em" text-anchor="middle" transform="rotate(-90 80 216)">RARER · FEWER · COMPOUNDS ↑</text>
|
||||
|
||||
<!-- Base layer: Short posts -->
|
||||
<polygon points="240,280 760,280 820,344 180,344" fill="#efeee5" stroke="rgba(11,13,11,0.12)" stroke-width="1"/>
|
||||
<text x="500" y="308" fill="#0b0d0b" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Short posts</text>
|
||||
<text x="500" y="324" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">daily · ~200 words</text>
|
||||
<text x="836" y="316" fill="#65655c" font-size="9" font-family="'Geist Mono', monospace" letter-spacing="0.08em">~240/yr</text>
|
||||
|
||||
<!-- L3: Essays -->
|
||||
<polygon points="300,216 700,216 760,280 240,280" fill="#efeee5" stroke="rgba(11,13,11,0.12)" stroke-width="1"/>
|
||||
<text x="500" y="244" fill="#0b0d0b" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Essays</text>
|
||||
<text x="500" y="260" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">weekly · 800–1,500 words</text>
|
||||
<text x="776" y="252" fill="#65655c" font-size="9" font-family="'Geist Mono', monospace" letter-spacing="0.08em">~48/yr</text>
|
||||
|
||||
<!-- L2: Long-form guides -->
|
||||
<polygon points="360,152 640,152 700,216 300,216" fill="#efeee5" stroke="rgba(11,13,11,0.12)" stroke-width="1"/>
|
||||
<text x="500" y="180" fill="#0b0d0b" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Long-form guides</text>
|
||||
<text x="500" y="196" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">quarterly · 4,000+ words</text>
|
||||
<text x="716" y="188" fill="#65655c" font-size="9" font-family="'Geist Mono', monospace" letter-spacing="0.08em">~4/yr</text>
|
||||
|
||||
<!-- Apex: Flagship book — CORAL FOCAL (triangle, pointed) -->
|
||||
<polygon points="500,76 640,152 360,152" fill="rgba(247,89,31,0.08)" stroke="#f7591f" stroke-width="1"/>
|
||||
<text x="500" y="120" fill="#0b0d0b" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Flagship book</text>
|
||||
<text x="500" y="136" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">every 3–5 years</text>
|
||||
<text x="656" y="112" fill="#f7591f" font-size="9" font-family="'Geist Mono', monospace" letter-spacing="0.08em">the apex</text>
|
||||
|
||||
<!-- Footnote under pyramid -->
|
||||
<text x="500" y="384" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif" text-anchor="middle" font-style="italic">The base funds the apex. The apex defines the base.</text>
|
||||
|
||||
<!-- Legend -->
|
||||
<line x1="40" y1="436" x2="960" y2="436" stroke="rgba(11,13,11,0.10)" stroke-width="0.8"/>
|
||||
<text x="40" y="452" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">LEGEND</text>
|
||||
|
||||
<rect x="40" y="464" width="16" height="12" fill="rgba(247,89,31,0.08)" stroke="#f7591f" stroke-width="1"/>
|
||||
<text x="64" y="474" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Apex — rarest, highest leverage</text>
|
||||
|
||||
<rect x="280" y="464" width="16" height="12" fill="#efeee5" stroke="rgba(11,13,11,0.25)" stroke-width="1"/>
|
||||
<text x="304" y="474" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Supporting layer — the volume work</text>
|
||||
|
||||
<text x="560" y="474" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif" font-style="italic">Layer width is honest: narrower = rarer shipping cadence.</text>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div class="cards">
|
||||
<div class="card">
|
||||
<p class="eyebrow">THE HEADLINE</p>
|
||||
<div class="card-header"><span class="card-dot coral"></span><h3>One coral layer, on purpose</h3></div>
|
||||
<p>A five-colour pyramid is a children's diagram. Reserving coral for the apex makes the whole structure actually say something: this is the rarest thing you'll make, and it's the one the rest of the pyramid is feeding.</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-header"><span class="card-dot ink"></span><h3>Width tells the truth</h3></div>
|
||||
<ul><li>~240 short posts a year</li><li>~48 essays</li><li>~4 long-form guides</li><li>1 flagship every 3–5 years</li></ul>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-header"><span class="card-dot muted"></span><h3>Pyramids only work for hierarchy</h3></div>
|
||||
<p>If your four categories don't have a rarity order — if a bullet list would communicate it — use bullets. Pyramids promise you're trading volume for value as you climb.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<span>content pyramid · what compounds</span>
|
||||
<span>example · diagram-design</span>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
117
skills/assets/example-pyramid.html
Normal file
117
skills/assets/example-pyramid.html
Normal file
@@ -0,0 +1,117 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Content pyramid · what compounds</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
:root {
|
||||
--color-paper: #f5f4ed;
|
||||
--color-ink: #0b0d0b;
|
||||
--color-muted: #52534e;
|
||||
--color-accent: #f7591f;
|
||||
--font-sans: 'Geist', system-ui, sans-serif;
|
||||
--font-serif: 'Instrument Serif', serif;
|
||||
--font-mono: 'Geist Mono', ui-monospace, monospace;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-sans);
|
||||
background: var(--color-paper);
|
||||
color: var(--color-ink);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 3rem 2rem;
|
||||
}
|
||||
|
||||
.frame { max-width: 1200px; width: 100%; }
|
||||
|
||||
.eyebrow {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.66rem;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.18em;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-muted);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-family: var(--font-serif);
|
||||
font-size: clamp(1.5rem, 2.4vw + 0.75rem, 2rem);
|
||||
font-weight: 400;
|
||||
letter-spacing: -0.02em;
|
||||
line-height: 1.15;
|
||||
color: var(--color-ink);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
svg { width: 100%; min-width: 900px; display: block; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="frame">
|
||||
<p class="eyebrow">Pyramid · Diagram Design</p>
|
||||
<h1>Content pyramid · what compounds</h1>
|
||||
|
||||
<svg viewBox="0 0 1000 500" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<pattern id="dots" width="22" height="22" patternUnits="userSpaceOnUse">
|
||||
<circle cx="1" cy="1" r="0.9" fill="rgba(11,13,11,0.10)"/>
|
||||
</pattern>
|
||||
</defs>
|
||||
|
||||
<rect width="100%" height="100%" fill="#f5f4ed"/>
|
||||
<rect width="100%" height="100%" fill="url(#dots)" opacity="0.55"/>
|
||||
|
||||
<!-- Left axis: direction of rarity -->
|
||||
<line x1="100" y1="112" x2="100" y2="320" stroke="rgba(11,13,11,0.30)" stroke-width="1"/>
|
||||
<polygon points="96,112 104,112 100,100" fill="rgba(11,13,11,0.45)"/>
|
||||
<text x="80" y="216" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em" text-anchor="middle" transform="rotate(-90 80 216)">RARER · FEWER · COMPOUNDS ↑</text>
|
||||
|
||||
<!-- Base layer: Short posts -->
|
||||
<polygon points="240,280 760,280 820,344 180,344" fill="#efeee5" stroke="rgba(11,13,11,0.12)" stroke-width="1"/>
|
||||
<text x="500" y="308" fill="#0b0d0b" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Short posts</text>
|
||||
<text x="500" y="324" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">daily · ~200 words</text>
|
||||
<text x="836" y="316" fill="#65655c" font-size="9" font-family="'Geist Mono', monospace" letter-spacing="0.08em">~240/yr</text>
|
||||
|
||||
<!-- L3: Essays -->
|
||||
<polygon points="300,216 700,216 760,280 240,280" fill="#efeee5" stroke="rgba(11,13,11,0.12)" stroke-width="1"/>
|
||||
<text x="500" y="244" fill="#0b0d0b" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Essays</text>
|
||||
<text x="500" y="260" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">weekly · 800–1,500 words</text>
|
||||
<text x="776" y="252" fill="#65655c" font-size="9" font-family="'Geist Mono', monospace" letter-spacing="0.08em">~48/yr</text>
|
||||
|
||||
<!-- L2: Long-form guides -->
|
||||
<polygon points="360,152 640,152 700,216 300,216" fill="#efeee5" stroke="rgba(11,13,11,0.12)" stroke-width="1"/>
|
||||
<text x="500" y="180" fill="#0b0d0b" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Long-form guides</text>
|
||||
<text x="500" y="196" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">quarterly · 4,000+ words</text>
|
||||
<text x="716" y="188" fill="#65655c" font-size="9" font-family="'Geist Mono', monospace" letter-spacing="0.08em">~4/yr</text>
|
||||
|
||||
<!-- Apex: Flagship book — CORAL FOCAL (triangle, pointed) -->
|
||||
<polygon points="500,76 640,152 360,152" fill="rgba(247,89,31,0.08)" stroke="#f7591f" stroke-width="1"/>
|
||||
<text x="500" y="120" fill="#0b0d0b" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Flagship book</text>
|
||||
<text x="500" y="136" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">every 3–5 years</text>
|
||||
<text x="656" y="112" fill="#f7591f" font-size="9" font-family="'Geist Mono', monospace" letter-spacing="0.08em">the apex</text>
|
||||
|
||||
<!-- Footnote under pyramid -->
|
||||
<text x="500" y="384" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif" text-anchor="middle" font-style="italic">The base funds the apex. The apex defines the base.</text>
|
||||
|
||||
<!-- Legend -->
|
||||
<line x1="40" y1="436" x2="960" y2="436" stroke="rgba(11,13,11,0.10)" stroke-width="0.8"/>
|
||||
<text x="40" y="452" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">LEGEND</text>
|
||||
|
||||
<rect x="40" y="464" width="16" height="12" fill="rgba(247,89,31,0.08)" stroke="#f7591f" stroke-width="1"/>
|
||||
<text x="64" y="474" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Apex — rarest, highest leverage</text>
|
||||
|
||||
<rect x="280" y="464" width="16" height="12" fill="#efeee5" stroke="rgba(11,13,11,0.25)" stroke-width="1"/>
|
||||
<text x="304" y="474" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Supporting layer — the volume work</text>
|
||||
|
||||
<text x="560" y="474" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif" font-style="italic">Layer width is honest: narrower = rarer shipping cadence.</text>
|
||||
</svg>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
133
skills/assets/example-quadrant-dark.html
Normal file
133
skills/assets/example-quadrant-dark.html
Normal file
@@ -0,0 +1,133 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Content ideas · Impact × Effort</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
:root {
|
||||
--color-paper: #1c1a17;
|
||||
--color-ink: #f1efe7;
|
||||
--color-muted: #a8a69d;
|
||||
--color-accent: #ff6a30;
|
||||
--font-sans: 'Geist', system-ui, sans-serif;
|
||||
--font-serif: 'Instrument Serif', serif;
|
||||
--font-mono: 'Geist Mono', ui-monospace, monospace;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-sans);
|
||||
background: var(--color-paper);
|
||||
color: var(--color-ink);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 3rem 2rem;
|
||||
}
|
||||
|
||||
.frame { max-width: 1200px; width: 100%; }
|
||||
|
||||
.eyebrow {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.66rem;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.18em;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-muted);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-family: var(--font-serif);
|
||||
font-size: clamp(1.5rem, 2.4vw + 0.75rem, 2rem);
|
||||
font-weight: 400;
|
||||
letter-spacing: -0.02em;
|
||||
line-height: 1.15;
|
||||
color: var(--color-ink);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
svg { width: 100%; min-width: 900px; display: block; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="frame">
|
||||
<p class="eyebrow">Quadrant · Diagram Design</p>
|
||||
<h1>Content ideas · Impact × Effort</h1>
|
||||
|
||||
<svg viewBox="0 0 1000 500" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<pattern id="dots" width="22" height="22" patternUnits="userSpaceOnUse">
|
||||
<circle cx="1" cy="1" r="0.9" fill="rgba(241,239,231,0.10)"/>
|
||||
</pattern>
|
||||
</defs>
|
||||
|
||||
<rect width="100%" height="100%" fill="#1c1a17"/>
|
||||
<rect width="100%" height="100%" fill="url(#dots)" opacity="0.55"/>
|
||||
|
||||
<!-- Quadrant backgrounds (very subtle, only coral on DO FIRST) -->
|
||||
<rect x="120" y="80" width="380" height="170" fill="rgba(255,106,48,0.03)"/>
|
||||
|
||||
<!-- Axis cross -->
|
||||
<line x1="120" y1="250" x2="880" y2="250" stroke="rgba(241,239,231,0.45)" stroke-width="1"/>
|
||||
<line x1="500" y1="80" x2="500" y2="420" stroke="rgba(241,239,231,0.45)" stroke-width="1"/>
|
||||
|
||||
<!-- Axis end labels -->
|
||||
<text x="880" y="266" fill="#a8a69d" font-size="8" font-family="'Geist Mono', monospace" text-anchor="end" letter-spacing="0.14em">HIGH EFFORT →</text>
|
||||
<text x="120" y="266" fill="#a8a69d" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.14em">← LOW EFFORT</text>
|
||||
<text x="512" y="80" fill="#a8a69d" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.14em">↑ HIGH IMPACT</text>
|
||||
<text x="512" y="432" fill="#a8a69d" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.14em">↓ LOW IMPACT</text>
|
||||
|
||||
<!-- Quadrant corner labels -->
|
||||
<text x="140" y="104" fill="#ff6a30" font-size="9" font-family="'Geist Mono', monospace" letter-spacing="0.18em" font-weight="600">DO FIRST</text>
|
||||
<text x="860" y="104" fill="#a8a69d" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end" letter-spacing="0.18em">MAJOR PROJECTS</text>
|
||||
<text x="140" y="412" fill="#a8a69d" font-size="9" font-family="'Geist Mono', monospace" letter-spacing="0.18em">QUICK WINS</text>
|
||||
<text x="860" y="412" fill="#a8a69d" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end" letter-spacing="0.18em">AVOID</text>
|
||||
|
||||
<!-- Items: TL (Do First) -->
|
||||
<!-- coral focal -->
|
||||
<circle cx="220" cy="140" r="6" fill="#ff6a30"/>
|
||||
<text x="232" y="144" fill="#f1efe7" font-size="11" font-weight="600" font-family="'Geist', sans-serif">diagram-design</text>
|
||||
|
||||
<circle cx="320" cy="200" r="4" fill="#f1efe7"/>
|
||||
<text x="332" y="204" fill="#a8a69d" font-size="11" font-family="'Geist', sans-serif">Update changelog</text>
|
||||
|
||||
<!-- Items: TR (Major Projects) -->
|
||||
<circle cx="620" cy="140" r="4" fill="#f1efe7"/>
|
||||
<text x="632" y="144" fill="#a8a69d" font-size="11" font-family="'Geist', sans-serif">Design v4 refresh</text>
|
||||
|
||||
<circle cx="760" cy="180" r="4" fill="#f1efe7"/>
|
||||
<text x="772" y="184" fill="#a8a69d" font-size="11" font-family="'Geist', sans-serif">New publication</text>
|
||||
|
||||
<!-- Items: BL (Quick Wins) -->
|
||||
<circle cx="260" cy="320" r="4" fill="#f1efe7"/>
|
||||
<text x="272" y="324" fill="#a8a69d" font-size="11" font-family="'Geist', sans-serif">Fix footer link</text>
|
||||
|
||||
<circle cx="360" cy="380" r="4" fill="#f1efe7"/>
|
||||
<text x="372" y="384" fill="#a8a69d" font-size="11" font-family="'Geist', sans-serif">Update OG tags</text>
|
||||
|
||||
<!-- Items: BR (Avoid) -->
|
||||
<circle cx="640" cy="380" r="4" fill="#f1efe7"/>
|
||||
<text x="652" y="384" fill="#a8a69d" font-size="11" font-family="'Geist', sans-serif">Rewrite build pipeline</text>
|
||||
|
||||
<circle cx="780" cy="320" r="4" fill="#f1efe7"/>
|
||||
<text x="792" y="324" fill="#a8a69d" font-size="11" font-family="'Geist', sans-serif">Port to Nuxt</text>
|
||||
|
||||
<!-- Legend -->
|
||||
<line x1="40" y1="456" x2="960" y2="456" stroke="rgba(241,239,231,0.10)" stroke-width="0.8"/>
|
||||
<text x="40" y="472" fill="#a8a69d" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">LEGEND</text>
|
||||
|
||||
<circle cx="52" cy="488" r="6" fill="#ff6a30"/>
|
||||
<text x="68" y="492" fill="#a8a69d" font-size="8.5" font-family="'Geist', sans-serif">Start tomorrow</text>
|
||||
|
||||
<circle cx="192" cy="488" r="4" fill="#f1efe7"/>
|
||||
<text x="208" y="492" fill="#a8a69d" font-size="8.5" font-family="'Geist', sans-serif">Candidate project</text>
|
||||
|
||||
<text x="336" y="492" fill="#a8a69d" font-size="8.5" font-family="'Geist', sans-serif" font-style="italic">Position is the signal. Colour is reserved for the single action item.</text>
|
||||
</svg>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
136
skills/assets/example-quadrant-full.html
Normal file
136
skills/assets/example-quadrant-full.html
Normal file
@@ -0,0 +1,136 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Content ideas · Impact × Effort</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
:root { --color-paper:#f5f4ed; --color-paper-2:#efeee5; --color-ink:#0b0d0b; --color-muted:#52534e; --color-soft:#65655c; --color-rule:rgba(11,13,11,0.12); --color-accent:#f7591f; --color-link:#1a70c7; --font-sans:'Geist',system-ui,sans-serif; --font-serif:'Instrument Serif',serif; --font-mono:'Geist Mono',ui-monospace,monospace; }
|
||||
body { font-family: var(--font-sans); background: var(--color-paper); min-height: 100vh; padding: 3rem 2rem; color: var(--color-ink); }
|
||||
.container { max-width: 1200px; margin: 0 auto; }
|
||||
.header { margin-bottom: 2.5rem; }
|
||||
.header-eyebrow { font-family: var(--font-mono); font-size: 0.66rem; font-weight: 500; letter-spacing: 0.18em; text-transform: uppercase; color: var(--color-muted); margin-bottom: 0.75rem; }
|
||||
h1 { font-family: var(--font-serif); font-size: clamp(1.75rem, 3vw + 1rem, 2.5rem); font-weight: 400; letter-spacing: -0.02em; line-height: 1.1; margin-bottom: 0.5rem; }
|
||||
.subtitle { font-size: 1rem; line-height: 1.55; color: var(--color-muted); max-width: 58ch; }
|
||||
.diagram-container { background: var(--color-paper-2); border-radius: 8px; border: 1px solid var(--color-rule); padding: 1.5rem; overflow-x: auto; }
|
||||
svg { width: 100%; min-width: 900px; display: block; }
|
||||
.cards { display: grid; grid-template-columns: 1.1fr 1fr 0.9fr; gap: 1rem; margin-top: 1.5rem; }
|
||||
@media (max-width: 820px) { .cards { grid-template-columns: 1fr; } }
|
||||
.card { background: #fff; border-radius: 6px; border: 1px solid var(--color-rule); padding: 1.25rem; }
|
||||
.card .eyebrow { font-family: var(--font-mono); font-size: 0.5rem; letter-spacing: 0.18em; text-transform: uppercase; color: var(--color-muted); margin-bottom: 0.5rem; }
|
||||
.card-header { display: flex; align-items: center; gap: 0.6rem; margin-bottom: 0.875rem; padding-bottom: 0.875rem; border-bottom: 1px solid rgba(11,13,11,0.08); }
|
||||
.card-dot { width: 7px; height: 7px; border-radius: 50%; }
|
||||
.card-dot.ink { background: var(--color-ink); } .card-dot.muted { background: var(--color-muted); } .card-dot.coral { background: var(--color-accent); }
|
||||
.card h3 { font-size: 0.875rem; font-weight: 600; }
|
||||
.card p, .card ul { color: var(--color-muted); font-size: 0.8125rem; line-height: 1.55; list-style: none; }
|
||||
.card li { margin-bottom: 0.3rem; padding-left: 0.875rem; position: relative; }
|
||||
.card li::before { content: '—'; position: absolute; left: 0; color: rgba(11,13,11,0.25); font-size: 0.75rem; }
|
||||
.footer { margin-top: 2rem; padding-top: 1.5rem; border-top: 1px solid rgba(11,13,11,0.10); font-family: var(--font-mono); font-size: 0.72rem; letter-spacing: 0.06em; color: var(--color-soft); display: flex; justify-content: space-between; flex-wrap: wrap; gap: 0.5rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<p class="header-eyebrow">Quadrant · Diagram Design</p>
|
||||
<h1>Content ideas · Impact × Effort</h1>
|
||||
<p class="subtitle">Eight candidate projects mapped by the pay-off they'd generate against the cost to ship. Position is the signal — color is reserved for the one item worth starting tomorrow.</p>
|
||||
</div>
|
||||
|
||||
<div class="diagram-container">
|
||||
<svg viewBox="0 0 1000 500" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<pattern id="dots" width="22" height="22" patternUnits="userSpaceOnUse">
|
||||
<circle cx="1" cy="1" r="0.9" fill="rgba(11,13,11,0.10)"/>
|
||||
</pattern>
|
||||
</defs>
|
||||
|
||||
<rect width="100%" height="100%" fill="#f5f4ed"/>
|
||||
<rect width="100%" height="100%" fill="url(#dots)" opacity="0.55"/>
|
||||
|
||||
<!-- Quadrant backgrounds (very subtle, only coral on DO FIRST) -->
|
||||
<rect x="120" y="80" width="380" height="170" fill="rgba(247,89,31,0.03)"/>
|
||||
|
||||
<!-- Axis cross -->
|
||||
<line x1="120" y1="250" x2="880" y2="250" stroke="rgba(11,13,11,0.45)" stroke-width="1"/>
|
||||
<line x1="500" y1="80" x2="500" y2="420" stroke="rgba(11,13,11,0.45)" stroke-width="1"/>
|
||||
|
||||
<!-- Axis end labels -->
|
||||
<text x="880" y="266" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" text-anchor="end" letter-spacing="0.14em">HIGH EFFORT →</text>
|
||||
<text x="120" y="266" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.14em">← LOW EFFORT</text>
|
||||
<text x="512" y="80" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.14em">↑ HIGH IMPACT</text>
|
||||
<text x="512" y="432" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.14em">↓ LOW IMPACT</text>
|
||||
|
||||
<!-- Quadrant corner labels -->
|
||||
<text x="140" y="104" fill="#f7591f" font-size="9" font-family="'Geist Mono', monospace" letter-spacing="0.18em" font-weight="600">DO FIRST</text>
|
||||
<text x="860" y="104" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end" letter-spacing="0.18em">MAJOR PROJECTS</text>
|
||||
<text x="140" y="412" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" letter-spacing="0.18em">QUICK WINS</text>
|
||||
<text x="860" y="412" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end" letter-spacing="0.18em">AVOID</text>
|
||||
|
||||
<!-- Items: TL (Do First) -->
|
||||
<!-- coral focal -->
|
||||
<circle cx="220" cy="140" r="6" fill="#f7591f"/>
|
||||
<text x="232" y="144" fill="#0b0d0b" font-size="11" font-weight="600" font-family="'Geist', sans-serif">diagram-design</text>
|
||||
|
||||
<circle cx="320" cy="200" r="4" fill="#0b0d0b"/>
|
||||
<text x="332" y="204" fill="#52534e" font-size="11" font-family="'Geist', sans-serif">Update changelog</text>
|
||||
|
||||
<!-- Items: TR (Major Projects) -->
|
||||
<circle cx="620" cy="140" r="4" fill="#0b0d0b"/>
|
||||
<text x="632" y="144" fill="#52534e" font-size="11" font-family="'Geist', sans-serif">Design v4 refresh</text>
|
||||
|
||||
<circle cx="760" cy="180" r="4" fill="#0b0d0b"/>
|
||||
<text x="772" y="184" fill="#52534e" font-size="11" font-family="'Geist', sans-serif">New publication</text>
|
||||
|
||||
<!-- Items: BL (Quick Wins) -->
|
||||
<circle cx="260" cy="320" r="4" fill="#0b0d0b"/>
|
||||
<text x="272" y="324" fill="#52534e" font-size="11" font-family="'Geist', sans-serif">Fix footer link</text>
|
||||
|
||||
<circle cx="360" cy="380" r="4" fill="#0b0d0b"/>
|
||||
<text x="372" y="384" fill="#52534e" font-size="11" font-family="'Geist', sans-serif">Update OG tags</text>
|
||||
|
||||
<!-- Items: BR (Avoid) -->
|
||||
<circle cx="640" cy="380" r="4" fill="#0b0d0b"/>
|
||||
<text x="652" y="384" fill="#52534e" font-size="11" font-family="'Geist', sans-serif">Rewrite build pipeline</text>
|
||||
|
||||
<circle cx="780" cy="320" r="4" fill="#0b0d0b"/>
|
||||
<text x="792" y="324" fill="#52534e" font-size="11" font-family="'Geist', sans-serif">Port to Nuxt</text>
|
||||
|
||||
<!-- Legend -->
|
||||
<line x1="40" y1="456" x2="960" y2="456" stroke="rgba(11,13,11,0.10)" stroke-width="0.8"/>
|
||||
<text x="40" y="472" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">LEGEND</text>
|
||||
|
||||
<circle cx="52" cy="488" r="6" fill="#f7591f"/>
|
||||
<text x="68" y="492" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Start tomorrow</text>
|
||||
|
||||
<circle cx="192" cy="488" r="4" fill="#0b0d0b"/>
|
||||
<text x="208" y="492" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Candidate project</text>
|
||||
|
||||
<text x="336" y="492" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif" font-style="italic">Position is the signal. Colour is reserved for the single action item.</text>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div class="cards">
|
||||
<div class="card">
|
||||
<p class="eyebrow">THE HEADLINE</p>
|
||||
<div class="card-header"><span class="card-dot coral"></span><h3>One coral dot, on purpose</h3></div>
|
||||
<p>A 2×2 with four coloured quadrants is a poster, not a decision tool. Reserving coral for the single item you commit to tomorrow makes the matrix actually prioritise.</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-header"><span class="card-dot ink"></span><h3>Axes labelled at ends</h3></div>
|
||||
<ul><li>HIGH / LOW at the extremes</li><li>Arrows show direction</li><li>No labels at midpoints</li><li>The cross is 1px ink, not a box</li></ul>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-header"><span class="card-dot muted"></span><h3>Empty BR isn't wasted</h3></div>
|
||||
<p>Two items in "Avoid" is informative — it says the team considered them and rejected them. A blank quadrant would leave you wondering.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<span>content ideas · impact × effort</span>
|
||||
<span>example · diagram-design</span>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
133
skills/assets/example-quadrant.html
Normal file
133
skills/assets/example-quadrant.html
Normal file
@@ -0,0 +1,133 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Content ideas · Impact × Effort</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
:root {
|
||||
--color-paper: #f5f4ed;
|
||||
--color-ink: #0b0d0b;
|
||||
--color-muted: #52534e;
|
||||
--color-accent: #f7591f;
|
||||
--font-sans: 'Geist', system-ui, sans-serif;
|
||||
--font-serif: 'Instrument Serif', serif;
|
||||
--font-mono: 'Geist Mono', ui-monospace, monospace;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-sans);
|
||||
background: var(--color-paper);
|
||||
color: var(--color-ink);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 3rem 2rem;
|
||||
}
|
||||
|
||||
.frame { max-width: 1200px; width: 100%; }
|
||||
|
||||
.eyebrow {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.66rem;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.18em;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-muted);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-family: var(--font-serif);
|
||||
font-size: clamp(1.5rem, 2.4vw + 0.75rem, 2rem);
|
||||
font-weight: 400;
|
||||
letter-spacing: -0.02em;
|
||||
line-height: 1.15;
|
||||
color: var(--color-ink);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
svg { width: 100%; min-width: 900px; display: block; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="frame">
|
||||
<p class="eyebrow">Quadrant · Diagram Design</p>
|
||||
<h1>Content ideas · Impact × Effort</h1>
|
||||
|
||||
<svg viewBox="0 0 1000 500" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<pattern id="dots" width="22" height="22" patternUnits="userSpaceOnUse">
|
||||
<circle cx="1" cy="1" r="0.9" fill="rgba(11,13,11,0.10)"/>
|
||||
</pattern>
|
||||
</defs>
|
||||
|
||||
<rect width="100%" height="100%" fill="#f5f4ed"/>
|
||||
<rect width="100%" height="100%" fill="url(#dots)" opacity="0.55"/>
|
||||
|
||||
<!-- Quadrant backgrounds (very subtle, only coral on DO FIRST) -->
|
||||
<rect x="120" y="80" width="380" height="170" fill="rgba(247,89,31,0.03)"/>
|
||||
|
||||
<!-- Axis cross -->
|
||||
<line x1="120" y1="250" x2="880" y2="250" stroke="rgba(11,13,11,0.45)" stroke-width="1"/>
|
||||
<line x1="500" y1="80" x2="500" y2="420" stroke="rgba(11,13,11,0.45)" stroke-width="1"/>
|
||||
|
||||
<!-- Axis end labels -->
|
||||
<text x="880" y="266" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" text-anchor="end" letter-spacing="0.14em">HIGH EFFORT →</text>
|
||||
<text x="120" y="266" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.14em">← LOW EFFORT</text>
|
||||
<text x="512" y="80" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.14em">↑ HIGH IMPACT</text>
|
||||
<text x="512" y="432" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.14em">↓ LOW IMPACT</text>
|
||||
|
||||
<!-- Quadrant corner labels -->
|
||||
<text x="140" y="104" fill="#f7591f" font-size="9" font-family="'Geist Mono', monospace" letter-spacing="0.18em" font-weight="600">DO FIRST</text>
|
||||
<text x="860" y="104" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end" letter-spacing="0.18em">MAJOR PROJECTS</text>
|
||||
<text x="140" y="412" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" letter-spacing="0.18em">QUICK WINS</text>
|
||||
<text x="860" y="412" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end" letter-spacing="0.18em">AVOID</text>
|
||||
|
||||
<!-- Items: TL (Do First) -->
|
||||
<!-- coral focal -->
|
||||
<circle cx="220" cy="140" r="6" fill="#f7591f"/>
|
||||
<text x="232" y="144" fill="#0b0d0b" font-size="11" font-weight="600" font-family="'Geist', sans-serif">diagram-design</text>
|
||||
|
||||
<circle cx="320" cy="200" r="4" fill="#0b0d0b"/>
|
||||
<text x="332" y="204" fill="#52534e" font-size="11" font-family="'Geist', sans-serif">Update changelog</text>
|
||||
|
||||
<!-- Items: TR (Major Projects) -->
|
||||
<circle cx="620" cy="140" r="4" fill="#0b0d0b"/>
|
||||
<text x="632" y="144" fill="#52534e" font-size="11" font-family="'Geist', sans-serif">Design v4 refresh</text>
|
||||
|
||||
<circle cx="760" cy="180" r="4" fill="#0b0d0b"/>
|
||||
<text x="772" y="184" fill="#52534e" font-size="11" font-family="'Geist', sans-serif">New publication</text>
|
||||
|
||||
<!-- Items: BL (Quick Wins) -->
|
||||
<circle cx="260" cy="320" r="4" fill="#0b0d0b"/>
|
||||
<text x="272" y="324" fill="#52534e" font-size="11" font-family="'Geist', sans-serif">Fix footer link</text>
|
||||
|
||||
<circle cx="360" cy="380" r="4" fill="#0b0d0b"/>
|
||||
<text x="372" y="384" fill="#52534e" font-size="11" font-family="'Geist', sans-serif">Update OG tags</text>
|
||||
|
||||
<!-- Items: BR (Avoid) -->
|
||||
<circle cx="640" cy="380" r="4" fill="#0b0d0b"/>
|
||||
<text x="652" y="384" fill="#52534e" font-size="11" font-family="'Geist', sans-serif">Rewrite build pipeline</text>
|
||||
|
||||
<circle cx="780" cy="320" r="4" fill="#0b0d0b"/>
|
||||
<text x="792" y="324" fill="#52534e" font-size="11" font-family="'Geist', sans-serif">Port to Nuxt</text>
|
||||
|
||||
<!-- Legend -->
|
||||
<line x1="40" y1="456" x2="960" y2="456" stroke="rgba(11,13,11,0.10)" stroke-width="0.8"/>
|
||||
<text x="40" y="472" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">LEGEND</text>
|
||||
|
||||
<circle cx="52" cy="488" r="6" fill="#f7591f"/>
|
||||
<text x="68" y="492" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Start tomorrow</text>
|
||||
|
||||
<circle cx="192" cy="488" r="4" fill="#0b0d0b"/>
|
||||
<text x="208" y="492" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Candidate project</text>
|
||||
|
||||
<text x="336" y="492" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif" font-style="italic">Position is the signal. Colour is reserved for the single action item.</text>
|
||||
</svg>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
220
skills/assets/example-sequence-dark.html
Normal file
220
skills/assets/example-sequence-dark.html
Normal file
@@ -0,0 +1,220 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Article request · Sequence</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
:root {
|
||||
--color-paper: #1c1a17;
|
||||
--color-ink: #f1efe7;
|
||||
--color-muted: #a8a69d;
|
||||
--color-accent: #ff6a30;
|
||||
--font-sans: 'Geist', system-ui, sans-serif;
|
||||
--font-serif: 'Instrument Serif', serif;
|
||||
--font-mono: 'Geist Mono', ui-monospace, monospace;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-sans);
|
||||
background: var(--color-paper);
|
||||
color: var(--color-ink);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 3rem 2rem;
|
||||
}
|
||||
|
||||
.frame { max-width: 1200px; width: 100%; }
|
||||
|
||||
.eyebrow {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.66rem;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.18em;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-muted);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-family: var(--font-serif);
|
||||
font-size: clamp(1.5rem, 2.4vw + 0.75rem, 2rem);
|
||||
font-weight: 400;
|
||||
letter-spacing: -0.02em;
|
||||
line-height: 1.15;
|
||||
color: var(--color-ink);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
svg { width: 100%; min-width: 900px; display: block; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="frame">
|
||||
<p class="eyebrow">Sequence · Diagram Design</p>
|
||||
<h1>Article request, cold cache</h1>
|
||||
|
||||
<svg viewBox="0 0 1000 584" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<!-- Dot grid background -->
|
||||
<pattern id="dots" width="22" height="22" patternUnits="userSpaceOnUse">
|
||||
<circle cx="1" cy="1" r="0.9" fill="rgba(241,239,231,0.10)"/>
|
||||
</pattern>
|
||||
|
||||
<!-- Arrow markers -->
|
||||
<marker id="arrow" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
|
||||
<polygon points="0 0, 8 3, 0 6" fill="#a8a69d"/>
|
||||
</marker>
|
||||
<marker id="arrow-accent" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
|
||||
<polygon points="0 0, 8 3, 0 6" fill="#ff6a30"/>
|
||||
</marker>
|
||||
<marker id="arrow-link" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
|
||||
<polygon points="0 0, 8 3, 0 6" fill="#5ba8eb"/>
|
||||
</marker>
|
||||
</defs>
|
||||
|
||||
<!-- Background: paper + dot grid -->
|
||||
<rect width="100%" height="100%" fill="#1c1a17"/>
|
||||
<rect width="100%" height="100%" fill="url(#dots)" opacity="0.55"/>
|
||||
|
||||
<!-- =================================================================
|
||||
LIFELINES — dashed vertical lines from each actor, behind everything.
|
||||
Actor lifeline x-coords: 128, 352, 584, 800
|
||||
================================================================= -->
|
||||
<line x1="128" y1="128" x2="128" y2="488" stroke="rgba(241,239,231,0.22)" stroke-width="1" stroke-dasharray="3,3"/>
|
||||
<line x1="352" y1="128" x2="352" y2="488" stroke="rgba(241,239,231,0.22)" stroke-width="1" stroke-dasharray="3,3"/>
|
||||
<line x1="584" y1="128" x2="584" y2="488" stroke="rgba(241,239,231,0.22)" stroke-width="1" stroke-dasharray="3,3"/>
|
||||
<line x1="800" y1="128" x2="800" y2="488" stroke="rgba(241,239,231,0.22)" stroke-width="1" stroke-dasharray="3,3"/>
|
||||
|
||||
<!-- =================================================================
|
||||
ACTIVATION BARS — w=8 rects on lifelines showing control duration.
|
||||
Drawn before message arrows so arrows land on their edges.
|
||||
================================================================= -->
|
||||
<!-- Cloudflare activation: receives at y=176, responds at y=408 -->
|
||||
<rect x="348" y="180" width="8" height="232" fill="rgba(241,239,231,0.06)" stroke="#a8a69d" stroke-width="0.8"/>
|
||||
<!-- Astro activation: called at y=232, returns at y=352 -->
|
||||
<rect x="580" y="236" width="8" height="120" fill="rgba(241,239,231,0.06)" stroke="#a8a69d" stroke-width="0.8"/>
|
||||
|
||||
<!-- =================================================================
|
||||
MESSAGE ARROWS — time flows top→down.
|
||||
Draw before labels so label masks cover the line.
|
||||
================================================================= -->
|
||||
|
||||
<!-- M1: Reader → Cloudflare (HTTPS request · link-blue) -->
|
||||
<line x1="128" y1="176" x2="352" y2="176" stroke="#5ba8eb" stroke-width="1.2" marker-end="url(#arrow-link)"/>
|
||||
|
||||
<!-- M2: Cloudflare → Astro (cache miss · muted) -->
|
||||
<line x1="352" y1="232" x2="580" y2="232" stroke="#a8a69d" stroke-width="1.2" marker-end="url(#arrow)"/>
|
||||
|
||||
<!-- M3: Astro self-message (render MDX · muted U-loop) -->
|
||||
<path d="M 588 284 L 624 284 L 624 316 L 588 316" fill="none" stroke="#a8a69d" stroke-width="1.2" marker-end="url(#arrow)"/>
|
||||
|
||||
<!-- M4: Astro → Cloudflare (return HTML · muted dashed) -->
|
||||
<line x1="580" y1="352" x2="356" y2="352" stroke="#a8a69d" stroke-width="1.2" stroke-dasharray="5,4" marker-end="url(#arrow)"/>
|
||||
|
||||
<!-- M5: Cloudflare → Reader (primary success · coral) -->
|
||||
<line x1="348" y1="408" x2="128" y2="408" stroke="#ff6a30" stroke-width="1.4" marker-end="url(#arrow-accent)"/>
|
||||
|
||||
<!-- M6: Reader → Analytics (async beacon · muted dashed) -->
|
||||
<line x1="128" y1="464" x2="800" y2="464" stroke="#a8a69d" stroke-width="1.2" stroke-dasharray="5,4" marker-end="url(#arrow)"/>
|
||||
|
||||
<!-- =================================================================
|
||||
MESSAGE LABELS — each with an opaque paper-colored mask.
|
||||
================================================================= -->
|
||||
|
||||
<!-- M1 label -->
|
||||
<rect x="188" y="160" width="104" height="12" rx="2" fill="#1c1a17"/>
|
||||
<text x="240" y="170" fill="#5ba8eb" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">GET /ARTICLES/SLUG</text>
|
||||
|
||||
<!-- M2 label -->
|
||||
<rect x="416" y="216" width="100" height="12" rx="2" fill="#1c1a17"/>
|
||||
<text x="466" y="226" fill="#a8a69d" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">CACHE MISS · ORIGIN</text>
|
||||
|
||||
<!-- M3 label (to the right of the self-loop) -->
|
||||
<rect x="632" y="292" width="72" height="12" rx="2" fill="#1c1a17"/>
|
||||
<text x="668" y="302" fill="#a8a69d" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">RENDER MDX</text>
|
||||
|
||||
<!-- M4 label -->
|
||||
<rect x="420" y="336" width="96" height="12" rx="2" fill="#1c1a17"/>
|
||||
<text x="468" y="346" fill="#a8a69d" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">200 · HTML + MAX-AGE</text>
|
||||
|
||||
<!-- M5 label (coral · primary response) -->
|
||||
<rect x="192" y="392" width="96" height="12" rx="2" fill="#1c1a17"/>
|
||||
<text x="240" y="402" fill="#ff6a30" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">200 · EDGE-CACHED</text>
|
||||
|
||||
<!-- M6 label (placed between Astro and Analytics lifelines, a clear gap) -->
|
||||
<rect x="648" y="448" width="96" height="12" rx="2" fill="#1c1a17"/>
|
||||
<text x="696" y="458" fill="#a8a69d" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">PAGEVIEW BEACON</text>
|
||||
|
||||
<!-- =================================================================
|
||||
ACTOR BOXES — drawn after arrows/labels.
|
||||
Each actor: 144–160 wide × 56 tall. Centers: 128, 352, 584, 800.
|
||||
================================================================= -->
|
||||
|
||||
<!-- Actor 1: Reader (external / soft) -->
|
||||
<rect x="56" y="72" width="144" height="56" rx="6" fill="#1c1a17"/>
|
||||
<rect x="56" y="72" width="144" height="56" rx="6" fill="rgba(168,166,157,0.10)" stroke="#8e8c83" stroke-width="1"/>
|
||||
<rect x="64" y="80" width="28" height="12" rx="2" fill="transparent" stroke="rgba(142,140,131,0.40)" stroke-width="0.8"/>
|
||||
<text x="78" y="89" fill="#8e8c83" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">EXT</text>
|
||||
<text x="128" y="104" fill="#f1efe7" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Reader</text>
|
||||
<text x="128" y="119" fill="#a8a69d" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">Browser</text>
|
||||
|
||||
<!-- Actor 2: Cloudflare (cloud / muted) -->
|
||||
<rect x="280" y="72" width="144" height="56" rx="6" fill="#1c1a17"/>
|
||||
<rect x="280" y="72" width="144" height="56" rx="6" fill="rgba(241,239,231,0.03)" stroke="rgba(241,239,231,0.30)" stroke-width="1"/>
|
||||
<rect x="288" y="80" width="32" height="12" rx="2" fill="transparent" stroke="rgba(241,239,231,0.22)" stroke-width="0.8"/>
|
||||
<text x="304" y="89" fill="#8e8c83" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">EDGE</text>
|
||||
<text x="352" y="104" fill="#f1efe7" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Cloudflare</text>
|
||||
<text x="352" y="119" fill="#a8a69d" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">Pages · cache</text>
|
||||
|
||||
<!-- Actor 3: Astro Origin (focal / coral) -->
|
||||
<rect x="504" y="72" width="160" height="56" rx="6" fill="#1c1a17"/>
|
||||
<rect x="504" y="72" width="160" height="56" rx="6" fill="rgba(255,106,48,0.08)" stroke="#ff6a30" stroke-width="1"/>
|
||||
<rect x="512" y="80" width="32" height="12" rx="2" fill="transparent" stroke="rgba(255,106,48,0.50)" stroke-width="0.8"/>
|
||||
<text x="528" y="89" fill="#ff6a30" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">ORIG</text>
|
||||
<text x="584" y="104" fill="#f1efe7" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Astro Origin</text>
|
||||
<text x="584" y="119" fill="#a8a69d" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">SSR + MDX</text>
|
||||
|
||||
<!-- Actor 4: Analytics (optional / dashed) -->
|
||||
<rect x="728" y="72" width="144" height="56" rx="6" fill="#1c1a17"/>
|
||||
<rect x="728" y="72" width="144" height="56" rx="6" fill="rgba(241,239,231,0.02)" stroke="rgba(241,239,231,0.22)" stroke-width="1" stroke-dasharray="4,3"/>
|
||||
<rect x="736" y="80" width="28" height="12" rx="2" fill="transparent" stroke="rgba(241,239,231,0.22)" stroke-width="0.8"/>
|
||||
<text x="750" y="89" fill="#8e8c83" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">ASY</text>
|
||||
<text x="800" y="104" fill="#f1efe7" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Analytics</text>
|
||||
<text x="800" y="119" fill="#a8a69d" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">Beacon · async</text>
|
||||
|
||||
<!-- =================================================================
|
||||
LEGEND — horizontal strip at the bottom.
|
||||
Separator at y=504, eyebrow at y=520, items at y=540–548.
|
||||
================================================================= -->
|
||||
<line x1="56" y1="504" x2="944" y2="504" stroke="rgba(241,239,231,0.10)" stroke-width="0.8"/>
|
||||
<text x="56" y="520" fill="#a8a69d" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">LEGEND</text>
|
||||
|
||||
<!-- Item 1: Actor swatch (coral focal) -->
|
||||
<rect x="56" y="540" width="14" height="10" rx="2" fill="rgba(255,106,48,0.08)" stroke="#ff6a30" stroke-width="1"/>
|
||||
<text x="76" y="548" fill="#a8a69d" font-size="8.5" font-family="'Geist', sans-serif">Focal actor</text>
|
||||
|
||||
<!-- Item 2: Activation bar swatch -->
|
||||
<rect x="188" y="536" width="4" height="18" fill="rgba(241,239,231,0.06)" stroke="#a8a69d" stroke-width="0.8"/>
|
||||
<text x="200" y="548" fill="#a8a69d" font-size="8.5" font-family="'Geist', sans-serif">Activation</text>
|
||||
|
||||
<!-- Item 3: Request arrow (link-blue) -->
|
||||
<line x1="308" y1="546" x2="336" y2="546" stroke="#5ba8eb" stroke-width="1.2" marker-end="url(#arrow-link)"/>
|
||||
<text x="344" y="548" fill="#a8a69d" font-size="8.5" font-family="'Geist', sans-serif">HTTP request</text>
|
||||
|
||||
<!-- Item 4: Return / async (muted dashed) -->
|
||||
<line x1="476" y1="546" x2="504" y2="546" stroke="#a8a69d" stroke-width="1.2" stroke-dasharray="5,4" marker-end="url(#arrow)"/>
|
||||
<text x="512" y="548" fill="#a8a69d" font-size="8.5" font-family="'Geist', sans-serif">Return / async</text>
|
||||
|
||||
<!-- Item 5: Primary response (coral) -->
|
||||
<line x1="652" y1="546" x2="680" y2="546" stroke="#ff6a30" stroke-width="1.4" marker-end="url(#arrow-accent)"/>
|
||||
<text x="688" y="548" fill="#a8a69d" font-size="8.5" font-family="'Geist', sans-serif">Primary response</text>
|
||||
|
||||
</svg>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
386
skills/assets/example-sequence-full.html
Normal file
386
skills/assets/example-sequence-full.html
Normal file
@@ -0,0 +1,386 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Article Request · Sequence</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
:root {
|
||||
--color-paper: #f5f4ed;
|
||||
--color-paper-2: #efeee5;
|
||||
--color-ink: #0b0d0b;
|
||||
--color-muted: #52534e;
|
||||
--color-soft: #65655c;
|
||||
--color-rule: rgba(11,13,11,0.12);
|
||||
--color-rule-solid: rgba(135,139,134,0.25);
|
||||
--color-accent: #f7591f;
|
||||
--color-accent-tint: rgba(247,89,31,0.08);
|
||||
--color-link: #1a70c7;
|
||||
--font-sans: 'Geist', system-ui, sans-serif;
|
||||
--font-serif: 'Instrument Serif', 'Times New Roman', serif;
|
||||
--font-mono: 'Geist Mono', ui-monospace, Menlo, monospace;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-sans);
|
||||
background: var(--color-paper);
|
||||
min-height: 100vh;
|
||||
padding: 3rem 2rem;
|
||||
color: var(--color-ink);
|
||||
}
|
||||
|
||||
.container { max-width: 1200px; margin: 0 auto; }
|
||||
|
||||
.header { margin-bottom: 2.5rem; }
|
||||
|
||||
.header-eyebrow {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.66rem;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.18em;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-muted);
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-family: var(--font-serif);
|
||||
font-size: clamp(1.75rem, 3vw + 1rem, 2.5rem);
|
||||
font-weight: 400;
|
||||
letter-spacing: -0.02em;
|
||||
line-height: 1.1;
|
||||
color: var(--color-ink);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-family: var(--font-sans);
|
||||
font-size: 1rem;
|
||||
line-height: 1.55;
|
||||
color: var(--color-muted);
|
||||
max-width: 58ch;
|
||||
}
|
||||
|
||||
.diagram-container {
|
||||
background: var(--color-paper-2);
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--color-rule);
|
||||
padding: 1.5rem;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
svg { width: 100%; min-width: 900px; display: block; }
|
||||
|
||||
.cards {
|
||||
display: grid;
|
||||
grid-template-columns: 1.1fr 1fr 0.9fr;
|
||||
gap: 1rem;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
@media (max-width: 820px) {
|
||||
.cards { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
.card {
|
||||
background: #ffffff;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--color-rule);
|
||||
padding: 1.25rem;
|
||||
}
|
||||
|
||||
.card .eyebrow {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.5rem;
|
||||
letter-spacing: 0.18em;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-muted);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.6rem;
|
||||
margin-bottom: 0.875rem;
|
||||
padding-bottom: 0.875rem;
|
||||
border-bottom: 1px solid rgba(11,13,11,0.08);
|
||||
}
|
||||
|
||||
.card-dot {
|
||||
width: 7px; height: 7px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.card-dot.ink { background: var(--color-ink); }
|
||||
.card-dot.muted { background: var(--color-muted); }
|
||||
.card-dot.coral { background: var(--color-accent); }
|
||||
.card-dot.link { background: var(--color-link); }
|
||||
|
||||
.card h3 {
|
||||
font-family: var(--font-sans);
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-ink);
|
||||
letter-spacing: -0.005em;
|
||||
}
|
||||
|
||||
.card p {
|
||||
color: var(--color-muted);
|
||||
font-size: 0.8125rem;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.card ul {
|
||||
list-style: none;
|
||||
color: var(--color-muted);
|
||||
font-size: 0.8125rem;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.card li {
|
||||
margin-bottom: 0.3rem;
|
||||
padding-left: 0.875rem;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.card li::before {
|
||||
content: '—';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
color: rgba(11,13,11,0.25);
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.footer {
|
||||
margin-top: 2rem;
|
||||
padding-top: 1.5rem;
|
||||
border-top: 1px solid rgba(11,13,11,0.10);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.72rem;
|
||||
letter-spacing: 0.06em;
|
||||
color: var(--color-soft);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
|
||||
<!-- Header -->
|
||||
<div class="header">
|
||||
<p class="header-eyebrow">Sequence · Diagram Design</p>
|
||||
<h1>Article request, cold cache</h1>
|
||||
<p class="subtitle">How littlemight.com serves a reader when the requested slug isn't already sitting in Cloudflare's edge cache — origin render, beacon, and back in one round trip.</p>
|
||||
</div>
|
||||
|
||||
<!-- Sequence Diagram -->
|
||||
<div class="diagram-container">
|
||||
<svg viewBox="0 0 1000 584" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<!-- Dot grid background -->
|
||||
<pattern id="dots" width="22" height="22" patternUnits="userSpaceOnUse">
|
||||
<circle cx="1" cy="1" r="0.9" fill="rgba(11,13,11,0.10)"/>
|
||||
</pattern>
|
||||
|
||||
<!-- Arrow markers -->
|
||||
<marker id="arrow" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
|
||||
<polygon points="0 0, 8 3, 0 6" fill="#52534e"/>
|
||||
</marker>
|
||||
<marker id="arrow-accent" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
|
||||
<polygon points="0 0, 8 3, 0 6" fill="#f7591f"/>
|
||||
</marker>
|
||||
<marker id="arrow-link" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
|
||||
<polygon points="0 0, 8 3, 0 6" fill="#1a70c7"/>
|
||||
</marker>
|
||||
</defs>
|
||||
|
||||
<!-- Background: paper + dot grid -->
|
||||
<rect width="100%" height="100%" fill="#f5f4ed"/>
|
||||
<rect width="100%" height="100%" fill="url(#dots)" opacity="0.55"/>
|
||||
|
||||
<!-- =================================================================
|
||||
LIFELINES — dashed vertical lines from each actor, behind everything.
|
||||
Actor lifeline x-coords: 128, 352, 584, 800
|
||||
================================================================= -->
|
||||
<line x1="128" y1="128" x2="128" y2="488" stroke="rgba(11,13,11,0.22)" stroke-width="1" stroke-dasharray="3,3"/>
|
||||
<line x1="352" y1="128" x2="352" y2="488" stroke="rgba(11,13,11,0.22)" stroke-width="1" stroke-dasharray="3,3"/>
|
||||
<line x1="584" y1="128" x2="584" y2="488" stroke="rgba(11,13,11,0.22)" stroke-width="1" stroke-dasharray="3,3"/>
|
||||
<line x1="800" y1="128" x2="800" y2="488" stroke="rgba(11,13,11,0.22)" stroke-width="1" stroke-dasharray="3,3"/>
|
||||
|
||||
<!-- =================================================================
|
||||
ACTIVATION BARS — w=8 rects on lifelines showing control duration.
|
||||
Drawn before message arrows so arrows land on their edges.
|
||||
================================================================= -->
|
||||
<!-- Cloudflare activation: receives at y=176, responds at y=408 -->
|
||||
<rect x="348" y="180" width="8" height="232" fill="rgba(11,13,11,0.06)" stroke="#52534e" stroke-width="0.8"/>
|
||||
<!-- Astro activation: called at y=232, returns at y=352 -->
|
||||
<rect x="580" y="236" width="8" height="120" fill="rgba(11,13,11,0.06)" stroke="#52534e" stroke-width="0.8"/>
|
||||
|
||||
<!-- =================================================================
|
||||
MESSAGE ARROWS — time flows top→down.
|
||||
Draw before labels so label masks cover the line.
|
||||
================================================================= -->
|
||||
|
||||
<!-- M1: Reader → Cloudflare (HTTPS request · link-blue) -->
|
||||
<line x1="128" y1="176" x2="352" y2="176" stroke="#1a70c7" stroke-width="1.2" marker-end="url(#arrow-link)"/>
|
||||
|
||||
<!-- M2: Cloudflare → Astro (cache miss · muted) -->
|
||||
<line x1="352" y1="232" x2="580" y2="232" stroke="#52534e" stroke-width="1.2" marker-end="url(#arrow)"/>
|
||||
|
||||
<!-- M3: Astro self-message (render MDX · muted U-loop) -->
|
||||
<path d="M 588 284 L 624 284 L 624 316 L 588 316" fill="none" stroke="#52534e" stroke-width="1.2" marker-end="url(#arrow)"/>
|
||||
|
||||
<!-- M4: Astro → Cloudflare (return HTML · muted dashed) -->
|
||||
<line x1="580" y1="352" x2="356" y2="352" stroke="#52534e" stroke-width="1.2" stroke-dasharray="5,4" marker-end="url(#arrow)"/>
|
||||
|
||||
<!-- M5: Cloudflare → Reader (primary success · coral) -->
|
||||
<line x1="348" y1="408" x2="128" y2="408" stroke="#f7591f" stroke-width="1.4" marker-end="url(#arrow-accent)"/>
|
||||
|
||||
<!-- M6: Reader → Analytics (async beacon · muted dashed) -->
|
||||
<line x1="128" y1="464" x2="800" y2="464" stroke="#52534e" stroke-width="1.2" stroke-dasharray="5,4" marker-end="url(#arrow)"/>
|
||||
|
||||
<!-- =================================================================
|
||||
MESSAGE LABELS — each with an opaque paper-colored mask.
|
||||
================================================================= -->
|
||||
|
||||
<!-- M1 label -->
|
||||
<rect x="188" y="160" width="104" height="12" rx="2" fill="#f5f4ed"/>
|
||||
<text x="240" y="170" fill="#1a70c7" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">GET /ARTICLES/SLUG</text>
|
||||
|
||||
<!-- M2 label -->
|
||||
<rect x="416" y="216" width="100" height="12" rx="2" fill="#f5f4ed"/>
|
||||
<text x="466" y="226" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">CACHE MISS · ORIGIN</text>
|
||||
|
||||
<!-- M3 label (to the right of the self-loop) -->
|
||||
<rect x="632" y="292" width="72" height="12" rx="2" fill="#f5f4ed"/>
|
||||
<text x="668" y="302" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">RENDER MDX</text>
|
||||
|
||||
<!-- M4 label -->
|
||||
<rect x="420" y="336" width="96" height="12" rx="2" fill="#f5f4ed"/>
|
||||
<text x="468" y="346" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">200 · HTML + MAX-AGE</text>
|
||||
|
||||
<!-- M5 label (coral · primary response) -->
|
||||
<rect x="192" y="392" width="96" height="12" rx="2" fill="#f5f4ed"/>
|
||||
<text x="240" y="402" fill="#f7591f" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">200 · EDGE-CACHED</text>
|
||||
|
||||
<!-- M6 label (placed between Astro and Analytics lifelines, a clear gap) -->
|
||||
<rect x="648" y="448" width="96" height="12" rx="2" fill="#f5f4ed"/>
|
||||
<text x="696" y="458" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">PAGEVIEW BEACON</text>
|
||||
|
||||
<!-- =================================================================
|
||||
ACTOR BOXES — drawn after arrows/labels.
|
||||
Each actor: 144–160 wide × 56 tall. Centers: 128, 352, 584, 800.
|
||||
================================================================= -->
|
||||
|
||||
<!-- Actor 1: Reader (external / soft) -->
|
||||
<rect x="56" y="72" width="144" height="56" rx="6" fill="#f5f4ed"/>
|
||||
<rect x="56" y="72" width="144" height="56" rx="6" fill="rgba(82,83,78,0.10)" stroke="#65655c" stroke-width="1"/>
|
||||
<rect x="64" y="80" width="28" height="12" rx="2" fill="transparent" stroke="rgba(101,101,92,0.40)" stroke-width="0.8"/>
|
||||
<text x="78" y="89" fill="#65655c" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">EXT</text>
|
||||
<text x="128" y="104" fill="#0b0d0b" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Reader</text>
|
||||
<text x="128" y="119" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">Browser</text>
|
||||
|
||||
<!-- Actor 2: Cloudflare (cloud / muted) -->
|
||||
<rect x="280" y="72" width="144" height="56" rx="6" fill="#f5f4ed"/>
|
||||
<rect x="280" y="72" width="144" height="56" rx="6" fill="rgba(11,13,11,0.03)" stroke="rgba(11,13,11,0.30)" stroke-width="1"/>
|
||||
<rect x="288" y="80" width="32" height="12" rx="2" fill="transparent" stroke="rgba(11,13,11,0.22)" stroke-width="0.8"/>
|
||||
<text x="304" y="89" fill="#65655c" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">EDGE</text>
|
||||
<text x="352" y="104" fill="#0b0d0b" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Cloudflare</text>
|
||||
<text x="352" y="119" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">Pages · cache</text>
|
||||
|
||||
<!-- Actor 3: Astro Origin (focal / coral) -->
|
||||
<rect x="504" y="72" width="160" height="56" rx="6" fill="#f5f4ed"/>
|
||||
<rect x="504" y="72" width="160" height="56" rx="6" fill="rgba(247,89,31,0.08)" stroke="#f7591f" stroke-width="1"/>
|
||||
<rect x="512" y="80" width="32" height="12" rx="2" fill="transparent" stroke="rgba(247,89,31,0.50)" stroke-width="0.8"/>
|
||||
<text x="528" y="89" fill="#f7591f" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">ORIG</text>
|
||||
<text x="584" y="104" fill="#0b0d0b" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Astro Origin</text>
|
||||
<text x="584" y="119" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">SSR + MDX</text>
|
||||
|
||||
<!-- Actor 4: Analytics (optional / dashed) -->
|
||||
<rect x="728" y="72" width="144" height="56" rx="6" fill="#f5f4ed"/>
|
||||
<rect x="728" y="72" width="144" height="56" rx="6" fill="rgba(11,13,11,0.02)" stroke="rgba(11,13,11,0.22)" stroke-width="1" stroke-dasharray="4,3"/>
|
||||
<rect x="736" y="80" width="28" height="12" rx="2" fill="transparent" stroke="rgba(11,13,11,0.22)" stroke-width="0.8"/>
|
||||
<text x="750" y="89" fill="#65655c" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">ASY</text>
|
||||
<text x="800" y="104" fill="#0b0d0b" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Analytics</text>
|
||||
<text x="800" y="119" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">Beacon · async</text>
|
||||
|
||||
<!-- =================================================================
|
||||
LEGEND — horizontal strip at the bottom.
|
||||
Separator at y=504, eyebrow at y=520, items at y=540–548.
|
||||
================================================================= -->
|
||||
<line x1="56" y1="504" x2="944" y2="504" stroke="rgba(11,13,11,0.10)" stroke-width="0.8"/>
|
||||
<text x="56" y="520" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">LEGEND</text>
|
||||
|
||||
<!-- Item 1: Actor swatch (coral focal) -->
|
||||
<rect x="56" y="540" width="14" height="10" rx="2" fill="rgba(247,89,31,0.08)" stroke="#f7591f" stroke-width="1"/>
|
||||
<text x="76" y="548" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Focal actor</text>
|
||||
|
||||
<!-- Item 2: Activation bar swatch -->
|
||||
<rect x="188" y="536" width="4" height="18" fill="rgba(11,13,11,0.06)" stroke="#52534e" stroke-width="0.8"/>
|
||||
<text x="200" y="548" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Activation</text>
|
||||
|
||||
<!-- Item 3: Request arrow (link-blue) -->
|
||||
<line x1="308" y1="546" x2="336" y2="546" stroke="#1a70c7" stroke-width="1.2" marker-end="url(#arrow-link)"/>
|
||||
<text x="344" y="548" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">HTTP request</text>
|
||||
|
||||
<!-- Item 4: Return / async (muted dashed) -->
|
||||
<line x1="476" y1="546" x2="504" y2="546" stroke="#52534e" stroke-width="1.2" stroke-dasharray="5,4" marker-end="url(#arrow)"/>
|
||||
<text x="512" y="548" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Return / async</text>
|
||||
|
||||
<!-- Item 5: Primary response (coral) -->
|
||||
<line x1="652" y1="546" x2="680" y2="546" stroke="#f7591f" stroke-width="1.4" marker-end="url(#arrow-accent)"/>
|
||||
<text x="688" y="548" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Primary response</text>
|
||||
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<!-- Summary cards -->
|
||||
<div class="cards">
|
||||
<div class="card">
|
||||
<p class="eyebrow">THE HEADLINE</p>
|
||||
<div class="card-header">
|
||||
<span class="card-dot coral"></span>
|
||||
<h3>Edge handles the hot path</h3>
|
||||
</div>
|
||||
<p>On subsequent reads the whole exchange collapses to M1 → M5. Cloudflare serves the cached HTML without waking the origin. The coral arrow is the only one the reader ever perceives.</p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<span class="card-dot ink"></span>
|
||||
<h3>Origin render on miss</h3>
|
||||
</div>
|
||||
<ul>
|
||||
<li>Astro SSRs MDX on cold cache</li>
|
||||
<li>Returns HTML + cache headers</li>
|
||||
<li>Edge stores the result</li>
|
||||
<li>Next reader skips the origin trip</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<span class="card-dot muted"></span>
|
||||
<h3>Analytics is fire-and-forget</h3>
|
||||
</div>
|
||||
<p>The pageview beacon is dashed for a reason: the reader never waits on it, and a failed beacon never breaks the page.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="footer">
|
||||
<span>littlemight.com · article request sequence</span>
|
||||
<span>example · diagram-design</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
220
skills/assets/example-sequence.html
Normal file
220
skills/assets/example-sequence.html
Normal file
@@ -0,0 +1,220 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Article request · Sequence</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
:root {
|
||||
--color-paper: #f5f4ed;
|
||||
--color-ink: #0b0d0b;
|
||||
--color-muted: #52534e;
|
||||
--color-accent: #f7591f;
|
||||
--font-sans: 'Geist', system-ui, sans-serif;
|
||||
--font-serif: 'Instrument Serif', serif;
|
||||
--font-mono: 'Geist Mono', ui-monospace, monospace;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-sans);
|
||||
background: var(--color-paper);
|
||||
color: var(--color-ink);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 3rem 2rem;
|
||||
}
|
||||
|
||||
.frame { max-width: 1200px; width: 100%; }
|
||||
|
||||
.eyebrow {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.66rem;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.18em;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-muted);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-family: var(--font-serif);
|
||||
font-size: clamp(1.5rem, 2.4vw + 0.75rem, 2rem);
|
||||
font-weight: 400;
|
||||
letter-spacing: -0.02em;
|
||||
line-height: 1.15;
|
||||
color: var(--color-ink);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
svg { width: 100%; min-width: 900px; display: block; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="frame">
|
||||
<p class="eyebrow">Sequence · Diagram Design</p>
|
||||
<h1>Article request, cold cache</h1>
|
||||
|
||||
<svg viewBox="0 0 1000 584" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<!-- Dot grid background -->
|
||||
<pattern id="dots" width="22" height="22" patternUnits="userSpaceOnUse">
|
||||
<circle cx="1" cy="1" r="0.9" fill="rgba(11,13,11,0.10)"/>
|
||||
</pattern>
|
||||
|
||||
<!-- Arrow markers -->
|
||||
<marker id="arrow" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
|
||||
<polygon points="0 0, 8 3, 0 6" fill="#52534e"/>
|
||||
</marker>
|
||||
<marker id="arrow-accent" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
|
||||
<polygon points="0 0, 8 3, 0 6" fill="#f7591f"/>
|
||||
</marker>
|
||||
<marker id="arrow-link" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
|
||||
<polygon points="0 0, 8 3, 0 6" fill="#1a70c7"/>
|
||||
</marker>
|
||||
</defs>
|
||||
|
||||
<!-- Background: paper + dot grid -->
|
||||
<rect width="100%" height="100%" fill="#f5f4ed"/>
|
||||
<rect width="100%" height="100%" fill="url(#dots)" opacity="0.55"/>
|
||||
|
||||
<!-- =================================================================
|
||||
LIFELINES — dashed vertical lines from each actor, behind everything.
|
||||
Actor lifeline x-coords: 128, 352, 584, 800
|
||||
================================================================= -->
|
||||
<line x1="128" y1="128" x2="128" y2="488" stroke="rgba(11,13,11,0.22)" stroke-width="1" stroke-dasharray="3,3"/>
|
||||
<line x1="352" y1="128" x2="352" y2="488" stroke="rgba(11,13,11,0.22)" stroke-width="1" stroke-dasharray="3,3"/>
|
||||
<line x1="584" y1="128" x2="584" y2="488" stroke="rgba(11,13,11,0.22)" stroke-width="1" stroke-dasharray="3,3"/>
|
||||
<line x1="800" y1="128" x2="800" y2="488" stroke="rgba(11,13,11,0.22)" stroke-width="1" stroke-dasharray="3,3"/>
|
||||
|
||||
<!-- =================================================================
|
||||
ACTIVATION BARS — w=8 rects on lifelines showing control duration.
|
||||
Drawn before message arrows so arrows land on their edges.
|
||||
================================================================= -->
|
||||
<!-- Cloudflare activation: receives at y=176, responds at y=408 -->
|
||||
<rect x="348" y="180" width="8" height="232" fill="rgba(11,13,11,0.06)" stroke="#52534e" stroke-width="0.8"/>
|
||||
<!-- Astro activation: called at y=232, returns at y=352 -->
|
||||
<rect x="580" y="236" width="8" height="120" fill="rgba(11,13,11,0.06)" stroke="#52534e" stroke-width="0.8"/>
|
||||
|
||||
<!-- =================================================================
|
||||
MESSAGE ARROWS — time flows top→down.
|
||||
Draw before labels so label masks cover the line.
|
||||
================================================================= -->
|
||||
|
||||
<!-- M1: Reader → Cloudflare (HTTPS request · link-blue) -->
|
||||
<line x1="128" y1="176" x2="352" y2="176" stroke="#1a70c7" stroke-width="1.2" marker-end="url(#arrow-link)"/>
|
||||
|
||||
<!-- M2: Cloudflare → Astro (cache miss · muted) -->
|
||||
<line x1="352" y1="232" x2="580" y2="232" stroke="#52534e" stroke-width="1.2" marker-end="url(#arrow)"/>
|
||||
|
||||
<!-- M3: Astro self-message (render MDX · muted U-loop) -->
|
||||
<path d="M 588 284 L 624 284 L 624 316 L 588 316" fill="none" stroke="#52534e" stroke-width="1.2" marker-end="url(#arrow)"/>
|
||||
|
||||
<!-- M4: Astro → Cloudflare (return HTML · muted dashed) -->
|
||||
<line x1="580" y1="352" x2="356" y2="352" stroke="#52534e" stroke-width="1.2" stroke-dasharray="5,4" marker-end="url(#arrow)"/>
|
||||
|
||||
<!-- M5: Cloudflare → Reader (primary success · coral) -->
|
||||
<line x1="348" y1="408" x2="128" y2="408" stroke="#f7591f" stroke-width="1.4" marker-end="url(#arrow-accent)"/>
|
||||
|
||||
<!-- M6: Reader → Analytics (async beacon · muted dashed) -->
|
||||
<line x1="128" y1="464" x2="800" y2="464" stroke="#52534e" stroke-width="1.2" stroke-dasharray="5,4" marker-end="url(#arrow)"/>
|
||||
|
||||
<!-- =================================================================
|
||||
MESSAGE LABELS — each with an opaque paper-colored mask.
|
||||
================================================================= -->
|
||||
|
||||
<!-- M1 label -->
|
||||
<rect x="188" y="160" width="104" height="12" rx="2" fill="#f5f4ed"/>
|
||||
<text x="240" y="170" fill="#1a70c7" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">GET /ARTICLES/SLUG</text>
|
||||
|
||||
<!-- M2 label -->
|
||||
<rect x="416" y="216" width="100" height="12" rx="2" fill="#f5f4ed"/>
|
||||
<text x="466" y="226" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">CACHE MISS · ORIGIN</text>
|
||||
|
||||
<!-- M3 label (to the right of the self-loop) -->
|
||||
<rect x="632" y="292" width="72" height="12" rx="2" fill="#f5f4ed"/>
|
||||
<text x="668" y="302" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">RENDER MDX</text>
|
||||
|
||||
<!-- M4 label -->
|
||||
<rect x="420" y="336" width="96" height="12" rx="2" fill="#f5f4ed"/>
|
||||
<text x="468" y="346" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">200 · HTML + MAX-AGE</text>
|
||||
|
||||
<!-- M5 label (coral · primary response) -->
|
||||
<rect x="192" y="392" width="96" height="12" rx="2" fill="#f5f4ed"/>
|
||||
<text x="240" y="402" fill="#f7591f" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">200 · EDGE-CACHED</text>
|
||||
|
||||
<!-- M6 label (placed between Astro and Analytics lifelines, a clear gap) -->
|
||||
<rect x="648" y="448" width="96" height="12" rx="2" fill="#f5f4ed"/>
|
||||
<text x="696" y="458" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">PAGEVIEW BEACON</text>
|
||||
|
||||
<!-- =================================================================
|
||||
ACTOR BOXES — drawn after arrows/labels.
|
||||
Each actor: 144–160 wide × 56 tall. Centers: 128, 352, 584, 800.
|
||||
================================================================= -->
|
||||
|
||||
<!-- Actor 1: Reader (external / soft) -->
|
||||
<rect x="56" y="72" width="144" height="56" rx="6" fill="#f5f4ed"/>
|
||||
<rect x="56" y="72" width="144" height="56" rx="6" fill="rgba(82,83,78,0.10)" stroke="#65655c" stroke-width="1"/>
|
||||
<rect x="64" y="80" width="28" height="12" rx="2" fill="transparent" stroke="rgba(101,101,92,0.40)" stroke-width="0.8"/>
|
||||
<text x="78" y="89" fill="#65655c" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">EXT</text>
|
||||
<text x="128" y="104" fill="#0b0d0b" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Reader</text>
|
||||
<text x="128" y="119" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">Browser</text>
|
||||
|
||||
<!-- Actor 2: Cloudflare (cloud / muted) -->
|
||||
<rect x="280" y="72" width="144" height="56" rx="6" fill="#f5f4ed"/>
|
||||
<rect x="280" y="72" width="144" height="56" rx="6" fill="rgba(11,13,11,0.03)" stroke="rgba(11,13,11,0.30)" stroke-width="1"/>
|
||||
<rect x="288" y="80" width="32" height="12" rx="2" fill="transparent" stroke="rgba(11,13,11,0.22)" stroke-width="0.8"/>
|
||||
<text x="304" y="89" fill="#65655c" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">EDGE</text>
|
||||
<text x="352" y="104" fill="#0b0d0b" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Cloudflare</text>
|
||||
<text x="352" y="119" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">Pages · cache</text>
|
||||
|
||||
<!-- Actor 3: Astro Origin (focal / coral) -->
|
||||
<rect x="504" y="72" width="160" height="56" rx="6" fill="#f5f4ed"/>
|
||||
<rect x="504" y="72" width="160" height="56" rx="6" fill="rgba(247,89,31,0.08)" stroke="#f7591f" stroke-width="1"/>
|
||||
<rect x="512" y="80" width="32" height="12" rx="2" fill="transparent" stroke="rgba(247,89,31,0.50)" stroke-width="0.8"/>
|
||||
<text x="528" y="89" fill="#f7591f" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">ORIG</text>
|
||||
<text x="584" y="104" fill="#0b0d0b" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Astro Origin</text>
|
||||
<text x="584" y="119" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">SSR + MDX</text>
|
||||
|
||||
<!-- Actor 4: Analytics (optional / dashed) -->
|
||||
<rect x="728" y="72" width="144" height="56" rx="6" fill="#f5f4ed"/>
|
||||
<rect x="728" y="72" width="144" height="56" rx="6" fill="rgba(11,13,11,0.02)" stroke="rgba(11,13,11,0.22)" stroke-width="1" stroke-dasharray="4,3"/>
|
||||
<rect x="736" y="80" width="28" height="12" rx="2" fill="transparent" stroke="rgba(11,13,11,0.22)" stroke-width="0.8"/>
|
||||
<text x="750" y="89" fill="#65655c" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">ASY</text>
|
||||
<text x="800" y="104" fill="#0b0d0b" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Analytics</text>
|
||||
<text x="800" y="119" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">Beacon · async</text>
|
||||
|
||||
<!-- =================================================================
|
||||
LEGEND — horizontal strip at the bottom.
|
||||
Separator at y=504, eyebrow at y=520, items at y=540–548.
|
||||
================================================================= -->
|
||||
<line x1="56" y1="504" x2="944" y2="504" stroke="rgba(11,13,11,0.10)" stroke-width="0.8"/>
|
||||
<text x="56" y="520" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">LEGEND</text>
|
||||
|
||||
<!-- Item 1: Actor swatch (coral focal) -->
|
||||
<rect x="56" y="540" width="14" height="10" rx="2" fill="rgba(247,89,31,0.08)" stroke="#f7591f" stroke-width="1"/>
|
||||
<text x="76" y="548" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Focal actor</text>
|
||||
|
||||
<!-- Item 2: Activation bar swatch -->
|
||||
<rect x="188" y="536" width="4" height="18" fill="rgba(11,13,11,0.06)" stroke="#52534e" stroke-width="0.8"/>
|
||||
<text x="200" y="548" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Activation</text>
|
||||
|
||||
<!-- Item 3: Request arrow (link-blue) -->
|
||||
<line x1="308" y1="546" x2="336" y2="546" stroke="#1a70c7" stroke-width="1.2" marker-end="url(#arrow-link)"/>
|
||||
<text x="344" y="548" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">HTTP request</text>
|
||||
|
||||
<!-- Item 4: Return / async (muted dashed) -->
|
||||
<line x1="476" y1="546" x2="504" y2="546" stroke="#52534e" stroke-width="1.2" stroke-dasharray="5,4" marker-end="url(#arrow)"/>
|
||||
<text x="512" y="548" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Return / async</text>
|
||||
|
||||
<!-- Item 5: Primary response (coral) -->
|
||||
<line x1="652" y1="546" x2="680" y2="546" stroke="#f7591f" stroke-width="1.4" marker-end="url(#arrow-accent)"/>
|
||||
<text x="688" y="548" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Primary response</text>
|
||||
|
||||
</svg>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
145
skills/assets/example-state-dark.html
Normal file
145
skills/assets/example-state-dark.html
Normal file
@@ -0,0 +1,145 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Article lifecycle · State machine</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
:root {
|
||||
--color-paper: #1c1a17;
|
||||
--color-ink: #f1efe7;
|
||||
--color-muted: #a8a69d;
|
||||
--color-accent: #ff6a30;
|
||||
--font-sans: 'Geist', system-ui, sans-serif;
|
||||
--font-serif: 'Instrument Serif', serif;
|
||||
--font-mono: 'Geist Mono', ui-monospace, monospace;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-sans);
|
||||
background: var(--color-paper);
|
||||
color: var(--color-ink);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 3rem 2rem;
|
||||
}
|
||||
|
||||
.frame { max-width: 1200px; width: 100%; }
|
||||
|
||||
.eyebrow {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.66rem;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.18em;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-muted);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-family: var(--font-serif);
|
||||
font-size: clamp(1.5rem, 2.4vw + 0.75rem, 2rem);
|
||||
font-weight: 400;
|
||||
letter-spacing: -0.02em;
|
||||
line-height: 1.15;
|
||||
color: var(--color-ink);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
svg { width: 100%; min-width: 900px; display: block; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="frame">
|
||||
<p class="eyebrow">State machine · Diagram Design</p>
|
||||
<h1>Article lifecycle</h1>
|
||||
|
||||
<svg viewBox="0 0 1000 460" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<pattern id="dots" width="22" height="22" patternUnits="userSpaceOnUse">
|
||||
<circle cx="1" cy="1" r="0.9" fill="rgba(241,239,231,0.10)"/>
|
||||
</pattern>
|
||||
<marker id="arrow" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0, 8 3, 0 6" fill="#a8a69d"/></marker>
|
||||
<marker id="arrow-accent" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0, 8 3, 0 6" fill="#ff6a30"/></marker>
|
||||
</defs>
|
||||
|
||||
<rect width="100%" height="100%" fill="#1c1a17"/>
|
||||
<rect width="100%" height="100%" fill="url(#dots)" opacity="0.55"/>
|
||||
|
||||
<!-- Transitions (drawn first) -->
|
||||
<!-- Start → Draft -->
|
||||
<line x1="68" y1="200" x2="120" y2="200" stroke="#a8a69d" stroke-width="1.2" marker-end="url(#arrow)"/>
|
||||
<!-- Draft → In Review -->
|
||||
<line x1="280" y1="200" x2="340" y2="200" stroke="#a8a69d" stroke-width="1.2" marker-end="url(#arrow)"/>
|
||||
<!-- In Review → Published (coral — happy path) -->
|
||||
<line x1="500" y1="200" x2="560" y2="200" stroke="#ff6a30" stroke-width="1.4" marker-end="url(#arrow-accent)"/>
|
||||
<!-- Published → Archived (down) -->
|
||||
<line x1="640" y1="240" x2="640" y2="300" stroke="#a8a69d" stroke-width="1.2" marker-end="url(#arrow)"/>
|
||||
<!-- Archived → End (down) -->
|
||||
<line x1="640" y1="400" x2="640" y2="432" stroke="#a8a69d" stroke-width="1.2" marker-end="url(#arrow)"/>
|
||||
<!-- In Review → Draft (curved back up-and-left) -->
|
||||
<path d="M 420 160 C 420 96, 200 96, 200 160" fill="none" stroke="#a8a69d" stroke-width="1.2" stroke-dasharray="5,4" marker-end="url(#arrow)"/>
|
||||
|
||||
<!-- Transition labels -->
|
||||
<rect x="172" y="184" width="48" height="12" rx="2" fill="#1c1a17"/>
|
||||
<text x="196" y="194" fill="#a8a69d" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">CREATE</text>
|
||||
|
||||
<rect x="288" y="184" width="48" height="12" rx="2" fill="#1c1a17"/>
|
||||
<text x="312" y="194" fill="#a8a69d" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">SUBMIT</text>
|
||||
|
||||
<rect x="500" y="184" width="60" height="12" rx="2" fill="#1c1a17"/>
|
||||
<text x="530" y="194" fill="#ff6a30" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">APPROVE</text>
|
||||
|
||||
<rect x="612" y="264" width="56" height="12" rx="2" fill="#1c1a17"/>
|
||||
<text x="640" y="274" fill="#a8a69d" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">EXPIRE</text>
|
||||
|
||||
<rect x="620" y="416" width="40" height="12" rx="2" fill="#1c1a17"/>
|
||||
<text x="640" y="426" fill="#a8a69d" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">PURGE</text>
|
||||
|
||||
<!-- Reject label on curved path -->
|
||||
<rect x="276" y="92" width="80" height="12" rx="2" fill="#1c1a17"/>
|
||||
<text x="316" y="102" fill="#a8a69d" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">REJECT · REVISE</text>
|
||||
|
||||
<!-- Start dot (filled ink) -->
|
||||
<circle cx="60" cy="200" r="6" fill="#f1efe7"/>
|
||||
|
||||
<!-- State: Draft -->
|
||||
<rect x="120" y="160" width="160" height="80" rx="8" fill="#2a2723" stroke="#f1efe7" stroke-width="1"/>
|
||||
<rect x="128" y="168" width="40" height="12" rx="2" fill="transparent" stroke="rgba(241,239,231,0.40)" stroke-width="0.8"/>
|
||||
<text x="148" y="177" fill="#f1efe7" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">STATE</text>
|
||||
<text x="200" y="208" fill="#f1efe7" font-size="14" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Draft</text>
|
||||
<text x="200" y="224" fill="#a8a69d" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">unpublished</text>
|
||||
|
||||
<!-- State: In Review -->
|
||||
<rect x="340" y="160" width="160" height="80" rx="8" fill="#2a2723" stroke="#f1efe7" stroke-width="1"/>
|
||||
<rect x="348" y="168" width="40" height="12" rx="2" fill="transparent" stroke="rgba(241,239,231,0.40)" stroke-width="0.8"/>
|
||||
<text x="368" y="177" fill="#f1efe7" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">STATE</text>
|
||||
<text x="420" y="208" fill="#f1efe7" font-size="14" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">In Review</text>
|
||||
<text x="420" y="224" fill="#a8a69d" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">awaiting approval</text>
|
||||
|
||||
<!-- State: Published (focal coral) -->
|
||||
<rect x="560" y="160" width="160" height="80" rx="8" fill="rgba(255,106,48,0.08)" stroke="#ff6a30" stroke-width="1"/>
|
||||
<rect x="568" y="168" width="40" height="12" rx="2" fill="transparent" stroke="rgba(255,106,48,0.50)" stroke-width="0.8"/>
|
||||
<text x="588" y="177" fill="#ff6a30" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">STATE</text>
|
||||
<text x="640" y="208" fill="#f1efe7" font-size="14" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Published</text>
|
||||
<text x="640" y="224" fill="#a8a69d" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">live on site</text>
|
||||
|
||||
<!-- State: Archived -->
|
||||
<rect x="560" y="300" width="160" height="100" rx="8" fill="rgba(241,239,231,0.05)" stroke="#a8a69d" stroke-width="1"/>
|
||||
<rect x="568" y="308" width="40" height="12" rx="2" fill="transparent" stroke="rgba(168,166,157,0.50)" stroke-width="0.8"/>
|
||||
<text x="588" y="317" fill="#a8a69d" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">STATE</text>
|
||||
<text x="640" y="348" fill="#f1efe7" font-size="14" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Archived</text>
|
||||
<text x="640" y="364" fill="#a8a69d" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">noindex · hidden</text>
|
||||
<text x="640" y="382" fill="#a8a69d" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">redirect retained</text>
|
||||
|
||||
<!-- End ring dot -->
|
||||
<circle cx="640" cy="440" r="8" fill="none" stroke="#f1efe7" stroke-width="1"/>
|
||||
<circle cx="640" cy="440" r="5" fill="#f1efe7"/>
|
||||
</svg>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
148
skills/assets/example-state-full.html
Normal file
148
skills/assets/example-state-full.html
Normal file
@@ -0,0 +1,148 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Article lifecycle · state machine</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
:root { --color-paper:#f5f4ed; --color-paper-2:#efeee5; --color-ink:#0b0d0b; --color-muted:#52534e; --color-soft:#65655c; --color-rule:rgba(11,13,11,0.12); --color-accent:#f7591f; --color-link:#1a70c7; --font-sans:'Geist',system-ui,sans-serif; --font-serif:'Instrument Serif',serif; --font-mono:'Geist Mono',ui-monospace,monospace; }
|
||||
body { font-family: var(--font-sans); background: var(--color-paper); min-height: 100vh; padding: 3rem 2rem; color: var(--color-ink); }
|
||||
.container { max-width: 1200px; margin: 0 auto; }
|
||||
.header { margin-bottom: 2.5rem; }
|
||||
.header-eyebrow { font-family: var(--font-mono); font-size: 0.66rem; font-weight: 500; letter-spacing: 0.18em; text-transform: uppercase; color: var(--color-muted); margin-bottom: 0.75rem; }
|
||||
h1 { font-family: var(--font-serif); font-size: clamp(1.75rem, 3vw + 1rem, 2.5rem); font-weight: 400; letter-spacing: -0.02em; line-height: 1.1; margin-bottom: 0.5rem; }
|
||||
.subtitle { font-size: 1rem; line-height: 1.55; color: var(--color-muted); max-width: 58ch; }
|
||||
.diagram-container { background: var(--color-paper-2); border-radius: 8px; border: 1px solid var(--color-rule); padding: 1.5rem; overflow-x: auto; }
|
||||
svg { width: 100%; min-width: 900px; display: block; }
|
||||
.cards { display: grid; grid-template-columns: 1.1fr 1fr 0.9fr; gap: 1rem; margin-top: 1.5rem; }
|
||||
@media (max-width: 820px) { .cards { grid-template-columns: 1fr; } }
|
||||
.card { background: #fff; border-radius: 6px; border: 1px solid var(--color-rule); padding: 1.25rem; }
|
||||
.card .eyebrow { font-family: var(--font-mono); font-size: 0.5rem; letter-spacing: 0.18em; text-transform: uppercase; color: var(--color-muted); margin-bottom: 0.5rem; }
|
||||
.card-header { display: flex; align-items: center; gap: 0.6rem; margin-bottom: 0.875rem; padding-bottom: 0.875rem; border-bottom: 1px solid rgba(11,13,11,0.08); }
|
||||
.card-dot { width: 7px; height: 7px; border-radius: 50%; }
|
||||
.card-dot.ink { background: var(--color-ink); } .card-dot.muted { background: var(--color-muted); } .card-dot.coral { background: var(--color-accent); }
|
||||
.card h3 { font-size: 0.875rem; font-weight: 600; }
|
||||
.card p, .card ul { color: var(--color-muted); font-size: 0.8125rem; line-height: 1.55; list-style: none; }
|
||||
.card li { margin-bottom: 0.3rem; padding-left: 0.875rem; position: relative; }
|
||||
.card li::before { content: '—'; position: absolute; left: 0; color: rgba(11,13,11,0.25); font-size: 0.75rem; }
|
||||
.footer { margin-top: 2rem; padding-top: 1.5rem; border-top: 1px solid rgba(11,13,11,0.10); font-family: var(--font-mono); font-size: 0.72rem; letter-spacing: 0.06em; color: var(--color-soft); display: flex; justify-content: space-between; flex-wrap: wrap; gap: 0.5rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<p class="header-eyebrow">State machine · Diagram Design</p>
|
||||
<h1>Article lifecycle</h1>
|
||||
<p class="subtitle">The four states a post passes through from draft to archive, with the rejection loop made explicit. Published is the state the team optimizes for — hence coral.</p>
|
||||
</div>
|
||||
|
||||
<div class="diagram-container">
|
||||
<svg viewBox="0 0 1000 460" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<pattern id="dots" width="22" height="22" patternUnits="userSpaceOnUse">
|
||||
<circle cx="1" cy="1" r="0.9" fill="rgba(11,13,11,0.10)"/>
|
||||
</pattern>
|
||||
<marker id="arrow" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0, 8 3, 0 6" fill="#52534e"/></marker>
|
||||
<marker id="arrow-accent" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0, 8 3, 0 6" fill="#f7591f"/></marker>
|
||||
</defs>
|
||||
|
||||
<rect width="100%" height="100%" fill="#f5f4ed"/>
|
||||
<rect width="100%" height="100%" fill="url(#dots)" opacity="0.55"/>
|
||||
|
||||
<!-- Transitions (drawn first) -->
|
||||
<!-- Start → Draft -->
|
||||
<line x1="68" y1="200" x2="120" y2="200" stroke="#52534e" stroke-width="1.2" marker-end="url(#arrow)"/>
|
||||
<!-- Draft → In Review -->
|
||||
<line x1="280" y1="200" x2="340" y2="200" stroke="#52534e" stroke-width="1.2" marker-end="url(#arrow)"/>
|
||||
<!-- In Review → Published (coral — happy path) -->
|
||||
<line x1="500" y1="200" x2="560" y2="200" stroke="#f7591f" stroke-width="1.4" marker-end="url(#arrow-accent)"/>
|
||||
<!-- Published → Archived (down) -->
|
||||
<line x1="640" y1="240" x2="640" y2="300" stroke="#52534e" stroke-width="1.2" marker-end="url(#arrow)"/>
|
||||
<!-- Archived → End (down) -->
|
||||
<line x1="640" y1="400" x2="640" y2="432" stroke="#52534e" stroke-width="1.2" marker-end="url(#arrow)"/>
|
||||
<!-- In Review → Draft (curved back up-and-left) -->
|
||||
<path d="M 420 160 C 420 96, 200 96, 200 160" fill="none" stroke="#52534e" stroke-width="1.2" stroke-dasharray="5,4" marker-end="url(#arrow)"/>
|
||||
|
||||
<!-- Transition labels -->
|
||||
<rect x="172" y="184" width="48" height="12" rx="2" fill="#f5f4ed"/>
|
||||
<text x="196" y="194" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">CREATE</text>
|
||||
|
||||
<rect x="288" y="184" width="48" height="12" rx="2" fill="#f5f4ed"/>
|
||||
<text x="312" y="194" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">SUBMIT</text>
|
||||
|
||||
<rect x="500" y="184" width="60" height="12" rx="2" fill="#f5f4ed"/>
|
||||
<text x="530" y="194" fill="#f7591f" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">APPROVE</text>
|
||||
|
||||
<rect x="612" y="264" width="56" height="12" rx="2" fill="#f5f4ed"/>
|
||||
<text x="640" y="274" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">EXPIRE</text>
|
||||
|
||||
<rect x="620" y="416" width="40" height="12" rx="2" fill="#f5f4ed"/>
|
||||
<text x="640" y="426" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">PURGE</text>
|
||||
|
||||
<!-- Reject label on curved path -->
|
||||
<rect x="276" y="92" width="80" height="12" rx="2" fill="#f5f4ed"/>
|
||||
<text x="316" y="102" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">REJECT · REVISE</text>
|
||||
|
||||
<!-- Start dot (filled ink) -->
|
||||
<circle cx="60" cy="200" r="6" fill="#0b0d0b"/>
|
||||
|
||||
<!-- State: Draft -->
|
||||
<rect x="120" y="160" width="160" height="80" rx="8" fill="#ffffff" stroke="#0b0d0b" stroke-width="1"/>
|
||||
<rect x="128" y="168" width="40" height="12" rx="2" fill="transparent" stroke="rgba(11,13,11,0.40)" stroke-width="0.8"/>
|
||||
<text x="148" y="177" fill="#0b0d0b" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">STATE</text>
|
||||
<text x="200" y="208" fill="#0b0d0b" font-size="14" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Draft</text>
|
||||
<text x="200" y="224" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">unpublished</text>
|
||||
|
||||
<!-- State: In Review -->
|
||||
<rect x="340" y="160" width="160" height="80" rx="8" fill="#ffffff" stroke="#0b0d0b" stroke-width="1"/>
|
||||
<rect x="348" y="168" width="40" height="12" rx="2" fill="transparent" stroke="rgba(11,13,11,0.40)" stroke-width="0.8"/>
|
||||
<text x="368" y="177" fill="#0b0d0b" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">STATE</text>
|
||||
<text x="420" y="208" fill="#0b0d0b" font-size="14" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">In Review</text>
|
||||
<text x="420" y="224" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">awaiting approval</text>
|
||||
|
||||
<!-- State: Published (focal coral) -->
|
||||
<rect x="560" y="160" width="160" height="80" rx="8" fill="rgba(247,89,31,0.08)" stroke="#f7591f" stroke-width="1"/>
|
||||
<rect x="568" y="168" width="40" height="12" rx="2" fill="transparent" stroke="rgba(247,89,31,0.50)" stroke-width="0.8"/>
|
||||
<text x="588" y="177" fill="#f7591f" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">STATE</text>
|
||||
<text x="640" y="208" fill="#0b0d0b" font-size="14" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Published</text>
|
||||
<text x="640" y="224" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">live on site</text>
|
||||
|
||||
<!-- State: Archived -->
|
||||
<rect x="560" y="300" width="160" height="100" rx="8" fill="rgba(11,13,11,0.05)" stroke="#52534e" stroke-width="1"/>
|
||||
<rect x="568" y="308" width="40" height="12" rx="2" fill="transparent" stroke="rgba(82,83,78,0.50)" stroke-width="0.8"/>
|
||||
<text x="588" y="317" fill="#52534e" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">STATE</text>
|
||||
<text x="640" y="348" fill="#0b0d0b" font-size="14" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Archived</text>
|
||||
<text x="640" y="364" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">noindex · hidden</text>
|
||||
<text x="640" y="382" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">redirect retained</text>
|
||||
|
||||
<!-- End ring dot -->
|
||||
<circle cx="640" cy="440" r="8" fill="none" stroke="#0b0d0b" stroke-width="1"/>
|
||||
<circle cx="640" cy="440" r="5" fill="#0b0d0b"/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div class="cards">
|
||||
<div class="card">
|
||||
<p class="eyebrow">THE HEADLINE</p>
|
||||
<div class="card-header"><span class="card-dot coral"></span><h3>Published is the win state</h3></div>
|
||||
<p>Everything flows toward Published. The rejection loop is dashed because it's a detour, not a failure — the article is still in-progress.</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-header"><span class="card-dot ink"></span><h3>Reject is a loop, not a dead-end</h3></div>
|
||||
<ul><li>Dashed to signal detour</li><li>Returns to Draft, not a new state</li><li>Review feedback is the action</li></ul>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-header"><span class="card-dot muted"></span><h3>Archive keeps the URL</h3></div>
|
||||
<p>Archived preserves the redirect map so inbound links survive. Only Purge removes the record entirely.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<span>article lifecycle · state machine</span>
|
||||
<span>example · diagram-design</span>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
145
skills/assets/example-state.html
Normal file
145
skills/assets/example-state.html
Normal file
@@ -0,0 +1,145 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Article lifecycle · State machine</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
:root {
|
||||
--color-paper: #f5f4ed;
|
||||
--color-ink: #0b0d0b;
|
||||
--color-muted: #52534e;
|
||||
--color-accent: #f7591f;
|
||||
--font-sans: 'Geist', system-ui, sans-serif;
|
||||
--font-serif: 'Instrument Serif', serif;
|
||||
--font-mono: 'Geist Mono', ui-monospace, monospace;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-sans);
|
||||
background: var(--color-paper);
|
||||
color: var(--color-ink);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 3rem 2rem;
|
||||
}
|
||||
|
||||
.frame { max-width: 1200px; width: 100%; }
|
||||
|
||||
.eyebrow {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.66rem;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.18em;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-muted);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-family: var(--font-serif);
|
||||
font-size: clamp(1.5rem, 2.4vw + 0.75rem, 2rem);
|
||||
font-weight: 400;
|
||||
letter-spacing: -0.02em;
|
||||
line-height: 1.15;
|
||||
color: var(--color-ink);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
svg { width: 100%; min-width: 900px; display: block; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="frame">
|
||||
<p class="eyebrow">State machine · Diagram Design</p>
|
||||
<h1>Article lifecycle</h1>
|
||||
|
||||
<svg viewBox="0 0 1000 460" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<pattern id="dots" width="22" height="22" patternUnits="userSpaceOnUse">
|
||||
<circle cx="1" cy="1" r="0.9" fill="rgba(11,13,11,0.10)"/>
|
||||
</pattern>
|
||||
<marker id="arrow" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0, 8 3, 0 6" fill="#52534e"/></marker>
|
||||
<marker id="arrow-accent" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0, 8 3, 0 6" fill="#f7591f"/></marker>
|
||||
</defs>
|
||||
|
||||
<rect width="100%" height="100%" fill="#f5f4ed"/>
|
||||
<rect width="100%" height="100%" fill="url(#dots)" opacity="0.55"/>
|
||||
|
||||
<!-- Transitions (drawn first) -->
|
||||
<!-- Start → Draft -->
|
||||
<line x1="68" y1="200" x2="120" y2="200" stroke="#52534e" stroke-width="1.2" marker-end="url(#arrow)"/>
|
||||
<!-- Draft → In Review -->
|
||||
<line x1="280" y1="200" x2="340" y2="200" stroke="#52534e" stroke-width="1.2" marker-end="url(#arrow)"/>
|
||||
<!-- In Review → Published (coral — happy path) -->
|
||||
<line x1="500" y1="200" x2="560" y2="200" stroke="#f7591f" stroke-width="1.4" marker-end="url(#arrow-accent)"/>
|
||||
<!-- Published → Archived (down) -->
|
||||
<line x1="640" y1="240" x2="640" y2="300" stroke="#52534e" stroke-width="1.2" marker-end="url(#arrow)"/>
|
||||
<!-- Archived → End (down) -->
|
||||
<line x1="640" y1="400" x2="640" y2="432" stroke="#52534e" stroke-width="1.2" marker-end="url(#arrow)"/>
|
||||
<!-- In Review → Draft (curved back up-and-left) -->
|
||||
<path d="M 420 160 C 420 96, 200 96, 200 160" fill="none" stroke="#52534e" stroke-width="1.2" stroke-dasharray="5,4" marker-end="url(#arrow)"/>
|
||||
|
||||
<!-- Transition labels -->
|
||||
<rect x="172" y="184" width="48" height="12" rx="2" fill="#f5f4ed"/>
|
||||
<text x="196" y="194" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">CREATE</text>
|
||||
|
||||
<rect x="288" y="184" width="48" height="12" rx="2" fill="#f5f4ed"/>
|
||||
<text x="312" y="194" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">SUBMIT</text>
|
||||
|
||||
<rect x="500" y="184" width="60" height="12" rx="2" fill="#f5f4ed"/>
|
||||
<text x="530" y="194" fill="#f7591f" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">APPROVE</text>
|
||||
|
||||
<rect x="612" y="264" width="56" height="12" rx="2" fill="#f5f4ed"/>
|
||||
<text x="640" y="274" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">EXPIRE</text>
|
||||
|
||||
<rect x="620" y="416" width="40" height="12" rx="2" fill="#f5f4ed"/>
|
||||
<text x="640" y="426" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">PURGE</text>
|
||||
|
||||
<!-- Reject label on curved path -->
|
||||
<rect x="276" y="92" width="80" height="12" rx="2" fill="#f5f4ed"/>
|
||||
<text x="316" y="102" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">REJECT · REVISE</text>
|
||||
|
||||
<!-- Start dot (filled ink) -->
|
||||
<circle cx="60" cy="200" r="6" fill="#0b0d0b"/>
|
||||
|
||||
<!-- State: Draft -->
|
||||
<rect x="120" y="160" width="160" height="80" rx="8" fill="#ffffff" stroke="#0b0d0b" stroke-width="1"/>
|
||||
<rect x="128" y="168" width="40" height="12" rx="2" fill="transparent" stroke="rgba(11,13,11,0.40)" stroke-width="0.8"/>
|
||||
<text x="148" y="177" fill="#0b0d0b" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">STATE</text>
|
||||
<text x="200" y="208" fill="#0b0d0b" font-size="14" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Draft</text>
|
||||
<text x="200" y="224" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">unpublished</text>
|
||||
|
||||
<!-- State: In Review -->
|
||||
<rect x="340" y="160" width="160" height="80" rx="8" fill="#ffffff" stroke="#0b0d0b" stroke-width="1"/>
|
||||
<rect x="348" y="168" width="40" height="12" rx="2" fill="transparent" stroke="rgba(11,13,11,0.40)" stroke-width="0.8"/>
|
||||
<text x="368" y="177" fill="#0b0d0b" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">STATE</text>
|
||||
<text x="420" y="208" fill="#0b0d0b" font-size="14" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">In Review</text>
|
||||
<text x="420" y="224" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">awaiting approval</text>
|
||||
|
||||
<!-- State: Published (focal coral) -->
|
||||
<rect x="560" y="160" width="160" height="80" rx="8" fill="rgba(247,89,31,0.08)" stroke="#f7591f" stroke-width="1"/>
|
||||
<rect x="568" y="168" width="40" height="12" rx="2" fill="transparent" stroke="rgba(247,89,31,0.50)" stroke-width="0.8"/>
|
||||
<text x="588" y="177" fill="#f7591f" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">STATE</text>
|
||||
<text x="640" y="208" fill="#0b0d0b" font-size="14" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Published</text>
|
||||
<text x="640" y="224" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">live on site</text>
|
||||
|
||||
<!-- State: Archived -->
|
||||
<rect x="560" y="300" width="160" height="100" rx="8" fill="rgba(11,13,11,0.05)" stroke="#52534e" stroke-width="1"/>
|
||||
<rect x="568" y="308" width="40" height="12" rx="2" fill="transparent" stroke="rgba(82,83,78,0.50)" stroke-width="0.8"/>
|
||||
<text x="588" y="317" fill="#52534e" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">STATE</text>
|
||||
<text x="640" y="348" fill="#0b0d0b" font-size="14" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Archived</text>
|
||||
<text x="640" y="364" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">noindex · hidden</text>
|
||||
<text x="640" y="382" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">redirect retained</text>
|
||||
|
||||
<!-- End ring dot -->
|
||||
<circle cx="640" cy="440" r="8" fill="none" stroke="#0b0d0b" stroke-width="1"/>
|
||||
<circle cx="640" cy="440" r="5" fill="#0b0d0b"/>
|
||||
</svg>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
170
skills/assets/example-swimlane-dark.html
Normal file
170
skills/assets/example-swimlane-dark.html
Normal file
@@ -0,0 +1,170 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Publishing an article · Swimlane</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
:root {
|
||||
--color-paper: #1c1a17;
|
||||
--color-ink: #f1efe7;
|
||||
--color-muted: #a8a69d;
|
||||
--color-accent: #ff6a30;
|
||||
--font-sans: 'Geist', system-ui, sans-serif;
|
||||
--font-serif: 'Instrument Serif', serif;
|
||||
--font-mono: 'Geist Mono', ui-monospace, monospace;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-sans);
|
||||
background: var(--color-paper);
|
||||
color: var(--color-ink);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 3rem 2rem;
|
||||
}
|
||||
|
||||
.frame { max-width: 1200px; width: 100%; }
|
||||
|
||||
.eyebrow {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.66rem;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.18em;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-muted);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-family: var(--font-serif);
|
||||
font-size: clamp(1.5rem, 2.4vw + 0.75rem, 2rem);
|
||||
font-weight: 400;
|
||||
letter-spacing: -0.02em;
|
||||
line-height: 1.15;
|
||||
color: var(--color-ink);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
svg { width: 100%; min-width: 900px; display: block; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="frame">
|
||||
<p class="eyebrow">Swimlane · Diagram Design</p>
|
||||
<h1>Publishing an article</h1>
|
||||
|
||||
<svg viewBox="0 0 1000 480" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<pattern id="dots" width="22" height="22" patternUnits="userSpaceOnUse">
|
||||
<circle cx="1" cy="1" r="0.9" fill="rgba(241,239,231,0.10)"/>
|
||||
</pattern>
|
||||
<marker id="arrow" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0, 8 3, 0 6" fill="#a8a69d"/></marker>
|
||||
<marker id="arrow-accent" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0, 8 3, 0 6" fill="#ff6a30"/></marker>
|
||||
</defs>
|
||||
|
||||
<rect width="100%" height="100%" fill="#1c1a17"/>
|
||||
<rect width="100%" height="100%" fill="url(#dots)" opacity="0.55"/>
|
||||
|
||||
<!-- Lane dividers -->
|
||||
<line x1="40" y1="80" x2="960" y2="80" stroke="rgba(241,239,231,0.22)" stroke-width="1"/>
|
||||
<line x1="40" y1="160" x2="960" y2="160" stroke="rgba(241,239,231,0.10)" stroke-width="1"/>
|
||||
<line x1="40" y1="240" x2="960" y2="240" stroke="rgba(241,239,231,0.10)" stroke-width="1"/>
|
||||
<line x1="40" y1="320" x2="960" y2="320" stroke="rgba(241,239,231,0.10)" stroke-width="1"/>
|
||||
<line x1="40" y1="400" x2="960" y2="400" stroke="rgba(241,239,231,0.22)" stroke-width="1"/>
|
||||
|
||||
<!-- Actor column divider -->
|
||||
<line x1="160" y1="80" x2="160" y2="400" stroke="rgba(241,239,231,0.22)" stroke-width="1"/>
|
||||
|
||||
<!-- Lane labels -->
|
||||
<text x="60" y="124" fill="#a8a69d" font-size="9" font-family="'Geist Mono', monospace" letter-spacing="0.18em">AUTHOR</text>
|
||||
<text x="60" y="204" fill="#a8a69d" font-size="9" font-family="'Geist Mono', monospace" letter-spacing="0.18em">REVIEWER</text>
|
||||
<text x="60" y="284" fill="#a8a69d" font-size="9" font-family="'Geist Mono', monospace" letter-spacing="0.18em">EDITOR</text>
|
||||
<text x="60" y="364" fill="#a8a69d" font-size="9" font-family="'Geist Mono', monospace" letter-spacing="0.18em">CI / CD</text>
|
||||
|
||||
<!-- Arrows (drawn first) -->
|
||||
<!-- 1. Draft → Open PR (within Author) -->
|
||||
<line x1="300" y1="120" x2="340" y2="120" stroke="#a8a69d" stroke-width="1.2" marker-end="url(#arrow)"/>
|
||||
<!-- 2. Open PR → Review (Author → Reviewer handoff, diagonal down-right) -->
|
||||
<line x1="460" y1="144" x2="500" y2="176" stroke="#a8a69d" stroke-width="1.2" marker-end="url(#arrow)"/>
|
||||
<!-- 3. Review → Polish (Reviewer → Editor, vertical down) -->
|
||||
<line x1="570" y1="224" x2="570" y2="256" stroke="#a8a69d" stroke-width="1.2" stroke-dasharray="5,4" marker-end="url(#arrow)"/>
|
||||
<!-- 4. Polish → Approve (within Editor) -->
|
||||
<line x1="640" y1="280" x2="680" y2="280" stroke="#a8a69d" stroke-width="1.2" marker-end="url(#arrow)"/>
|
||||
<!-- 5. Approve → Build (Editor → CI/CD, coral handoff) -->
|
||||
<line x1="750" y1="304" x2="720" y2="336" stroke="#ff6a30" stroke-width="1.4" marker-end="url(#arrow-accent)"/>
|
||||
<!-- 6. Build → Deploy (within CI/CD) -->
|
||||
<line x1="760" y1="360" x2="800" y2="360" stroke="#a8a69d" stroke-width="1.2" marker-end="url(#arrow)"/>
|
||||
|
||||
<!-- Arrow labels -->
|
||||
<rect x="452" y="140" width="60" height="12" rx="2" fill="#1c1a17"/>
|
||||
<text x="482" y="150" fill="#a8a69d" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.12em">HANDOFF</text>
|
||||
|
||||
<rect x="548" y="228" width="52" height="12" rx="2" fill="#2a2723"/>
|
||||
<text x="574" y="238" fill="#a8a69d" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.12em">REVISE</text>
|
||||
|
||||
<rect x="712" y="308" width="80" height="12" rx="2" fill="#1c1a17"/>
|
||||
<text x="752" y="318" fill="#ff6a30" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.12em">DEPLOY TRIGGER</text>
|
||||
|
||||
<!-- Steps -->
|
||||
<!-- Author: Draft MDX -->
|
||||
<rect x="180" y="96" width="120" height="48" rx="6" fill="#2a2723" stroke="#f1efe7" stroke-width="1"/>
|
||||
<text x="240" y="118" fill="#f1efe7" font-size="11" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Draft MDX</text>
|
||||
<text x="240" y="132" fill="#a8a69d" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">src/content/…</text>
|
||||
|
||||
<!-- Author: Open PR -->
|
||||
<rect x="340" y="96" width="120" height="48" rx="6" fill="#2a2723" stroke="#f1efe7" stroke-width="1"/>
|
||||
<text x="400" y="118" fill="#f1efe7" font-size="11" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Open PR</text>
|
||||
<text x="400" y="132" fill="#a8a69d" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">gh pr create</text>
|
||||
|
||||
<!-- Reviewer: Review content -->
|
||||
<rect x="500" y="176" width="140" height="48" rx="6" fill="#2a2723" stroke="#f1efe7" stroke-width="1"/>
|
||||
<text x="570" y="198" fill="#f1efe7" font-size="11" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Review content</text>
|
||||
<text x="570" y="212" fill="#a8a69d" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">fact-check · voice</text>
|
||||
|
||||
<!-- Editor: Polish copy -->
|
||||
<rect x="500" y="256" width="140" height="48" rx="6" fill="#2a2723" stroke="#f1efe7" stroke-width="1"/>
|
||||
<text x="570" y="278" fill="#f1efe7" font-size="11" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Polish copy</text>
|
||||
<text x="570" y="292" fill="#a8a69d" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">style · line edits</text>
|
||||
|
||||
<!-- Editor: Approve merge -->
|
||||
<rect x="680" y="256" width="140" height="48" rx="6" fill="#2a2723" stroke="#f1efe7" stroke-width="1"/>
|
||||
<text x="750" y="278" fill="#f1efe7" font-size="11" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Approve merge</text>
|
||||
<text x="750" y="292" fill="#a8a69d" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">squash · main</text>
|
||||
|
||||
<!-- CI/CD: Build -->
|
||||
<rect x="680" y="336" width="80" height="48" rx="6" fill="#2a2723" stroke="#f1efe7" stroke-width="1"/>
|
||||
<text x="720" y="358" fill="#f1efe7" font-size="11" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Build</text>
|
||||
<text x="720" y="372" fill="#a8a69d" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">astro build</text>
|
||||
|
||||
<!-- CI/CD: Deploy (coral focal) -->
|
||||
<rect x="800" y="336" width="120" height="48" rx="6" fill="rgba(255,106,48,0.08)" stroke="#ff6a30" stroke-width="1"/>
|
||||
<text x="860" y="358" fill="#f1efe7" font-size="11" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Deploy</text>
|
||||
<text x="860" y="372" fill="#a8a69d" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">cloudflare pages</text>
|
||||
|
||||
<!-- Legend -->
|
||||
<line x1="40" y1="428" x2="960" y2="428" stroke="rgba(241,239,231,0.10)" stroke-width="0.8"/>
|
||||
<text x="40" y="444" fill="#a8a69d" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">LEGEND</text>
|
||||
|
||||
<rect x="40" y="460" width="14" height="10" rx="2" fill="#2a2723" stroke="#f1efe7" stroke-width="1"/>
|
||||
<text x="60" y="468" fill="#a8a69d" font-size="8.5" font-family="'Geist', sans-serif">Step</text>
|
||||
|
||||
<rect x="132" y="460" width="14" height="10" rx="2" fill="rgba(255,106,48,0.08)" stroke="#ff6a30" stroke-width="1"/>
|
||||
<text x="152" y="468" fill="#a8a69d" font-size="8.5" font-family="'Geist', sans-serif">Focal outcome</text>
|
||||
|
||||
<line x1="268" y1="466" x2="296" y2="466" stroke="#a8a69d" stroke-width="1.2" marker-end="url(#arrow)"/>
|
||||
<text x="304" y="468" fill="#a8a69d" font-size="8.5" font-family="'Geist', sans-serif">Within-lane step</text>
|
||||
|
||||
<line x1="444" y1="466" x2="472" y2="466" stroke="#a8a69d" stroke-width="1.2" stroke-dasharray="4,3" marker-end="url(#arrow)"/>
|
||||
<text x="480" y="468" fill="#a8a69d" font-size="8.5" font-family="'Geist', sans-serif">Revision loop</text>
|
||||
|
||||
<line x1="608" y1="466" x2="636" y2="466" stroke="#ff6a30" stroke-width="1.4" marker-end="url(#arrow-accent)"/>
|
||||
<text x="644" y="468" fill="#a8a69d" font-size="8.5" font-family="'Geist', sans-serif">Critical handoff</text>
|
||||
</svg>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
173
skills/assets/example-swimlane-full.html
Normal file
173
skills/assets/example-swimlane-full.html
Normal file
@@ -0,0 +1,173 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Publishing an article · Swimlane</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
:root { --color-paper:#f5f4ed; --color-paper-2:#efeee5; --color-ink:#0b0d0b; --color-muted:#52534e; --color-soft:#65655c; --color-rule:rgba(11,13,11,0.12); --color-accent:#f7591f; --color-link:#1a70c7; --font-sans:'Geist',system-ui,sans-serif; --font-serif:'Instrument Serif',serif; --font-mono:'Geist Mono',ui-monospace,monospace; }
|
||||
body { font-family: var(--font-sans); background: var(--color-paper); min-height: 100vh; padding: 3rem 2rem; color: var(--color-ink); }
|
||||
.container { max-width: 1200px; margin: 0 auto; }
|
||||
.header { margin-bottom: 2.5rem; }
|
||||
.header-eyebrow { font-family: var(--font-mono); font-size: 0.66rem; font-weight: 500; letter-spacing: 0.18em; text-transform: uppercase; color: var(--color-muted); margin-bottom: 0.75rem; }
|
||||
h1 { font-family: var(--font-serif); font-size: clamp(1.75rem, 3vw + 1rem, 2.5rem); font-weight: 400; letter-spacing: -0.02em; line-height: 1.1; margin-bottom: 0.5rem; }
|
||||
.subtitle { font-size: 1rem; line-height: 1.55; color: var(--color-muted); max-width: 58ch; }
|
||||
.diagram-container { background: var(--color-paper-2); border-radius: 8px; border: 1px solid var(--color-rule); padding: 1.5rem; overflow-x: auto; }
|
||||
svg { width: 100%; min-width: 900px; display: block; }
|
||||
.cards { display: grid; grid-template-columns: 1.1fr 1fr 0.9fr; gap: 1rem; margin-top: 1.5rem; }
|
||||
@media (max-width: 820px) { .cards { grid-template-columns: 1fr; } }
|
||||
.card { background: #fff; border-radius: 6px; border: 1px solid var(--color-rule); padding: 1.25rem; }
|
||||
.card .eyebrow { font-family: var(--font-mono); font-size: 0.5rem; letter-spacing: 0.18em; text-transform: uppercase; color: var(--color-muted); margin-bottom: 0.5rem; }
|
||||
.card-header { display: flex; align-items: center; gap: 0.6rem; margin-bottom: 0.875rem; padding-bottom: 0.875rem; border-bottom: 1px solid rgba(11,13,11,0.08); }
|
||||
.card-dot { width: 7px; height: 7px; border-radius: 50%; }
|
||||
.card-dot.ink { background: var(--color-ink); } .card-dot.muted { background: var(--color-muted); } .card-dot.coral { background: var(--color-accent); }
|
||||
.card h3 { font-size: 0.875rem; font-weight: 600; }
|
||||
.card p, .card ul { color: var(--color-muted); font-size: 0.8125rem; line-height: 1.55; list-style: none; }
|
||||
.card li { margin-bottom: 0.3rem; padding-left: 0.875rem; position: relative; }
|
||||
.card li::before { content: '—'; position: absolute; left: 0; color: rgba(11,13,11,0.25); font-size: 0.75rem; }
|
||||
.footer { margin-top: 2rem; padding-top: 1.5rem; border-top: 1px solid rgba(11,13,11,0.10); font-family: var(--font-mono); font-size: 0.72rem; letter-spacing: 0.06em; color: var(--color-soft); display: flex; justify-content: space-between; flex-wrap: wrap; gap: 0.5rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<p class="header-eyebrow">Swimlane · Diagram Design</p>
|
||||
<h1>Publishing an article</h1>
|
||||
<p class="subtitle">Four actors, seven steps, three handoffs. The step each person owns lives in their lane — arrows crossing lanes are where coordination happens.</p>
|
||||
</div>
|
||||
|
||||
<div class="diagram-container">
|
||||
<svg viewBox="0 0 1000 480" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<pattern id="dots" width="22" height="22" patternUnits="userSpaceOnUse">
|
||||
<circle cx="1" cy="1" r="0.9" fill="rgba(11,13,11,0.10)"/>
|
||||
</pattern>
|
||||
<marker id="arrow" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0, 8 3, 0 6" fill="#52534e"/></marker>
|
||||
<marker id="arrow-accent" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0, 8 3, 0 6" fill="#f7591f"/></marker>
|
||||
</defs>
|
||||
|
||||
<rect width="100%" height="100%" fill="#f5f4ed"/>
|
||||
<rect width="100%" height="100%" fill="url(#dots)" opacity="0.55"/>
|
||||
|
||||
<!-- Lane dividers -->
|
||||
<line x1="40" y1="80" x2="960" y2="80" stroke="rgba(11,13,11,0.22)" stroke-width="1"/>
|
||||
<line x1="40" y1="160" x2="960" y2="160" stroke="rgba(11,13,11,0.10)" stroke-width="1"/>
|
||||
<line x1="40" y1="240" x2="960" y2="240" stroke="rgba(11,13,11,0.10)" stroke-width="1"/>
|
||||
<line x1="40" y1="320" x2="960" y2="320" stroke="rgba(11,13,11,0.10)" stroke-width="1"/>
|
||||
<line x1="40" y1="400" x2="960" y2="400" stroke="rgba(11,13,11,0.22)" stroke-width="1"/>
|
||||
|
||||
<!-- Actor column divider -->
|
||||
<line x1="160" y1="80" x2="160" y2="400" stroke="rgba(11,13,11,0.22)" stroke-width="1"/>
|
||||
|
||||
<!-- Lane labels -->
|
||||
<text x="60" y="124" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" letter-spacing="0.18em">AUTHOR</text>
|
||||
<text x="60" y="204" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" letter-spacing="0.18em">REVIEWER</text>
|
||||
<text x="60" y="284" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" letter-spacing="0.18em">EDITOR</text>
|
||||
<text x="60" y="364" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" letter-spacing="0.18em">CI / CD</text>
|
||||
|
||||
<!-- Arrows (drawn first) -->
|
||||
<!-- 1. Draft → Open PR (within Author) -->
|
||||
<line x1="300" y1="120" x2="340" y2="120" stroke="#52534e" stroke-width="1.2" marker-end="url(#arrow)"/>
|
||||
<!-- 2. Open PR → Review (Author → Reviewer handoff, diagonal down-right) -->
|
||||
<line x1="460" y1="144" x2="500" y2="176" stroke="#52534e" stroke-width="1.2" marker-end="url(#arrow)"/>
|
||||
<!-- 3. Review → Polish (Reviewer → Editor, vertical down) -->
|
||||
<line x1="570" y1="224" x2="570" y2="256" stroke="#52534e" stroke-width="1.2" stroke-dasharray="5,4" marker-end="url(#arrow)"/>
|
||||
<!-- 4. Polish → Approve (within Editor) -->
|
||||
<line x1="640" y1="280" x2="680" y2="280" stroke="#52534e" stroke-width="1.2" marker-end="url(#arrow)"/>
|
||||
<!-- 5. Approve → Build (Editor → CI/CD, coral handoff) -->
|
||||
<line x1="750" y1="304" x2="720" y2="336" stroke="#f7591f" stroke-width="1.4" marker-end="url(#arrow-accent)"/>
|
||||
<!-- 6. Build → Deploy (within CI/CD) -->
|
||||
<line x1="760" y1="360" x2="800" y2="360" stroke="#52534e" stroke-width="1.2" marker-end="url(#arrow)"/>
|
||||
|
||||
<!-- Arrow labels -->
|
||||
<rect x="452" y="140" width="60" height="12" rx="2" fill="#f5f4ed"/>
|
||||
<text x="482" y="150" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.12em">HANDOFF</text>
|
||||
|
||||
<rect x="548" y="228" width="52" height="12" rx="2" fill="#efeee5"/>
|
||||
<text x="574" y="238" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.12em">REVISE</text>
|
||||
|
||||
<rect x="712" y="308" width="80" height="12" rx="2" fill="#f5f4ed"/>
|
||||
<text x="752" y="318" fill="#f7591f" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.12em">DEPLOY TRIGGER</text>
|
||||
|
||||
<!-- Steps -->
|
||||
<!-- Author: Draft MDX -->
|
||||
<rect x="180" y="96" width="120" height="48" rx="6" fill="#ffffff" stroke="#0b0d0b" stroke-width="1"/>
|
||||
<text x="240" y="118" fill="#0b0d0b" font-size="11" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Draft MDX</text>
|
||||
<text x="240" y="132" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">src/content/…</text>
|
||||
|
||||
<!-- Author: Open PR -->
|
||||
<rect x="340" y="96" width="120" height="48" rx="6" fill="#ffffff" stroke="#0b0d0b" stroke-width="1"/>
|
||||
<text x="400" y="118" fill="#0b0d0b" font-size="11" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Open PR</text>
|
||||
<text x="400" y="132" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">gh pr create</text>
|
||||
|
||||
<!-- Reviewer: Review content -->
|
||||
<rect x="500" y="176" width="140" height="48" rx="6" fill="#ffffff" stroke="#0b0d0b" stroke-width="1"/>
|
||||
<text x="570" y="198" fill="#0b0d0b" font-size="11" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Review content</text>
|
||||
<text x="570" y="212" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">fact-check · voice</text>
|
||||
|
||||
<!-- Editor: Polish copy -->
|
||||
<rect x="500" y="256" width="140" height="48" rx="6" fill="#ffffff" stroke="#0b0d0b" stroke-width="1"/>
|
||||
<text x="570" y="278" fill="#0b0d0b" font-size="11" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Polish copy</text>
|
||||
<text x="570" y="292" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">style · line edits</text>
|
||||
|
||||
<!-- Editor: Approve merge -->
|
||||
<rect x="680" y="256" width="140" height="48" rx="6" fill="#ffffff" stroke="#0b0d0b" stroke-width="1"/>
|
||||
<text x="750" y="278" fill="#0b0d0b" font-size="11" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Approve merge</text>
|
||||
<text x="750" y="292" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">squash · main</text>
|
||||
|
||||
<!-- CI/CD: Build -->
|
||||
<rect x="680" y="336" width="80" height="48" rx="6" fill="#ffffff" stroke="#0b0d0b" stroke-width="1"/>
|
||||
<text x="720" y="358" fill="#0b0d0b" font-size="11" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Build</text>
|
||||
<text x="720" y="372" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">astro build</text>
|
||||
|
||||
<!-- CI/CD: Deploy (coral focal) -->
|
||||
<rect x="800" y="336" width="120" height="48" rx="6" fill="rgba(247,89,31,0.08)" stroke="#f7591f" stroke-width="1"/>
|
||||
<text x="860" y="358" fill="#0b0d0b" font-size="11" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Deploy</text>
|
||||
<text x="860" y="372" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">cloudflare pages</text>
|
||||
|
||||
<!-- Legend -->
|
||||
<line x1="40" y1="428" x2="960" y2="428" stroke="rgba(11,13,11,0.10)" stroke-width="0.8"/>
|
||||
<text x="40" y="444" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">LEGEND</text>
|
||||
|
||||
<rect x="40" y="460" width="14" height="10" rx="2" fill="#ffffff" stroke="#0b0d0b" stroke-width="1"/>
|
||||
<text x="60" y="468" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Step</text>
|
||||
|
||||
<rect x="132" y="460" width="14" height="10" rx="2" fill="rgba(247,89,31,0.08)" stroke="#f7591f" stroke-width="1"/>
|
||||
<text x="152" y="468" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Focal outcome</text>
|
||||
|
||||
<line x1="268" y1="466" x2="296" y2="466" stroke="#52534e" stroke-width="1.2" marker-end="url(#arrow)"/>
|
||||
<text x="304" y="468" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Within-lane step</text>
|
||||
|
||||
<line x1="444" y1="466" x2="472" y2="466" stroke="#52534e" stroke-width="1.2" stroke-dasharray="4,3" marker-end="url(#arrow)"/>
|
||||
<text x="480" y="468" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Revision loop</text>
|
||||
|
||||
<line x1="608" y1="466" x2="636" y2="466" stroke="#f7591f" stroke-width="1.4" marker-end="url(#arrow-accent)"/>
|
||||
<text x="644" y="468" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Critical handoff</text>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div class="cards">
|
||||
<div class="card">
|
||||
<p class="eyebrow">THE HEADLINE</p>
|
||||
<div class="card-header"><span class="card-dot coral"></span><h3>The deploy trigger is the risky edge</h3></div>
|
||||
<p>Every other step lives inside one person's head. The Editor→CI/CD handoff is where automation takes over and humans lose the ability to undo — hence coral.</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-header"><span class="card-dot ink"></span><h3>One owner per step</h3></div>
|
||||
<ul><li>No step crosses two lanes</li><li>Handoffs are arrows, not shared steps</li><li>Dashed arrow = revision detour</li></ul>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-header"><span class="card-dot muted"></span><h3>Uneven step counts are fine</h3></div>
|
||||
<p>The Reviewer lane has one step; CI/CD has two. Lanes don't need to match — they exist to make ownership unambiguous.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<span>publishing an article · swimlane</span>
|
||||
<span>example · diagram-design</span>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
170
skills/assets/example-swimlane.html
Normal file
170
skills/assets/example-swimlane.html
Normal file
@@ -0,0 +1,170 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Publishing an article · Swimlane</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
:root {
|
||||
--color-paper: #f5f4ed;
|
||||
--color-ink: #0b0d0b;
|
||||
--color-muted: #52534e;
|
||||
--color-accent: #f7591f;
|
||||
--font-sans: 'Geist', system-ui, sans-serif;
|
||||
--font-serif: 'Instrument Serif', serif;
|
||||
--font-mono: 'Geist Mono', ui-monospace, monospace;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-sans);
|
||||
background: var(--color-paper);
|
||||
color: var(--color-ink);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 3rem 2rem;
|
||||
}
|
||||
|
||||
.frame { max-width: 1200px; width: 100%; }
|
||||
|
||||
.eyebrow {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.66rem;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.18em;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-muted);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-family: var(--font-serif);
|
||||
font-size: clamp(1.5rem, 2.4vw + 0.75rem, 2rem);
|
||||
font-weight: 400;
|
||||
letter-spacing: -0.02em;
|
||||
line-height: 1.15;
|
||||
color: var(--color-ink);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
svg { width: 100%; min-width: 900px; display: block; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="frame">
|
||||
<p class="eyebrow">Swimlane · Diagram Design</p>
|
||||
<h1>Publishing an article</h1>
|
||||
|
||||
<svg viewBox="0 0 1000 480" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<pattern id="dots" width="22" height="22" patternUnits="userSpaceOnUse">
|
||||
<circle cx="1" cy="1" r="0.9" fill="rgba(11,13,11,0.10)"/>
|
||||
</pattern>
|
||||
<marker id="arrow" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0, 8 3, 0 6" fill="#52534e"/></marker>
|
||||
<marker id="arrow-accent" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0, 8 3, 0 6" fill="#f7591f"/></marker>
|
||||
</defs>
|
||||
|
||||
<rect width="100%" height="100%" fill="#f5f4ed"/>
|
||||
<rect width="100%" height="100%" fill="url(#dots)" opacity="0.55"/>
|
||||
|
||||
<!-- Lane dividers -->
|
||||
<line x1="40" y1="80" x2="960" y2="80" stroke="rgba(11,13,11,0.22)" stroke-width="1"/>
|
||||
<line x1="40" y1="160" x2="960" y2="160" stroke="rgba(11,13,11,0.10)" stroke-width="1"/>
|
||||
<line x1="40" y1="240" x2="960" y2="240" stroke="rgba(11,13,11,0.10)" stroke-width="1"/>
|
||||
<line x1="40" y1="320" x2="960" y2="320" stroke="rgba(11,13,11,0.10)" stroke-width="1"/>
|
||||
<line x1="40" y1="400" x2="960" y2="400" stroke="rgba(11,13,11,0.22)" stroke-width="1"/>
|
||||
|
||||
<!-- Actor column divider -->
|
||||
<line x1="160" y1="80" x2="160" y2="400" stroke="rgba(11,13,11,0.22)" stroke-width="1"/>
|
||||
|
||||
<!-- Lane labels -->
|
||||
<text x="60" y="124" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" letter-spacing="0.18em">AUTHOR</text>
|
||||
<text x="60" y="204" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" letter-spacing="0.18em">REVIEWER</text>
|
||||
<text x="60" y="284" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" letter-spacing="0.18em">EDITOR</text>
|
||||
<text x="60" y="364" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" letter-spacing="0.18em">CI / CD</text>
|
||||
|
||||
<!-- Arrows (drawn first) -->
|
||||
<!-- 1. Draft → Open PR (within Author) -->
|
||||
<line x1="300" y1="120" x2="340" y2="120" stroke="#52534e" stroke-width="1.2" marker-end="url(#arrow)"/>
|
||||
<!-- 2. Open PR → Review (Author → Reviewer handoff, diagonal down-right) -->
|
||||
<line x1="460" y1="144" x2="500" y2="176" stroke="#52534e" stroke-width="1.2" marker-end="url(#arrow)"/>
|
||||
<!-- 3. Review → Polish (Reviewer → Editor, vertical down) -->
|
||||
<line x1="570" y1="224" x2="570" y2="256" stroke="#52534e" stroke-width="1.2" stroke-dasharray="5,4" marker-end="url(#arrow)"/>
|
||||
<!-- 4. Polish → Approve (within Editor) -->
|
||||
<line x1="640" y1="280" x2="680" y2="280" stroke="#52534e" stroke-width="1.2" marker-end="url(#arrow)"/>
|
||||
<!-- 5. Approve → Build (Editor → CI/CD, coral handoff) -->
|
||||
<line x1="750" y1="304" x2="720" y2="336" stroke="#f7591f" stroke-width="1.4" marker-end="url(#arrow-accent)"/>
|
||||
<!-- 6. Build → Deploy (within CI/CD) -->
|
||||
<line x1="760" y1="360" x2="800" y2="360" stroke="#52534e" stroke-width="1.2" marker-end="url(#arrow)"/>
|
||||
|
||||
<!-- Arrow labels -->
|
||||
<rect x="452" y="140" width="60" height="12" rx="2" fill="#f5f4ed"/>
|
||||
<text x="482" y="150" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.12em">HANDOFF</text>
|
||||
|
||||
<rect x="548" y="228" width="52" height="12" rx="2" fill="#efeee5"/>
|
||||
<text x="574" y="238" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.12em">REVISE</text>
|
||||
|
||||
<rect x="712" y="308" width="80" height="12" rx="2" fill="#f5f4ed"/>
|
||||
<text x="752" y="318" fill="#f7591f" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.12em">DEPLOY TRIGGER</text>
|
||||
|
||||
<!-- Steps -->
|
||||
<!-- Author: Draft MDX -->
|
||||
<rect x="180" y="96" width="120" height="48" rx="6" fill="#ffffff" stroke="#0b0d0b" stroke-width="1"/>
|
||||
<text x="240" y="118" fill="#0b0d0b" font-size="11" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Draft MDX</text>
|
||||
<text x="240" y="132" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">src/content/…</text>
|
||||
|
||||
<!-- Author: Open PR -->
|
||||
<rect x="340" y="96" width="120" height="48" rx="6" fill="#ffffff" stroke="#0b0d0b" stroke-width="1"/>
|
||||
<text x="400" y="118" fill="#0b0d0b" font-size="11" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Open PR</text>
|
||||
<text x="400" y="132" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">gh pr create</text>
|
||||
|
||||
<!-- Reviewer: Review content -->
|
||||
<rect x="500" y="176" width="140" height="48" rx="6" fill="#ffffff" stroke="#0b0d0b" stroke-width="1"/>
|
||||
<text x="570" y="198" fill="#0b0d0b" font-size="11" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Review content</text>
|
||||
<text x="570" y="212" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">fact-check · voice</text>
|
||||
|
||||
<!-- Editor: Polish copy -->
|
||||
<rect x="500" y="256" width="140" height="48" rx="6" fill="#ffffff" stroke="#0b0d0b" stroke-width="1"/>
|
||||
<text x="570" y="278" fill="#0b0d0b" font-size="11" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Polish copy</text>
|
||||
<text x="570" y="292" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">style · line edits</text>
|
||||
|
||||
<!-- Editor: Approve merge -->
|
||||
<rect x="680" y="256" width="140" height="48" rx="6" fill="#ffffff" stroke="#0b0d0b" stroke-width="1"/>
|
||||
<text x="750" y="278" fill="#0b0d0b" font-size="11" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Approve merge</text>
|
||||
<text x="750" y="292" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">squash · main</text>
|
||||
|
||||
<!-- CI/CD: Build -->
|
||||
<rect x="680" y="336" width="80" height="48" rx="6" fill="#ffffff" stroke="#0b0d0b" stroke-width="1"/>
|
||||
<text x="720" y="358" fill="#0b0d0b" font-size="11" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Build</text>
|
||||
<text x="720" y="372" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">astro build</text>
|
||||
|
||||
<!-- CI/CD: Deploy (coral focal) -->
|
||||
<rect x="800" y="336" width="120" height="48" rx="6" fill="rgba(247,89,31,0.08)" stroke="#f7591f" stroke-width="1"/>
|
||||
<text x="860" y="358" fill="#0b0d0b" font-size="11" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Deploy</text>
|
||||
<text x="860" y="372" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">cloudflare pages</text>
|
||||
|
||||
<!-- Legend -->
|
||||
<line x1="40" y1="428" x2="960" y2="428" stroke="rgba(11,13,11,0.10)" stroke-width="0.8"/>
|
||||
<text x="40" y="444" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">LEGEND</text>
|
||||
|
||||
<rect x="40" y="460" width="14" height="10" rx="2" fill="#ffffff" stroke="#0b0d0b" stroke-width="1"/>
|
||||
<text x="60" y="468" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Step</text>
|
||||
|
||||
<rect x="132" y="460" width="14" height="10" rx="2" fill="rgba(247,89,31,0.08)" stroke="#f7591f" stroke-width="1"/>
|
||||
<text x="152" y="468" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Focal outcome</text>
|
||||
|
||||
<line x1="268" y1="466" x2="296" y2="466" stroke="#52534e" stroke-width="1.2" marker-end="url(#arrow)"/>
|
||||
<text x="304" y="468" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Within-lane step</text>
|
||||
|
||||
<line x1="444" y1="466" x2="472" y2="466" stroke="#52534e" stroke-width="1.2" stroke-dasharray="4,3" marker-end="url(#arrow)"/>
|
||||
<text x="480" y="468" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Revision loop</text>
|
||||
|
||||
<line x1="608" y1="466" x2="636" y2="466" stroke="#f7591f" stroke-width="1.4" marker-end="url(#arrow-accent)"/>
|
||||
<text x="644" y="468" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Critical handoff</text>
|
||||
</svg>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
133
skills/assets/example-timeline-dark.html
Normal file
133
skills/assets/example-timeline-dark.html
Normal file
@@ -0,0 +1,133 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>littlemight milestones · Timeline</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
:root {
|
||||
--color-paper: #1c1a17;
|
||||
--color-ink: #f1efe7;
|
||||
--color-muted: #a8a69d;
|
||||
--color-accent: #ff6a30;
|
||||
--font-sans: 'Geist', system-ui, sans-serif;
|
||||
--font-serif: 'Instrument Serif', serif;
|
||||
--font-mono: 'Geist Mono', ui-monospace, monospace;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-sans);
|
||||
background: var(--color-paper);
|
||||
color: var(--color-ink);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 3rem 2rem;
|
||||
}
|
||||
|
||||
.frame { max-width: 1200px; width: 100%; }
|
||||
|
||||
.eyebrow {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.66rem;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.18em;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-muted);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-family: var(--font-serif);
|
||||
font-size: clamp(1.5rem, 2.4vw + 0.75rem, 2rem);
|
||||
font-weight: 400;
|
||||
letter-spacing: -0.02em;
|
||||
line-height: 1.15;
|
||||
color: var(--color-ink);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
svg { width: 100%; min-width: 900px; display: block; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="frame">
|
||||
<p class="eyebrow">Timeline · Diagram Design</p>
|
||||
<h1>littlemight, fourteen months</h1>
|
||||
|
||||
<svg viewBox="0 0 1000 420" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<pattern id="dots" width="22" height="22" patternUnits="userSpaceOnUse">
|
||||
<circle cx="1" cy="1" r="0.9" fill="rgba(241,239,231,0.10)"/>
|
||||
</pattern>
|
||||
</defs>
|
||||
|
||||
<rect width="100%" height="100%" fill="#1c1a17"/>
|
||||
<rect width="100%" height="100%" fill="url(#dots)" opacity="0.55"/>
|
||||
|
||||
<!-- Year markers (background) -->
|
||||
<text x="60" y="340" fill="rgba(241,239,231,0.06)" font-size="72" font-weight="600" font-family="'Geist Mono', monospace">2025</text>
|
||||
<text x="700" y="340" fill="rgba(241,239,231,0.06)" font-size="72" font-weight="600" font-family="'Geist Mono', monospace">2026</text>
|
||||
|
||||
<!-- Baseline -->
|
||||
<line x1="80" y1="240" x2="920" y2="240" stroke="rgba(150,146,138,0.45)" stroke-width="1"/>
|
||||
|
||||
<!-- Year boundary tick (between 2025 and 2026) -->
|
||||
<line x1="680" y1="232" x2="680" y2="248" stroke="rgba(241,239,231,0.20)" stroke-width="1"/>
|
||||
<text x="680" y="260" fill="#8e8c83" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.14em">JAN '26</text>
|
||||
|
||||
<!-- Start / end caps -->
|
||||
<line x1="80" y1="232" x2="80" y2="248" stroke="rgba(241,239,231,0.20)" stroke-width="1"/>
|
||||
<line x1="920" y1="232" x2="920" y2="248" stroke="rgba(241,239,231,0.20)" stroke-width="1"/>
|
||||
|
||||
<!-- Event 1: FEB 2025 · First post (below) -->
|
||||
<line x1="100" y1="240" x2="100" y2="296" stroke="rgba(241,239,231,0.30)" stroke-width="1"/>
|
||||
<circle cx="100" cy="240" r="4" fill="#f1efe7"/>
|
||||
<text x="100" y="312" fill="#a8a69d" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.14em">FEB 2025</text>
|
||||
<text x="100" y="328" fill="#f1efe7" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">First post</text>
|
||||
|
||||
<!-- Event 2: APR 2025 · Design v1 (above) -->
|
||||
<line x1="240" y1="184" x2="240" y2="240" stroke="rgba(241,239,231,0.30)" stroke-width="1"/>
|
||||
<circle cx="240" cy="240" r="4" fill="#f1efe7"/>
|
||||
<text x="240" y="156" fill="#a8a69d" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.14em">APR 2025</text>
|
||||
<text x="240" y="172" fill="#f1efe7" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Design v1</text>
|
||||
|
||||
<!-- Event 3: SEP 2025 · Design v2 (below) -->
|
||||
<line x1="500" y1="240" x2="500" y2="296" stroke="rgba(241,239,231,0.30)" stroke-width="1"/>
|
||||
<circle cx="500" cy="240" r="4" fill="#f1efe7"/>
|
||||
<text x="500" y="312" fill="#a8a69d" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.14em">SEP 2025</text>
|
||||
<text x="500" y="328" fill="#f1efe7" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Design v2</text>
|
||||
<text x="500" y="344" fill="#a8a69d" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">typography pass</text>
|
||||
|
||||
<!-- Event 4: JAN 2026 · Design v3 (above, coral major) -->
|
||||
<line x1="740" y1="160" x2="740" y2="240" stroke="#ff6a30" stroke-width="1"/>
|
||||
<circle cx="740" cy="240" r="6" fill="#ff6a30"/>
|
||||
<text x="740" y="128" fill="#ff6a30" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.14em">JAN 2026</text>
|
||||
<text x="740" y="148" fill="#f1efe7" font-size="14" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Design v3</text>
|
||||
<text x="740" y="164" fill="#a8a69d" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">complexity budget</text>
|
||||
|
||||
<!-- Event 5: APR 2026 · now · Diagram Design skill (below, coral) -->
|
||||
<line x1="900" y1="240" x2="900" y2="296" stroke="#ff6a30" stroke-width="1"/>
|
||||
<circle cx="900" cy="240" r="6" fill="#ff6a30"/>
|
||||
<text x="900" y="312" fill="#ff6a30" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.14em">APR 2026 · NOW</text>
|
||||
<text x="900" y="328" fill="#f1efe7" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">diagram-design</text>
|
||||
<text x="900" y="344" fill="#a8a69d" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">eight diagram types</text>
|
||||
|
||||
<!-- Legend -->
|
||||
<line x1="40" y1="376" x2="960" y2="376" stroke="rgba(241,239,231,0.10)" stroke-width="0.8"/>
|
||||
<text x="40" y="392" fill="#a8a69d" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">LEGEND</text>
|
||||
|
||||
<circle cx="52" cy="408" r="4" fill="#f1efe7"/>
|
||||
<text x="68" y="412" fill="#a8a69d" font-size="8.5" font-family="'Geist', sans-serif">Event</text>
|
||||
|
||||
<circle cx="148" cy="408" r="6" fill="#ff6a30"/>
|
||||
<text x="164" y="412" fill="#a8a69d" font-size="8.5" font-family="'Geist', sans-serif">Major milestone</text>
|
||||
|
||||
<text x="296" y="412" fill="#a8a69d" font-size="8.5" font-family="'Geist', sans-serif" font-style="italic">Spacing is proportional to real elapsed time.</text>
|
||||
</svg>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
136
skills/assets/example-timeline-full.html
Normal file
136
skills/assets/example-timeline-full.html
Normal file
@@ -0,0 +1,136 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>littlemight milestones · Timeline</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
:root { --color-paper:#f5f4ed; --color-paper-2:#efeee5; --color-ink:#0b0d0b; --color-muted:#52534e; --color-soft:#65655c; --color-rule:rgba(11,13,11,0.12); --color-accent:#f7591f; --color-link:#1a70c7; --font-sans:'Geist',system-ui,sans-serif; --font-serif:'Instrument Serif',serif; --font-mono:'Geist Mono',ui-monospace,monospace; }
|
||||
body { font-family: var(--font-sans); background: var(--color-paper); min-height: 100vh; padding: 3rem 2rem; color: var(--color-ink); }
|
||||
.container { max-width: 1200px; margin: 0 auto; }
|
||||
.header { margin-bottom: 2.5rem; }
|
||||
.header-eyebrow { font-family: var(--font-mono); font-size: 0.66rem; font-weight: 500; letter-spacing: 0.18em; text-transform: uppercase; color: var(--color-muted); margin-bottom: 0.75rem; }
|
||||
h1 { font-family: var(--font-serif); font-size: clamp(1.75rem, 3vw + 1rem, 2.5rem); font-weight: 400; letter-spacing: -0.02em; line-height: 1.1; margin-bottom: 0.5rem; }
|
||||
.subtitle { font-size: 1rem; line-height: 1.55; color: var(--color-muted); max-width: 58ch; }
|
||||
.diagram-container { background: var(--color-paper-2); border-radius: 8px; border: 1px solid var(--color-rule); padding: 1.5rem; overflow-x: auto; }
|
||||
svg { width: 100%; min-width: 900px; display: block; }
|
||||
.cards { display: grid; grid-template-columns: 1.1fr 1fr 0.9fr; gap: 1rem; margin-top: 1.5rem; }
|
||||
@media (max-width: 820px) { .cards { grid-template-columns: 1fr; } }
|
||||
.card { background: #fff; border-radius: 6px; border: 1px solid var(--color-rule); padding: 1.25rem; }
|
||||
.card .eyebrow { font-family: var(--font-mono); font-size: 0.5rem; letter-spacing: 0.18em; text-transform: uppercase; color: var(--color-muted); margin-bottom: 0.5rem; }
|
||||
.card-header { display: flex; align-items: center; gap: 0.6rem; margin-bottom: 0.875rem; padding-bottom: 0.875rem; border-bottom: 1px solid rgba(11,13,11,0.08); }
|
||||
.card-dot { width: 7px; height: 7px; border-radius: 50%; }
|
||||
.card-dot.ink { background: var(--color-ink); } .card-dot.muted { background: var(--color-muted); } .card-dot.coral { background: var(--color-accent); }
|
||||
.card h3 { font-size: 0.875rem; font-weight: 600; }
|
||||
.card p, .card ul { color: var(--color-muted); font-size: 0.8125rem; line-height: 1.55; list-style: none; }
|
||||
.card li { margin-bottom: 0.3rem; padding-left: 0.875rem; position: relative; }
|
||||
.card li::before { content: '—'; position: absolute; left: 0; color: rgba(11,13,11,0.25); font-size: 0.75rem; }
|
||||
.footer { margin-top: 2rem; padding-top: 1.5rem; border-top: 1px solid rgba(11,13,11,0.10); font-family: var(--font-mono); font-size: 0.72rem; letter-spacing: 0.06em; color: var(--color-soft); display: flex; justify-content: space-between; flex-wrap: wrap; gap: 0.5rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<p class="header-eyebrow">Timeline · Diagram Design</p>
|
||||
<h1>littlemight, fourteen months</h1>
|
||||
<p class="subtitle">Five design moments from the first post to the current diagram-design overhaul. Spacing is proportional to calendar time — the five-month gap between v1 and v2 is genuinely wider on the page.</p>
|
||||
</div>
|
||||
|
||||
<div class="diagram-container">
|
||||
<svg viewBox="0 0 1000 420" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<pattern id="dots" width="22" height="22" patternUnits="userSpaceOnUse">
|
||||
<circle cx="1" cy="1" r="0.9" fill="rgba(11,13,11,0.10)"/>
|
||||
</pattern>
|
||||
</defs>
|
||||
|
||||
<rect width="100%" height="100%" fill="#f5f4ed"/>
|
||||
<rect width="100%" height="100%" fill="url(#dots)" opacity="0.55"/>
|
||||
|
||||
<!-- Year markers (background) -->
|
||||
<text x="60" y="340" fill="rgba(11,13,11,0.06)" font-size="72" font-weight="600" font-family="'Geist Mono', monospace">2025</text>
|
||||
<text x="700" y="340" fill="rgba(11,13,11,0.06)" font-size="72" font-weight="600" font-family="'Geist Mono', monospace">2026</text>
|
||||
|
||||
<!-- Baseline -->
|
||||
<line x1="80" y1="240" x2="920" y2="240" stroke="rgba(135,139,134,0.45)" stroke-width="1"/>
|
||||
|
||||
<!-- Year boundary tick (between 2025 and 2026) -->
|
||||
<line x1="680" y1="232" x2="680" y2="248" stroke="rgba(11,13,11,0.20)" stroke-width="1"/>
|
||||
<text x="680" y="260" fill="#65655c" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.14em">JAN '26</text>
|
||||
|
||||
<!-- Start / end caps -->
|
||||
<line x1="80" y1="232" x2="80" y2="248" stroke="rgba(11,13,11,0.20)" stroke-width="1"/>
|
||||
<line x1="920" y1="232" x2="920" y2="248" stroke="rgba(11,13,11,0.20)" stroke-width="1"/>
|
||||
|
||||
<!-- Event 1: FEB 2025 · First post (below) -->
|
||||
<line x1="100" y1="240" x2="100" y2="296" stroke="rgba(11,13,11,0.30)" stroke-width="1"/>
|
||||
<circle cx="100" cy="240" r="4" fill="#0b0d0b"/>
|
||||
<text x="100" y="312" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.14em">FEB 2025</text>
|
||||
<text x="100" y="328" fill="#0b0d0b" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">First post</text>
|
||||
|
||||
<!-- Event 2: APR 2025 · Design v1 (above) -->
|
||||
<line x1="240" y1="184" x2="240" y2="240" stroke="rgba(11,13,11,0.30)" stroke-width="1"/>
|
||||
<circle cx="240" cy="240" r="4" fill="#0b0d0b"/>
|
||||
<text x="240" y="156" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.14em">APR 2025</text>
|
||||
<text x="240" y="172" fill="#0b0d0b" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Design v1</text>
|
||||
|
||||
<!-- Event 3: SEP 2025 · Design v2 (below) -->
|
||||
<line x1="500" y1="240" x2="500" y2="296" stroke="rgba(11,13,11,0.30)" stroke-width="1"/>
|
||||
<circle cx="500" cy="240" r="4" fill="#0b0d0b"/>
|
||||
<text x="500" y="312" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.14em">SEP 2025</text>
|
||||
<text x="500" y="328" fill="#0b0d0b" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Design v2</text>
|
||||
<text x="500" y="344" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">typography pass</text>
|
||||
|
||||
<!-- Event 4: JAN 2026 · Design v3 (above, coral major) -->
|
||||
<line x1="740" y1="160" x2="740" y2="240" stroke="#f7591f" stroke-width="1"/>
|
||||
<circle cx="740" cy="240" r="6" fill="#f7591f"/>
|
||||
<text x="740" y="128" fill="#f7591f" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.14em">JAN 2026</text>
|
||||
<text x="740" y="148" fill="#0b0d0b" font-size="14" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Design v3</text>
|
||||
<text x="740" y="164" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">complexity budget</text>
|
||||
|
||||
<!-- Event 5: APR 2026 · now · Diagram Design skill (below, coral) -->
|
||||
<line x1="900" y1="240" x2="900" y2="296" stroke="#f7591f" stroke-width="1"/>
|
||||
<circle cx="900" cy="240" r="6" fill="#f7591f"/>
|
||||
<text x="900" y="312" fill="#f7591f" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.14em">APR 2026 · NOW</text>
|
||||
<text x="900" y="328" fill="#0b0d0b" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">diagram-design</text>
|
||||
<text x="900" y="344" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">eight diagram types</text>
|
||||
|
||||
<!-- Legend -->
|
||||
<line x1="40" y1="376" x2="960" y2="376" stroke="rgba(11,13,11,0.10)" stroke-width="0.8"/>
|
||||
<text x="40" y="392" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">LEGEND</text>
|
||||
|
||||
<circle cx="52" cy="408" r="4" fill="#0b0d0b"/>
|
||||
<text x="68" y="412" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Event</text>
|
||||
|
||||
<circle cx="148" cy="408" r="6" fill="#f7591f"/>
|
||||
<text x="164" y="412" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Major milestone</text>
|
||||
|
||||
<text x="296" y="412" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif" font-style="italic">Spacing is proportional to real elapsed time.</text>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div class="cards">
|
||||
<div class="card">
|
||||
<p class="eyebrow">THE HEADLINE</p>
|
||||
<div class="card-header"><span class="card-dot coral"></span><h3>v3 set the complexity budget</h3></div>
|
||||
<p>January's design overhaul is the pivot — the rule that no diagram exceeds nine components comes from there, and it's why this skill exists in the form it does.</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-header"><span class="card-dot ink"></span><h3>Honest spacing</h3></div>
|
||||
<ul><li>5 months between v1 and v2</li><li>4 months between v2 and v3</li><li>3 months between v3 and now</li><li>Cadence is tightening, not gaming the axis</li></ul>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-header"><span class="card-dot muted"></span><h3>Alternate label placement</h3></div>
|
||||
<p>Above / below flipping prevents label collision without forcing a second row. Five events on one baseline stays legible.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<span>littlemight · design lineage</span>
|
||||
<span>example · diagram-design</span>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
133
skills/assets/example-timeline.html
Normal file
133
skills/assets/example-timeline.html
Normal file
@@ -0,0 +1,133 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>littlemight milestones · Timeline</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
:root {
|
||||
--color-paper: #f5f4ed;
|
||||
--color-ink: #0b0d0b;
|
||||
--color-muted: #52534e;
|
||||
--color-accent: #f7591f;
|
||||
--font-sans: 'Geist', system-ui, sans-serif;
|
||||
--font-serif: 'Instrument Serif', serif;
|
||||
--font-mono: 'Geist Mono', ui-monospace, monospace;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-sans);
|
||||
background: var(--color-paper);
|
||||
color: var(--color-ink);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 3rem 2rem;
|
||||
}
|
||||
|
||||
.frame { max-width: 1200px; width: 100%; }
|
||||
|
||||
.eyebrow {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.66rem;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.18em;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-muted);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-family: var(--font-serif);
|
||||
font-size: clamp(1.5rem, 2.4vw + 0.75rem, 2rem);
|
||||
font-weight: 400;
|
||||
letter-spacing: -0.02em;
|
||||
line-height: 1.15;
|
||||
color: var(--color-ink);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
svg { width: 100%; min-width: 900px; display: block; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="frame">
|
||||
<p class="eyebrow">Timeline · Diagram Design</p>
|
||||
<h1>littlemight, fourteen months</h1>
|
||||
|
||||
<svg viewBox="0 0 1000 420" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<pattern id="dots" width="22" height="22" patternUnits="userSpaceOnUse">
|
||||
<circle cx="1" cy="1" r="0.9" fill="rgba(11,13,11,0.10)"/>
|
||||
</pattern>
|
||||
</defs>
|
||||
|
||||
<rect width="100%" height="100%" fill="#f5f4ed"/>
|
||||
<rect width="100%" height="100%" fill="url(#dots)" opacity="0.55"/>
|
||||
|
||||
<!-- Year markers (background) -->
|
||||
<text x="60" y="340" fill="rgba(11,13,11,0.06)" font-size="72" font-weight="600" font-family="'Geist Mono', monospace">2025</text>
|
||||
<text x="700" y="340" fill="rgba(11,13,11,0.06)" font-size="72" font-weight="600" font-family="'Geist Mono', monospace">2026</text>
|
||||
|
||||
<!-- Baseline -->
|
||||
<line x1="80" y1="240" x2="920" y2="240" stroke="rgba(135,139,134,0.45)" stroke-width="1"/>
|
||||
|
||||
<!-- Year boundary tick (between 2025 and 2026) -->
|
||||
<line x1="680" y1="232" x2="680" y2="248" stroke="rgba(11,13,11,0.20)" stroke-width="1"/>
|
||||
<text x="680" y="260" fill="#65655c" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.14em">JAN '26</text>
|
||||
|
||||
<!-- Start / end caps -->
|
||||
<line x1="80" y1="232" x2="80" y2="248" stroke="rgba(11,13,11,0.20)" stroke-width="1"/>
|
||||
<line x1="920" y1="232" x2="920" y2="248" stroke="rgba(11,13,11,0.20)" stroke-width="1"/>
|
||||
|
||||
<!-- Event 1: FEB 2025 · First post (below) -->
|
||||
<line x1="100" y1="240" x2="100" y2="296" stroke="rgba(11,13,11,0.30)" stroke-width="1"/>
|
||||
<circle cx="100" cy="240" r="4" fill="#0b0d0b"/>
|
||||
<text x="100" y="312" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.14em">FEB 2025</text>
|
||||
<text x="100" y="328" fill="#0b0d0b" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">First post</text>
|
||||
|
||||
<!-- Event 2: APR 2025 · Design v1 (above) -->
|
||||
<line x1="240" y1="184" x2="240" y2="240" stroke="rgba(11,13,11,0.30)" stroke-width="1"/>
|
||||
<circle cx="240" cy="240" r="4" fill="#0b0d0b"/>
|
||||
<text x="240" y="156" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.14em">APR 2025</text>
|
||||
<text x="240" y="172" fill="#0b0d0b" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Design v1</text>
|
||||
|
||||
<!-- Event 3: SEP 2025 · Design v2 (below) -->
|
||||
<line x1="500" y1="240" x2="500" y2="296" stroke="rgba(11,13,11,0.30)" stroke-width="1"/>
|
||||
<circle cx="500" cy="240" r="4" fill="#0b0d0b"/>
|
||||
<text x="500" y="312" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.14em">SEP 2025</text>
|
||||
<text x="500" y="328" fill="#0b0d0b" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Design v2</text>
|
||||
<text x="500" y="344" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">typography pass</text>
|
||||
|
||||
<!-- Event 4: JAN 2026 · Design v3 (above, coral major) -->
|
||||
<line x1="740" y1="160" x2="740" y2="240" stroke="#f7591f" stroke-width="1"/>
|
||||
<circle cx="740" cy="240" r="6" fill="#f7591f"/>
|
||||
<text x="740" y="128" fill="#f7591f" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.14em">JAN 2026</text>
|
||||
<text x="740" y="148" fill="#0b0d0b" font-size="14" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Design v3</text>
|
||||
<text x="740" y="164" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">complexity budget</text>
|
||||
|
||||
<!-- Event 5: APR 2026 · now · Diagram Design skill (below, coral) -->
|
||||
<line x1="900" y1="240" x2="900" y2="296" stroke="#f7591f" stroke-width="1"/>
|
||||
<circle cx="900" cy="240" r="6" fill="#f7591f"/>
|
||||
<text x="900" y="312" fill="#f7591f" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.14em">APR 2026 · NOW</text>
|
||||
<text x="900" y="328" fill="#0b0d0b" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">diagram-design</text>
|
||||
<text x="900" y="344" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">eight diagram types</text>
|
||||
|
||||
<!-- Legend -->
|
||||
<line x1="40" y1="376" x2="960" y2="376" stroke="rgba(11,13,11,0.10)" stroke-width="0.8"/>
|
||||
<text x="40" y="392" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">LEGEND</text>
|
||||
|
||||
<circle cx="52" cy="408" r="4" fill="#0b0d0b"/>
|
||||
<text x="68" y="412" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Event</text>
|
||||
|
||||
<circle cx="148" cy="408" r="6" fill="#f7591f"/>
|
||||
<text x="164" y="412" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Major milestone</text>
|
||||
|
||||
<text x="296" y="412" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif" font-style="italic">Spacing is proportional to real elapsed time.</text>
|
||||
</svg>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
171
skills/assets/example-tree-dark.html
Normal file
171
skills/assets/example-tree-dark.html
Normal file
@@ -0,0 +1,171 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Claude Code skill taxonomy · Tree</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
:root {
|
||||
--color-paper: #1c1a17;
|
||||
--color-ink: #f1efe7;
|
||||
--color-muted: #a8a69d;
|
||||
--color-accent: #ff6a30;
|
||||
--font-sans: 'Geist', system-ui, sans-serif;
|
||||
--font-serif: 'Instrument Serif', serif;
|
||||
--font-mono: 'Geist Mono', ui-monospace, monospace;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-sans);
|
||||
background: var(--color-paper);
|
||||
color: var(--color-ink);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 3rem 2rem;
|
||||
}
|
||||
|
||||
.frame { max-width: 1200px; width: 100%; }
|
||||
|
||||
.eyebrow {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.66rem;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.18em;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-muted);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-family: var(--font-serif);
|
||||
font-size: clamp(1.5rem, 2.4vw + 0.75rem, 2rem);
|
||||
font-weight: 400;
|
||||
letter-spacing: -0.02em;
|
||||
line-height: 1.15;
|
||||
color: var(--color-ink);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
svg { width: 100%; min-width: 900px; display: block; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="frame">
|
||||
<p class="eyebrow">Tree · Diagram Design</p>
|
||||
<h1>Claude Code skill taxonomy</h1>
|
||||
|
||||
<svg viewBox="0 0 1000 480" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<pattern id="dots" width="22" height="22" patternUnits="userSpaceOnUse">
|
||||
<circle cx="1" cy="1" r="0.9" fill="rgba(241,239,231,0.10)"/>
|
||||
</pattern>
|
||||
</defs>
|
||||
|
||||
<rect width="100%" height="100%" fill="#1c1a17"/>
|
||||
<rect width="100%" height="100%" fill="url(#dots)" opacity="0.55"/>
|
||||
|
||||
<!-- Tier tags (left margin) -->
|
||||
<text x="40" y="108" fill="#a8a69d" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">TIER 0 · ROOT</text>
|
||||
<text x="40" y="224" fill="#a8a69d" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em" text-anchor="start">TIER 1</text>
|
||||
<text x="40" y="324" fill="#a8a69d" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em" text-anchor="start">TIER 2</text>
|
||||
|
||||
<!-- Connectors drawn first (behind nodes) -->
|
||||
<!-- Root → Tier 1 bus -->
|
||||
<path d="M 500 128 L 500 168 L 220 168 L 220 208" fill="none" stroke="#a8a69d" stroke-width="1"/>
|
||||
<path d="M 500 168 L 500 208" fill="none" stroke="#a8a69d" stroke-width="1"/>
|
||||
<path d="M 500 168 L 780 168 L 780 208" fill="none" stroke="#a8a69d" stroke-width="1"/>
|
||||
|
||||
<!-- Design → leaves -->
|
||||
<path d="M 220 256 L 220 296 L 140 296 L 140 336" fill="none" stroke="#a8a69d" stroke-width="1"/>
|
||||
<path d="M 220 296 L 300 296 L 300 336" fill="none" stroke="#a8a69d" stroke-width="1"/>
|
||||
|
||||
<!-- Engineering → leaves -->
|
||||
<path d="M 500 256 L 500 296 L 480 296 L 480 336" fill="none" stroke="#a8a69d" stroke-width="1"/>
|
||||
<path d="M 500 296 L 660 296 L 660 336" fill="none" stroke="#a8a69d" stroke-width="1"/>
|
||||
|
||||
<!-- Research → single leaf -->
|
||||
<path d="M 780 256 L 780 296 L 840 296 L 840 336" fill="none" stroke="#a8a69d" stroke-width="1"/>
|
||||
|
||||
<!-- Root node: Skills (coral focal) -->
|
||||
<rect x="420" y="80" width="160" height="48" rx="6" fill="#1c1a17"/>
|
||||
<rect x="420" y="80" width="160" height="48" rx="6" fill="rgba(255,106,48,0.10)" stroke="#ff6a30" stroke-width="1"/>
|
||||
<rect x="428" y="88" width="32" height="12" rx="2" fill="transparent" stroke="rgba(255,106,48,0.50)" stroke-width="0.8"/>
|
||||
<text x="444" y="97" fill="#ff6a30" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">ROOT</text>
|
||||
<text x="500" y="118" fill="#f1efe7" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Skills</text>
|
||||
|
||||
<!-- Tier 1: Design -->
|
||||
<rect x="140" y="208" width="160" height="48" rx="6" fill="#1c1a17"/>
|
||||
<rect x="140" y="208" width="160" height="48" rx="6" fill="rgba(241,239,231,0.04)" stroke="#f1efe7" stroke-width="1"/>
|
||||
<rect x="148" y="216" width="28" height="12" rx="2" fill="transparent" stroke="rgba(241,239,231,0.40)" stroke-width="0.8"/>
|
||||
<text x="162" y="225" fill="#f1efe7" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">CAT</text>
|
||||
<text x="220" y="240" fill="#f1efe7" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Design</text>
|
||||
<text x="220" y="252" fill="#a8a69d" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">ui · visual · ux</text>
|
||||
|
||||
<!-- Tier 1: Engineering -->
|
||||
<rect x="420" y="208" width="160" height="48" rx="6" fill="#1c1a17"/>
|
||||
<rect x="420" y="208" width="160" height="48" rx="6" fill="rgba(241,239,231,0.04)" stroke="#f1efe7" stroke-width="1"/>
|
||||
<rect x="428" y="216" width="28" height="12" rx="2" fill="transparent" stroke="rgba(241,239,231,0.40)" stroke-width="0.8"/>
|
||||
<text x="442" y="225" fill="#f1efe7" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">CAT</text>
|
||||
<text x="500" y="240" fill="#f1efe7" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Engineering</text>
|
||||
<text x="500" y="252" fill="#a8a69d" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">ship · review · test</text>
|
||||
|
||||
<!-- Tier 1: Research -->
|
||||
<rect x="700" y="208" width="160" height="48" rx="6" fill="#1c1a17"/>
|
||||
<rect x="700" y="208" width="160" height="48" rx="6" fill="rgba(241,239,231,0.04)" stroke="#f1efe7" stroke-width="1"/>
|
||||
<rect x="708" y="216" width="28" height="12" rx="2" fill="transparent" stroke="rgba(241,239,231,0.40)" stroke-width="0.8"/>
|
||||
<text x="722" y="225" fill="#f1efe7" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">CAT</text>
|
||||
<text x="780" y="240" fill="#f1efe7" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Research</text>
|
||||
<text x="780" y="252" fill="#a8a69d" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">investigate · analyze</text>
|
||||
|
||||
<!-- Tier 2: polish -->
|
||||
<rect x="60" y="336" width="160" height="48" rx="6" fill="#1c1a17"/>
|
||||
<rect x="60" y="336" width="160" height="48" rx="6" fill="rgba(241,239,231,0.05)" stroke="#a8a69d" stroke-width="0.8"/>
|
||||
<text x="140" y="360" fill="#f1efe7" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">polish</text>
|
||||
<text x="140" y="372" fill="#a8a69d" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">align · space · rhythm</text>
|
||||
|
||||
<!-- Tier 2: critique -->
|
||||
<rect x="220" y="336" width="160" height="48" rx="6" fill="#1c1a17"/>
|
||||
<rect x="220" y="336" width="160" height="48" rx="6" fill="rgba(241,239,231,0.05)" stroke="#a8a69d" stroke-width="0.8"/>
|
||||
<text x="300" y="360" fill="#f1efe7" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">critique</text>
|
||||
<text x="300" y="372" fill="#a8a69d" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">hierarchy · density</text>
|
||||
|
||||
<!-- Tier 2: review -->
|
||||
<rect x="400" y="336" width="160" height="48" rx="6" fill="#1c1a17"/>
|
||||
<rect x="400" y="336" width="160" height="48" rx="6" fill="rgba(241,239,231,0.05)" stroke="#a8a69d" stroke-width="0.8"/>
|
||||
<text x="480" y="360" fill="#f1efe7" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">review</text>
|
||||
<text x="480" y="372" fill="#a8a69d" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">pre-land diff · sql</text>
|
||||
|
||||
<!-- Tier 2: ship -->
|
||||
<rect x="580" y="336" width="160" height="48" rx="6" fill="#1c1a17"/>
|
||||
<rect x="580" y="336" width="160" height="48" rx="6" fill="rgba(241,239,231,0.05)" stroke="#a8a69d" stroke-width="0.8"/>
|
||||
<text x="660" y="360" fill="#f1efe7" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">ship</text>
|
||||
<text x="660" y="372" fill="#a8a69d" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">merge · deploy · verify</text>
|
||||
|
||||
<!-- Tier 2: investigate -->
|
||||
<rect x="760" y="336" width="160" height="48" rx="6" fill="#1c1a17"/>
|
||||
<rect x="760" y="336" width="160" height="48" rx="6" fill="rgba(241,239,231,0.05)" stroke="#a8a69d" stroke-width="0.8"/>
|
||||
<text x="840" y="360" fill="#f1efe7" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">investigate</text>
|
||||
<text x="840" y="372" fill="#a8a69d" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">root cause · evidence</text>
|
||||
|
||||
<!-- Legend strip -->
|
||||
<line x1="40" y1="412" x2="960" y2="412" stroke="rgba(241,239,231,0.10)" stroke-width="0.8"/>
|
||||
<text x="40" y="428" fill="#a8a69d" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">LEGEND</text>
|
||||
|
||||
<rect x="40" y="444" width="14" height="10" rx="2" fill="rgba(255,106,48,0.10)" stroke="#ff6a30" stroke-width="1"/>
|
||||
<text x="60" y="452" fill="#a8a69d" font-size="8.5" font-family="'Geist', sans-serif">Root · focal</text>
|
||||
|
||||
<rect x="180" y="444" width="14" height="10" rx="2" fill="rgba(241,239,231,0.04)" stroke="#f1efe7" stroke-width="1"/>
|
||||
<text x="200" y="452" fill="#a8a69d" font-size="8.5" font-family="'Geist', sans-serif">Category branch</text>
|
||||
|
||||
<rect x="340" y="444" width="14" height="10" rx="2" fill="rgba(241,239,231,0.05)" stroke="#a8a69d" stroke-width="0.8"/>
|
||||
<text x="360" y="452" fill="#a8a69d" font-size="8.5" font-family="'Geist', sans-serif">Leaf skill</text>
|
||||
|
||||
<text x="500" y="452" fill="#a8a69d" font-size="8.5" font-family="'Geist', sans-serif" font-style="italic">Orthogonal connectors only. Coral marks the root — every branch descends from one idea.</text>
|
||||
</svg>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
174
skills/assets/example-tree-full.html
Normal file
174
skills/assets/example-tree-full.html
Normal file
@@ -0,0 +1,174 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Claude Code skill taxonomy · Tree</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
:root { --color-paper:#f5f4ed; --color-paper-2:#efeee5; --color-ink:#0b0d0b; --color-muted:#52534e; --color-soft:#65655c; --color-rule:rgba(11,13,11,0.12); --color-accent:#f7591f; --color-link:#1a70c7; --font-sans:'Geist',system-ui,sans-serif; --font-serif:'Instrument Serif',serif; --font-mono:'Geist Mono',ui-monospace,monospace; }
|
||||
body { font-family: var(--font-sans); background: var(--color-paper); min-height: 100vh; padding: 3rem 2rem; color: var(--color-ink); }
|
||||
.container { max-width: 1200px; margin: 0 auto; }
|
||||
.header { margin-bottom: 2.5rem; }
|
||||
.header-eyebrow { font-family: var(--font-mono); font-size: 0.66rem; font-weight: 500; letter-spacing: 0.18em; text-transform: uppercase; color: var(--color-muted); margin-bottom: 0.75rem; }
|
||||
h1 { font-family: var(--font-serif); font-size: clamp(1.75rem, 3vw + 1rem, 2.5rem); font-weight: 400; letter-spacing: -0.02em; line-height: 1.1; margin-bottom: 0.5rem; }
|
||||
.subtitle { font-size: 1rem; line-height: 1.55; color: var(--color-muted); max-width: 58ch; }
|
||||
.diagram-container { background: var(--color-paper-2); border-radius: 8px; border: 1px solid var(--color-rule); padding: 1.5rem; overflow-x: auto; }
|
||||
svg { width: 100%; min-width: 900px; display: block; }
|
||||
.cards { display: grid; grid-template-columns: 1.1fr 1fr 0.9fr; gap: 1rem; margin-top: 1.5rem; }
|
||||
@media (max-width: 820px) { .cards { grid-template-columns: 1fr; } }
|
||||
.card { background: #fff; border-radius: 6px; border: 1px solid var(--color-rule); padding: 1.25rem; }
|
||||
.card .eyebrow { font-family: var(--font-mono); font-size: 0.5rem; letter-spacing: 0.18em; text-transform: uppercase; color: var(--color-muted); margin-bottom: 0.5rem; }
|
||||
.card-header { display: flex; align-items: center; gap: 0.6rem; margin-bottom: 0.875rem; padding-bottom: 0.875rem; border-bottom: 1px solid rgba(11,13,11,0.08); }
|
||||
.card-dot { width: 7px; height: 7px; border-radius: 50%; }
|
||||
.card-dot.ink { background: var(--color-ink); } .card-dot.muted { background: var(--color-muted); } .card-dot.coral { background: var(--color-accent); }
|
||||
.card h3 { font-size: 0.875rem; font-weight: 600; }
|
||||
.card p, .card ul { color: var(--color-muted); font-size: 0.8125rem; line-height: 1.55; list-style: none; }
|
||||
.card li { margin-bottom: 0.3rem; padding-left: 0.875rem; position: relative; }
|
||||
.card li::before { content: '—'; position: absolute; left: 0; color: rgba(11,13,11,0.25); font-size: 0.75rem; }
|
||||
.footer { margin-top: 2rem; padding-top: 1.5rem; border-top: 1px solid rgba(11,13,11,0.10); font-family: var(--font-mono); font-size: 0.72rem; letter-spacing: 0.06em; color: var(--color-soft); display: flex; justify-content: space-between; flex-wrap: wrap; gap: 0.5rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<p class="header-eyebrow">Tree · Diagram Design</p>
|
||||
<h1>Claude Code skill taxonomy</h1>
|
||||
<p class="subtitle">Three tiers, one root. The full skill library fans out from a single idea — and every leaf descends through exactly one category. Orthogonal connectors, one coral accent, nothing else.</p>
|
||||
</div>
|
||||
|
||||
<div class="diagram-container">
|
||||
<svg viewBox="0 0 1000 480" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<pattern id="dots" width="22" height="22" patternUnits="userSpaceOnUse">
|
||||
<circle cx="1" cy="1" r="0.9" fill="rgba(11,13,11,0.10)"/>
|
||||
</pattern>
|
||||
</defs>
|
||||
|
||||
<rect width="100%" height="100%" fill="#f5f4ed"/>
|
||||
<rect width="100%" height="100%" fill="url(#dots)" opacity="0.55"/>
|
||||
|
||||
<!-- Tier tags (left margin) -->
|
||||
<text x="40" y="108" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">TIER 0 · ROOT</text>
|
||||
<text x="40" y="224" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em" text-anchor="start">TIER 1</text>
|
||||
<text x="40" y="324" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em" text-anchor="start">TIER 2</text>
|
||||
|
||||
<!-- Connectors drawn first (behind nodes) -->
|
||||
<!-- Root → Tier 1 bus -->
|
||||
<path d="M 500 128 L 500 168 L 220 168 L 220 208" fill="none" stroke="#52534e" stroke-width="1"/>
|
||||
<path d="M 500 168 L 500 208" fill="none" stroke="#52534e" stroke-width="1"/>
|
||||
<path d="M 500 168 L 780 168 L 780 208" fill="none" stroke="#52534e" stroke-width="1"/>
|
||||
|
||||
<!-- Design → leaves -->
|
||||
<path d="M 220 256 L 220 296 L 140 296 L 140 336" fill="none" stroke="#52534e" stroke-width="1"/>
|
||||
<path d="M 220 296 L 300 296 L 300 336" fill="none" stroke="#52534e" stroke-width="1"/>
|
||||
|
||||
<!-- Engineering → leaves -->
|
||||
<path d="M 500 256 L 500 296 L 480 296 L 480 336" fill="none" stroke="#52534e" stroke-width="1"/>
|
||||
<path d="M 500 296 L 660 296 L 660 336" fill="none" stroke="#52534e" stroke-width="1"/>
|
||||
|
||||
<!-- Research → single leaf -->
|
||||
<path d="M 780 256 L 780 296 L 840 296 L 840 336" fill="none" stroke="#52534e" stroke-width="1"/>
|
||||
|
||||
<!-- Root node: Skills (coral focal) -->
|
||||
<rect x="420" y="80" width="160" height="48" rx="6" fill="#f5f4ed"/>
|
||||
<rect x="420" y="80" width="160" height="48" rx="6" fill="rgba(247,89,31,0.08)" stroke="#f7591f" stroke-width="1"/>
|
||||
<rect x="428" y="88" width="32" height="12" rx="2" fill="transparent" stroke="rgba(247,89,31,0.50)" stroke-width="0.8"/>
|
||||
<text x="444" y="97" fill="#f7591f" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">ROOT</text>
|
||||
<text x="500" y="118" fill="#0b0d0b" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Skills</text>
|
||||
|
||||
<!-- Tier 1: Design -->
|
||||
<rect x="140" y="208" width="160" height="48" rx="6" fill="#f5f4ed"/>
|
||||
<rect x="140" y="208" width="160" height="48" rx="6" fill="#ffffff" stroke="#0b0d0b" stroke-width="1"/>
|
||||
<rect x="148" y="216" width="28" height="12" rx="2" fill="transparent" stroke="rgba(11,13,11,0.40)" stroke-width="0.8"/>
|
||||
<text x="162" y="225" fill="#0b0d0b" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">CAT</text>
|
||||
<text x="220" y="240" fill="#0b0d0b" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Design</text>
|
||||
<text x="220" y="252" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">ui · visual · ux</text>
|
||||
|
||||
<!-- Tier 1: Engineering -->
|
||||
<rect x="420" y="208" width="160" height="48" rx="6" fill="#f5f4ed"/>
|
||||
<rect x="420" y="208" width="160" height="48" rx="6" fill="#ffffff" stroke="#0b0d0b" stroke-width="1"/>
|
||||
<rect x="428" y="216" width="28" height="12" rx="2" fill="transparent" stroke="rgba(11,13,11,0.40)" stroke-width="0.8"/>
|
||||
<text x="442" y="225" fill="#0b0d0b" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">CAT</text>
|
||||
<text x="500" y="240" fill="#0b0d0b" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Engineering</text>
|
||||
<text x="500" y="252" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">ship · review · test</text>
|
||||
|
||||
<!-- Tier 1: Research -->
|
||||
<rect x="700" y="208" width="160" height="48" rx="6" fill="#f5f4ed"/>
|
||||
<rect x="700" y="208" width="160" height="48" rx="6" fill="#ffffff" stroke="#0b0d0b" stroke-width="1"/>
|
||||
<rect x="708" y="216" width="28" height="12" rx="2" fill="transparent" stroke="rgba(11,13,11,0.40)" stroke-width="0.8"/>
|
||||
<text x="722" y="225" fill="#0b0d0b" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">CAT</text>
|
||||
<text x="780" y="240" fill="#0b0d0b" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Research</text>
|
||||
<text x="780" y="252" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">investigate · analyze</text>
|
||||
|
||||
<!-- Tier 2: polish -->
|
||||
<rect x="60" y="336" width="160" height="48" rx="6" fill="#f5f4ed"/>
|
||||
<rect x="60" y="336" width="160" height="48" rx="6" fill="rgba(11,13,11,0.05)" stroke="#52534e" stroke-width="0.8"/>
|
||||
<text x="140" y="360" fill="#0b0d0b" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">polish</text>
|
||||
<text x="140" y="372" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">align · space · rhythm</text>
|
||||
|
||||
<!-- Tier 2: critique -->
|
||||
<rect x="220" y="336" width="160" height="48" rx="6" fill="#f5f4ed"/>
|
||||
<rect x="220" y="336" width="160" height="48" rx="6" fill="rgba(11,13,11,0.05)" stroke="#52534e" stroke-width="0.8"/>
|
||||
<text x="300" y="360" fill="#0b0d0b" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">critique</text>
|
||||
<text x="300" y="372" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">hierarchy · density</text>
|
||||
|
||||
<!-- Tier 2: review -->
|
||||
<rect x="400" y="336" width="160" height="48" rx="6" fill="#f5f4ed"/>
|
||||
<rect x="400" y="336" width="160" height="48" rx="6" fill="rgba(11,13,11,0.05)" stroke="#52534e" stroke-width="0.8"/>
|
||||
<text x="480" y="360" fill="#0b0d0b" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">review</text>
|
||||
<text x="480" y="372" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">pre-land diff · sql</text>
|
||||
|
||||
<!-- Tier 2: ship -->
|
||||
<rect x="580" y="336" width="160" height="48" rx="6" fill="#f5f4ed"/>
|
||||
<rect x="580" y="336" width="160" height="48" rx="6" fill="rgba(11,13,11,0.05)" stroke="#52534e" stroke-width="0.8"/>
|
||||
<text x="660" y="360" fill="#0b0d0b" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">ship</text>
|
||||
<text x="660" y="372" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">merge · deploy · verify</text>
|
||||
|
||||
<!-- Tier 2: investigate -->
|
||||
<rect x="760" y="336" width="160" height="48" rx="6" fill="#f5f4ed"/>
|
||||
<rect x="760" y="336" width="160" height="48" rx="6" fill="rgba(11,13,11,0.05)" stroke="#52534e" stroke-width="0.8"/>
|
||||
<text x="840" y="360" fill="#0b0d0b" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">investigate</text>
|
||||
<text x="840" y="372" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">root cause · evidence</text>
|
||||
|
||||
<!-- Legend strip -->
|
||||
<line x1="40" y1="412" x2="960" y2="412" stroke="rgba(11,13,11,0.10)" stroke-width="0.8"/>
|
||||
<text x="40" y="428" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">LEGEND</text>
|
||||
|
||||
<rect x="40" y="444" width="14" height="10" rx="2" fill="rgba(247,89,31,0.08)" stroke="#f7591f" stroke-width="1"/>
|
||||
<text x="60" y="452" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Root · focal</text>
|
||||
|
||||
<rect x="180" y="444" width="14" height="10" rx="2" fill="#ffffff" stroke="#0b0d0b" stroke-width="1"/>
|
||||
<text x="200" y="452" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Category branch</text>
|
||||
|
||||
<rect x="340" y="444" width="14" height="10" rx="2" fill="rgba(11,13,11,0.05)" stroke="#52534e" stroke-width="0.8"/>
|
||||
<text x="360" y="452" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Leaf skill</text>
|
||||
|
||||
<text x="500" y="452" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif" font-style="italic">Orthogonal connectors only. Coral marks the root — every branch descends from one idea.</text>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div class="cards">
|
||||
<div class="card">
|
||||
<p class="eyebrow">THE HEADLINE</p>
|
||||
<div class="card-header"><span class="card-dot coral"></span><h3>One root, one coral dot</h3></div>
|
||||
<p>A taxonomy is a promise: every child has exactly one parent. Coral lives on the root because that's the only node the tree is actually about — everything else is a descent.</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-header"><span class="card-dot ink"></span><h3>Connectors are orthogonal</h3></div>
|
||||
<ul><li>Parent drops vertical</li><li>Horizontal bus connects siblings</li><li>Each child drops into its top edge</li><li>No diagonals, no curves</li></ul>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-header"><span class="card-dot muted"></span><h3>Uneven breadth is honest</h3></div>
|
||||
<p>Design and Engineering fan to two leaves; Research drops to one. The layout reflects the real shape of the library, not a forced symmetry.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<span>claude code skill taxonomy</span>
|
||||
<span>example · diagram-design</span>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
171
skills/assets/example-tree.html
Normal file
171
skills/assets/example-tree.html
Normal file
@@ -0,0 +1,171 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Claude Code skill taxonomy · Tree</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
:root {
|
||||
--color-paper: #f5f4ed;
|
||||
--color-ink: #0b0d0b;
|
||||
--color-muted: #52534e;
|
||||
--color-accent: #f7591f;
|
||||
--font-sans: 'Geist', system-ui, sans-serif;
|
||||
--font-serif: 'Instrument Serif', serif;
|
||||
--font-mono: 'Geist Mono', ui-monospace, monospace;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-sans);
|
||||
background: var(--color-paper);
|
||||
color: var(--color-ink);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 3rem 2rem;
|
||||
}
|
||||
|
||||
.frame { max-width: 1200px; width: 100%; }
|
||||
|
||||
.eyebrow {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.66rem;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.18em;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-muted);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-family: var(--font-serif);
|
||||
font-size: clamp(1.5rem, 2.4vw + 0.75rem, 2rem);
|
||||
font-weight: 400;
|
||||
letter-spacing: -0.02em;
|
||||
line-height: 1.15;
|
||||
color: var(--color-ink);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
svg { width: 100%; min-width: 900px; display: block; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="frame">
|
||||
<p class="eyebrow">Tree · Diagram Design</p>
|
||||
<h1>Claude Code skill taxonomy</h1>
|
||||
|
||||
<svg viewBox="0 0 1000 480" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<pattern id="dots" width="22" height="22" patternUnits="userSpaceOnUse">
|
||||
<circle cx="1" cy="1" r="0.9" fill="rgba(11,13,11,0.10)"/>
|
||||
</pattern>
|
||||
</defs>
|
||||
|
||||
<rect width="100%" height="100%" fill="#f5f4ed"/>
|
||||
<rect width="100%" height="100%" fill="url(#dots)" opacity="0.55"/>
|
||||
|
||||
<!-- Tier tags (left margin) -->
|
||||
<text x="40" y="108" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">TIER 0 · ROOT</text>
|
||||
<text x="40" y="224" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em" text-anchor="start">TIER 1</text>
|
||||
<text x="40" y="324" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em" text-anchor="start">TIER 2</text>
|
||||
|
||||
<!-- Connectors drawn first (behind nodes) -->
|
||||
<!-- Root → Tier 1 bus -->
|
||||
<path d="M 500 128 L 500 168 L 220 168 L 220 208" fill="none" stroke="#52534e" stroke-width="1"/>
|
||||
<path d="M 500 168 L 500 208" fill="none" stroke="#52534e" stroke-width="1"/>
|
||||
<path d="M 500 168 L 780 168 L 780 208" fill="none" stroke="#52534e" stroke-width="1"/>
|
||||
|
||||
<!-- Design → leaves -->
|
||||
<path d="M 220 256 L 220 296 L 140 296 L 140 336" fill="none" stroke="#52534e" stroke-width="1"/>
|
||||
<path d="M 220 296 L 300 296 L 300 336" fill="none" stroke="#52534e" stroke-width="1"/>
|
||||
|
||||
<!-- Engineering → leaves -->
|
||||
<path d="M 500 256 L 500 296 L 480 296 L 480 336" fill="none" stroke="#52534e" stroke-width="1"/>
|
||||
<path d="M 500 296 L 660 296 L 660 336" fill="none" stroke="#52534e" stroke-width="1"/>
|
||||
|
||||
<!-- Research → single leaf -->
|
||||
<path d="M 780 256 L 780 296 L 840 296 L 840 336" fill="none" stroke="#52534e" stroke-width="1"/>
|
||||
|
||||
<!-- Root node: Skills (coral focal) -->
|
||||
<rect x="420" y="80" width="160" height="48" rx="6" fill="#f5f4ed"/>
|
||||
<rect x="420" y="80" width="160" height="48" rx="6" fill="rgba(247,89,31,0.08)" stroke="#f7591f" stroke-width="1"/>
|
||||
<rect x="428" y="88" width="32" height="12" rx="2" fill="transparent" stroke="rgba(247,89,31,0.50)" stroke-width="0.8"/>
|
||||
<text x="444" y="97" fill="#f7591f" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">ROOT</text>
|
||||
<text x="500" y="118" fill="#0b0d0b" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Skills</text>
|
||||
|
||||
<!-- Tier 1: Design -->
|
||||
<rect x="140" y="208" width="160" height="48" rx="6" fill="#f5f4ed"/>
|
||||
<rect x="140" y="208" width="160" height="48" rx="6" fill="#ffffff" stroke="#0b0d0b" stroke-width="1"/>
|
||||
<rect x="148" y="216" width="28" height="12" rx="2" fill="transparent" stroke="rgba(11,13,11,0.40)" stroke-width="0.8"/>
|
||||
<text x="162" y="225" fill="#0b0d0b" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">CAT</text>
|
||||
<text x="220" y="240" fill="#0b0d0b" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Design</text>
|
||||
<text x="220" y="252" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">ui · visual · ux</text>
|
||||
|
||||
<!-- Tier 1: Engineering -->
|
||||
<rect x="420" y="208" width="160" height="48" rx="6" fill="#f5f4ed"/>
|
||||
<rect x="420" y="208" width="160" height="48" rx="6" fill="#ffffff" stroke="#0b0d0b" stroke-width="1"/>
|
||||
<rect x="428" y="216" width="28" height="12" rx="2" fill="transparent" stroke="rgba(11,13,11,0.40)" stroke-width="0.8"/>
|
||||
<text x="442" y="225" fill="#0b0d0b" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">CAT</text>
|
||||
<text x="500" y="240" fill="#0b0d0b" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Engineering</text>
|
||||
<text x="500" y="252" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">ship · review · test</text>
|
||||
|
||||
<!-- Tier 1: Research -->
|
||||
<rect x="700" y="208" width="160" height="48" rx="6" fill="#f5f4ed"/>
|
||||
<rect x="700" y="208" width="160" height="48" rx="6" fill="#ffffff" stroke="#0b0d0b" stroke-width="1"/>
|
||||
<rect x="708" y="216" width="28" height="12" rx="2" fill="transparent" stroke="rgba(11,13,11,0.40)" stroke-width="0.8"/>
|
||||
<text x="722" y="225" fill="#0b0d0b" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">CAT</text>
|
||||
<text x="780" y="240" fill="#0b0d0b" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Research</text>
|
||||
<text x="780" y="252" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">investigate · analyze</text>
|
||||
|
||||
<!-- Tier 2: polish -->
|
||||
<rect x="60" y="336" width="160" height="48" rx="6" fill="#f5f4ed"/>
|
||||
<rect x="60" y="336" width="160" height="48" rx="6" fill="rgba(11,13,11,0.05)" stroke="#52534e" stroke-width="0.8"/>
|
||||
<text x="140" y="360" fill="#0b0d0b" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">polish</text>
|
||||
<text x="140" y="372" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">align · space · rhythm</text>
|
||||
|
||||
<!-- Tier 2: critique -->
|
||||
<rect x="220" y="336" width="160" height="48" rx="6" fill="#f5f4ed"/>
|
||||
<rect x="220" y="336" width="160" height="48" rx="6" fill="rgba(11,13,11,0.05)" stroke="#52534e" stroke-width="0.8"/>
|
||||
<text x="300" y="360" fill="#0b0d0b" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">critique</text>
|
||||
<text x="300" y="372" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">hierarchy · density</text>
|
||||
|
||||
<!-- Tier 2: review -->
|
||||
<rect x="400" y="336" width="160" height="48" rx="6" fill="#f5f4ed"/>
|
||||
<rect x="400" y="336" width="160" height="48" rx="6" fill="rgba(11,13,11,0.05)" stroke="#52534e" stroke-width="0.8"/>
|
||||
<text x="480" y="360" fill="#0b0d0b" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">review</text>
|
||||
<text x="480" y="372" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">pre-land diff · sql</text>
|
||||
|
||||
<!-- Tier 2: ship -->
|
||||
<rect x="580" y="336" width="160" height="48" rx="6" fill="#f5f4ed"/>
|
||||
<rect x="580" y="336" width="160" height="48" rx="6" fill="rgba(11,13,11,0.05)" stroke="#52534e" stroke-width="0.8"/>
|
||||
<text x="660" y="360" fill="#0b0d0b" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">ship</text>
|
||||
<text x="660" y="372" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">merge · deploy · verify</text>
|
||||
|
||||
<!-- Tier 2: investigate -->
|
||||
<rect x="760" y="336" width="160" height="48" rx="6" fill="#f5f4ed"/>
|
||||
<rect x="760" y="336" width="160" height="48" rx="6" fill="rgba(11,13,11,0.05)" stroke="#52534e" stroke-width="0.8"/>
|
||||
<text x="840" y="360" fill="#0b0d0b" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">investigate</text>
|
||||
<text x="840" y="372" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">root cause · evidence</text>
|
||||
|
||||
<!-- Legend strip -->
|
||||
<line x1="40" y1="412" x2="960" y2="412" stroke="rgba(11,13,11,0.10)" stroke-width="0.8"/>
|
||||
<text x="40" y="428" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">LEGEND</text>
|
||||
|
||||
<rect x="40" y="444" width="14" height="10" rx="2" fill="rgba(247,89,31,0.08)" stroke="#f7591f" stroke-width="1"/>
|
||||
<text x="60" y="452" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Root · focal</text>
|
||||
|
||||
<rect x="180" y="444" width="14" height="10" rx="2" fill="#ffffff" stroke="#0b0d0b" stroke-width="1"/>
|
||||
<text x="200" y="452" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Category branch</text>
|
||||
|
||||
<rect x="340" y="444" width="14" height="10" rx="2" fill="rgba(11,13,11,0.05)" stroke="#52534e" stroke-width="0.8"/>
|
||||
<text x="360" y="452" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Leaf skill</text>
|
||||
|
||||
<text x="500" y="452" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif" font-style="italic">Orthogonal connectors only. Coral marks the root — every branch descends from one idea.</text>
|
||||
</svg>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
130
skills/assets/example-venn-dark.html
Normal file
130
skills/assets/example-venn-dark.html
Normal file
@@ -0,0 +1,130 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Good design · Desirable × Feasible × Viable</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
:root {
|
||||
--color-paper: #1c1a17;
|
||||
--color-ink: #f1efe7;
|
||||
--color-muted: #a8a69d;
|
||||
--color-accent: #ff6a30;
|
||||
--font-sans: 'Geist', system-ui, sans-serif;
|
||||
--font-serif: 'Instrument Serif', serif;
|
||||
--font-mono: 'Geist Mono', ui-monospace, monospace;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-sans);
|
||||
background: var(--color-paper);
|
||||
color: var(--color-ink);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 3rem 2rem;
|
||||
}
|
||||
|
||||
.frame { max-width: 1200px; width: 100%; }
|
||||
|
||||
.eyebrow {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.66rem;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.18em;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-muted);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-family: var(--font-serif);
|
||||
font-size: clamp(1.5rem, 2.4vw + 0.75rem, 2rem);
|
||||
font-weight: 400;
|
||||
letter-spacing: -0.02em;
|
||||
line-height: 1.15;
|
||||
color: var(--color-ink);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
svg { width: 100%; min-width: 900px; display: block; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="frame">
|
||||
<p class="eyebrow">Venn · Diagram Design</p>
|
||||
<h1>Good design · Desirable × Feasible × Viable</h1>
|
||||
|
||||
<svg viewBox="0 0 1000 500" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<pattern id="dots" width="22" height="22" patternUnits="userSpaceOnUse">
|
||||
<circle cx="1" cy="1" r="0.9" fill="rgba(241,239,231,0.10)"/>
|
||||
</pattern>
|
||||
<clipPath id="clip-center">
|
||||
<circle cx="500" cy="180" r="140"/>
|
||||
</clipPath>
|
||||
<clipPath id="clip-center-2">
|
||||
<circle cx="428" cy="320" r="140"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
|
||||
<rect width="100%" height="100%" fill="#1c1a17"/>
|
||||
<rect width="100%" height="100%" fill="url(#dots)" opacity="0.55"/>
|
||||
|
||||
<!-- Three set circles (stroke + very-low-opacity tint; tints compound in overlaps) -->
|
||||
<!-- Desirable — top, ink -->
|
||||
<circle cx="500" cy="180" r="140" fill="rgba(241,239,231,0.04)" stroke="#f1efe7" stroke-width="1"/>
|
||||
<!-- Feasible — bottom-left, muted -->
|
||||
<circle cx="428" cy="320" r="140" fill="rgba(168,166,157,0.05)" stroke="#a8a69d" stroke-width="1"/>
|
||||
<!-- Viable — bottom-right, soft -->
|
||||
<circle cx="572" cy="320" r="140" fill="rgba(142,140,131,0.05)" stroke="#8e8c83" stroke-width="1"/>
|
||||
|
||||
<!-- Coral focal tint on the all-three intersection -->
|
||||
<g clip-path="url(#clip-center)">
|
||||
<g clip-path="url(#clip-center-2)">
|
||||
<circle cx="572" cy="320" r="140" fill="rgba(255,106,48,0.14)"/>
|
||||
</g>
|
||||
</g>
|
||||
|
||||
<!-- Set labels -->
|
||||
<text x="500" y="20" fill="#f1efe7" font-size="14" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Desirable</text>
|
||||
<text x="500" y="36" fill="#a8a69d" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.14em">PEOPLE WANT IT</text>
|
||||
|
||||
<text x="152" y="400" fill="#a8a69d" font-size="14" font-weight="600" font-family="'Geist', sans-serif" text-anchor="start">Feasible</text>
|
||||
<text x="152" y="416" fill="#a8a69d" font-size="9" font-family="'Geist Mono', monospace" text-anchor="start" letter-spacing="0.14em">WE CAN BUILD IT</text>
|
||||
|
||||
<text x="848" y="400" fill="#8e8c83" font-size="14" font-weight="600" font-family="'Geist', sans-serif" text-anchor="end">Viable</text>
|
||||
<text x="848" y="416" fill="#a8a69d" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end" letter-spacing="0.14em">BUSINESS SUSTAINS</text>
|
||||
|
||||
<!-- Pairwise intersection labels -->
|
||||
<text x="360" y="232" fill="#a8a69d" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Prototype</text>
|
||||
<text x="360" y="248" fill="#a8a69d" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">no business model</text>
|
||||
|
||||
<text x="640" y="232" fill="#a8a69d" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Vaporware</text>
|
||||
<text x="640" y="248" fill="#a8a69d" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">can't build it</text>
|
||||
|
||||
<text x="500" y="368" fill="#a8a69d" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Internal tool</text>
|
||||
<text x="500" y="384" fill="#a8a69d" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">nobody wants it</text>
|
||||
|
||||
<!-- Focal: all-three center -->
|
||||
<text x="500" y="260" fill="#ff6a30" font-size="14" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Shippable</text>
|
||||
<text x="500" y="276" fill="#a8a69d" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.14em">THE SWEET SPOT</text>
|
||||
|
||||
<!-- Legend strip -->
|
||||
<line x1="40" y1="456" x2="960" y2="456" stroke="rgba(241,239,231,0.10)" stroke-width="0.8"/>
|
||||
<text x="40" y="472" fill="#a8a69d" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">LEGEND</text>
|
||||
|
||||
<circle cx="52" cy="488" r="6" fill="none" stroke="#ff6a30" stroke-width="1.2"/>
|
||||
<text x="68" y="492" fill="#a8a69d" font-size="8.5" font-family="'Geist', sans-serif">All three — ship it</text>
|
||||
|
||||
<circle cx="192" cy="488" r="6" fill="none" stroke="#a8a69d" stroke-width="1"/>
|
||||
<text x="208" y="492" fill="#a8a69d" font-size="8.5" font-family="'Geist', sans-serif">Two of three — incomplete</text>
|
||||
|
||||
<text x="380" y="492" fill="#a8a69d" font-size="8.5" font-family="'Geist', sans-serif" font-style="italic">Coral marks the intersection that earns the work. The others name the traps.</text>
|
||||
</svg>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
133
skills/assets/example-venn-full.html
Normal file
133
skills/assets/example-venn-full.html
Normal file
@@ -0,0 +1,133 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Good design · Desirable × Feasible × Viable</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
:root { --color-paper:#f5f4ed; --color-paper-2:#efeee5; --color-ink:#0b0d0b; --color-muted:#52534e; --color-soft:#65655c; --color-rule:rgba(11,13,11,0.12); --color-accent:#f7591f; --color-link:#1a70c7; --font-sans:'Geist',system-ui,sans-serif; --font-serif:'Instrument Serif',serif; --font-mono:'Geist Mono',ui-monospace,monospace; }
|
||||
body { font-family: var(--font-sans); background: var(--color-paper); min-height: 100vh; padding: 3rem 2rem; color: var(--color-ink); }
|
||||
.container { max-width: 1200px; margin: 0 auto; }
|
||||
.header { margin-bottom: 2.5rem; }
|
||||
.header-eyebrow { font-family: var(--font-mono); font-size: 0.66rem; font-weight: 500; letter-spacing: 0.18em; text-transform: uppercase; color: var(--color-muted); margin-bottom: 0.75rem; }
|
||||
h1 { font-family: var(--font-serif); font-size: clamp(1.75rem, 3vw + 1rem, 2.5rem); font-weight: 400; letter-spacing: -0.02em; line-height: 1.1; margin-bottom: 0.5rem; }
|
||||
.subtitle { font-size: 1rem; line-height: 1.55; color: var(--color-muted); max-width: 58ch; }
|
||||
.diagram-container { background: var(--color-paper-2); border-radius: 8px; border: 1px solid var(--color-rule); padding: 1.5rem; overflow-x: auto; }
|
||||
svg { width: 100%; min-width: 900px; display: block; }
|
||||
.cards { display: grid; grid-template-columns: 1.1fr 1fr 0.9fr; gap: 1rem; margin-top: 1.5rem; }
|
||||
@media (max-width: 820px) { .cards { grid-template-columns: 1fr; } }
|
||||
.card { background: #fff; border-radius: 6px; border: 1px solid var(--color-rule); padding: 1.25rem; }
|
||||
.card .eyebrow { font-family: var(--font-mono); font-size: 0.5rem; letter-spacing: 0.18em; text-transform: uppercase; color: var(--color-muted); margin-bottom: 0.5rem; }
|
||||
.card-header { display: flex; align-items: center; gap: 0.6rem; margin-bottom: 0.875rem; padding-bottom: 0.875rem; border-bottom: 1px solid rgba(11,13,11,0.08); }
|
||||
.card-dot { width: 7px; height: 7px; border-radius: 50%; }
|
||||
.card-dot.ink { background: var(--color-ink); } .card-dot.muted { background: var(--color-muted); } .card-dot.coral { background: var(--color-accent); }
|
||||
.card h3 { font-size: 0.875rem; font-weight: 600; }
|
||||
.card p, .card ul { color: var(--color-muted); font-size: 0.8125rem; line-height: 1.55; list-style: none; }
|
||||
.card li { margin-bottom: 0.3rem; padding-left: 0.875rem; position: relative; }
|
||||
.card li::before { content: '—'; position: absolute; left: 0; color: rgba(11,13,11,0.25); font-size: 0.75rem; }
|
||||
.footer { margin-top: 2rem; padding-top: 1.5rem; border-top: 1px solid rgba(11,13,11,0.10); font-family: var(--font-mono); font-size: 0.72rem; letter-spacing: 0.06em; color: var(--color-soft); display: flex; justify-content: space-between; flex-wrap: wrap; gap: 0.5rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<p class="header-eyebrow">Venn · Diagram Design</p>
|
||||
<h1>Good design · Desirable × Feasible × Viable</h1>
|
||||
<p class="subtitle">Three tests every product has to pass before it earns the word "shippable." Miss one and you get a prototype, a vaporware demo, or an internal tool nobody asked for. Coral marks the intersection worth the work.</p>
|
||||
</div>
|
||||
|
||||
<div class="diagram-container">
|
||||
<svg viewBox="0 0 1000 500" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<pattern id="dots" width="22" height="22" patternUnits="userSpaceOnUse">
|
||||
<circle cx="1" cy="1" r="0.9" fill="rgba(11,13,11,0.10)"/>
|
||||
</pattern>
|
||||
<clipPath id="clip-center">
|
||||
<circle cx="500" cy="180" r="140"/>
|
||||
</clipPath>
|
||||
<clipPath id="clip-center-2">
|
||||
<circle cx="428" cy="320" r="140"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
|
||||
<rect width="100%" height="100%" fill="#f5f4ed"/>
|
||||
<rect width="100%" height="100%" fill="url(#dots)" opacity="0.55"/>
|
||||
|
||||
<!-- Three set circles (stroke + very-low-opacity tint; tints compound in overlaps) -->
|
||||
<!-- Desirable — top, ink -->
|
||||
<circle cx="500" cy="180" r="140" fill="rgba(11,13,11,0.04)" stroke="#0b0d0b" stroke-width="1"/>
|
||||
<!-- Feasible — bottom-left, muted -->
|
||||
<circle cx="428" cy="320" r="140" fill="rgba(82,83,78,0.05)" stroke="#52534e" stroke-width="1"/>
|
||||
<!-- Viable — bottom-right, soft -->
|
||||
<circle cx="572" cy="320" r="140" fill="rgba(101,101,92,0.05)" stroke="#65655c" stroke-width="1"/>
|
||||
|
||||
<!-- Coral focal tint on the all-three intersection -->
|
||||
<g clip-path="url(#clip-center)">
|
||||
<g clip-path="url(#clip-center-2)">
|
||||
<circle cx="572" cy="320" r="140" fill="rgba(247,89,31,0.10)"/>
|
||||
</g>
|
||||
</g>
|
||||
|
||||
<!-- Set labels -->
|
||||
<text x="500" y="20" fill="#0b0d0b" font-size="14" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Desirable</text>
|
||||
<text x="500" y="36" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.14em">PEOPLE WANT IT</text>
|
||||
|
||||
<text x="152" y="400" fill="#52534e" font-size="14" font-weight="600" font-family="'Geist', sans-serif" text-anchor="start">Feasible</text>
|
||||
<text x="152" y="416" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="start" letter-spacing="0.14em">WE CAN BUILD IT</text>
|
||||
|
||||
<text x="848" y="400" fill="#65655c" font-size="14" font-weight="600" font-family="'Geist', sans-serif" text-anchor="end">Viable</text>
|
||||
<text x="848" y="416" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end" letter-spacing="0.14em">BUSINESS SUSTAINS</text>
|
||||
|
||||
<!-- Pairwise intersection labels -->
|
||||
<text x="360" y="232" fill="#52534e" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Prototype</text>
|
||||
<text x="360" y="248" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">no business model</text>
|
||||
|
||||
<text x="640" y="232" fill="#52534e" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Vaporware</text>
|
||||
<text x="640" y="248" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">can't build it</text>
|
||||
|
||||
<text x="500" y="368" fill="#52534e" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Internal tool</text>
|
||||
<text x="500" y="384" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">nobody wants it</text>
|
||||
|
||||
<!-- Focal: all-three center -->
|
||||
<text x="500" y="260" fill="#f7591f" font-size="14" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Shippable</text>
|
||||
<text x="500" y="276" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.14em">THE SWEET SPOT</text>
|
||||
|
||||
<!-- Legend strip -->
|
||||
<line x1="40" y1="456" x2="960" y2="456" stroke="rgba(11,13,11,0.10)" stroke-width="0.8"/>
|
||||
<text x="40" y="472" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">LEGEND</text>
|
||||
|
||||
<circle cx="52" cy="488" r="6" fill="none" stroke="#f7591f" stroke-width="1.2"/>
|
||||
<text x="68" y="492" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">All three — ship it</text>
|
||||
|
||||
<circle cx="192" cy="488" r="6" fill="none" stroke="#52534e" stroke-width="1"/>
|
||||
<text x="208" y="492" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Two of three — incomplete</text>
|
||||
|
||||
<text x="380" y="492" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif" font-style="italic">Coral marks the intersection that earns the work. The others name the traps.</text>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div class="cards">
|
||||
<div class="card">
|
||||
<p class="eyebrow">THE HEADLINE</p>
|
||||
<div class="card-header"><span class="card-dot coral"></span><h3>One sweet spot, in coral</h3></div>
|
||||
<p>Three overlapping circles create seven regions. Six of them are diagnostic — they name the failure mode. Only the center earns the coral. If every region is colored, the diagram stops prioritizing anything.</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-header"><span class="card-dot ink"></span><h3>The three tests</h3></div>
|
||||
<ul><li>Desirable — someone pulls for it</li><li>Feasible — the team can actually build it</li><li>Viable — the economics hold up over time</li><li>All three, or you don't ship</li></ul>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-header"><span class="card-dot muted"></span><h3>The named traps</h3></div>
|
||||
<p>Prototype (loved, buildable, no model). Vaporware (loved, profitable, un-buildable). Internal tool (buildable, profitable, unloved). Labeling each one makes the map more useful than the center alone.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<span>good design · desirable × feasible × viable</span>
|
||||
<span>example · diagram-design</span>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
137
skills/assets/example-venn.html
Normal file
137
skills/assets/example-venn.html
Normal file
@@ -0,0 +1,137 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Good design · Desirable × Feasible × Viable</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
:root {
|
||||
--color-paper: #f5f4ed;
|
||||
--color-ink: #0b0d0b;
|
||||
--color-muted: #52534e;
|
||||
--color-accent: #f7591f;
|
||||
--font-sans: 'Geist', system-ui, sans-serif;
|
||||
--font-serif: 'Instrument Serif', serif;
|
||||
--font-mono: 'Geist Mono', ui-monospace, monospace;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-sans);
|
||||
background: var(--color-paper);
|
||||
color: var(--color-ink);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 3rem 2rem;
|
||||
}
|
||||
|
||||
.frame { max-width: 1200px; width: 100%; }
|
||||
|
||||
.eyebrow {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.66rem;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.18em;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-muted);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-family: var(--font-serif);
|
||||
font-size: clamp(1.5rem, 2.4vw + 0.75rem, 2rem);
|
||||
font-weight: 400;
|
||||
letter-spacing: -0.02em;
|
||||
line-height: 1.15;
|
||||
color: var(--color-ink);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
svg { width: 100%; min-width: 900px; display: block; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="frame">
|
||||
<p class="eyebrow">Venn · Diagram Design</p>
|
||||
<h1>Good design · Desirable × Feasible × Viable</h1>
|
||||
|
||||
<svg viewBox="0 0 1000 500" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<pattern id="dots" width="22" height="22" patternUnits="userSpaceOnUse">
|
||||
<circle cx="1" cy="1" r="0.9" fill="rgba(11,13,11,0.10)"/>
|
||||
</pattern>
|
||||
<!-- Clip to the all-three intersection (centroid region) for the coral tint -->
|
||||
<clipPath id="clip-center">
|
||||
<circle cx="500" cy="180" r="140"/>
|
||||
</clipPath>
|
||||
<clipPath id="clip-center-2">
|
||||
<circle cx="428" cy="320" r="140"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
|
||||
<rect width="100%" height="100%" fill="#f5f4ed"/>
|
||||
<rect width="100%" height="100%" fill="url(#dots)" opacity="0.55"/>
|
||||
|
||||
<!-- Three set circles (stroke + very-low-opacity tint; tints compound in overlaps) -->
|
||||
<!-- Desirable — top, ink -->
|
||||
<circle cx="500" cy="180" r="140" fill="rgba(11,13,11,0.04)" stroke="#0b0d0b" stroke-width="1"/>
|
||||
<!-- Feasible — bottom-left, muted -->
|
||||
<circle cx="428" cy="320" r="140" fill="rgba(82,83,78,0.05)" stroke="#52534e" stroke-width="1"/>
|
||||
<!-- Viable — bottom-right, soft -->
|
||||
<circle cx="572" cy="320" r="140" fill="rgba(101,101,92,0.05)" stroke="#65655c" stroke-width="1"/>
|
||||
|
||||
<!-- Coral focal tint on the all-three intersection -->
|
||||
<g clip-path="url(#clip-center)">
|
||||
<g clip-path="url(#clip-center-2)">
|
||||
<circle cx="572" cy="320" r="140" fill="rgba(247,89,31,0.10)"/>
|
||||
</g>
|
||||
</g>
|
||||
|
||||
<!-- Set labels (outside circles, pinned to far side) -->
|
||||
<!-- Desirable -->
|
||||
<text x="500" y="20" fill="#0b0d0b" font-size="14" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Desirable</text>
|
||||
<text x="500" y="36" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.14em">PEOPLE WANT IT</text>
|
||||
|
||||
<!-- Feasible -->
|
||||
<text x="152" y="400" fill="#52534e" font-size="14" font-weight="600" font-family="'Geist', sans-serif" text-anchor="start">Feasible</text>
|
||||
<text x="152" y="416" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="start" letter-spacing="0.14em">WE CAN BUILD IT</text>
|
||||
|
||||
<!-- Viable -->
|
||||
<text x="848" y="400" fill="#65655c" font-size="14" font-weight="600" font-family="'Geist', sans-serif" text-anchor="end">Viable</text>
|
||||
<text x="848" y="416" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end" letter-spacing="0.14em">BUSINESS SUSTAINS</text>
|
||||
|
||||
<!-- Pairwise intersection labels (inside overlap regions) -->
|
||||
<!-- Desirable ∩ Feasible (upper-left overlap): "Prototype" — between top and BL -->
|
||||
<text x="360" y="232" fill="#52534e" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Prototype</text>
|
||||
<text x="360" y="248" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">no business model</text>
|
||||
|
||||
<!-- Desirable ∩ Viable (upper-right overlap): "Vaporware" -->
|
||||
<text x="640" y="232" fill="#52534e" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Vaporware</text>
|
||||
<text x="640" y="248" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">can't build it</text>
|
||||
|
||||
<!-- Feasible ∩ Viable (lower overlap): "Internal tool" -->
|
||||
<text x="500" y="368" fill="#52534e" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Internal tool</text>
|
||||
<text x="500" y="384" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">nobody wants it</text>
|
||||
|
||||
<!-- Focal: all-three center -->
|
||||
<text x="500" y="260" fill="#f7591f" font-size="14" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Shippable</text>
|
||||
<text x="500" y="276" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.14em">THE SWEET SPOT</text>
|
||||
|
||||
<!-- Legend strip -->
|
||||
<line x1="40" y1="456" x2="960" y2="456" stroke="rgba(11,13,11,0.10)" stroke-width="0.8"/>
|
||||
<text x="40" y="472" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">LEGEND</text>
|
||||
|
||||
<circle cx="52" cy="488" r="6" fill="none" stroke="#f7591f" stroke-width="1.2"/>
|
||||
<text x="68" y="492" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">All three — ship it</text>
|
||||
|
||||
<circle cx="192" cy="488" r="6" fill="none" stroke="#52534e" stroke-width="1"/>
|
||||
<text x="208" y="492" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Two of three — incomplete</text>
|
||||
|
||||
<text x="380" y="492" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif" font-style="italic">Coral marks the intersection that earns the work. The others name the traps.</text>
|
||||
</svg>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
9
skills/assets/fonts.css
Normal file
9
skills/assets/fonts.css
Normal file
@@ -0,0 +1,9 @@
|
||||
/* html-ppt :: shared webfonts */
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@200;300;400;500;600;700;800;900&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@200;300;400;500;600;700;900&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Noto+Serif+SC:wght@300;400;600;700&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,400;0,600;0,800;1,400&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@300;400;500;700&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Archivo+Black&display=swap');
|
||||
144
skills/assets/generic.md
Normal file
144
skills/assets/generic.md
Normal file
@@ -0,0 +1,144 @@
|
||||
<!-- Updated: 2026-02-07 -->
|
||||
# Generic Business SEO Strategy Template
|
||||
|
||||
## Overview
|
||||
|
||||
This template applies to businesses that don't fit neatly into SaaS, local service, e-commerce, publisher, or agency categories. Customize based on your specific business model.
|
||||
|
||||
## Recommended Site Architecture
|
||||
|
||||
```
|
||||
/
|
||||
├── Home
|
||||
├── /products (or /services)
|
||||
│ ├── /product-1
|
||||
│ ├── /product-2
|
||||
│ └── ...
|
||||
├── /solutions (if applicable)
|
||||
│ ├── /solution-1
|
||||
│ └── ...
|
||||
├── /about
|
||||
│ ├── /team
|
||||
│ ├── /history
|
||||
│ └── /values
|
||||
├── /resources
|
||||
│ ├── /blog
|
||||
│ ├── /guides
|
||||
│ ├── /faq
|
||||
│ └── /glossary
|
||||
├── /contact
|
||||
├── /support
|
||||
└── /legal
|
||||
├── /privacy
|
||||
└── /terms
|
||||
```
|
||||
|
||||
## Universal SEO Principles
|
||||
|
||||
### Every Page Should Have
|
||||
- Unique title tag (30-60 chars)
|
||||
- Unique meta description (120-160 chars)
|
||||
- Single H1 matching page intent
|
||||
- Logical heading hierarchy (H1→H2→H3)
|
||||
- Internal links to related content
|
||||
- Clear call-to-action
|
||||
|
||||
### Schema for All Sites
|
||||
| Page Type | Schema Types |
|
||||
|-----------|-------------|
|
||||
| Homepage | Organization, WebSite |
|
||||
| About | Organization, AboutPage |
|
||||
| Contact | ContactPage |
|
||||
| Blog | Article, BlogPosting |
|
||||
| FAQ | (FAQPage only for gov/health) |
|
||||
| Product/Service | Product or Service |
|
||||
|
||||
## Content Quality Standards
|
||||
|
||||
### Minimum Word Counts
|
||||
| Page Type | Min Words |
|
||||
|-----------|-----------|
|
||||
| Homepage | 500 |
|
||||
| Product/Service | 800 |
|
||||
| Blog Post | 1,500 |
|
||||
| About Page | 400 |
|
||||
| Landing Page | 600 |
|
||||
|
||||
### E-E-A-T Essentials
|
||||
1. **Experience**: Share real examples and case studies
|
||||
2. **Expertise**: Display credentials and qualifications
|
||||
3. **Authoritativeness**: Earn mentions and citations
|
||||
4. **Trustworthiness**: Full contact info, policies visible
|
||||
|
||||
## Technical Foundations
|
||||
|
||||
### Must-Haves
|
||||
- [ ] HTTPS enabled
|
||||
- [ ] Mobile-responsive design
|
||||
- [ ] robots.txt configured
|
||||
- [ ] XML sitemap submitted
|
||||
- [ ] Google Search Console verified
|
||||
- [ ] Core Web Vitals passing (LCP <2.5s, INP <200ms, CLS <0.1)
|
||||
|
||||
### Should-Haves
|
||||
- [ ] Structured data on key pages
|
||||
- [ ] Internal linking strategy
|
||||
- [ ] 404 error page optimized
|
||||
- [ ] Redirect chains eliminated
|
||||
- [ ] Image optimization (WebP, lazy loading)
|
||||
|
||||
## Content Priorities
|
||||
|
||||
### Phase 1: Foundation (weeks 1-4)
|
||||
1. Homepage optimization
|
||||
2. Core product/service pages
|
||||
3. About and contact pages
|
||||
4. Basic schema implementation
|
||||
|
||||
### Phase 2: Expansion (weeks 5-12)
|
||||
1. Blog launch (2-4 posts/month)
|
||||
2. FAQ page
|
||||
3. Additional product/service pages
|
||||
4. Internal linking audit
|
||||
|
||||
### Phase 3: Growth (weeks 13-24)
|
||||
1. Consistent content publishing
|
||||
2. Link building outreach
|
||||
3. GEO optimization
|
||||
4. Performance optimization
|
||||
|
||||
### Phase 4: Authority (months 7-12)
|
||||
1. Thought leadership content
|
||||
2. Original research
|
||||
3. PR and media mentions
|
||||
4. Advanced schema
|
||||
|
||||
## Key Metrics to Track
|
||||
|
||||
- Organic traffic (overall and by section)
|
||||
- Keyword rankings (branded and non-branded)
|
||||
- Conversion rate from organic
|
||||
- Pages indexed
|
||||
- Core Web Vitals scores
|
||||
- Backlinks acquired
|
||||
|
||||
## Customization Points
|
||||
|
||||
Adjust this template based on:
|
||||
|
||||
1. **Business Model**: B2B vs B2C vs D2C
|
||||
2. **Geographic Scope**: Local, national, or international
|
||||
3. **Content Type**: Product-focused vs content-heavy
|
||||
4. **Competition Level**: Niche vs competitive market
|
||||
5. **Resources**: Budget and team capacity
|
||||
|
||||
## Generative Engine Optimization (GEO) Checklist
|
||||
|
||||
- [ ] Include clear, quotable facts and statistics that AI systems can extract and cite
|
||||
- [ ] Use structured data (Schema.org) to help AI systems understand content
|
||||
- [ ] Build topical authority through comprehensive content clusters
|
||||
- [ ] Provide original data, research, or unique perspectives AI cannot find elsewhere
|
||||
- [ ] Maintain consistent entity information (brand, people, products) across the web
|
||||
- [ ] Structure content with clear headings, definitions, and step-by-step formats
|
||||
- [ ] Consider adding an `llms.txt` file at site root (emerging convention for AI crawlers: Google treats it as a regular text file)
|
||||
- [ ] Monitor AI citation across Google AI Overviews, ChatGPT, Perplexity, and Bing Copilot
|
||||
23
skills/assets/image-prompt-template.txt
Normal file
23
skills/assets/image-prompt-template.txt
Normal file
@@ -0,0 +1,23 @@
|
||||
Transform the subject into a Funko Pop / Pop Mart blind box style 3D figurine.
|
||||
|
||||
Style:
|
||||
- Cute cartoon proportions (large head, small body)
|
||||
- 3D rendered (C4D/Octane quality), premium plastic/vinyl finish
|
||||
- Clean white background, soft studio lighting
|
||||
|
||||
Subject handling:
|
||||
- Person: preserve facial features, hairstyle, clothing
|
||||
- Animal/Pet: preserve species, fur color, markings
|
||||
- Object: stylize into cute mascot figurine
|
||||
- Logo/Icon: transform to 3D toy, preserve original colors and shape
|
||||
|
||||
Action: {action}
|
||||
Caption: "{caption}"
|
||||
|
||||
Caption rendering (CRITICAL — follow exactly):
|
||||
- Black bold text with thick white outline stroke
|
||||
- Large, clear sans-serif font (e.g. Impact, Helvetica Bold)
|
||||
- MUST be placed at the absolute bottom center of the image as a standalone text banner
|
||||
- MUST NOT appear on the character's body, clothing, or any accessory
|
||||
- Leave visible gap between the character's feet and the caption text
|
||||
- Text must have sharp anti-aliased edges — it must survive video animation without warping
|
||||
206
skills/assets/index.html
Normal file
206
skills/assets/index.html
Normal file
@@ -0,0 +1,206 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Diagram Design · Gallery</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
:root {
|
||||
--paper: #f5f4ed;
|
||||
--paper-2: #efeee5;
|
||||
--ink: #0b0d0b;
|
||||
--muted: #52534e;
|
||||
--soft: #65655c;
|
||||
--rule: rgba(11,13,11,0.12);
|
||||
--rule-strong: rgba(11,13,11,0.25);
|
||||
--accent: #f7591f;
|
||||
--sans: 'Geist', system-ui, sans-serif;
|
||||
--serif: 'Instrument Serif', serif;
|
||||
--mono: 'Geist Mono', ui-monospace, monospace;
|
||||
}
|
||||
html, body { height: 100%; }
|
||||
body {
|
||||
font-family: var(--sans);
|
||||
background: var(--paper);
|
||||
color: var(--ink);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.topbar {
|
||||
padding: 1rem 1.25rem 0.625rem;
|
||||
border-bottom: 1px solid var(--rule);
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
.title-row {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 0.75rem;
|
||||
gap: 1rem;
|
||||
}
|
||||
.title-row h1 {
|
||||
font-family: var(--serif);
|
||||
font-size: 1.5rem;
|
||||
font-weight: 400;
|
||||
letter-spacing: -0.01em;
|
||||
line-height: 1;
|
||||
}
|
||||
.title-row .meta {
|
||||
font-family: var(--mono);
|
||||
font-size: 0.66rem;
|
||||
letter-spacing: 0.18em;
|
||||
text-transform: uppercase;
|
||||
color: var(--muted);
|
||||
}
|
||||
.title-row .open-raw {
|
||||
font-family: var(--mono);
|
||||
font-size: 0.66rem;
|
||||
letter-spacing: 0.06em;
|
||||
color: var(--muted);
|
||||
text-decoration: none;
|
||||
padding: 0.3rem 0.625rem;
|
||||
border: 1px solid var(--rule);
|
||||
border-radius: 4px;
|
||||
transition: color 0.15s, border-color 0.15s;
|
||||
}
|
||||
.open-raw:hover { color: var(--accent); border-color: var(--accent); }
|
||||
|
||||
.tabs {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.375rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.tabs.variants {
|
||||
margin-bottom: 0;
|
||||
padding-top: 0.5rem;
|
||||
border-top: 1px solid var(--rule);
|
||||
}
|
||||
.tab {
|
||||
font-family: var(--sans);
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 500;
|
||||
padding: 0.375rem 0.75rem;
|
||||
background: transparent;
|
||||
color: var(--muted);
|
||||
border: 1px solid var(--rule);
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: all 0.12s;
|
||||
letter-spacing: -0.005em;
|
||||
}
|
||||
.tab:hover {
|
||||
color: var(--ink);
|
||||
border-color: var(--rule-strong);
|
||||
}
|
||||
.tab.active {
|
||||
background: var(--ink);
|
||||
color: var(--paper);
|
||||
border-color: var(--ink);
|
||||
}
|
||||
.tab .eyebrow {
|
||||
display: inline-block;
|
||||
margin-right: 0.25rem;
|
||||
font-family: var(--mono);
|
||||
font-size: 0.56rem;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
color: inherit;
|
||||
opacity: 0.5;
|
||||
}
|
||||
.tab.new::after {
|
||||
content: 'NEW';
|
||||
display: inline-block;
|
||||
margin-left: 0.35rem;
|
||||
font-family: var(--mono);
|
||||
font-size: 0.54rem;
|
||||
letter-spacing: 0.12em;
|
||||
color: var(--accent);
|
||||
vertical-align: 1px;
|
||||
}
|
||||
.tab.active.new::after { color: var(--paper); }
|
||||
|
||||
main {
|
||||
flex: 1 1 auto;
|
||||
position: relative;
|
||||
background: var(--paper-2);
|
||||
overflow: hidden;
|
||||
}
|
||||
iframe {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 0;
|
||||
background: var(--paper);
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header class="topbar">
|
||||
<div class="title-row">
|
||||
<h1>Diagram Design <span class="meta" style="font-family: var(--mono); font-size: 0.66rem; margin-left: 0.5rem;">· gallery</span></h1>
|
||||
<a class="open-raw" id="open-raw" href="#" target="_blank" rel="noopener">open in new tab ↗</a>
|
||||
</div>
|
||||
|
||||
<div class="tabs" id="type-tabs" role="tablist" aria-label="Diagram type">
|
||||
<button class="tab active" data-type="architecture"><span class="eyebrow">01</span>Architecture</button>
|
||||
<button class="tab" data-type="flowchart"><span class="eyebrow">02</span>Flowchart</button>
|
||||
<button class="tab" data-type="sequence"><span class="eyebrow">03</span>Sequence</button>
|
||||
<button class="tab" data-type="state"><span class="eyebrow">04</span>State</button>
|
||||
<button class="tab" data-type="er"><span class="eyebrow">05</span>ER</button>
|
||||
<button class="tab" data-type="timeline"><span class="eyebrow">06</span>Timeline</button>
|
||||
<button class="tab" data-type="swimlane"><span class="eyebrow">07</span>Swimlane</button>
|
||||
<button class="tab" data-type="quadrant"><span class="eyebrow">08</span>Quadrant</button>
|
||||
<button class="tab new" data-type="nested"><span class="eyebrow">09</span>Nested</button>
|
||||
<button class="tab new" data-type="tree"><span class="eyebrow">10</span>Tree</button>
|
||||
<button class="tab new" data-type="layers"><span class="eyebrow">11</span>Layers</button>
|
||||
<button class="tab new" data-type="venn"><span class="eyebrow">12</span>Venn</button>
|
||||
<button class="tab new" data-type="pyramid"><span class="eyebrow">13</span>Pyramid</button>
|
||||
</div>
|
||||
|
||||
<div class="tabs variants" id="variant-tabs" role="tablist" aria-label="Variant">
|
||||
<button class="tab active" data-variant="">Minimal light</button>
|
||||
<button class="tab" data-variant="-dark">Minimal dark</button>
|
||||
<button class="tab" data-variant="-full">Full editorial</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<iframe id="preview" src="example-architecture.html" title="Preview"></iframe>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
const state = { type: 'architecture', variant: '' };
|
||||
const iframe = document.getElementById('preview');
|
||||
const openRaw = document.getElementById('open-raw');
|
||||
|
||||
function update() {
|
||||
const src = `example-${state.type}${state.variant}.html`;
|
||||
iframe.src = src;
|
||||
openRaw.href = src;
|
||||
}
|
||||
|
||||
function bindTabs(containerId, key) {
|
||||
const container = document.getElementById(containerId);
|
||||
container.addEventListener('click', (e) => {
|
||||
const btn = e.target.closest('.tab');
|
||||
if (!btn) return;
|
||||
container.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
state[key] = btn.dataset[key];
|
||||
update();
|
||||
});
|
||||
}
|
||||
|
||||
bindTabs('type-tabs', 'type');
|
||||
bindTabs('variant-tabs', 'variant');
|
||||
update();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
192
skills/assets/ios_frame.jsx
Normal file
192
skills/assets/ios_frame.jsx
Normal file
@@ -0,0 +1,192 @@
|
||||
/**
|
||||
* IosFrame — iPhone设备边框
|
||||
*
|
||||
* 参考iPhone 15 Pro(393×852 logical pixels)
|
||||
* 含:灵动岛 + 状态栏(时间/信号/电池)+ Home Indicator + 圆角
|
||||
*
|
||||
* 用法:
|
||||
* <IosFrame time="9:41" battery={85}>
|
||||
* <YourAppContent />
|
||||
* </IosFrame>
|
||||
*
|
||||
* 自定义:
|
||||
* <IosFrame width={390} height={844} darkMode showKeyboard>
|
||||
* ...
|
||||
* </IosFrame>
|
||||
*/
|
||||
|
||||
const iosFrameStyles = {
|
||||
wrapper: {
|
||||
display: 'inline-block',
|
||||
padding: 12,
|
||||
background: '#000',
|
||||
borderRadius: 60,
|
||||
boxShadow: '0 0 0 2px #1f2937, 0 20px 60px rgba(0,0,0,0.3)',
|
||||
position: 'relative',
|
||||
},
|
||||
screen: {
|
||||
position: 'relative',
|
||||
borderRadius: 48,
|
||||
overflow: 'hidden',
|
||||
background: '#fff',
|
||||
},
|
||||
statusBar: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: 54,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
padding: '0 32px 0 32px',
|
||||
fontSize: 16,
|
||||
fontWeight: 600,
|
||||
fontFamily: '-apple-system, "SF Pro Text", sans-serif',
|
||||
zIndex: 20,
|
||||
pointerEvents: 'none',
|
||||
},
|
||||
dynamicIsland: {
|
||||
position: 'absolute',
|
||||
top: 12,
|
||||
left: '50%',
|
||||
transform: 'translateX(-50%)',
|
||||
width: 124,
|
||||
height: 36,
|
||||
background: '#000',
|
||||
borderRadius: 999,
|
||||
zIndex: 30,
|
||||
},
|
||||
statusIcons: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 6,
|
||||
},
|
||||
signalIcon: {
|
||||
display: 'flex',
|
||||
alignItems: 'flex-end',
|
||||
gap: 2,
|
||||
height: 12,
|
||||
},
|
||||
signalBar: {
|
||||
width: 3,
|
||||
background: 'currentColor',
|
||||
borderRadius: 1,
|
||||
},
|
||||
wifiIcon: {
|
||||
width: 16,
|
||||
height: 12,
|
||||
position: 'relative',
|
||||
},
|
||||
batteryIcon: {
|
||||
width: 26,
|
||||
height: 12,
|
||||
border: '1.5px solid currentColor',
|
||||
borderRadius: 3,
|
||||
padding: 1,
|
||||
position: 'relative',
|
||||
opacity: 0.8,
|
||||
},
|
||||
batteryCap: {
|
||||
position: 'absolute',
|
||||
top: 3,
|
||||
right: -3,
|
||||
width: 2,
|
||||
height: 6,
|
||||
background: 'currentColor',
|
||||
borderRadius: '0 1px 1px 0',
|
||||
},
|
||||
content: {
|
||||
position: 'absolute',
|
||||
top: 54,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 34,
|
||||
overflow: 'auto',
|
||||
},
|
||||
homeIndicator: {
|
||||
position: 'absolute',
|
||||
bottom: 10,
|
||||
left: '50%',
|
||||
transform: 'translateX(-50%)',
|
||||
width: 140,
|
||||
height: 5,
|
||||
background: 'rgba(0,0,0,0.3)',
|
||||
borderRadius: 999,
|
||||
zIndex: 10,
|
||||
},
|
||||
homeIndicatorDark: {
|
||||
background: 'rgba(255,255,255,0.5)',
|
||||
},
|
||||
};
|
||||
|
||||
function IosFrame({
|
||||
children,
|
||||
width = 393,
|
||||
height = 852,
|
||||
time = '9:41',
|
||||
battery = 100,
|
||||
darkMode = false,
|
||||
showStatusBar = true,
|
||||
showDynamicIsland = true,
|
||||
showHomeIndicator = true,
|
||||
}) {
|
||||
const textColor = darkMode ? '#fff' : '#000';
|
||||
|
||||
return (
|
||||
<div style={iosFrameStyles.wrapper}>
|
||||
<div style={{
|
||||
...iosFrameStyles.screen,
|
||||
width,
|
||||
height,
|
||||
background: darkMode ? '#000' : '#fff',
|
||||
}}>
|
||||
{showStatusBar && (
|
||||
<div style={{ ...iosFrameStyles.statusBar, color: textColor }}>
|
||||
<span>{time}</span>
|
||||
<div style={iosFrameStyles.statusIcons}>
|
||||
<div style={iosFrameStyles.signalIcon}>
|
||||
<div style={{ ...iosFrameStyles.signalBar, height: 4 }} />
|
||||
<div style={{ ...iosFrameStyles.signalBar, height: 6 }} />
|
||||
<div style={{ ...iosFrameStyles.signalBar, height: 9 }} />
|
||||
<div style={{ ...iosFrameStyles.signalBar, height: 11 }} />
|
||||
</div>
|
||||
<svg width="16" height="12" viewBox="0 0 16 12" fill="none" style={{ color: textColor }}>
|
||||
<path d="M8 11.5a1 1 0 100-2 1 1 0 000 2z" fill="currentColor" />
|
||||
<path d="M3 7.5a7 7 0 0110 0" stroke="currentColor" strokeWidth="1.3" fill="none" strokeLinecap="round" />
|
||||
<path d="M1 4.5a11 11 0 0114 0" stroke="currentColor" strokeWidth="1.3" fill="none" strokeLinecap="round" opacity="0.7" />
|
||||
</svg>
|
||||
<div style={iosFrameStyles.batteryIcon}>
|
||||
<div style={{
|
||||
width: `${battery}%`,
|
||||
height: '100%',
|
||||
background: 'currentColor',
|
||||
borderRadius: 1,
|
||||
opacity: 0.9,
|
||||
}} />
|
||||
<div style={iosFrameStyles.batteryCap} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showDynamicIsland && <div style={iosFrameStyles.dynamicIsland} />}
|
||||
|
||||
<div style={iosFrameStyles.content}>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
{showHomeIndicator && (
|
||||
<div style={{
|
||||
...iosFrameStyles.homeIndicator,
|
||||
...(darkMode ? iosFrameStyles.homeIndicatorDark : {}),
|
||||
}} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
window.IosFrame = IosFrame;
|
||||
}
|
||||
160
skills/assets/local-service.md
Normal file
160
skills/assets/local-service.md
Normal file
@@ -0,0 +1,160 @@
|
||||
<!-- Updated: 2026-02-07 -->
|
||||
# Local Service Business SEO Strategy Template
|
||||
|
||||
## Industry Characteristics
|
||||
|
||||
- Geographic-focused searches
|
||||
- High intent, quick decision making
|
||||
- Reviews heavily influence decisions
|
||||
- Phone calls are primary conversion
|
||||
- Mobile-first user behavior
|
||||
- Emergency/urgent service needs
|
||||
|
||||
## Recommended Site Architecture
|
||||
|
||||
```
|
||||
/
|
||||
├── Home
|
||||
├── /services
|
||||
│ ├── /service-1
|
||||
│ ├── /service-2
|
||||
│ └── ...
|
||||
├── /locations
|
||||
│ ├── /city-1
|
||||
│ │ ├── /service-1-city-1
|
||||
│ │ └── ...
|
||||
│ ├── /city-2
|
||||
│ └── ...
|
||||
├── /about
|
||||
├── /reviews
|
||||
├── /gallery (or /portfolio)
|
||||
├── /blog
|
||||
├── /contact
|
||||
├── /emergency (if applicable)
|
||||
└── /faq
|
||||
```
|
||||
|
||||
## Quality Gates
|
||||
|
||||
### Location Page Limits
|
||||
- ⚠️ **WARNING** at 30+ location pages
|
||||
- 🛑 **HARD STOP** at 50+ location pages
|
||||
|
||||
### Unique Content Requirements
|
||||
| Page Type | Min Words | Unique % |
|
||||
|-----------|-----------|----------|
|
||||
| Primary Location | 600 | 60%+ |
|
||||
| Service Area | 500 | 40%+ |
|
||||
| Service Page | 800 | 100% |
|
||||
|
||||
### What Makes Location Pages Unique
|
||||
- Local landmarks and neighborhoods
|
||||
- Specific services offered at that location
|
||||
- Local team members
|
||||
- Location-specific testimonials
|
||||
- Community involvement
|
||||
- Local regulations or considerations
|
||||
|
||||
## Schema Recommendations
|
||||
|
||||
| Page Type | Schema Types |
|
||||
|-----------|-------------|
|
||||
| Homepage | LocalBusiness, Organization |
|
||||
| Service Pages | Service, LocalBusiness |
|
||||
| Location Pages | LocalBusiness (with geo) |
|
||||
| Contact | ContactPage, LocalBusiness |
|
||||
| Reviews | LocalBusiness (with AggregateRating) |
|
||||
|
||||
### LocalBusiness Schema Example
|
||||
```json
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "LocalBusiness",
|
||||
"name": "Business Name",
|
||||
"address": {
|
||||
"@type": "PostalAddress",
|
||||
"streetAddress": "123 Main St",
|
||||
"addressLocality": "City",
|
||||
"addressRegion": "State",
|
||||
"postalCode": "12345"
|
||||
},
|
||||
"telephone": "+1-555-555-5555",
|
||||
"openingHours": "Mo-Fr 08:00-18:00",
|
||||
"geo": {
|
||||
"@type": "GeoCoordinates",
|
||||
"latitude": "40.7128",
|
||||
"longitude": "-74.0060"
|
||||
},
|
||||
"areaServed": ["City 1", "City 2"],
|
||||
"priceRange": "$$"
|
||||
}
|
||||
```
|
||||
|
||||
## Google Business Profile Integration
|
||||
|
||||
- Ensure NAP consistency (Name, Address, Phone)
|
||||
- Sync service categories
|
||||
- Regular post updates
|
||||
- Photo uploads
|
||||
- Review response strategy
|
||||
|
||||
### Google Business Profile Updates (2025-2026)
|
||||
|
||||
- **Video verification** is now standard: postcard verification has been largely phased out. Prepare for a short video verification process showing the business location or service area.
|
||||
- **WhatsApp integration** replaced Google Business Chat (deprecated). Businesses can connect WhatsApp as their primary messaging channel.
|
||||
- **Q&A removed from Maps**: replaced by AI-generated answers. Ensure your GBP description, services, and website FAQ are comprehensive, as Google AI uses them to answer queries.
|
||||
- **Business hours are a top-5 ranking factor**: "Business is open at time of search" ranked as a top individual factor for the first time (Whitespark 2026 Local Search Ranking Factors Report). Keep hours accurate; consider extended hours if feasible.
|
||||
- **Review "Stories" format**: Google Maps now shows review snippets in a swipeable Stories format on mobile. Encourage detailed, descriptive reviews with photos.
|
||||
|
||||
### Service Area Business (SAB) Update (June 2025)
|
||||
|
||||
Google updated SAB guidelines to **disallow entire states or countries** as service areas. SABs must specify: cities, postal/ZIP codes, or neighborhoods. If you serve an entire metro area, list the major cities within it rather than the state.
|
||||
|
||||
### AI Visibility for Local Businesses
|
||||
|
||||
AI Overviews appear for only ~0.14% of local keywords (March 2025 data), local SEO faces significantly less AI disruption than other verticals. However, ChatGPT and Perplexity are increasingly used for local recommendations.
|
||||
|
||||
To optimize for AI local visibility:
|
||||
- Ensure presence on expert-curated "best of" lists (ranked #1 AI visibility factor in Whitespark 2026 report)
|
||||
- Maintain consistent NAP (Name, Address, Phone) across all platforms
|
||||
- Build genuine review volume and quality
|
||||
- Use LocalBusiness schema with complete properties (geo, openingHours, priceRange, areaServed)
|
||||
|
||||
## Content Priorities
|
||||
|
||||
### High Priority
|
||||
1. Homepage with clear service area
|
||||
2. Core service pages
|
||||
3. Primary city page
|
||||
4. Contact page with all locations
|
||||
|
||||
### Medium Priority
|
||||
1. Service + location combination pages
|
||||
2. FAQ page
|
||||
3. About/team page
|
||||
4. Reviews/testimonials page
|
||||
|
||||
### Blog Topics
|
||||
- Seasonal maintenance tips
|
||||
- How to choose a [service provider]
|
||||
- Warning signs of [problem]
|
||||
- DIY vs professional comparisons
|
||||
- Local regulations and permits
|
||||
|
||||
## Key Metrics to Track
|
||||
|
||||
- Local pack rankings
|
||||
- Phone call volume from organic
|
||||
- Direction requests
|
||||
- Google Business Profile insights
|
||||
- Reviews count and rating
|
||||
|
||||
## Generative Engine Optimization (GEO) for Local
|
||||
|
||||
- [ ] Include clear, quotable service descriptions and pricing ranges
|
||||
- [ ] Use LocalBusiness schema with complete geo, openingHours, and areaServed
|
||||
- [ ] Build presence on curated "best of" and local directory lists
|
||||
- [ ] Maintain consistent NAP across all platforms (Google, Yelp, Apple Maps)
|
||||
- [ ] Include original photos of work, team, and location
|
||||
- [ ] Structure FAQ content for common local service questions
|
||||
- [ ] Monitor AI citation in ChatGPT and Perplexity local recommendations
|
||||
96
skills/assets/macos_window.jsx
Normal file
96
skills/assets/macos_window.jsx
Normal file
@@ -0,0 +1,96 @@
|
||||
/**
|
||||
* MacosWindow — macOS应用窗口边框(含traffic lights)
|
||||
*
|
||||
* 用法:
|
||||
* <MacosWindow title="Finder">
|
||||
* <YourAppContent />
|
||||
* </MacosWindow>
|
||||
*/
|
||||
|
||||
const macosWindowStyles = {
|
||||
window: {
|
||||
display: 'inline-block',
|
||||
background: '#fff',
|
||||
borderRadius: 10,
|
||||
overflow: 'hidden',
|
||||
boxShadow: '0 30px 80px rgba(0,0,0,0.25), 0 0 0 0.5px rgba(0,0,0,0.15)',
|
||||
},
|
||||
titleBar: {
|
||||
height: 38,
|
||||
background: 'linear-gradient(to bottom, #e8e8e8, #d8d8d8)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
padding: '0 14px',
|
||||
borderBottom: '0.5px solid rgba(0,0,0,0.1)',
|
||||
position: 'relative',
|
||||
userSelect: 'none',
|
||||
},
|
||||
trafficLights: {
|
||||
display: 'flex',
|
||||
gap: 8,
|
||||
alignItems: 'center',
|
||||
},
|
||||
light: {
|
||||
width: 12,
|
||||
height: 12,
|
||||
borderRadius: '50%',
|
||||
border: '0.5px solid rgba(0,0,0,0.15)',
|
||||
},
|
||||
close: { background: '#ff5f57' },
|
||||
minimize: { background: '#febc2e' },
|
||||
maximize: { background: '#28c840' },
|
||||
title: {
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
right: 0,
|
||||
textAlign: 'center',
|
||||
fontSize: 13,
|
||||
color: '#333',
|
||||
fontWeight: 500,
|
||||
fontFamily: '-apple-system, "SF Pro Text", sans-serif',
|
||||
pointerEvents: 'none',
|
||||
},
|
||||
content: {
|
||||
position: 'relative',
|
||||
overflow: 'auto',
|
||||
},
|
||||
titleBarDark: {
|
||||
background: 'linear-gradient(to bottom, #3c3c3c, #2c2c2c)',
|
||||
borderBottom: '0.5px solid rgba(255,255,255,0.1)',
|
||||
},
|
||||
titleDark: {
|
||||
color: '#ddd',
|
||||
},
|
||||
};
|
||||
|
||||
function MacosWindow({ title = '', width = 900, height = 600, darkMode = false, children }) {
|
||||
return (
|
||||
<div style={{ ...macosWindowStyles.window, background: darkMode ? '#1e1e1e' : '#fff' }}>
|
||||
<div style={{
|
||||
...macosWindowStyles.titleBar,
|
||||
...(darkMode ? macosWindowStyles.titleBarDark : {}),
|
||||
}}>
|
||||
<div style={macosWindowStyles.trafficLights}>
|
||||
<div style={{ ...macosWindowStyles.light, ...macosWindowStyles.close }} />
|
||||
<div style={{ ...macosWindowStyles.light, ...macosWindowStyles.minimize }} />
|
||||
<div style={{ ...macosWindowStyles.light, ...macosWindowStyles.maximize }} />
|
||||
</div>
|
||||
{title && (
|
||||
<div style={{
|
||||
...macosWindowStyles.title,
|
||||
...(darkMode ? macosWindowStyles.titleDark : {}),
|
||||
}}>
|
||||
{title}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div style={{ ...macosWindowStyles.content, width, height }}>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
window.MacosWindow = MacosWindow;
|
||||
}
|
||||
71
skills/assets/personal-asset-index.example.json
Normal file
71
skills/assets/personal-asset-index.example.json
Normal file
@@ -0,0 +1,71 @@
|
||||
{
|
||||
"_meta": {
|
||||
"description": "个人素材索引模板 — 复制此文件并填入你的真实数据",
|
||||
"how_to_use": "1. 复制此文件到 ~/.claude/memory/personal-asset-index.json 2. 填入你的真实信息 3. design-philosophy skill 会自动读取",
|
||||
"note": "真实数据文件不要放在 skill 目录内,避免随 skill 分发泄露隐私"
|
||||
},
|
||||
|
||||
"identity": {
|
||||
"real_name": "你的真名",
|
||||
"pen_names": ["笔名1", "笔名2"],
|
||||
"english_name": "English Name",
|
||||
"title": "你的头衔/一句话介绍",
|
||||
"bio_short": "50-100字简介",
|
||||
"bio_long": "200-300字详细介绍",
|
||||
"avatar_url": "头像URL",
|
||||
"source": "数据来源备注"
|
||||
},
|
||||
|
||||
"contact": {
|
||||
"email": "your@email.com",
|
||||
"wechat_personal": "微信号",
|
||||
"source": "数据来源备注"
|
||||
},
|
||||
|
||||
"social_media": {
|
||||
"github": {
|
||||
"url": "https://github.com/yourname",
|
||||
"username": "yourname"
|
||||
},
|
||||
"youtube": {
|
||||
"url": "https://www.youtube.com/@YourChannel",
|
||||
"channel_name": "频道名"
|
||||
},
|
||||
"source": "数据来源备注"
|
||||
},
|
||||
|
||||
"websites": {
|
||||
"main_site": {
|
||||
"url": "https://yoursite.com",
|
||||
"description": "网站描述",
|
||||
"local_path": "/path/to/local/project/"
|
||||
}
|
||||
},
|
||||
|
||||
"products": {
|
||||
"product_1": {
|
||||
"name": "产品名",
|
||||
"type": "iOS App / Web App / CLI Tool / 电子书",
|
||||
"achievement": "主要成就",
|
||||
"icon_path": "/path/to/icon.png",
|
||||
"project_path": "/path/to/project/"
|
||||
}
|
||||
},
|
||||
|
||||
"stats": {
|
||||
"social_followers": "粉丝数",
|
||||
"product_users": "用户数",
|
||||
"source": "数据来源备注"
|
||||
},
|
||||
|
||||
"design_assets": {
|
||||
"article_images": {
|
||||
"base_path": "/path/to/images/",
|
||||
"notable_sets": []
|
||||
}
|
||||
},
|
||||
|
||||
"knowledge_base": {
|
||||
"wechat_articles": "/path/to/knowledge_base/"
|
||||
}
|
||||
}
|
||||
153
skills/assets/publisher.md
Normal file
153
skills/assets/publisher.md
Normal file
@@ -0,0 +1,153 @@
|
||||
<!-- Updated: 2026-02-07 -->
|
||||
# Publisher/Media SEO Strategy Template
|
||||
|
||||
## Industry Characteristics
|
||||
|
||||
- High content volume
|
||||
- Time-sensitive content (news)
|
||||
- Ad revenue dependent on traffic
|
||||
- Authority and trust critical
|
||||
- Competing with social platforms
|
||||
- AI Overviews impact on traffic
|
||||
|
||||
## Recommended Site Architecture
|
||||
|
||||
```
|
||||
/
|
||||
├── Home
|
||||
├── /news (or /latest)
|
||||
├── /topics
|
||||
│ ├── /topic-1
|
||||
│ ├── /topic-2
|
||||
│ └── ...
|
||||
├── /authors
|
||||
│ ├── /author-1
|
||||
│ └── ...
|
||||
├── /opinion
|
||||
├── /reviews
|
||||
├── /guides
|
||||
├── /videos
|
||||
├── /podcasts
|
||||
├── /newsletter
|
||||
├── /about
|
||||
│ ├── /editorial-policy
|
||||
│ ├── /corrections
|
||||
│ └── /contact
|
||||
└── /[year]/[month]/[slug] (article URLs)
|
||||
```
|
||||
|
||||
## Schema Recommendations
|
||||
|
||||
| Page Type | Schema Types |
|
||||
|-----------|-------------|
|
||||
| Article | NewsArticle or Article, Person (author), Organization (publisher) |
|
||||
| Author Page | Person, ProfilePage |
|
||||
| Topic Page | CollectionPage, ItemList |
|
||||
| Homepage | WebSite, Organization |
|
||||
| Video | VideoObject |
|
||||
| Podcast | PodcastEpisode, PodcastSeries |
|
||||
|
||||
### NewsArticle Schema Example
|
||||
```json
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "NewsArticle",
|
||||
"headline": "Article Headline",
|
||||
"datePublished": "2026-02-07T10:00:00Z",
|
||||
"dateModified": "2026-02-07T14:30:00Z",
|
||||
"author": {
|
||||
"@type": "Person",
|
||||
"name": "Author Name",
|
||||
"url": "https://example.com/authors/author-name"
|
||||
},
|
||||
"publisher": {
|
||||
"@type": "Organization",
|
||||
"name": "Publication Name",
|
||||
"logo": {
|
||||
"@type": "ImageObject",
|
||||
"url": "https://example.com/logo.png"
|
||||
}
|
||||
},
|
||||
"image": ["https://example.com/article-image.jpg"],
|
||||
"mainEntityOfPage": {
|
||||
"@type": "WebPage",
|
||||
"@id": "https://example.com/article-url"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## E-E-A-T Requirements
|
||||
|
||||
Publishers face highest E-E-A-T scrutiny.
|
||||
|
||||
### Author Pages Must Include
|
||||
- Full name and photo
|
||||
- Bio and credentials
|
||||
- Areas of expertise
|
||||
- Contact information
|
||||
- Social profiles (sameAs)
|
||||
- Previous articles by this author
|
||||
|
||||
### Editorial Standards
|
||||
- Clear correction policy
|
||||
- Transparent editorial process
|
||||
- Fact-checking procedures
|
||||
- Conflict of interest disclosures
|
||||
|
||||
## Content Priorities
|
||||
|
||||
### High Priority
|
||||
1. Breaking news (speed matters)
|
||||
2. Evergreen guides on core topics
|
||||
3. Author pages with credentials
|
||||
4. Topic hubs/pillar pages
|
||||
|
||||
### Medium Priority
|
||||
1. Opinion/analysis pieces
|
||||
2. Video content
|
||||
3. Interactive content
|
||||
4. Newsletter landing pages
|
||||
|
||||
### GEO Considerations
|
||||
- Clear, quotable facts in articles
|
||||
- Tables for data-heavy content
|
||||
- Expert quotes with attribution
|
||||
- Update dates prominently displayed
|
||||
- Structured headings (H2/H3)
|
||||
- First-party data and original research are highly cited by AI systems
|
||||
- Ensure author entities are clearly defined with Person schema + sameAs links
|
||||
- Monitor AI citation frequency across Google AI Overviews, AI Mode, ChatGPT, Perplexity
|
||||
- Treat AI citation as a standalone KPI alongside organic traffic
|
||||
|
||||
### Publisher SEO Updates (2025-2026)
|
||||
|
||||
- **Google News automatic inclusion:** Google News no longer accepts manual applications (since March 2025). Inclusion is fully automatic based on Google's content quality criteria. Focus on Google News sitemap markup and consistent, high-quality publishing cadence.
|
||||
- **KPI shift:** Traffic-based KPIs (sessions, pageviews) are declining in relevance as AI Overviews reduce click-through rates. Leading publishers are shifting to: subscriber conversions, time on page, scroll depth, newsletter signups, AI citation frequency, and revenue per visitor.
|
||||
- **Site reputation abuse risk:** Publishers hosting third-party content (coupons, product reviews, affiliate content) under their domain are at high risk. Google penalized Forbes, WSJ, Time, and CNN for this in late 2024. If hosting third-party content, ensure strong editorial oversight and clear first-party involvement.
|
||||
|
||||
## Technical Considerations
|
||||
|
||||
### Core Web Vitals
|
||||
- Ad placement affects CLS
|
||||
- Lazy load ads and images below fold
|
||||
- Optimize hero images for LCP
|
||||
- Minimize render-blocking resources
|
||||
|
||||
### AMP (if used)
|
||||
- Consider dropping AMP (no longer required for Top Stories)
|
||||
- Ensure canonical setup is correct
|
||||
- Monitor performance vs non-AMP
|
||||
|
||||
### Pagination
|
||||
- Proper pagination for multi-page articles
|
||||
- Or infinite scroll with proper indexing
|
||||
- Canonical to page 1 or full article
|
||||
|
||||
## Key Metrics to Track
|
||||
|
||||
- Page views from organic
|
||||
- Time on page
|
||||
- Pages per session
|
||||
- Newsletter signups from organic
|
||||
- Google News/Discover traffic
|
||||
- AI Overview appearances
|
||||
879
skills/assets/runtime.js
Normal file
879
skills/assets/runtime.js
Normal file
@@ -0,0 +1,879 @@
|
||||
/* html-ppt :: runtime.js
|
||||
* Keyboard-driven deck runtime. Zero dependencies.
|
||||
*
|
||||
* Features:
|
||||
* ← → / space / PgUp PgDn / Home End navigation
|
||||
* F fullscreen
|
||||
* S presenter mode (opens a NEW WINDOW with current/next slide preview + notes + timer)
|
||||
* The original window stays as audience view, synced via BroadcastChannel.
|
||||
* Slide previews use CSS transform:scale() at design resolution for pixel-perfect layout.
|
||||
* N quick notes overlay (bottom drawer)
|
||||
* O slide overview grid
|
||||
* T cycle themes (reads data-themes on <html> or <body>)
|
||||
* A cycle demo animation on current slide
|
||||
* URL hash #/N deep-link to slide N (1-based)
|
||||
* Progress bar auto-managed
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
const ANIMS = ['fade-up','fade-down','fade-left','fade-right','rise-in','drop-in',
|
||||
'zoom-pop','blur-in','glitch-in','typewriter','neon-glow','shimmer-sweep',
|
||||
'gradient-flow','stagger-list','counter-up','path-draw','parallax-tilt',
|
||||
'card-flip-3d','cube-rotate-3d','page-turn-3d','perspective-zoom',
|
||||
'marquee-scroll','kenburns','confetti-burst','spotlight','morph-shape','ripple-reveal'];
|
||||
|
||||
function ready(fn){ if(document.readyState!='loading')fn(); else document.addEventListener('DOMContentLoaded',fn);}
|
||||
|
||||
/* ========== Parse URL for preview-only mode ==========
|
||||
* When loaded as iframe.src = "index.html?preview=3", runtime enters a
|
||||
* locked single-slide mode: only slide N is visible, no chrome, no keys,
|
||||
* no hash updates. This is how the presenter window shows pixel-perfect
|
||||
* previews — by loading the actual deck file in an iframe and telling it
|
||||
* to display only a specific slide.
|
||||
*/
|
||||
function getPreviewIdx() {
|
||||
const m = /[?&]preview=(\d+)/.exec(location.search || '');
|
||||
return m ? parseInt(m[1], 10) - 1 : -1;
|
||||
}
|
||||
|
||||
ready(function () {
|
||||
const deck = document.querySelector('.deck');
|
||||
if (!deck) return;
|
||||
const slides = Array.from(deck.querySelectorAll('.slide'));
|
||||
if (!slides.length) return;
|
||||
|
||||
const previewOnlyIdx = getPreviewIdx();
|
||||
const isPreviewMode = previewOnlyIdx >= 0 && previewOnlyIdx < slides.length;
|
||||
|
||||
/* ===== Preview-only mode: show one slide, hide everything else ===== */
|
||||
if (isPreviewMode) {
|
||||
function showSlide(i) {
|
||||
slides.forEach((s, j) => {
|
||||
const active = (j === i);
|
||||
s.classList.toggle('is-active', active);
|
||||
s.style.display = active ? '' : 'none';
|
||||
if (active) {
|
||||
s.style.opacity = '1';
|
||||
s.style.transform = 'none';
|
||||
s.style.pointerEvents = 'auto';
|
||||
}
|
||||
});
|
||||
}
|
||||
showSlide(previewOnlyIdx);
|
||||
/* Hide chrome that the presenter shouldn't see in preview */
|
||||
const hideSel = '.progress-bar, .notes-overlay, .overview, .notes, aside.notes, .speaker-notes';
|
||||
document.querySelectorAll(hideSel).forEach(el => { el.style.display = 'none'; });
|
||||
document.documentElement.setAttribute('data-preview', '1');
|
||||
document.body.setAttribute('data-preview', '1');
|
||||
/* Auto-detect theme base path for theme switching in preview mode */
|
||||
function getPreviewThemeBase() {
|
||||
const base = document.documentElement.getAttribute('data-theme-base');
|
||||
if (base) return base;
|
||||
const tl = document.getElementById('theme-link');
|
||||
if (tl) {
|
||||
const raw = tl.getAttribute('href') || '';
|
||||
const ls = raw.lastIndexOf('/');
|
||||
if (ls >= 0) return raw.substring(0, ls + 1);
|
||||
}
|
||||
return 'assets/themes/';
|
||||
}
|
||||
const previewThemeBase = getPreviewThemeBase();
|
||||
|
||||
/* Listen for postMessage from parent presenter window:
|
||||
* - preview-goto: switch visible slide WITHOUT reloading
|
||||
* - preview-theme: switch theme CSS link to match audience window */
|
||||
window.addEventListener('message', function(e) {
|
||||
if (!e.data) return;
|
||||
if (e.data.type === 'preview-goto') {
|
||||
const n = parseInt(e.data.idx, 10);
|
||||
if (n >= 0 && n < slides.length) showSlide(n);
|
||||
} else if (e.data.type === 'preview-theme' && e.data.name) {
|
||||
let link = document.getElementById('theme-link');
|
||||
if (!link) {
|
||||
link = document.createElement('link');
|
||||
link.rel = 'stylesheet';
|
||||
link.id = 'theme-link';
|
||||
document.head.appendChild(link);
|
||||
}
|
||||
link.href = previewThemeBase + e.data.name + '.css';
|
||||
document.documentElement.setAttribute('data-theme', e.data.name);
|
||||
}
|
||||
});
|
||||
/* Signal to parent that preview iframe is ready */
|
||||
try { window.parent && window.parent.postMessage({ type: 'preview-ready' }, '*'); } catch(e) {}
|
||||
return;
|
||||
}
|
||||
|
||||
let idx = 0;
|
||||
const total = slides.length;
|
||||
|
||||
/* ===== BroadcastChannel for presenter sync ===== */
|
||||
const CHANNEL_NAME = 'html-ppt-presenter-' + location.pathname;
|
||||
let bc;
|
||||
try { bc = new BroadcastChannel(CHANNEL_NAME); } catch(e) { bc = null; }
|
||||
|
||||
// Are we running inside the presenter popup? (legacy flag, now unused)
|
||||
const isPresenterWindow = false;
|
||||
|
||||
/* ===== progress bar ===== */
|
||||
let bar = document.querySelector('.progress-bar');
|
||||
if (!bar) {
|
||||
bar = document.createElement('div');
|
||||
bar.className = 'progress-bar';
|
||||
bar.innerHTML = '<span></span>';
|
||||
document.body.appendChild(bar);
|
||||
}
|
||||
const barFill = bar.querySelector('span');
|
||||
|
||||
/* ===== notes overlay (N key) ===== */
|
||||
let notes = document.querySelector('.notes-overlay');
|
||||
if (!notes) {
|
||||
notes = document.createElement('div');
|
||||
notes.className = 'notes-overlay';
|
||||
document.body.appendChild(notes);
|
||||
}
|
||||
|
||||
/* ===== overview grid (O key) ===== */
|
||||
let overview = document.querySelector('.overview');
|
||||
if (!overview) {
|
||||
overview = document.createElement('div');
|
||||
overview.className = 'overview';
|
||||
slides.forEach((s, i) => {
|
||||
const t = document.createElement('div');
|
||||
t.className = 'thumb';
|
||||
const title = s.getAttribute('data-title') ||
|
||||
(s.querySelector('h1,h2,h3')||{}).textContent || ('Slide '+(i+1));
|
||||
t.innerHTML = '<div class="n">'+(i+1)+'</div><div class="t">'+title.trim().slice(0,80)+'</div>';
|
||||
t.addEventListener('click', () => { go(i); toggleOverview(false); });
|
||||
overview.appendChild(t);
|
||||
});
|
||||
document.body.appendChild(overview);
|
||||
}
|
||||
|
||||
/* ===== navigation ===== */
|
||||
function go(n, fromRemote){
|
||||
n = Math.max(0, Math.min(total-1, n));
|
||||
slides.forEach((s,i) => {
|
||||
s.classList.toggle('is-active', i===n);
|
||||
s.classList.toggle('is-prev', i<n);
|
||||
});
|
||||
idx = n;
|
||||
barFill.style.width = ((n+1)/total*100)+'%';
|
||||
const numEl = document.querySelector('.slide-number');
|
||||
if (numEl) { numEl.setAttribute('data-current', n+1); numEl.setAttribute('data-total', total); }
|
||||
|
||||
// notes (bottom overlay)
|
||||
const note = slides[n].querySelector('.notes, aside.notes, .speaker-notes');
|
||||
notes.innerHTML = note ? note.innerHTML : '';
|
||||
|
||||
// hash
|
||||
const hashTarget = '#/'+(n+1);
|
||||
if (location.hash !== hashTarget && !isPresenterWindow) {
|
||||
history.replaceState(null,'', hashTarget);
|
||||
}
|
||||
|
||||
// re-trigger entry animations
|
||||
slides[n].querySelectorAll('[data-anim]').forEach(el => {
|
||||
const a = el.getAttribute('data-anim');
|
||||
el.classList.remove('anim-'+a);
|
||||
void el.offsetWidth;
|
||||
el.classList.add('anim-'+a);
|
||||
});
|
||||
|
||||
// counter-up
|
||||
slides[n].querySelectorAll('.counter').forEach(el => {
|
||||
const target = parseFloat(el.getAttribute('data-to')||el.textContent);
|
||||
const dur = parseInt(el.getAttribute('data-dur')||'1200',10);
|
||||
const start = performance.now();
|
||||
const from = 0;
|
||||
function tick(now){
|
||||
const t = Math.min(1,(now-start)/dur);
|
||||
const v = from + (target-from)*(1-Math.pow(1-t,3));
|
||||
el.textContent = (target % 1 === 0) ? Math.round(v) : v.toFixed(1);
|
||||
if (t<1) requestAnimationFrame(tick);
|
||||
}
|
||||
requestAnimationFrame(tick);
|
||||
});
|
||||
|
||||
// Broadcast to other window (audience ↔ presenter)
|
||||
if (!fromRemote && bc) {
|
||||
bc.postMessage({ type: 'go', idx: n });
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== listen for remote navigation / theme changes ===== */
|
||||
if (bc) {
|
||||
bc.onmessage = function(e) {
|
||||
if (!e.data) return;
|
||||
if (e.data.type === 'go' && typeof e.data.idx === 'number') {
|
||||
go(e.data.idx, true);
|
||||
} else if (e.data.type === 'theme' && e.data.name) {
|
||||
/* Sync theme across windows */
|
||||
const i = themes.indexOf(e.data.name);
|
||||
if (i >= 0) themeIdx = i;
|
||||
applyTheme(e.data.name);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function toggleNotes(force){ notes.classList.toggle('open', force!==undefined?force:!notes.classList.contains('open')); }
|
||||
function toggleOverview(force){ overview.classList.toggle('open', force!==undefined?force:!overview.classList.contains('open')); }
|
||||
|
||||
/* ========== PRESENTER MODE — Magnetic-card popup window ========== */
|
||||
/* Opens a new window with 4 draggable, resizable cards:
|
||||
* CURRENT — iframe(?preview=N) pixel-perfect preview of current slide
|
||||
* NEXT — iframe(?preview=N+1) pixel-perfect preview of next slide
|
||||
* SCRIPT — large speaker notes (逐字稿)
|
||||
* TIMER — elapsed timer + page counter + controls
|
||||
* Cards remember position/size in localStorage.
|
||||
* Two windows sync via BroadcastChannel.
|
||||
*/
|
||||
let presenterWin = null;
|
||||
|
||||
function openPresenterWindow() {
|
||||
if (presenterWin && !presenterWin.closed) {
|
||||
presenterWin.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
// Build absolute URL of THIS deck file (without hash/query)
|
||||
const deckUrl = location.protocol + '//' + location.host + location.pathname;
|
||||
|
||||
// Collect slide titles + notes (HTML strings)
|
||||
const slideMeta = slides.map((s, i) => {
|
||||
const note = s.querySelector('.notes, aside.notes, .speaker-notes');
|
||||
return {
|
||||
title: s.getAttribute('data-title') ||
|
||||
(s.querySelector('h1,h2,h3')||{}).textContent || ('Slide '+(i+1)),
|
||||
notes: note ? note.innerHTML : ''
|
||||
};
|
||||
});
|
||||
|
||||
/* Capture current theme so presenter previews match the audience */
|
||||
const currentTheme = root.getAttribute('data-theme') || (themes[themeIdx] || '');
|
||||
const presenterHTML = buildPresenterHTML(deckUrl, slideMeta, total, idx, CHANNEL_NAME, currentTheme);
|
||||
|
||||
presenterWin = window.open('', 'html-ppt-presenter', 'width=1280,height=820,menubar=no,toolbar=no');
|
||||
if (!presenterWin) {
|
||||
alert('请允许弹出窗口以使用演讲者视图');
|
||||
return;
|
||||
}
|
||||
presenterWin.document.open();
|
||||
presenterWin.document.write(presenterHTML);
|
||||
presenterWin.document.close();
|
||||
}
|
||||
|
||||
function buildPresenterHTML(deckUrl, slideMeta, total, startIdx, channelName, currentTheme) {
|
||||
const metaJSON = JSON.stringify(slideMeta);
|
||||
const deckUrlJSON = JSON.stringify(deckUrl);
|
||||
const channelJSON = JSON.stringify(channelName);
|
||||
const themeJSON = JSON.stringify(currentTheme || '');
|
||||
const storageKey = 'html-ppt-presenter:' + location.pathname;
|
||||
|
||||
// Build the document as a single template string for clarity
|
||||
return `<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Presenter View</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
html, body {
|
||||
width: 100%; height: 100%; overflow: hidden;
|
||||
background: #1a1d24;
|
||||
background-image:
|
||||
radial-gradient(circle at 20% 30%, rgba(88,166,255,.04), transparent 50%),
|
||||
radial-gradient(circle at 80% 70%, rgba(188,140,255,.04), transparent 50%);
|
||||
color: #e6edf3;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans SC", sans-serif;
|
||||
}
|
||||
/* Stage: positioned area where cards live */
|
||||
#stage { position: absolute; inset: 0; overflow: hidden; }
|
||||
|
||||
/* Magnetic card */
|
||||
.pcard {
|
||||
position: absolute;
|
||||
background: #0d1117;
|
||||
border: 1px solid rgba(255,255,255,.1);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 8px 32px rgba(0,0,0,.45), 0 0 0 1px rgba(255,255,255,.02);
|
||||
display: flex; flex-direction: column;
|
||||
overflow: hidden;
|
||||
min-width: 180px; min-height: 100px;
|
||||
transition: box-shadow .2s, border-color .2s;
|
||||
}
|
||||
.pcard.dragging { box-shadow: 0 16px 48px rgba(0,0,0,.6), 0 0 0 2px rgba(88,166,255,.5); border-color: #58a6ff; transition: none; z-index: 9999; }
|
||||
.pcard.resizing { box-shadow: 0 16px 48px rgba(0,0,0,.6), 0 0 0 2px rgba(63,185,80,.5); border-color: #3fb950; transition: none; z-index: 9999; }
|
||||
.pcard:hover { border-color: rgba(88,166,255,.3); }
|
||||
|
||||
/* Card header (drag handle) */
|
||||
.pcard-head {
|
||||
display: flex; align-items: center; gap: 10px;
|
||||
padding: 8px 12px;
|
||||
background: rgba(255,255,255,.04);
|
||||
border-bottom: 1px solid rgba(255,255,255,.06);
|
||||
cursor: move;
|
||||
user-select: none;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.pcard-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--dot-color, #58a6ff); flex-shrink: 0; }
|
||||
.pcard-title {
|
||||
font-size: 11px; letter-spacing: .15em; text-transform: uppercase;
|
||||
font-weight: 700; color: #8b949e; flex: 1;
|
||||
}
|
||||
.pcard-meta { font-size: 11px; color: #6e7681; }
|
||||
|
||||
/* Card body */
|
||||
.pcard-body { flex: 1; position: relative; overflow: hidden; min-height: 0; }
|
||||
|
||||
/* Preview cards (CURRENT/NEXT) — iframe-based pixel-perfect render */
|
||||
.pcard-preview .pcard-body { background: #000; }
|
||||
.pcard-preview iframe {
|
||||
position: absolute; top: 0; left: 0;
|
||||
width: 1920px; height: 1080px;
|
||||
border: none;
|
||||
transform-origin: top left;
|
||||
pointer-events: none;
|
||||
background: transparent;
|
||||
}
|
||||
.pcard-preview .preview-end {
|
||||
position: absolute; inset: 0;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
color: #484f58; font-size: 14px; letter-spacing: .12em;
|
||||
}
|
||||
|
||||
/* Notes card */
|
||||
.pcard-notes .pcard-body {
|
||||
padding: 14px 18px;
|
||||
overflow-y: auto;
|
||||
font-size: 18px; line-height: 1.75;
|
||||
color: #d0d7de;
|
||||
font-family: "Noto Sans SC", -apple-system, sans-serif;
|
||||
}
|
||||
.pcard-notes .pcard-body p { margin: 0 0 .7em 0; }
|
||||
.pcard-notes .pcard-body strong { color: #f0883e; }
|
||||
.pcard-notes .pcard-body em { color: #58a6ff; font-style: normal; }
|
||||
.pcard-notes .pcard-body code {
|
||||
font-family: "SF Mono", monospace; font-size: .9em;
|
||||
background: rgba(255,255,255,.08); padding: 1px 6px; border-radius: 4px;
|
||||
}
|
||||
.pcard-notes .empty { color: #484f58; font-style: italic; }
|
||||
|
||||
/* Timer card */
|
||||
.pcard-timer .pcard-body {
|
||||
display: flex; flex-direction: column; gap: 14px;
|
||||
padding: 18px 20px; justify-content: center;
|
||||
}
|
||||
.timer-display {
|
||||
font-family: "SF Mono", "JetBrains Mono", monospace;
|
||||
font-size: 42px; font-weight: 700;
|
||||
color: #3fb950;
|
||||
letter-spacing: .04em;
|
||||
line-height: 1;
|
||||
}
|
||||
.timer-row {
|
||||
display: flex; align-items: center; gap: 12px;
|
||||
font-size: 14px; color: #8b949e;
|
||||
}
|
||||
.timer-row .label { font-size: 10px; letter-spacing: .15em; text-transform: uppercase; color: #6e7681; }
|
||||
.timer-row .val { color: #e6edf3; font-weight: 600; font-family: "SF Mono", monospace; }
|
||||
.timer-controls { display: flex; gap: 8px; flex-wrap: wrap; }
|
||||
.timer-btn {
|
||||
background: rgba(255,255,255,.06);
|
||||
border: 1px solid rgba(255,255,255,.1);
|
||||
color: #e6edf3;
|
||||
padding: 6px 12px;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
}
|
||||
.timer-btn:hover { background: rgba(88,166,255,.15); border-color: #58a6ff; }
|
||||
.timer-btn:active { transform: translateY(1px); }
|
||||
|
||||
/* Resize handle */
|
||||
.pcard-resize {
|
||||
position: absolute; right: 0; bottom: 0;
|
||||
width: 18px; height: 18px;
|
||||
cursor: nwse-resize;
|
||||
background: linear-gradient(135deg, transparent 50%, rgba(255,255,255,.25) 50%, rgba(255,255,255,.25) 60%, transparent 60%, transparent 70%, rgba(255,255,255,.25) 70%, rgba(255,255,255,.25) 80%, transparent 80%);
|
||||
z-index: 5;
|
||||
}
|
||||
.pcard-resize:hover { background: linear-gradient(135deg, transparent 50%, #58a6ff 50%, #58a6ff 60%, transparent 60%, transparent 70%, #58a6ff 70%, #58a6ff 80%, transparent 80%); }
|
||||
|
||||
/* Bottom hint bar */
|
||||
.hint-bar {
|
||||
position: fixed; bottom: 0; left: 0; right: 0;
|
||||
background: rgba(0,0,0,.6);
|
||||
backdrop-filter: blur(10px);
|
||||
border-top: 1px solid rgba(255,255,255,.08);
|
||||
padding: 6px 16px;
|
||||
font-size: 11px; color: #8b949e;
|
||||
display: flex; gap: 18px; align-items: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
.hint-bar kbd {
|
||||
background: rgba(255,255,255,.08);
|
||||
padding: 1px 6px; border-radius: 3px;
|
||||
font-family: "SF Mono", monospace;
|
||||
font-size: 10px;
|
||||
border: 1px solid rgba(255,255,255,.1);
|
||||
color: #e6edf3;
|
||||
}
|
||||
.hint-bar .reset-layout {
|
||||
margin-left: auto;
|
||||
background: transparent; border: 1px solid rgba(255,255,255,.15);
|
||||
color: #8b949e; padding: 3px 10px; border-radius: 4px;
|
||||
font-size: 11px; cursor: pointer; font-family: inherit;
|
||||
}
|
||||
.hint-bar .reset-layout:hover { background: rgba(248,81,73,.15); border-color: #f85149; color: #f85149; }
|
||||
|
||||
body.is-dragging-card * { user-select: none !important; }
|
||||
body.is-dragging-card iframe { pointer-events: none !important; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div id="stage">
|
||||
<div class="pcard pcard-preview" id="card-cur" style="--dot-color:#58a6ff">
|
||||
<div class="pcard-head" data-drag>
|
||||
<span class="pcard-dot"></span>
|
||||
<span class="pcard-title">CURRENT</span>
|
||||
<span class="pcard-meta" id="cur-meta">—</span>
|
||||
</div>
|
||||
<div class="pcard-body"><iframe id="iframe-cur"></iframe></div>
|
||||
<div class="pcard-resize" data-resize></div>
|
||||
</div>
|
||||
|
||||
<div class="pcard pcard-preview" id="card-nxt" style="--dot-color:#bc8cff">
|
||||
<div class="pcard-head" data-drag>
|
||||
<span class="pcard-dot"></span>
|
||||
<span class="pcard-title">NEXT</span>
|
||||
<span class="pcard-meta" id="nxt-meta">—</span>
|
||||
</div>
|
||||
<div class="pcard-body"><iframe id="iframe-nxt"></iframe></div>
|
||||
<div class="pcard-resize" data-resize></div>
|
||||
</div>
|
||||
|
||||
<div class="pcard pcard-notes" id="card-notes" style="--dot-color:#f0883e">
|
||||
<div class="pcard-head" data-drag>
|
||||
<span class="pcard-dot"></span>
|
||||
<span class="pcard-title">SPEAKER SCRIPT · 逐字稿</span>
|
||||
</div>
|
||||
<div class="pcard-body" id="notes-body"></div>
|
||||
<div class="pcard-resize" data-resize></div>
|
||||
</div>
|
||||
|
||||
<div class="pcard pcard-timer" id="card-timer" style="--dot-color:#3fb950">
|
||||
<div class="pcard-head" data-drag>
|
||||
<span class="pcard-dot"></span>
|
||||
<span class="pcard-title">TIMER</span>
|
||||
</div>
|
||||
<div class="pcard-body">
|
||||
<div class="timer-display" id="timer-display">00:00</div>
|
||||
<div class="timer-row">
|
||||
<span class="label">Slide</span>
|
||||
<span class="val" id="timer-count">1 / ${total}</span>
|
||||
</div>
|
||||
<div class="timer-controls">
|
||||
<button class="timer-btn" id="btn-prev">← Prev</button>
|
||||
<button class="timer-btn" id="btn-next">Next →</button>
|
||||
<button class="timer-btn" id="btn-reset">⏱ Reset</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pcard-resize" data-resize></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hint-bar">
|
||||
<span><kbd>← →</kbd> 翻页</span>
|
||||
<span><kbd>R</kbd> 重置计时</span>
|
||||
<span><kbd>Esc</kbd> 关闭</span>
|
||||
<span style="color:#6e7681">拖动卡片头部移动 · 拖动右下角调整大小</span>
|
||||
<button class="reset-layout" id="reset-layout">重置布局</button>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function(){
|
||||
var slideMeta = ${metaJSON};
|
||||
var total = ${total};
|
||||
var idx = ${startIdx};
|
||||
var deckUrl = ${deckUrlJSON};
|
||||
var STORAGE_KEY = ${JSON.stringify(storageKey)};
|
||||
var bc;
|
||||
try { bc = new BroadcastChannel(${channelJSON}); } catch(e) {}
|
||||
|
||||
var iframeCur = document.getElementById('iframe-cur');
|
||||
var iframeNxt = document.getElementById('iframe-nxt');
|
||||
var notesBody = document.getElementById('notes-body');
|
||||
var curMeta = document.getElementById('cur-meta');
|
||||
var nxtMeta = document.getElementById('nxt-meta');
|
||||
var timerDisplay = document.getElementById('timer-display');
|
||||
var timerCount = document.getElementById('timer-count');
|
||||
|
||||
/* ===== Default card layout ===== */
|
||||
function defaultLayout() {
|
||||
var w = window.innerWidth;
|
||||
var h = window.innerHeight - 36; /* leave room for hint bar */
|
||||
return {
|
||||
'card-cur': { x: 16, y: 16, w: Math.round(w*0.55) - 24, h: Math.round(h*0.62) - 16 },
|
||||
'card-nxt': { x: Math.round(w*0.55) + 8, y: 16, w: w - Math.round(w*0.55) - 24, h: Math.round(h*0.42) - 16 },
|
||||
'card-notes': { x: Math.round(w*0.55) + 8, y: Math.round(h*0.42) + 8, w: w - Math.round(w*0.55) - 24, h: h - Math.round(h*0.42) - 16 },
|
||||
'card-timer': { x: 16, y: Math.round(h*0.62) + 8, w: Math.round(w*0.55) - 24, h: h - Math.round(h*0.62) - 16 }
|
||||
};
|
||||
}
|
||||
|
||||
/* ===== Apply / save / restore layout ===== */
|
||||
function applyLayout(layout) {
|
||||
Object.keys(layout).forEach(function(id){
|
||||
var el = document.getElementById(id);
|
||||
var l = layout[id];
|
||||
if (el && l) {
|
||||
el.style.left = l.x + 'px';
|
||||
el.style.top = l.y + 'px';
|
||||
el.style.width = l.w + 'px';
|
||||
el.style.height = l.h + 'px';
|
||||
}
|
||||
});
|
||||
rescaleAll();
|
||||
}
|
||||
function readLayout() {
|
||||
try {
|
||||
var saved = localStorage.getItem(STORAGE_KEY);
|
||||
if (saved) return JSON.parse(saved);
|
||||
} catch(e) {}
|
||||
return defaultLayout();
|
||||
}
|
||||
function saveLayout() {
|
||||
var layout = {};
|
||||
['card-cur','card-nxt','card-notes','card-timer'].forEach(function(id){
|
||||
var el = document.getElementById(id);
|
||||
if (el) {
|
||||
layout[id] = {
|
||||
x: parseInt(el.style.left,10) || 0,
|
||||
y: parseInt(el.style.top,10) || 0,
|
||||
w: parseInt(el.style.width,10) || 300,
|
||||
h: parseInt(el.style.height,10) || 200
|
||||
};
|
||||
}
|
||||
});
|
||||
try { localStorage.setItem(STORAGE_KEY, JSON.stringify(layout)); } catch(e) {}
|
||||
}
|
||||
|
||||
/* ===== iframe rescale to fit card body ===== */
|
||||
function rescaleIframe(iframe) {
|
||||
if (!iframe || iframe.style.display === 'none') return;
|
||||
var body = iframe.parentElement;
|
||||
var cw = body.clientWidth, ch = body.clientHeight;
|
||||
if (!cw || !ch) return;
|
||||
var s = Math.min(cw / 1920, ch / 1080);
|
||||
iframe.style.transform = 'scale(' + s + ')';
|
||||
/* Center the scaled iframe in the body */
|
||||
var sw = 1920 * s, sh = 1080 * s;
|
||||
iframe.style.left = Math.max(0, (cw - sw) / 2) + 'px';
|
||||
iframe.style.top = Math.max(0, (ch - sh) / 2) + 'px';
|
||||
}
|
||||
function rescaleAll() {
|
||||
rescaleIframe(iframeCur);
|
||||
rescaleIframe(iframeNxt);
|
||||
}
|
||||
window.addEventListener('resize', rescaleAll);
|
||||
|
||||
/* ===== Drag (move card by header) ===== */
|
||||
document.querySelectorAll('[data-drag]').forEach(function(handle){
|
||||
handle.addEventListener('mousedown', function(e){
|
||||
if (e.button !== 0) return;
|
||||
var card = handle.closest('.pcard');
|
||||
if (!card) return;
|
||||
e.preventDefault();
|
||||
card.classList.add('dragging');
|
||||
document.body.classList.add('is-dragging-card');
|
||||
var startX = e.clientX, startY = e.clientY;
|
||||
var startL = parseInt(card.style.left,10) || 0;
|
||||
var startT = parseInt(card.style.top,10) || 0;
|
||||
function onMove(ev){
|
||||
var nx = Math.max(0, Math.min(window.innerWidth - 100, startL + ev.clientX - startX));
|
||||
var ny = Math.max(0, Math.min(window.innerHeight - 50, startT + ev.clientY - startY));
|
||||
card.style.left = nx + 'px';
|
||||
card.style.top = ny + 'px';
|
||||
}
|
||||
function onUp(){
|
||||
card.classList.remove('dragging');
|
||||
document.body.classList.remove('is-dragging-card');
|
||||
document.removeEventListener('mousemove', onMove);
|
||||
document.removeEventListener('mouseup', onUp);
|
||||
saveLayout();
|
||||
}
|
||||
document.addEventListener('mousemove', onMove);
|
||||
document.addEventListener('mouseup', onUp);
|
||||
});
|
||||
});
|
||||
|
||||
/* ===== Resize (drag bottom-right corner) ===== */
|
||||
document.querySelectorAll('[data-resize]').forEach(function(handle){
|
||||
handle.addEventListener('mousedown', function(e){
|
||||
if (e.button !== 0) return;
|
||||
var card = handle.closest('.pcard');
|
||||
if (!card) return;
|
||||
e.preventDefault(); e.stopPropagation();
|
||||
card.classList.add('resizing');
|
||||
document.body.classList.add('is-dragging-card');
|
||||
var startX = e.clientX, startY = e.clientY;
|
||||
var startW = parseInt(card.style.width,10) || card.offsetWidth;
|
||||
var startH = parseInt(card.style.height,10) || card.offsetHeight;
|
||||
function onMove(ev){
|
||||
var nw = Math.max(180, startW + ev.clientX - startX);
|
||||
var nh = Math.max(100, startH + ev.clientY - startY);
|
||||
card.style.width = nw + 'px';
|
||||
card.style.height = nh + 'px';
|
||||
if (card.querySelector('iframe')) rescaleIframe(card.querySelector('iframe'));
|
||||
}
|
||||
function onUp(){
|
||||
card.classList.remove('resizing');
|
||||
document.body.classList.remove('is-dragging-card');
|
||||
document.removeEventListener('mousemove', onMove);
|
||||
document.removeEventListener('mouseup', onUp);
|
||||
rescaleAll();
|
||||
saveLayout();
|
||||
}
|
||||
document.addEventListener('mousemove', onMove);
|
||||
document.addEventListener('mouseup', onUp);
|
||||
});
|
||||
});
|
||||
|
||||
/* ===== Preview iframe ready tracking =====
|
||||
* Each iframe loads the deck ONCE with ?preview=1 on init. Subsequent
|
||||
* slide changes are sent via postMessage('preview-goto') so the iframe
|
||||
* just toggles visibility of a different .slide — no reload, no flicker.
|
||||
*/
|
||||
var iframeReady = { cur: false, nxt: false };
|
||||
var currentTheme = ${themeJSON};
|
||||
window.addEventListener('message', function(e) {
|
||||
if (!e.data || e.data.type !== 'preview-ready') return;
|
||||
var iframe = null;
|
||||
if (e.source === iframeCur.contentWindow) {
|
||||
iframeReady.cur = true;
|
||||
iframe = iframeCur;
|
||||
postPreviewGoto(iframeCur, idx);
|
||||
} else if (e.source === iframeNxt.contentWindow) {
|
||||
iframeReady.nxt = true;
|
||||
iframe = iframeNxt;
|
||||
postPreviewGoto(iframeNxt, idx + 1 < total ? idx + 1 : idx);
|
||||
}
|
||||
/* Sync current theme to the iframe */
|
||||
if (iframe && currentTheme) {
|
||||
try { iframe.contentWindow.postMessage({ type: 'preview-theme', name: currentTheme }, '*'); } catch(err) {}
|
||||
}
|
||||
if (iframe) rescaleIframe(iframe);
|
||||
});
|
||||
|
||||
function postPreviewGoto(iframe, n) {
|
||||
try {
|
||||
iframe.contentWindow.postMessage({ type: 'preview-goto', idx: n }, '*');
|
||||
} catch(e) {}
|
||||
}
|
||||
|
||||
/* ===== Update content =====
|
||||
* Smooth (no-reload) navigation: send postMessage to iframes instead of
|
||||
* resetting src. Iframes stay loaded, just switch visible .slide.
|
||||
*/
|
||||
function update(n) {
|
||||
n = Math.max(0, Math.min(total - 1, n));
|
||||
idx = n;
|
||||
|
||||
/* Current preview — postMessage (smooth) */
|
||||
if (iframeReady.cur) postPreviewGoto(iframeCur, n);
|
||||
curMeta.textContent = (n + 1) + '/' + total;
|
||||
|
||||
/* Next preview */
|
||||
if (n + 1 < total) {
|
||||
iframeNxt.style.display = '';
|
||||
var endEl = document.querySelector('#card-nxt .preview-end');
|
||||
if (endEl) endEl.remove();
|
||||
if (iframeReady.nxt) postPreviewGoto(iframeNxt, n + 1);
|
||||
nxtMeta.textContent = (n + 2) + '/' + total;
|
||||
} else {
|
||||
iframeNxt.style.display = 'none';
|
||||
var body = document.querySelector('#card-nxt .pcard-body');
|
||||
if (body && !body.querySelector('.preview-end')) {
|
||||
var end = document.createElement('div');
|
||||
end.className = 'preview-end';
|
||||
end.textContent = '— END OF DECK —';
|
||||
body.appendChild(end);
|
||||
}
|
||||
nxtMeta.textContent = 'END';
|
||||
}
|
||||
|
||||
/* Notes */
|
||||
var note = slideMeta[n].notes;
|
||||
notesBody.innerHTML = note || '<span class="empty">(这一页还没有逐字稿)</span>';
|
||||
|
||||
/* Timer count */
|
||||
timerCount.textContent = (n + 1) + ' / ' + total;
|
||||
}
|
||||
|
||||
/* ===== Timer ===== */
|
||||
var tStart = Date.now();
|
||||
setInterval(function(){
|
||||
var s = Math.floor((Date.now() - tStart) / 1000);
|
||||
var mm = String(Math.floor(s/60)).padStart(2,'0');
|
||||
var ss = String(s%60).padStart(2,'0');
|
||||
timerDisplay.textContent = mm + ':' + ss;
|
||||
}, 1000);
|
||||
function resetTimer(){ tStart = Date.now(); timerDisplay.textContent = '00:00'; }
|
||||
|
||||
/* ===== BroadcastChannel sync ===== */
|
||||
if (bc) {
|
||||
bc.onmessage = function(e){
|
||||
if (!e.data) return;
|
||||
if (e.data.type === 'go') update(e.data.idx);
|
||||
else if (e.data.type === 'theme' && e.data.name) {
|
||||
currentTheme = e.data.name;
|
||||
/* Forward theme change to preview iframes */
|
||||
[iframeCur, iframeNxt].forEach(function(iframe){
|
||||
try {
|
||||
iframe.contentWindow.postMessage({ type: 'preview-theme', name: e.data.name }, '*');
|
||||
} catch(err) {}
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
function go(n) {
|
||||
update(n);
|
||||
if (bc) bc.postMessage({ type: 'go', idx: idx });
|
||||
}
|
||||
|
||||
/* ===== Buttons ===== */
|
||||
document.getElementById('btn-prev').addEventListener('click', function(){ go(idx - 1); });
|
||||
document.getElementById('btn-next').addEventListener('click', function(){ go(idx + 1); });
|
||||
document.getElementById('btn-reset').addEventListener('click', resetTimer);
|
||||
document.getElementById('reset-layout').addEventListener('click', function(){
|
||||
if (confirm('恢复默认卡片布局?')) {
|
||||
try { localStorage.removeItem(STORAGE_KEY); } catch(e){}
|
||||
applyLayout(defaultLayout());
|
||||
}
|
||||
});
|
||||
|
||||
/* ===== Keyboard ===== */
|
||||
document.addEventListener('keydown', function(e){
|
||||
if (e.metaKey || e.ctrlKey || e.altKey) return;
|
||||
switch(e.key) {
|
||||
case 'ArrowRight': case ' ': case 'PageDown': go(idx + 1); e.preventDefault(); break;
|
||||
case 'ArrowLeft': case 'PageUp': go(idx - 1); e.preventDefault(); break;
|
||||
case 'Home': go(0); break;
|
||||
case 'End': go(total - 1); break;
|
||||
case 'r': case 'R': resetTimer(); break;
|
||||
case 'Escape': window.close(); break;
|
||||
}
|
||||
});
|
||||
|
||||
/* ===== Iframe load → rescale (catches initial size) ===== */
|
||||
iframeCur.addEventListener('load', function(){ rescaleIframe(iframeCur); });
|
||||
iframeNxt.addEventListener('load', function(){ rescaleIframe(iframeNxt); });
|
||||
|
||||
/* ===== Init =====
|
||||
* Load each iframe ONCE with the deck file. After they post
|
||||
* 'preview-ready', all subsequent navigation is via postMessage
|
||||
* (smooth, no reload, no flicker).
|
||||
*/
|
||||
applyLayout(readLayout());
|
||||
iframeCur.src = deckUrl + '?preview=' + (idx + 1);
|
||||
if (idx + 1 < total) iframeNxt.src = deckUrl + '?preview=' + (idx + 2);
|
||||
/* Initialize notes/timer/count without touching iframes */
|
||||
notesBody.innerHTML = slideMeta[idx].notes || '<span class="empty">(这一页还没有逐字稿)</span>';
|
||||
curMeta.textContent = (idx + 1) + '/' + total;
|
||||
nxtMeta.textContent = (idx + 2) + '/' + total;
|
||||
timerCount.textContent = (idx + 1) + ' / ' + total;
|
||||
})();
|
||||
</` + `script>
|
||||
</body></html>`;
|
||||
}
|
||||
function fullscreen(){ const el=document.documentElement;
|
||||
if (!document.fullscreenElement) el.requestFullscreen&&el.requestFullscreen();
|
||||
else document.exitFullscreen&&document.exitFullscreen();
|
||||
}
|
||||
|
||||
// theme cycling
|
||||
const root = document.documentElement;
|
||||
const themesAttr = root.getAttribute('data-themes') || document.body.getAttribute('data-themes');
|
||||
const themes = themesAttr ? themesAttr.split(',').map(s=>s.trim()).filter(Boolean) : [];
|
||||
let themeIdx = 0;
|
||||
|
||||
// Auto-detect theme base path from existing <link id="theme-link">
|
||||
let themeBase = root.getAttribute('data-theme-base');
|
||||
if (!themeBase) {
|
||||
const existingLink = document.getElementById('theme-link');
|
||||
if (existingLink) {
|
||||
// el.getAttribute('href') gives the raw relative path written in HTML
|
||||
const rawHref = existingLink.getAttribute('href') || '';
|
||||
const lastSlash = rawHref.lastIndexOf('/');
|
||||
themeBase = lastSlash >= 0 ? rawHref.substring(0, lastSlash + 1) : 'assets/themes/';
|
||||
} else {
|
||||
themeBase = 'assets/themes/';
|
||||
}
|
||||
}
|
||||
|
||||
function applyTheme(name) {
|
||||
let link = document.getElementById('theme-link');
|
||||
if (!link) {
|
||||
link = document.createElement('link');
|
||||
link.rel = 'stylesheet';
|
||||
link.id = 'theme-link';
|
||||
document.head.appendChild(link);
|
||||
}
|
||||
link.href = themeBase + name + '.css';
|
||||
root.setAttribute('data-theme', name);
|
||||
const ind = document.querySelector('.theme-indicator');
|
||||
if (ind) ind.textContent = name;
|
||||
}
|
||||
function cycleTheme(fromRemote){
|
||||
if (!themes.length) return;
|
||||
themeIdx = (themeIdx+1) % themes.length;
|
||||
const name = themes[themeIdx];
|
||||
applyTheme(name);
|
||||
/* Broadcast to other window (audience ↔ presenter) */
|
||||
if (!fromRemote && bc) bc.postMessage({ type: 'theme', name: name });
|
||||
}
|
||||
|
||||
// animation cycling on current slide
|
||||
let animIdx = 0;
|
||||
function cycleAnim(){
|
||||
animIdx = (animIdx+1) % ANIMS.length;
|
||||
const a = ANIMS[animIdx];
|
||||
const target = slides[idx].querySelector('[data-anim-target]') || slides[idx];
|
||||
ANIMS.forEach(x => target.classList.remove('anim-'+x));
|
||||
void target.offsetWidth;
|
||||
target.classList.add('anim-'+a);
|
||||
target.setAttribute('data-anim', a);
|
||||
const ind = document.querySelector('.anim-indicator');
|
||||
if (ind) ind.textContent = a;
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', function (e) {
|
||||
if (e.metaKey||e.ctrlKey||e.altKey) return;
|
||||
switch (e.key) {
|
||||
case 'ArrowRight': case ' ': case 'PageDown': case 'Enter': go(idx+1); e.preventDefault(); break;
|
||||
case 'ArrowLeft': case 'PageUp': case 'Backspace': go(idx-1); e.preventDefault(); break;
|
||||
case 'Home': go(0); break;
|
||||
case 'End': go(total-1); break;
|
||||
case 'f': case 'F': fullscreen(); break;
|
||||
case 's': case 'S': openPresenterWindow(); break;
|
||||
case 'n': case 'N': toggleNotes(); break;
|
||||
case 'o': case 'O': toggleOverview(); break;
|
||||
case 't': case 'T': cycleTheme(); break;
|
||||
case 'a': case 'A': cycleAnim(); break;
|
||||
case 'Escape': toggleOverview(false); toggleNotes(false); break;
|
||||
}
|
||||
});
|
||||
|
||||
// hash deep-link
|
||||
function fromHash(){
|
||||
const m = /^#\/(\d+)/.exec(location.hash||'');
|
||||
if (m) go(Math.max(0, parseInt(m[1],10)-1));
|
||||
}
|
||||
window.addEventListener('hashchange', fromHash);
|
||||
fromHash();
|
||||
go(idx);
|
||||
});
|
||||
})();
|
||||
135
skills/assets/saas.md
Normal file
135
skills/assets/saas.md
Normal file
@@ -0,0 +1,135 @@
|
||||
<!-- Updated: 2026-02-07 -->
|
||||
# SaaS SEO Strategy Template
|
||||
|
||||
## Industry Characteristics
|
||||
|
||||
- Long sales cycles with multiple touchpoints
|
||||
- Feature-focused decision making
|
||||
- Comparison shopping behavior
|
||||
- Heavy research phase before purchase
|
||||
- Integration and ecosystem considerations
|
||||
|
||||
## Recommended Site Architecture
|
||||
|
||||
```
|
||||
/
|
||||
├── Home
|
||||
├── /product (or /platform)
|
||||
│ ├── /features
|
||||
│ │ ├── /feature-1
|
||||
│ │ ├── /feature-2
|
||||
│ │ └── ...
|
||||
│ ├── /integrations
|
||||
│ │ ├── /integration-1
|
||||
│ │ └── ...
|
||||
│ └── /security
|
||||
├── /solutions
|
||||
│ ├── /by-industry
|
||||
│ │ ├── /industry-1
|
||||
│ │ └── ...
|
||||
│ └── /by-use-case
|
||||
│ ├── /use-case-1
|
||||
│ └── ...
|
||||
├── /pricing
|
||||
├── /customers
|
||||
│ ├── /case-studies
|
||||
│ │ ├── /case-study-1
|
||||
│ │ └── ...
|
||||
│ └── /testimonials
|
||||
├── /resources
|
||||
│ ├── /blog
|
||||
│ ├── /guides
|
||||
│ ├── /webinars
|
||||
│ ├── /templates
|
||||
│ └── /glossary
|
||||
├── /docs (or /help)
|
||||
│ └── /api
|
||||
├── /company
|
||||
│ ├── /about
|
||||
│ ├── /careers
|
||||
│ ├── /press
|
||||
│ └── /contact
|
||||
└── /compare
|
||||
├── /vs-competitor-1
|
||||
└── /vs-competitor-2
|
||||
```
|
||||
|
||||
## Content Priorities
|
||||
|
||||
### High Priority Pages
|
||||
1. Homepage (value proposition, social proof)
|
||||
2. Features overview
|
||||
3. Pricing page
|
||||
4. Key integrations
|
||||
5. Top 3-5 use case pages
|
||||
|
||||
### Medium Priority Pages
|
||||
1. Individual feature pages
|
||||
2. Industry solution pages
|
||||
3. Case studies (2-3 detailed ones)
|
||||
4. Comparison pages (vs competitors)
|
||||
|
||||
### Content Marketing Focus
|
||||
1. Bottom-of-funnel: Comparison guides, ROI calculators
|
||||
2. Middle-of-funnel: How-to guides, best practices
|
||||
3. Top-of-funnel: Industry trends, educational content
|
||||
|
||||
## Schema Recommendations
|
||||
|
||||
| Page Type | Schema Types |
|
||||
|-----------|-------------|
|
||||
| Homepage | Organization, WebSite, SoftwareApplication |
|
||||
| Product/Features | SoftwareApplication, Offer |
|
||||
| Pricing | SoftwareApplication, Offer (with pricing) |
|
||||
| Blog | Article, BlogPosting |
|
||||
| Case Studies | Article, Organization (customer) |
|
||||
| Documentation | TechArticle |
|
||||
|
||||
## Key Metrics to Track
|
||||
|
||||
- Organic traffic to pricing page
|
||||
- Demo/trial signups from organic
|
||||
- Blog → pricing page conversion
|
||||
- Comparison page rankings
|
||||
- Integration page performance
|
||||
|
||||
## Comparison & Alternative Pages
|
||||
|
||||
Comparison pages are among the highest-converting content types for SaaS, with conversion rates of **4-7%** vs. 0.5-1.8% for standard blog content (35.8% of marketers report comparison content performs "better than ever" per Intergrowth November 2025 survey).
|
||||
|
||||
**Recommended page types:**
|
||||
- `/{product}-vs-{competitor}`: Direct 1:1 comparison
|
||||
- `/{competitor}-alternative`: Targeting competitor brand searches
|
||||
- `/compare/{category}`: Category comparison hub
|
||||
- `/best-{category}-tools`: Roundup-style pages
|
||||
|
||||
**Best practices:**
|
||||
- Include structured comparison tables with pricing, features, pros/cons
|
||||
- Be factually accurate about competitors: verify claims regularly
|
||||
- Include customer testimonials from users who switched
|
||||
- Add FAQ schema for common comparison questions (valuable for AI search)
|
||||
- Update regularly: stale comparison data damages credibility
|
||||
- Cross-reference the `seo-competitor-pages` skill for detailed frameworks
|
||||
|
||||
**Legal considerations:**
|
||||
- Nominative fair use generally permits competitor brand mentions for comparison purposes
|
||||
- Do NOT imply endorsement or affiliation
|
||||
- Do NOT make false or unverifiable claims about competitor products
|
||||
- Different jurisdictions have different trademark laws: consult legal counsel
|
||||
|
||||
## Competitive Considerations
|
||||
|
||||
- Monitor competitor feature releases
|
||||
- Track competitor content strategies
|
||||
- Identify keyword gaps in feature coverage
|
||||
- Watch for new comparison opportunities
|
||||
|
||||
## Generative Engine Optimization (GEO) for SaaS
|
||||
|
||||
- [ ] Include clear, structured feature comparisons that AI systems can parse and cite
|
||||
- [ ] Use SoftwareApplication schema with complete feature lists and pricing
|
||||
- [ ] Publish original benchmark data, case studies, and ROI metrics
|
||||
- [ ] Build content clusters around key product categories and use cases
|
||||
- [ ] Ensure integration pages have clear, quotable descriptions
|
||||
- [ ] Structure pricing information in tables AI can extract
|
||||
- [ ] Monitor AI citation across Google AI Overviews, ChatGPT, and Perplexity
|
||||
BIN
skills/assets/sfx/container/card-flip.mp3
Normal file
BIN
skills/assets/sfx/container/card-flip.mp3
Normal file
Binary file not shown.
BIN
skills/assets/sfx/container/card-snap.mp3
Normal file
BIN
skills/assets/sfx/container/card-snap.mp3
Normal file
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user