Initial: pi-skill — 68 skills, 43 extensions, 11 themes for Pi
This commit is contained in:
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.
BIN
skills/assets/sfx/container/modal-open.mp3
Normal file
BIN
skills/assets/sfx/container/modal-open.mp3
Normal file
Binary file not shown.
BIN
skills/assets/sfx/container/stack-collapse.mp3
Normal file
BIN
skills/assets/sfx/container/stack-collapse.mp3
Normal file
Binary file not shown.
BIN
skills/assets/sfx/feedback/achievement.mp3
Normal file
BIN
skills/assets/sfx/feedback/achievement.mp3
Normal file
Binary file not shown.
BIN
skills/assets/sfx/feedback/error-tone.mp3
Normal file
BIN
skills/assets/sfx/feedback/error-tone.mp3
Normal file
Binary file not shown.
BIN
skills/assets/sfx/feedback/notification-pop.mp3
Normal file
BIN
skills/assets/sfx/feedback/notification-pop.mp3
Normal file
Binary file not shown.
BIN
skills/assets/sfx/feedback/success-chime.mp3
Normal file
BIN
skills/assets/sfx/feedback/success-chime.mp3
Normal file
Binary file not shown.
BIN
skills/assets/sfx/impact/brand-stamp.mp3
Normal file
BIN
skills/assets/sfx/impact/brand-stamp.mp3
Normal file
Binary file not shown.
BIN
skills/assets/sfx/impact/drop-thud.mp3
Normal file
BIN
skills/assets/sfx/impact/drop-thud.mp3
Normal file
Binary file not shown.
BIN
skills/assets/sfx/impact/logo-reveal-v2.mp3
Normal file
BIN
skills/assets/sfx/impact/logo-reveal-v2.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