emberflow-skills 1.10.1 → 1.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/ember-publish-explainer/SKILL.md +46 -15
- package/.claude/skills/ember-publish-explainer/templates/diverse-viz-explainer.json +40 -0
- package/README.md +79 -20
- package/bin/install.js +214 -47
- package/package.json +1 -1
- package/skills/ember-publish-dataset/SKILL.md +134 -0
- package/skills/ember-publish-explainer/SKILL.md +46 -15
- package/skills/ember-publish-explainer/templates/diverse-viz-explainer.json +40 -0
|
@@ -10,6 +10,8 @@ Generate a **structured JSON** explainer that the Emberflow platform renders as
|
|
|
10
10
|
|
|
11
11
|
You choose the best visualization type for each slide. A single explainer can mix different viz types across slides — a timeline on slide 2, a data table on slide 3, a chart on slide 4.
|
|
12
12
|
|
|
13
|
+
**Visual diversity is critical.** Every explainer should feel bespoke. Don't fall back on "grid of stat cards" or "list of bordered boxes" as the default — those are just one option among many. Think about what shape the data actually has: is it a flow? Use a Sankey or funnel. A ranking? Use a horizontal bar chart or podium layout. A distribution? Use a heatmap or scatter. A before/after? Use a split comparison. A hierarchy? Use a tree or nested rings. A process? Use a swimlane or animated pipeline. Match the visualization to the shape of the information, not the other way around.
|
|
14
|
+
|
|
13
15
|
## Output
|
|
14
16
|
|
|
15
17
|
Write a single JSON file to `{topic-slug}-explainer.json` in the current working directory. Then publish it to Emberflow.
|
|
@@ -163,32 +165,47 @@ No `script` field is needed for diagram slides — the platform handles all anim
|
|
|
163
165
|
|
|
164
166
|
## B. Visualization Primitive Catalog
|
|
165
167
|
|
|
166
|
-
|
|
168
|
+
This is a **starting point, not a menu to order from**. The best explainers invent visualizations that fit the content perfectly. Combine, adapt, or create entirely new primitives. If none of these fit — build something original.
|
|
167
169
|
|
|
168
170
|
### Data Display
|
|
169
171
|
|
|
170
|
-
- **KPI / stat cards** — Grid of boxes with label, large value, subtitle.
|
|
171
|
-
- **Data table** — `<table>` with muted uppercase headers, monospace numbers, color-coded values
|
|
172
|
-
- **Comparison matrix** — Table with checkmark/cross SVG icons per cell.
|
|
172
|
+
- **KPI / stat cards** — Grid of boxes with label, large value, subtitle. Good for overview slides, but don't default to this for everything.
|
|
173
|
+
- **Data table** — `<table>` with muted uppercase headers, monospace numbers, color-coded values. Category dots on row labels.
|
|
174
|
+
- **Comparison matrix** — Table with checkmark/cross SVG icons per cell. Active column highlighted with accent border.
|
|
175
|
+
- **Heatmap grid** — CSS grid where cell background intensity maps to a value. Use color gradients (green→yellow→red or cool→warm). Great for showing density, coverage, or correlation.
|
|
176
|
+
- **Scorecard** — Single large metric in a ring or gauge, with trend sparkline below. Different from KPI cards — more visual weight, fewer items.
|
|
173
177
|
|
|
174
178
|
### Charts
|
|
175
179
|
|
|
176
|
-
- **Vertical bar chart** — Flex row of bars, height as percentage of max. Color-code by threshold
|
|
177
|
-
- **Horizontal bar chart** — Rows with label left, bar extending right. Good for ranked lists
|
|
178
|
-
- **Donut / ring chart** — SVG circle with `stroke-dasharray`/`stroke-dashoffset`. Percentage label centered
|
|
179
|
-
- **Funnel diagram** — Stacked horizontal bars decreasing in width, centered.
|
|
180
|
+
- **Vertical bar chart** — Flex row of bars, height as percentage of max. Color-code by threshold. Animate height on entrance.
|
|
181
|
+
- **Horizontal bar chart** — Rows with label left, bar extending right. Good for ranked lists, leaderboards, survey results.
|
|
182
|
+
- **Donut / ring chart** — SVG circle with `stroke-dasharray`/`stroke-dashoffset`. Percentage label centered.
|
|
183
|
+
- **Funnel diagram** — Stacked horizontal bars decreasing in width, centered. Show conversion rates between stages.
|
|
184
|
+
- **Waterfall chart** — Bars that float from the previous bar's end. Show how values build up or break down (budget additions/subtractions, funnel drop-offs).
|
|
185
|
+
- **Sparklines / mini line charts** — SVG polylines inside cards or rows. Show trends without axes. Great for time-series context within other layouts.
|
|
186
|
+
- **Stacked bar / segmented bar** — Single horizontal bar divided into colored segments. Show composition at a glance (e.g., traffic sources, time allocation).
|
|
187
|
+
- **Scatter / bubble plot** — SVG circles positioned by x/y data. Size encodes a third dimension. Good for correlation or distribution.
|
|
188
|
+
|
|
189
|
+
### Flows & Processes
|
|
190
|
+
|
|
191
|
+
- **Sankey diagram** — SVG paths flowing from left to right with varying widths. Show how quantities split and merge across stages (budget allocation, user flow, data pipeline).
|
|
192
|
+
- **Swimlane diagram** — Horizontal lanes (rows) representing actors/systems. Steps flow left-to-right across lanes showing handoffs. Great for cross-team processes.
|
|
193
|
+
- **Animated pipeline** — Horizontal or vertical sequence of stages with animated dots/particles moving through. Show data or request flow in real-time feel.
|
|
194
|
+
- **Decision tree** — Binary branching layout. Each node is a question, branches lead to outcomes. Clickable to explore paths.
|
|
180
195
|
|
|
181
196
|
### Timelines & Sequences
|
|
182
197
|
|
|
183
198
|
- **Vertical timeline** — Left border line with dot markers. Each event has date, title, description, optional progress bar and status badge.
|
|
184
199
|
- **Horizontal timeline** — Flex row of connected nodes along a horizontal line. Good for fewer items (3-6).
|
|
185
200
|
- **Progress stepper** — Numbered circles connected by lines. Active step highlighted, completed steps filled.
|
|
201
|
+
- **Gantt-style chart** — Rows of horizontal bars on a time axis. Show overlapping phases, dependencies, or parallel workstreams.
|
|
202
|
+
- **Spiral timeline** — Events arranged along a spiral path (SVG). Unusual and memorable for cyclical or long-spanning histories.
|
|
186
203
|
|
|
187
204
|
### Relationships & Structure (Auto-Layout Diagrams)
|
|
188
205
|
|
|
189
206
|
For any node-and-edge visualization, use a **declarative diagram object** as the `viz` field. The platform auto-positions nodes and routes edges — no manual coordinate math needed.
|
|
190
207
|
|
|
191
|
-
- **Architecture diagram** — Use `viz: { nodes, edges, groups }` with color-coded nodes, meaningful icons, and labeled groups.
|
|
208
|
+
- **Architecture diagram** — Use `viz: { nodes, edges, groups }` with color-coded nodes, meaningful icons, and labeled groups. See Section A → "Architecture diagrams".
|
|
192
209
|
- **Flowchart** — Same declarative format with a single group or no groups. Set `direction: "TB"` for top-to-bottom flow.
|
|
193
210
|
- **Org chart / hierarchy** — Use `direction: "TB"` and groups for departments. Color-code by role.
|
|
194
211
|
- **Network / data flow** — Multiple groups connected by cross-group edges. Color edges by data type.
|
|
@@ -197,17 +214,27 @@ For any node-and-edge visualization, use a **declarative diagram object** as the
|
|
|
197
214
|
|
|
198
215
|
- **Periodic table / grid** — CSS grid of cards with colored top bar per category. Hover shows detail.
|
|
199
216
|
- **Kanban board** — Columns with card items. Cards can highlight or shift between columns per slide.
|
|
217
|
+
- **Tier list** — Labeled rows (S/A/B/C/F) with items placed in each tier. Color-coded by rank. Great for evaluations and comparisons.
|
|
200
218
|
|
|
201
219
|
### Status & Indicators
|
|
202
220
|
|
|
203
221
|
- **Risk cards** — Stacked cards with severity SVG icon, title, description, colored severity badge.
|
|
204
222
|
- **Stat delta** — Large number with up/down arrow SVG and percentage change.
|
|
223
|
+
- **Gauge / meter** — SVG arc filled to a percentage. More visual impact than a number alone.
|
|
205
224
|
- **Utilization bars** — Rows with label, horizontal progress bar, percentage.
|
|
206
225
|
- **Checklist** — Items with check/cross SVG icons. Grouped by category.
|
|
207
226
|
|
|
227
|
+
### Comparisons & Layouts
|
|
228
|
+
|
|
229
|
+
- **Split comparison** — Two panels side-by-side (before/after, option A/option B). Highlight differences with color.
|
|
230
|
+
- **Podium / ranking** — Three items in a 2-1-3 podium arrangement (second, first, third). Great for top-3 results.
|
|
231
|
+
- **Concentric rings** — Nested circles or rings showing layers (e.g., onion architecture, security perimeters, impact radius).
|
|
232
|
+
- **Quadrant chart** — 2×2 grid with labeled axes. Place items as dots or cards in quadrants (e.g., effort/impact, urgency/importance).
|
|
233
|
+
- **Radar / spider chart** — SVG polygon on radial axes. Compare multiple dimensions of a single entity or overlay two entities.
|
|
234
|
+
|
|
208
235
|
### Inventing New Primitives
|
|
209
236
|
|
|
210
|
-
The catalog above is a starting point. If the topic calls for a
|
|
237
|
+
The catalog above is a starting point. **The best explainers often use visualizations not on this list.** If the topic calls for something new — a custom game board, an animated state machine, a nested ring diagram, a radial burst — build it. The only constraints are the design tokens and the slide-based interaction model.
|
|
211
238
|
|
|
212
239
|
---
|
|
213
240
|
|
|
@@ -265,6 +292,8 @@ Before writing any code, plan 4-7 slides:
|
|
|
265
292
|
2. **Slides 2-6** = Each focuses on one concept or subset. Zoom in, highlight, or switch viz type.
|
|
266
293
|
3. **Last slide** (optional) = Summary or call-to-action.
|
|
267
294
|
|
|
295
|
+
**Vary the viz type across slides.** If slide 1 uses a diagram, slide 2 should use something different (a chart, timeline, table, etc.). Repeating the same viz structure on every slide makes the explainer feel static and monotonous. Each slide transition should feel like a new lens on the topic.
|
|
296
|
+
|
|
268
297
|
For each slide, define:
|
|
269
298
|
- **label**: Short uppercase label
|
|
270
299
|
- **title**: Heading text
|
|
@@ -389,14 +418,16 @@ Or use the Emberflow MCP/CLI to publish with `content_type: 'explainer'`.
|
|
|
389
418
|
|
|
390
419
|
## H. Reference Templates
|
|
391
420
|
|
|
392
|
-
|
|
421
|
+
Read **one** template before generating to understand the JSON structure, field conventions, and script patterns:
|
|
393
422
|
|
|
394
423
|
```
|
|
395
|
-
Read templates/
|
|
424
|
+
Read templates/diverse-viz-explainer.json — RECOMMENDED: showcases 5 different viz types across slides (funnel, heatmap, radar chart, waterfall, gauges). Start here if you're unsure which template to use.
|
|
425
|
+
|
|
426
|
+
Read templates/architecture-explainer.json — declarative diagram example (nodes + edges, active/dimmed states, staggered entrance).
|
|
396
427
|
|
|
397
|
-
Read templates/project-overview-explainer.json
|
|
428
|
+
Read templates/project-overview-explainer.json — mixed-viz example (KPIs, timeline, budget table, donut ring, risk cards — different viz type per slide).
|
|
398
429
|
|
|
399
|
-
Read templates/dashboard-explainer.json
|
|
430
|
+
Read templates/dashboard-explainer.json — data viz example (animated bar chart, color-coded values, dataset switching, insight callouts).
|
|
400
431
|
```
|
|
401
432
|
|
|
402
|
-
|
|
433
|
+
Use the templates for **structural reference only** — JSON shape, field naming, script scoping, and animation timing. Do NOT copy their visual style. Your explainer should look distinct from every template. Choose visualization types that match your specific content, not the template's content.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"css": ".fn { display: flex; flex-direction: column; gap: 6px; width: 100%; }\n.fn-stage { display: flex; align-items: center; gap: 12px; opacity: 0; transform: translateX(-12px); transition: all 0.5s cubic-bezier(0.4, 0, 0.2, 1); }\n.fn-stage.visible { opacity: 1; transform: translateX(0); }\n.fn-bar { height: 38px; border-radius: 6px; width: 0; transition: width 0.8s cubic-bezier(0.4, 0, 0.2, 1); flex-shrink: 0; }\n.fn-info { min-width: 90px; }\n.fn-name { font-size: 13px; font-weight: 600; color: var(--text); display: block; }\n.fn-sub { font-size: 11px; color: var(--text-muted); }\n.fn-rate { font-size: 12px; font-weight: 600; margin-left: auto; padding: 2px 10px; border-radius: 12px; white-space: nowrap; }\n.fn-drop { font-size: 10px; color: var(--text-muted); text-align: center; padding: 2px 0; opacity: 0; transition: opacity 0.4s; }\n.fn-drop.visible { opacity: 1; }\n\n.hm { width: 100%; }\n.hm-grid { display: flex; flex-direction: column; gap: 3px; }\n.hm-row { display: grid; grid-template-columns: 44px repeat(7, 1fr); gap: 3px; }\n.hm-label { font-size: 10px; color: var(--text-muted); display: flex; align-items: center; }\n.hm-head { font-size: 10px; color: var(--text-muted); text-align: center; font-weight: 600; text-transform: uppercase; }\n.hm-cell { aspect-ratio: 1.6; border-radius: 3px; opacity: 0; transition: opacity 0.3s; cursor: pointer; position: relative; }\n.hm-cell.visible { opacity: 1; }\n.hm-cell:hover { outline: 1px solid var(--text-muted); }\n.hm-cell:hover::after { content: attr(data-val); position: absolute; top: -20px; left: 50%; transform: translateX(-50%); font-size: 9px; color: var(--text); background: var(--bg); padding: 1px 5px; border-radius: 3px; border: 1px solid var(--border); white-space: nowrap; z-index: 1; }\n.hm-scale { display: flex; align-items: center; gap: 6px; margin-top: 8px; justify-content: flex-end; }\n.hm-scale-bar { width: 80px; height: 8px; border-radius: 4px; background: linear-gradient(90deg, rgba(234,88,12,0.05), rgba(234,88,12,0.9)); }\n.hm-scale span { font-size: 9px; color: var(--text-muted); }\n\n.rd { width: 100%; display: flex; flex-direction: column; align-items: center; }\n.rd-chart { position: relative; }\n.rd-axis { stroke: var(--border); stroke-width: 1; }\n.rd-ring { fill: none; stroke: var(--border); stroke-width: 0.5; stroke-dasharray: 4 4; }\n.rd-poly { fill-opacity: 0; stroke-width: 2; stroke-dasharray: 500; stroke-dashoffset: 500; transition: stroke-dashoffset 1.2s cubic-bezier(0.4, 0, 0.2, 1), fill-opacity 0.6s 0.8s; }\n.rd-poly.visible { stroke-dashoffset: 0; fill-opacity: 0.12; }\n.rd-poly.cur { stroke: #ea580c; fill: #ea580c; }\n.rd-poly.prev { stroke: #3b82f6; fill: #3b82f6; }\n.rd-dot { r: 3; fill: var(--bg); stroke-width: 2; opacity: 0; transition: opacity 0.3s 1s; }\n.rd-dot.visible { opacity: 1; }\n.rd-dot.cur { stroke: #ea580c; }\n.rd-dot.prev { stroke: #3b82f6; }\n.rd-lbl { font-size: 10px; fill: var(--text-muted); text-anchor: middle; }\n.rd-legend { display: flex; gap: 20px; justify-content: center; margin-top: 16px; font-size: 12px; color: var(--text-muted); }\n.rd-legend-dot { width: 10px; height: 10px; border-radius: 50%; display: inline-block; margin-right: 6px; vertical-align: middle; }\n\n.wf { width: 100%; }\n.wf-chart { display: flex; align-items: flex-end; gap: 4px; height: 240px; padding-bottom: 24px; position: relative; }\n.wf-chart::before { content: ''; position: absolute; left: 0; right: 0; bottom: 24px; height: 1px; background: var(--border); }\n.wf-col { flex: 1; position: relative; height: 100%; display: flex; flex-direction: column; justify-content: flex-end; align-items: center; }\n.wf-block { width: 65%; border-radius: 3px; position: relative; opacity: 0; transform: scaleY(0); transform-origin: bottom; transition: all 0.5s cubic-bezier(0.4, 0, 0.2, 1); }\n.wf-block.visible { opacity: 1; transform: scaleY(1); }\n.wf-block.add { background: #22c55e; }\n.wf-block.sub { background: #ef4444; transform-origin: top; }\n.wf-block.total { background: var(--link); }\n.wf-val { font-size: 10px; font-weight: 600; color: var(--text); text-align: center; margin-bottom: 4px; opacity: 0; transition: opacity 0.3s 0.4s; white-space: nowrap; }\n.wf-val.visible { opacity: 1; }\n.wf-lbl { font-size: 9px; color: var(--text-muted); text-align: center; white-space: nowrap; position: absolute; bottom: -18px; }\n.wf-connector { position: absolute; right: -4px; width: 8px; border-top: 1px dashed var(--text-muted); opacity: 0.4; }\n\n.gg-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 24px; width: 100%; max-width: 360px; margin: 0 auto; }\n.gg-item { display: flex; flex-direction: column; align-items: center; gap: 6px; opacity: 0; transform: scale(0.9); transition: all 0.5s cubic-bezier(0.4, 0, 0.2, 1); }\n.gg-item.visible { opacity: 1; transform: scale(1); }\n.gg-track { fill: none; stroke: var(--border); stroke-width: 10; stroke-linecap: round; }\n.gg-fill { fill: none; stroke-width: 10; stroke-linecap: round; transition: stroke-dashoffset 1.2s cubic-bezier(0.4, 0, 0.2, 1); }\n.gg-val { font-size: 18px; font-weight: 700; text-anchor: middle; fill: var(--text); }\n.gg-lbl { font-size: 12px; color: var(--text-muted); text-align: center; font-weight: 600; }\n.gg-sub { font-size: 10px; color: var(--text-muted); text-align: center; }",
|
|
3
|
+
"slides": [
|
|
4
|
+
{
|
|
5
|
+
"label": "Funnel",
|
|
6
|
+
"title": "Signup Conversion Funnel",
|
|
7
|
+
"prose": "<p>The signup funnel tracks users from first visit through to paid conversion. Each stage represents a meaningful commitment — from anonymous browsing to entering payment details.</p><p>The <strong>activation step</strong> (completing onboarding) is the biggest single drop-off at 44% loss. Users who activate convert to paid at a strong 36% rate, suggesting the product delivers value once users get past initial setup.</p><p>Overall visitor-to-paid conversion sits at <strong>6.5%</strong>, competitive for a self-serve SaaS product.</p>",
|
|
8
|
+
"viz": "<div class=\"fn\"><div class=\"fn-stage\"><div class=\"fn-bar\" style=\"background: #3b82f6;\" data-w=\"100%\"></div><div class=\"fn-info\"><span class=\"fn-name\">Visitors</span><span class=\"fn-sub\">24,500 unique</span></div><span class=\"fn-rate\" style=\"background: rgba(59,130,246,0.12); color: #3b82f6;\">100%</span></div><div class=\"fn-drop\" data-text=\"\u2193 68% drop-off\"></div><div class=\"fn-stage\"><div class=\"fn-bar\" style=\"background: #8b5cf6;\" data-w=\"32%\"></div><div class=\"fn-info\"><span class=\"fn-name\">Signups</span><span class=\"fn-sub\">7,840 accounts</span></div><span class=\"fn-rate\" style=\"background: rgba(139,92,246,0.12); color: #8b5cf6;\">32%</span></div><div class=\"fn-drop\" data-text=\"\u2193 44% drop-off\"></div><div class=\"fn-stage\"><div class=\"fn-bar\" style=\"background: #ea580c;\" data-w=\"18%\"></div><div class=\"fn-info\"><span class=\"fn-name\">Activated</span><span class=\"fn-sub\">4,410 onboarded</span></div><span class=\"fn-rate\" style=\"background: rgba(234,88,12,0.12); color: #ea580c;\">18%</span></div><div class=\"fn-drop\" data-text=\"\u2193 64% drop-off\"></div><div class=\"fn-stage\"><div class=\"fn-bar\" style=\"background: #22c55e;\" data-w=\"6.5%\"></div><div class=\"fn-info\"><span class=\"fn-name\">Paying</span><span class=\"fn-sub\">1,590 customers</span></div><span class=\"fn-rate\" style=\"background: rgba(34,197,94,0.12); color: #22c55e;\">6.5%</span></div></div>",
|
|
9
|
+
"script": "var stages = container.querySelectorAll('.fn-stage');\nvar drops = container.querySelectorAll('.fn-drop');\nstages.forEach(function(s, i) {\n setTimeout(function() {\n s.classList.add('visible');\n var bar = s.querySelector('.fn-bar');\n bar.style.width = bar.getAttribute('data-w');\n if (i > 0 && drops[i - 1]) {\n drops[i - 1].textContent = drops[i - 1].getAttribute('data-text');\n drops[i - 1].classList.add('visible');\n }\n }, 200 + i * 180);\n});"
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"label": "Activity",
|
|
13
|
+
"title": "Signup Heatmap",
|
|
14
|
+
"prose": "<p>Signups cluster heavily around weekday lunchtimes and early evenings. <strong>Tuesday 12\u2013\u20131pm</strong> is the single hottest slot, likely driven by users discovering the product during work breaks.</p><p>Weekend activity is uniformly low, suggesting a B2B-leaning user base. Late-night signups (10pm+) are near zero across all days.</p><p>This pattern informs ad scheduling — concentrating spend on weekday 11am\u20132pm captures peak intent with minimal waste.</p>",
|
|
15
|
+
"viz": "<div class=\"hm\"><div class=\"hm-grid\"><div class=\"hm-row\"><span></span><span class=\"hm-head\">Mon</span><span class=\"hm-head\">Tue</span><span class=\"hm-head\">Wed</span><span class=\"hm-head\">Thu</span><span class=\"hm-head\">Fri</span><span class=\"hm-head\">Sat</span><span class=\"hm-head\">Sun</span></div><div class=\"hm-row\"><span class=\"hm-label\">6\u2013\u20139am</span><span class=\"hm-cell\" data-val=\"42\" style=\"background:rgba(234,88,12,0.12)\"></span><span class=\"hm-cell\" data-val=\"38\" style=\"background:rgba(234,88,12,0.10)\"></span><span class=\"hm-cell\" data-val=\"45\" style=\"background:rgba(234,88,12,0.13)\"></span><span class=\"hm-cell\" data-val=\"40\" style=\"background:rgba(234,88,12,0.11)\"></span><span class=\"hm-cell\" data-val=\"35\" style=\"background:rgba(234,88,12,0.09)\"></span><span class=\"hm-cell\" data-val=\"12\" style=\"background:rgba(234,88,12,0.03)\"></span><span class=\"hm-cell\" data-val=\"8\" style=\"background:rgba(234,88,12,0.02)\"></span></div><div class=\"hm-row\"><span class=\"hm-label\">9\u2013\u201312pm</span><span class=\"hm-cell\" data-val=\"120\" style=\"background:rgba(234,88,12,0.45)\"></span><span class=\"hm-cell\" data-val=\"135\" style=\"background:rgba(234,88,12,0.52)\"></span><span class=\"hm-cell\" data-val=\"118\" style=\"background:rgba(234,88,12,0.44)\"></span><span class=\"hm-cell\" data-val=\"125\" style=\"background:rgba(234,88,12,0.48)\"></span><span class=\"hm-cell\" data-val=\"110\" style=\"background:rgba(234,88,12,0.40)\"></span><span class=\"hm-cell\" data-val=\"28\" style=\"background:rgba(234,88,12,0.07)\"></span><span class=\"hm-cell\" data-val=\"20\" style=\"background:rgba(234,88,12,0.05)\"></span></div><div class=\"hm-row\"><span class=\"hm-label\">12\u2013\u20133pm</span><span class=\"hm-cell\" data-val=\"180\" style=\"background:rgba(234,88,12,0.72)\"></span><span class=\"hm-cell\" data-val=\"210\" style=\"background:rgba(234,88,12,0.90)\"></span><span class=\"hm-cell\" data-val=\"175\" style=\"background:rgba(234,88,12,0.70)\"></span><span class=\"hm-cell\" data-val=\"190\" style=\"background:rgba(234,88,12,0.78)\"></span><span class=\"hm-cell\" data-val=\"160\" style=\"background:rgba(234,88,12,0.62)\"></span><span class=\"hm-cell\" data-val=\"35\" style=\"background:rgba(234,88,12,0.09)\"></span><span class=\"hm-cell\" data-val=\"22\" style=\"background:rgba(234,88,12,0.06)\"></span></div><div class=\"hm-row\"><span class=\"hm-label\">3\u2013\u20136pm</span><span class=\"hm-cell\" data-val=\"145\" style=\"background:rgba(234,88,12,0.56)\"></span><span class=\"hm-cell\" data-val=\"155\" style=\"background:rgba(234,88,12,0.60)\"></span><span class=\"hm-cell\" data-val=\"140\" style=\"background:rgba(234,88,12,0.54)\"></span><span class=\"hm-cell\" data-val=\"150\" style=\"background:rgba(234,88,12,0.58)\"></span><span class=\"hm-cell\" data-val=\"130\" style=\"background:rgba(234,88,12,0.50)\"></span><span class=\"hm-cell\" data-val=\"30\" style=\"background:rgba(234,88,12,0.08)\"></span><span class=\"hm-cell\" data-val=\"18\" style=\"background:rgba(234,88,12,0.04)\"></span></div><div class=\"hm-row\"><span class=\"hm-label\">6\u2013\u201310pm</span><span class=\"hm-cell\" data-val=\"85\" style=\"background:rgba(234,88,12,0.28)\"></span><span class=\"hm-cell\" data-val=\"92\" style=\"background:rgba(234,88,12,0.32)\"></span><span class=\"hm-cell\" data-val=\"78\" style=\"background:rgba(234,88,12,0.25)\"></span><span class=\"hm-cell\" data-val=\"88\" style=\"background:rgba(234,88,12,0.30)\"></span><span class=\"hm-cell\" data-val=\"95\" style=\"background:rgba(234,88,12,0.34)\"></span><span class=\"hm-cell\" data-val=\"45\" style=\"background:rgba(234,88,12,0.13)\"></span><span class=\"hm-cell\" data-val=\"40\" style=\"background:rgba(234,88,12,0.11)\"></span></div></div><div class=\"hm-scale\"><span>Low</span><div class=\"hm-scale-bar\"></div><span>High</span></div></div>",
|
|
16
|
+
"script": "var cells = container.querySelectorAll('.hm-cell');\ncells.forEach(function(c, i) {\n setTimeout(function() {\n c.classList.add('visible');\n }, 50 + i * 20);\n});"
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"label": "Channels",
|
|
20
|
+
"title": "Channel Quality Radar",
|
|
21
|
+
"prose": "<p>Not all acquisition channels are equal. The radar chart compares six dimensions across Q1 (orange) vs the previous quarter (blue).</p><p><strong>Organic search</strong> improved dramatically in volume and retention, driven by new SEO landing pages. <strong>Paid social</strong> scores high on volume but low on retention — users from Instagram ads churn 3x faster than organic users.</p><p>The ideal channel scores high on all axes. Organic search and referrals are closest to that ideal, while paid channels trade retention for volume.</p>",
|
|
22
|
+
"viz": "<div class=\"rd\"><div class=\"rd-chart\"><svg viewBox=\"0 0 300 300\" width=\"280\" height=\"280\"><g transform=\"translate(150,150)\"><circle class=\"rd-ring\" r=\"36\" /><circle class=\"rd-ring\" r=\"72\" /><circle class=\"rd-ring\" r=\"108\" /><line class=\"rd-axis\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"-115\" /><line class=\"rd-axis\" x1=\"0\" y1=\"0\" x2=\"99.6\" y2=\"-57.5\" /><line class=\"rd-axis\" x1=\"0\" y1=\"0\" x2=\"99.6\" y2=\"57.5\" /><line class=\"rd-axis\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"115\" /><line class=\"rd-axis\" x1=\"0\" y1=\"0\" x2=\"-99.6\" y2=\"57.5\" /><line class=\"rd-axis\" x1=\"0\" y1=\"0\" x2=\"-99.6\" y2=\"-57.5\" /><polygon class=\"rd-poly prev\" points=\"0,-65 69,-44 58,47 0,75 -43,52 -58,-37\" /><polygon class=\"rd-poly cur\" points=\"0,-97 76,-35 52,62 0,54 -86,50 -81,-29\" /><circle class=\"rd-dot prev\" cx=\"0\" cy=\"-65\" /><circle class=\"rd-dot prev\" cx=\"69\" cy=\"-44\" /><circle class=\"rd-dot prev\" cx=\"58\" cy=\"47\" /><circle class=\"rd-dot prev\" cx=\"0\" cy=\"75\" /><circle class=\"rd-dot prev\" cx=\"-43\" cy=\"52\" /><circle class=\"rd-dot prev\" cx=\"-58\" cy=\"-37\" /><circle class=\"rd-dot cur\" cx=\"0\" cy=\"-97\" /><circle class=\"rd-dot cur\" cx=\"76\" cy=\"-35\" /><circle class=\"rd-dot cur\" cx=\"52\" cy=\"62\" /><circle class=\"rd-dot cur\" cx=\"0\" cy=\"54\" /><circle class=\"rd-dot cur\" cx=\"-86\" cy=\"50\" /><circle class=\"rd-dot cur\" cx=\"-81\" cy=\"-29\" /><text class=\"rd-lbl\" x=\"0\" y=\"-122\">Volume</text><text class=\"rd-lbl\" x=\"115\" y=\"-57\">Speed</text><text class=\"rd-lbl\" x=\"115\" y=\"68\">Cost Eff.</text><text class=\"rd-lbl\" x=\"0\" y=\"132\">Retention</text><text class=\"rd-lbl\" x=\"-115\" y=\"68\">Quality</text><text class=\"rd-lbl\" x=\"-115\" y=\"-57\">LTV</text></g></svg></div><div class=\"rd-legend\"><span><span class=\"rd-legend-dot\" style=\"background:#ea580c\"></span>Q1 (current)</span><span><span class=\"rd-legend-dot\" style=\"background:#3b82f6\"></span>Q4 (previous)</span></div></div>",
|
|
23
|
+
"script": "var polys = container.querySelectorAll('.rd-poly');\nvar dots = container.querySelectorAll('.rd-dot');\npolys.forEach(function(p, i) {\n setTimeout(function() { p.classList.add('visible'); }, 200 + i * 300);\n});\ndots.forEach(function(d, i) {\n setTimeout(function() { d.classList.add('visible'); }, 400);\n});"
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"label": "Revenue",
|
|
27
|
+
"title": "Revenue Waterfall",
|
|
28
|
+
"prose": "<p>Revenue grew from <strong>$180k</strong> base to <strong>$200k</strong> net — a healthy 11% increase. The waterfall breaks down each contributing factor.</p><p>New customer acquisition added <strong>$40k</strong>, and upsells from existing accounts contributed another <strong>$15k</strong>. These gains were partially offset by <strong>$25k</strong> in churn and <strong>$10k</strong> in downgrades.</p><p>The churn figure is the primary concern — it erodes 63% of new revenue. Reducing churn by even 20% would shift net growth from 11% to 14%.</p>",
|
|
29
|
+
"viz": "<div class=\"wf\"><div class=\"wf-chart\"><div class=\"wf-col\"><div class=\"wf-val\" data-v=\"$180k\"></div><div class=\"wf-block total\" style=\"height: 76.6%;\" data-h=\"76.6%\"></div><span class=\"wf-lbl\">Base</span></div><div class=\"wf-col\"><div class=\"wf-val\" data-v=\"+$40k\"></div><div class=\"wf-block add\" style=\"height: 17%; margin-bottom: 76.6%;\" data-h=\"17%\"></div><span class=\"wf-lbl\">New</span></div><div class=\"wf-col\"><div class=\"wf-val\" data-v=\"+$15k\"></div><div class=\"wf-block add\" style=\"height: 6.4%; margin-bottom: 93.6%;\" data-h=\"6.4%\"></div><span class=\"wf-lbl\">Upsell</span></div><div class=\"wf-col\"><div class=\"wf-val\" data-v=\"-$25k\"></div><div class=\"wf-block sub\" style=\"height: 10.6%; margin-bottom: 85.1%;\" data-h=\"10.6%\"></div><span class=\"wf-lbl\">Churn</span></div><div class=\"wf-col\"><div class=\"wf-val\" data-v=\"-$10k\"></div><div class=\"wf-block sub\" style=\"height: 4.3%; margin-bottom: 85.1%;\" data-h=\"4.3%\"></div><span class=\"wf-lbl\">Down</span></div><div class=\"wf-col\"><div class=\"wf-val\" data-v=\"$200k\"></div><div class=\"wf-block total\" style=\"height: 85.1%;\" data-h=\"85.1%\"></div><span class=\"wf-lbl\">Net</span></div></div></div>",
|
|
30
|
+
"script": "var blocks = container.querySelectorAll('.wf-block');\nvar vals = container.querySelectorAll('.wf-val');\nblocks.forEach(function(b, i) {\n var h = b.style.height;\n b.style.height = '0';\n setTimeout(function() {\n b.style.height = h;\n b.classList.add('visible');\n }, 150 + i * 120);\n});\nvals.forEach(function(v, i) {\n setTimeout(function() {\n v.textContent = v.getAttribute('data-v');\n v.classList.add('visible');\n }, 300 + i * 120);\n});"
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"label": "Health",
|
|
34
|
+
"title": "System Health Gauges",
|
|
35
|
+
"prose": "<p>Platform health metrics show the infrastructure is holding up well under growth. <strong>Uptime</strong> at 99.7% includes two brief outages totaling 2.2 hours — both from database connection pool exhaustion during traffic spikes.</p><p><strong>Response time</strong> (P95) sits at 210ms, well within the 500ms target. <strong>Error rate</strong> is low at 0.3%, though upload-related 504s account for nearly half of all errors.</p><p><strong>Throughput headroom</strong> at 62% means the system can absorb roughly 1.6x current load before needing horizontal scaling.</p>",
|
|
36
|
+
"viz": "<div class=\"gg-grid\"><div class=\"gg-item\"><svg viewBox=\"0 0 120 75\" width=\"120\" height=\"75\"><path class=\"gg-track\" d=\"M 15 65 A 45 45 0 0 1 105 65\" /><path class=\"gg-fill\" d=\"M 15 65 A 45 45 0 0 1 105 65\" stroke=\"#22c55e\" data-pct=\"99.7\" style=\"stroke-dasharray: 141.4; stroke-dashoffset: 141.4;\" /><text class=\"gg-val\" x=\"60\" y=\"60\" data-v=\"99.7%\"></text></svg><div class=\"gg-lbl\">Uptime</div><div class=\"gg-sub\">Target: 99.9%</div></div><div class=\"gg-item\"><svg viewBox=\"0 0 120 75\" width=\"120\" height=\"75\"><path class=\"gg-track\" d=\"M 15 65 A 45 45 0 0 1 105 65\" /><path class=\"gg-fill\" d=\"M 15 65 A 45 45 0 0 1 105 65\" stroke=\"#22c55e\" data-pct=\"92\" style=\"stroke-dasharray: 141.4; stroke-dashoffset: 141.4;\" /><text class=\"gg-val\" x=\"60\" y=\"60\" data-v=\"210ms\"></text></svg><div class=\"gg-lbl\">P95 Latency</div><div class=\"gg-sub\">Target: <500ms</div></div><div class=\"gg-item\"><svg viewBox=\"0 0 120 75\" width=\"120\" height=\"75\"><path class=\"gg-track\" d=\"M 15 65 A 45 45 0 0 1 105 65\" /><path class=\"gg-fill\" d=\"M 15 65 A 45 45 0 0 1 105 65\" stroke=\"#22c55e\" data-pct=\"97\" style=\"stroke-dasharray: 141.4; stroke-dashoffset: 141.4;\" /><text class=\"gg-val\" x=\"60\" y=\"60\" data-v=\"0.3%\"></text></svg><div class=\"gg-lbl\">Error Rate</div><div class=\"gg-sub\">Target: <1%</div></div><div class=\"gg-item\"><svg viewBox=\"0 0 120 75\" width=\"120\" height=\"75\"><path class=\"gg-track\" d=\"M 15 65 A 45 45 0 0 1 105 65\" /><path class=\"gg-fill\" d=\"M 15 65 A 45 45 0 0 1 105 65\" stroke=\"#ea580c\" data-pct=\"62\" style=\"stroke-dasharray: 141.4; stroke-dashoffset: 141.4;\" /><text class=\"gg-val\" x=\"60\" y=\"60\" data-v=\"62%\"></text></svg><div class=\"gg-lbl\">Headroom</div><div class=\"gg-sub\">Scale at <30%</div></div></div>",
|
|
37
|
+
"script": "var items = container.querySelectorAll('.gg-item');\nitems.forEach(function(item, i) {\n setTimeout(function() {\n item.classList.add('visible');\n var fill = item.querySelector('.gg-fill');\n var pct = parseFloat(fill.getAttribute('data-pct')) / 100;\n var totalLen = 141.4;\n fill.style.strokeDashoffset = totalLen * (1 - pct);\n var valEl = item.querySelector('.gg-val');\n valEl.textContent = valEl.getAttribute('data-v');\n }, 200 + i * 150);\n});"
|
|
38
|
+
}
|
|
39
|
+
]
|
|
40
|
+
}
|
package/README.md
CHANGED
|
@@ -1,21 +1,28 @@
|
|
|
1
1
|
# Emberflow Skills
|
|
2
2
|
|
|
3
|
-
Publish beautiful docs from your AI coding tools to [Emberflow](https://www.emberflow.ai) — architecture diagrams,
|
|
3
|
+
Publish beautiful docs from your AI coding tools to [Emberflow](https://www.emberflow.ai) — architecture diagrams, interactive data viewers, JSON explorers, and markdown, hosted instantly.
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
7
|
-
The fastest way to get started:
|
|
8
|
-
|
|
9
7
|
```bash
|
|
10
8
|
npx emberflow-skills
|
|
11
9
|
```
|
|
12
10
|
|
|
13
|
-
|
|
11
|
+
The installer auto-detects which AI coding tools you use and installs skills in the correct format for each.
|
|
12
|
+
|
|
13
|
+
| Tool | Format | How to use |
|
|
14
|
+
|------|--------|------------|
|
|
15
|
+
| **Claude Code** | `.claude/skills/` | `/ember-publish [topic]` |
|
|
16
|
+
| **Cursor** | `.cursor/rules/*.mdc` | "publish this to Emberflow" |
|
|
17
|
+
| **Windsurf** | `.windsurf/rules/*.md` | "publish this to Emberflow" |
|
|
18
|
+
| **Codex** | `.codex/*.md` | "publish this to Emberflow" |
|
|
19
|
+
|
|
20
|
+
If multiple tools are detected, skills are installed for all of them.
|
|
14
21
|
|
|
15
22
|
### Options
|
|
16
23
|
|
|
17
24
|
```bash
|
|
18
|
-
# Install to current project (default)
|
|
25
|
+
# Install to current project (default — auto-detects tools)
|
|
19
26
|
npx emberflow-skills
|
|
20
27
|
|
|
21
28
|
# Install globally for Claude Code (available in all projects)
|
|
@@ -24,28 +31,73 @@ npx emberflow-skills --global
|
|
|
24
31
|
|
|
25
32
|
### What the installer does
|
|
26
33
|
|
|
27
|
-
1. Detects
|
|
28
|
-
2.
|
|
29
|
-
3.
|
|
34
|
+
1. Detects tool configs in your project (`.claude/`, `.cursor/`, `.windsurf/`, `.codex/`)
|
|
35
|
+
2. Converts skills to each tool's native format (SKILL.md, .mdc rules, markdown agents)
|
|
36
|
+
3. Copies reference templates for the explainer skill
|
|
37
|
+
4. Done — use the skills in your next conversation
|
|
38
|
+
|
|
39
|
+
## Skills
|
|
30
40
|
|
|
31
|
-
|
|
41
|
+
### `/ember-publish`
|
|
32
42
|
|
|
33
|
-
|
|
43
|
+
The smart router — automatically picks the right format based on your content. Just describe what you want and it figures out whether to publish a document, JSON explorer, explainer, dataset, or Space.
|
|
34
44
|
|
|
35
45
|
```
|
|
36
46
|
/ember-publish architecture overview for the payments service
|
|
37
47
|
```
|
|
38
48
|
|
|
39
|
-
|
|
49
|
+
### `/ember-publish-doc`
|
|
50
|
+
|
|
51
|
+
Publish a markdown document with interactive emberDiagrams (flowcharts, architecture maps, decision trees) — auto-laid-out with animations, no coordinates needed.
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
/ember-publish-doc the authentication flow for our API
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### `/ember-publish-json`
|
|
58
|
+
|
|
59
|
+
Publish JSON data as an interactive explorer with a node-graph visualization. Expand/collapse nodes, pan and zoom, search across keys and values, and switch between multiple payloads.
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
/ember-publish-json the API response from /api/users
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### `/ember-publish-dataset`
|
|
66
|
+
|
|
67
|
+
Publish CSV files as an interactive dataset viewer with virtual scroll (handles 10k+ rows), click-to-sort, per-column filters, search with highlighting, and CSV export.
|
|
68
|
+
|
|
69
|
+
```
|
|
70
|
+
/ember-publish-dataset sales data from data/transactions.csv
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### `/ember-publish-explainer`
|
|
74
|
+
|
|
75
|
+
Generate interactive visual explainers — the AI chooses the best visualization type (funnel, heatmap, radar chart, waterfall, timeline, architecture diagram, etc.) for the topic. Slide-based with animated transitions.
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
/ember-publish-explainer how our CI/CD pipeline works
|
|
79
|
+
```
|
|
40
80
|
|
|
41
|
-
|
|
81
|
+
### `/ember-publish-space`
|
|
42
82
|
|
|
43
|
-
-
|
|
83
|
+
Publish a directory of markdown files as a multi-page docs site with sidebar navigation, collapsible sections, and SPA page transitions.
|
|
84
|
+
|
|
85
|
+
```
|
|
86
|
+
/ember-publish-space the docs/ directory as API documentation
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## What you get
|
|
90
|
+
|
|
91
|
+
- Interactive emberDiagrams with zoom, pan, and fullscreen
|
|
92
|
+
- Virtual-scroll dataset tables with sort, filter, and export
|
|
93
|
+
- JSON node-graph explorer with drag, collapse, and search
|
|
94
|
+
- Slide-based visual explainers with animated charts and timelines
|
|
95
|
+
- Multi-page Spaces with sidebar navigation
|
|
44
96
|
- Syntax-highlighted code blocks (190+ languages)
|
|
45
97
|
- Auto-generated table of contents
|
|
46
98
|
- Per-block inline comments and discussions
|
|
47
|
-
- Dark mode with font selection
|
|
48
|
-
- Public or private docs with
|
|
99
|
+
- Dark/light mode with font selection
|
|
100
|
+
- Public or private docs with link sharing
|
|
49
101
|
|
|
50
102
|
## Manual install
|
|
51
103
|
|
|
@@ -53,12 +105,19 @@ If you prefer not to use npx:
|
|
|
53
105
|
|
|
54
106
|
```bash
|
|
55
107
|
git clone https://github.com/pmccurley87/emberflow-skills.git
|
|
56
|
-
|
|
108
|
+
|
|
109
|
+
# Claude Code
|
|
110
|
+
cp -r emberflow-skills/skills/* .claude/skills/
|
|
111
|
+
|
|
112
|
+
# Cursor — copy as .mdc rules
|
|
113
|
+
# Windsurf — copy to .windsurf/rules/
|
|
114
|
+
# Codex — copy to .codex/
|
|
57
115
|
```
|
|
58
116
|
|
|
59
117
|
## Works with
|
|
60
118
|
|
|
61
|
-
- Claude Code
|
|
62
|
-
- Cursor
|
|
63
|
-
-
|
|
64
|
-
-
|
|
119
|
+
- **Claude Code** — native skill system with `/slash-commands`
|
|
120
|
+
- **Cursor** — auto-attached rules via `.cursor/rules/`
|
|
121
|
+
- **Windsurf** — rules via `.windsurf/rules/`
|
|
122
|
+
- **Codex** — agent instructions via `.codex/`
|
|
123
|
+
- **Any MCP-compatible tool** — Emberflow also provides an MCP server
|
package/bin/install.js
CHANGED
|
@@ -7,7 +7,7 @@ const http = require('http');
|
|
|
7
7
|
const readline = require('readline');
|
|
8
8
|
const os = require('os');
|
|
9
9
|
|
|
10
|
-
const SKILL_NAMES = ['ember-publish', 'ember-publish-doc', 'ember-publish-explainer', 'ember-publish-json', 'ember-publish-space'];
|
|
10
|
+
const SKILL_NAMES = ['ember-publish', 'ember-publish-doc', 'ember-publish-dataset', 'ember-publish-explainer', 'ember-publish-json', 'ember-publish-space'];
|
|
11
11
|
const SKILLS_DIR = path.join(__dirname, '..', 'skills');
|
|
12
12
|
const EMBERFLOW_URL = 'https://www.emberflow.ai';
|
|
13
13
|
const TOKEN_PATH = path.join(os.homedir(), '.emberflow', 'token.json');
|
|
@@ -18,13 +18,38 @@ const green = (s) => `\x1b[32m${s}\x1b[0m`;
|
|
|
18
18
|
const cyan = (s) => `\x1b[36m${s}\x1b[0m`;
|
|
19
19
|
const orange = (s) => `\x1b[38;5;208m${s}\x1b[0m`;
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
21
|
+
// ── Tool definitions ──
|
|
22
|
+
|
|
23
|
+
const TOOLS = [
|
|
24
|
+
{
|
|
25
|
+
type: 'claude',
|
|
26
|
+
detect: ['.claude'],
|
|
27
|
+
projectDir: '.claude/skills',
|
|
28
|
+
globalDir: path.join(os.homedir(), '.claude', 'skills'),
|
|
29
|
+
label: 'Claude Code',
|
|
30
|
+
usage: '/ember-publish',
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
type: 'cursor',
|
|
34
|
+
detect: ['.cursor', '.cursorrules'],
|
|
35
|
+
projectDir: '.cursor/rules',
|
|
36
|
+
label: 'Cursor',
|
|
37
|
+
usage: '"publish this to Emberflow"',
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
type: 'windsurf',
|
|
41
|
+
detect: ['.windsurf', '.windsurfrules'],
|
|
42
|
+
projectDir: '.windsurf/rules',
|
|
43
|
+
label: 'Windsurf',
|
|
44
|
+
usage: '"publish this to Emberflow"',
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
type: 'codex',
|
|
48
|
+
detect: ['.codex', 'AGENTS.md'],
|
|
49
|
+
projectDir: '.codex',
|
|
50
|
+
label: 'Codex',
|
|
51
|
+
usage: '"publish this to Emberflow"',
|
|
52
|
+
},
|
|
28
53
|
];
|
|
29
54
|
|
|
30
55
|
const args = process.argv.slice(2);
|
|
@@ -77,7 +102,22 @@ function sleep(ms) {
|
|
|
77
102
|
return new Promise((r) => setTimeout(r, ms));
|
|
78
103
|
}
|
|
79
104
|
|
|
80
|
-
// ──
|
|
105
|
+
// ── SKILL.md parsing ──
|
|
106
|
+
|
|
107
|
+
function parseFrontmatter(content) {
|
|
108
|
+
const match = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
|
|
109
|
+
if (!match) return { meta: {}, body: content };
|
|
110
|
+
const meta = {};
|
|
111
|
+
for (const line of match[1].split('\n')) {
|
|
112
|
+
const idx = line.indexOf(':');
|
|
113
|
+
if (idx > 0) {
|
|
114
|
+
meta[line.slice(0, idx).trim()] = line.slice(idx + 1).trim();
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return { meta, body: match[2] };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ── File helpers ──
|
|
81
121
|
|
|
82
122
|
function copyDirRecursive(src, dest) {
|
|
83
123
|
fs.mkdirSync(dest, { recursive: true });
|
|
@@ -92,25 +132,147 @@ function copyDirRecursive(src, dest) {
|
|
|
92
132
|
}
|
|
93
133
|
}
|
|
94
134
|
|
|
95
|
-
function
|
|
135
|
+
function rewriteTemplatePaths(body, templatesRelPath) {
|
|
136
|
+
// Rewrite "Read templates/" to use the full relative path from project root
|
|
137
|
+
return body.replace(
|
|
138
|
+
/Read templates\//g,
|
|
139
|
+
`Read ${templatesRelPath}/`
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ── Installers per tool type ──
|
|
144
|
+
|
|
145
|
+
function installClaude(name, destDir) {
|
|
146
|
+
const srcDir = path.join(SKILLS_DIR, name);
|
|
147
|
+
const skillDir = path.join(destDir, name);
|
|
148
|
+
fs.mkdirSync(skillDir, { recursive: true });
|
|
149
|
+
fs.copyFileSync(path.join(srcDir, 'SKILL.md'), path.join(skillDir, 'SKILL.md'));
|
|
150
|
+
|
|
151
|
+
const templatesDir = path.join(srcDir, 'templates');
|
|
152
|
+
if (fs.existsSync(templatesDir)) {
|
|
153
|
+
copyDirRecursive(templatesDir, path.join(skillDir, 'templates'));
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function installCursor(name, destDir, cwd) {
|
|
158
|
+
const srcDir = path.join(SKILLS_DIR, name);
|
|
159
|
+
const skillMd = fs.readFileSync(path.join(srcDir, 'SKILL.md'), 'utf8');
|
|
160
|
+
const { meta, body } = parseFrontmatter(skillMd);
|
|
161
|
+
|
|
162
|
+
// Copy templates to .cursor/rules/<name>/templates/
|
|
163
|
+
const templatesDir = path.join(srcDir, 'templates');
|
|
164
|
+
const hasTemplates = fs.existsSync(templatesDir);
|
|
165
|
+
if (hasTemplates) {
|
|
166
|
+
copyDirRecursive(templatesDir, path.join(destDir, name, 'templates'));
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Rewrite template paths relative to project root
|
|
170
|
+
let content = body;
|
|
171
|
+
if (hasTemplates) {
|
|
172
|
+
const relPath = path.relative(cwd, path.join(destDir, name, 'templates'));
|
|
173
|
+
content = rewriteTemplatePaths(content, relPath);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Write .mdc file with Cursor frontmatter
|
|
177
|
+
const description = meta.description || name;
|
|
178
|
+
const hint = meta['argument-hint'] ? ` — ${meta['argument-hint']}` : '';
|
|
179
|
+
const mdc = `---
|
|
180
|
+
description: ${description}${hint}
|
|
181
|
+
globs:
|
|
182
|
+
alwaysApply: false
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
${content}`;
|
|
186
|
+
|
|
187
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
188
|
+
fs.writeFileSync(path.join(destDir, `${name}.mdc`), mdc);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function installWindsurf(name, destDir, cwd) {
|
|
192
|
+
const srcDir = path.join(SKILLS_DIR, name);
|
|
193
|
+
const skillMd = fs.readFileSync(path.join(srcDir, 'SKILL.md'), 'utf8');
|
|
194
|
+
const { body } = parseFrontmatter(skillMd);
|
|
195
|
+
|
|
196
|
+
// Copy templates
|
|
197
|
+
const templatesDir = path.join(srcDir, 'templates');
|
|
198
|
+
const hasTemplates = fs.existsSync(templatesDir);
|
|
199
|
+
if (hasTemplates) {
|
|
200
|
+
copyDirRecursive(templatesDir, path.join(destDir, name, 'templates'));
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Rewrite template paths
|
|
204
|
+
let content = body;
|
|
205
|
+
if (hasTemplates) {
|
|
206
|
+
const relPath = path.relative(cwd, path.join(destDir, name, 'templates'));
|
|
207
|
+
content = rewriteTemplatePaths(content, relPath);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
211
|
+
fs.writeFileSync(path.join(destDir, `${name}.md`), content);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function installCodex(name, destDir, cwd) {
|
|
215
|
+
const srcDir = path.join(SKILLS_DIR, name);
|
|
216
|
+
const skillMd = fs.readFileSync(path.join(srcDir, 'SKILL.md'), 'utf8');
|
|
217
|
+
const { meta, body } = parseFrontmatter(skillMd);
|
|
218
|
+
|
|
219
|
+
// Copy templates
|
|
220
|
+
const templatesDir = path.join(srcDir, 'templates');
|
|
221
|
+
const hasTemplates = fs.existsSync(templatesDir);
|
|
222
|
+
if (hasTemplates) {
|
|
223
|
+
copyDirRecursive(templatesDir, path.join(destDir, name, 'templates'));
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Rewrite template paths
|
|
227
|
+
let content = body;
|
|
228
|
+
if (hasTemplates) {
|
|
229
|
+
const relPath = path.relative(cwd, path.join(destDir, name, 'templates'));
|
|
230
|
+
content = rewriteTemplatePaths(content, relPath);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
234
|
+
fs.writeFileSync(path.join(destDir, `${name}.md`), content);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// ── Main install function ──
|
|
238
|
+
|
|
239
|
+
function install(destDir, tool, cwd) {
|
|
96
240
|
for (const name of SKILL_NAMES) {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
241
|
+
switch (tool.type) {
|
|
242
|
+
case 'claude':
|
|
243
|
+
installClaude(name, destDir);
|
|
244
|
+
break;
|
|
245
|
+
case 'cursor':
|
|
246
|
+
installCursor(name, destDir, cwd);
|
|
247
|
+
break;
|
|
248
|
+
case 'windsurf':
|
|
249
|
+
installWindsurf(name, destDir, cwd);
|
|
250
|
+
break;
|
|
251
|
+
case 'codex':
|
|
252
|
+
installCodex(name, destDir, cwd);
|
|
253
|
+
break;
|
|
107
254
|
}
|
|
108
|
-
|
|
109
|
-
console.log(` ${green('✓')}
|
|
255
|
+
const relDest = path.relative(cwd, destDir) || destDir;
|
|
256
|
+
console.log(` ${green('✓')} ${name} → ${dim(relDest)} ${dim(`(${tool.label})`)}`);
|
|
110
257
|
}
|
|
111
258
|
return true;
|
|
112
259
|
}
|
|
113
260
|
|
|
261
|
+
// ── Detection ──
|
|
262
|
+
|
|
263
|
+
function detectTools(cwd) {
|
|
264
|
+
const detected = [];
|
|
265
|
+
for (const tool of TOOLS) {
|
|
266
|
+
for (const marker of tool.detect) {
|
|
267
|
+
if (fs.existsSync(path.join(cwd, marker))) {
|
|
268
|
+
detected.push(tool);
|
|
269
|
+
break;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
return detected;
|
|
274
|
+
}
|
|
275
|
+
|
|
114
276
|
// ── Auth flow ──
|
|
115
277
|
|
|
116
278
|
function hasValidToken() {
|
|
@@ -128,7 +290,6 @@ async function authenticate() {
|
|
|
128
290
|
console.log(` ${dim('Your published docs will be attributed to your account.')}`);
|
|
129
291
|
console.log();
|
|
130
292
|
|
|
131
|
-
// Request device code
|
|
132
293
|
let resp;
|
|
133
294
|
try {
|
|
134
295
|
resp = await request('POST', `${EMBERFLOW_URL}/api/device-code`);
|
|
@@ -151,7 +312,6 @@ async function authenticate() {
|
|
|
151
312
|
console.log(` Your code: ${bold(code)}`);
|
|
152
313
|
console.log();
|
|
153
314
|
|
|
154
|
-
// Try to open the URL automatically
|
|
155
315
|
try {
|
|
156
316
|
const { exec } = require('child_process');
|
|
157
317
|
if (process.platform === 'win32') {
|
|
@@ -165,8 +325,7 @@ async function authenticate() {
|
|
|
165
325
|
|
|
166
326
|
process.stdout.write(` ${dim('Waiting for approval...')}`);
|
|
167
327
|
|
|
168
|
-
|
|
169
|
-
const maxAttempts = 60; // 3 minutes at 3s intervals
|
|
328
|
+
const maxAttempts = 60;
|
|
170
329
|
for (let i = 0; i < maxAttempts; i++) {
|
|
171
330
|
await sleep(3000);
|
|
172
331
|
|
|
@@ -174,7 +333,6 @@ async function authenticate() {
|
|
|
174
333
|
const status = await request('GET', `${EMBERFLOW_URL}/api/device-code/${code}`);
|
|
175
334
|
|
|
176
335
|
if (status.data.status === 'approved' && status.data.session_token) {
|
|
177
|
-
// Strip cookie name prefix if present (e.g. "__Secure-better-auth.session_token=VALUE" -> "VALUE")
|
|
178
336
|
const raw = status.data.session_token.replace(/^(?:__Secure-)?better-auth\.session_token=/, '');
|
|
179
337
|
fs.mkdirSync(path.dirname(TOKEN_PATH), { recursive: true });
|
|
180
338
|
fs.writeFileSync(TOKEN_PATH, JSON.stringify({ token: raw }, null, 2));
|
|
@@ -194,7 +352,6 @@ async function authenticate() {
|
|
|
194
352
|
// Network error, keep polling
|
|
195
353
|
}
|
|
196
354
|
|
|
197
|
-
// Spinner
|
|
198
355
|
const frames = ['⠋','⠙','⠹','⠸','⠼','⠴','⠦','⠧','⠇','⠏'];
|
|
199
356
|
process.stdout.clearLine(0);
|
|
200
357
|
process.stdout.cursorTo(0);
|
|
@@ -217,38 +374,48 @@ async function main() {
|
|
|
217
374
|
console.log();
|
|
218
375
|
|
|
219
376
|
if (!authOnly) {
|
|
377
|
+
const cwd = process.cwd();
|
|
220
378
|
let installed = 0;
|
|
221
379
|
|
|
222
380
|
if (isGlobal) {
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
381
|
+
// Global install — Claude Code only
|
|
382
|
+
const claudeTool = TOOLS[0];
|
|
383
|
+
install(claudeTool.globalDir, claudeTool, cwd);
|
|
384
|
+
installed++;
|
|
227
385
|
} else {
|
|
228
|
-
const
|
|
229
|
-
const detected = [];
|
|
230
|
-
|
|
231
|
-
for (const t of targets) {
|
|
232
|
-
const parent = path.dirname(path.join(cwd, t.dir));
|
|
233
|
-
if (fs.existsSync(path.join(cwd, t.dir)) || fs.existsSync(parent)) {
|
|
234
|
-
detected.push(t);
|
|
235
|
-
}
|
|
236
|
-
}
|
|
386
|
+
const detected = detectTools(cwd);
|
|
237
387
|
|
|
238
388
|
if (detected.length === 0) {
|
|
239
|
-
|
|
389
|
+
// Default to Claude Code
|
|
390
|
+
detected.push(TOOLS[0]);
|
|
391
|
+
console.log(` ${dim('No tool config detected — defaulting to Claude Code')}`);
|
|
392
|
+
console.log();
|
|
393
|
+
} else {
|
|
394
|
+
console.log(` ${dim(`Detected: ${detected.map(t => t.label).join(', ')}`)}`);
|
|
395
|
+
console.log();
|
|
240
396
|
}
|
|
241
397
|
|
|
242
|
-
for (const
|
|
243
|
-
|
|
398
|
+
for (const tool of detected) {
|
|
399
|
+
const destDir = path.join(cwd, tool.projectDir);
|
|
400
|
+
install(destDir, tool, cwd);
|
|
244
401
|
installed++;
|
|
402
|
+
console.log();
|
|
245
403
|
}
|
|
246
404
|
}
|
|
247
405
|
|
|
248
406
|
if (installed > 0) {
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
407
|
+
const detected = isGlobal ? [TOOLS[0]] : (detectTools(cwd).length > 0 ? detectTools(cwd) : [TOOLS[0]]);
|
|
408
|
+
const claudeDetected = detected.some(t => t.type === 'claude');
|
|
409
|
+
const othersDetected = detected.filter(t => t.type !== 'claude');
|
|
410
|
+
|
|
411
|
+
if (claudeDetected) {
|
|
412
|
+
console.log(` ${bold('Claude Code:')} ${cyan('/ember-publish')} ${dim('[topic]')} — auto-picks format`);
|
|
413
|
+
console.log(` ${cyan('/ember-publish-doc')} ${dim('[topic]')} ${cyan('/ember-publish-json')} ${dim('[data]')} ${cyan('/ember-publish-explainer')} ${dim('[topic]')}`);
|
|
414
|
+
}
|
|
415
|
+
if (othersDetected.length > 0) {
|
|
416
|
+
const labels = othersDetected.map(t => t.label).join(', ');
|
|
417
|
+
console.log(` ${bold(`${labels}:`)} Just ask ${cyan('"publish this to Emberflow"')} or ${cyan('"create an Emberflow explainer about X"')}`);
|
|
418
|
+
}
|
|
252
419
|
}
|
|
253
420
|
}
|
|
254
421
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ember-publish-dataset
|
|
3
|
+
description: Publish one or more CSV files to Emberflow as an interactive dataset viewer with multi-table tabs, search, sort, filter, pagination, and CSV export
|
|
4
|
+
argument-hint: [CSV file path(s) and a title]
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Emberflow Dataset Publisher
|
|
8
|
+
|
|
9
|
+
Publish tabular CSV data to Emberflow at **https://emberflow.ai** as an interactive dataset viewer with:
|
|
10
|
+
- Multi-table tabs (one per CSV file)
|
|
11
|
+
- Cross-column search with highlighted matches
|
|
12
|
+
- Click-to-sort on any column (asc/desc)
|
|
13
|
+
- Column show/hide filter
|
|
14
|
+
- Paginated rows (100 per page)
|
|
15
|
+
- CSV export of filtered data
|
|
16
|
+
|
|
17
|
+
## Step 1: Prepare your CSV files
|
|
18
|
+
|
|
19
|
+
Each CSV file becomes one tab in the viewer. The first row is treated as column headers. Example:
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
Region,Q1,Q2,Total
|
|
23
|
+
North America,142000,158000,300000
|
|
24
|
+
Europe,98000,112000,210000
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Step 2: Authenticate (if needed)
|
|
28
|
+
|
|
29
|
+
Session tokens are stored at `~/.emberflow/token.json`. Check if a valid session exists:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
cat ~/.emberflow/token.json 2>/dev/null
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
If no session exists or the token is expired, authenticate using the device flow:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
EMBERFLOW_URL="https://emberflow.ai"
|
|
39
|
+
|
|
40
|
+
# Step 1: Request a device code
|
|
41
|
+
RESP=$(curl -s -X POST "$EMBERFLOW_URL/api/device-code")
|
|
42
|
+
CODE=$(echo "$RESP" | jq -r .code)
|
|
43
|
+
URL=$(echo "$RESP" | jq -r .verification_url)
|
|
44
|
+
echo "Open in browser: $URL"
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Tell the user to open the URL and sign in. Then poll until approved:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
# Step 2: Poll until approved (every 3s)
|
|
51
|
+
while true; do
|
|
52
|
+
STATUS=$(curl -s "$EMBERFLOW_URL/api/device-code/$CODE")
|
|
53
|
+
S=$(echo "$STATUS" | jq -r .status)
|
|
54
|
+
if [ "$S" = "approved" ]; then
|
|
55
|
+
TOKEN=$(echo "$STATUS" | jq -r .session_token)
|
|
56
|
+
mkdir -p ~/.emberflow
|
|
57
|
+
echo "{\"token\":\"$TOKEN\"}" > ~/.emberflow/token.json
|
|
58
|
+
break
|
|
59
|
+
fi
|
|
60
|
+
if [ "$S" = "expired" ]; then echo "Code expired. Try again."; break; fi
|
|
61
|
+
sleep 3
|
|
62
|
+
done
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Step 3: Build and publish the dataset
|
|
66
|
+
|
|
67
|
+
Parse each CSV and build the dataset JSON, then POST with `content_type: "dataset"`.
|
|
68
|
+
|
|
69
|
+
### Data format
|
|
70
|
+
|
|
71
|
+
```json
|
|
72
|
+
{
|
|
73
|
+
"tables": [
|
|
74
|
+
{
|
|
75
|
+
"name": "Sales by Region",
|
|
76
|
+
"columns": ["Region", "Q1", "Q2", "Total"],
|
|
77
|
+
"rows": [
|
|
78
|
+
["North America", 142000, 158000, 300000],
|
|
79
|
+
["Europe", 98000, 112000, 210000]
|
|
80
|
+
]
|
|
81
|
+
}
|
|
82
|
+
]
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
- `columns`: array of header strings
|
|
87
|
+
- `rows`: array of arrays — numeric values as numbers, strings as strings
|
|
88
|
+
- Multiple tables → multiple tabs in the viewer
|
|
89
|
+
|
|
90
|
+
### Publish with curl
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
EMBERFLOW_URL="https://emberflow.ai"
|
|
94
|
+
TITLE="My Dataset"
|
|
95
|
+
SLUG=$(echo "$TITLE" | tr '[:upper:]' '[:lower:]' | tr -cs 'a-z0-9' '-' | sed 's/^-//;s/-$//')
|
|
96
|
+
TOKEN=$(jq -r .token ~/.emberflow/token.json)
|
|
97
|
+
|
|
98
|
+
# Build dataset JSON from one CSV file (requires python3 or similar)
|
|
99
|
+
CONTENT=$(python3 -c "
|
|
100
|
+
import csv, json, sys
|
|
101
|
+
with open('data.csv') as f:
|
|
102
|
+
rows = list(csv.reader(f))
|
|
103
|
+
cols = rows[0]
|
|
104
|
+
data = []
|
|
105
|
+
for r in rows[1:]:
|
|
106
|
+
row = []
|
|
107
|
+
for v in r:
|
|
108
|
+
try: row.append(float(v) if '.' in v else int(v))
|
|
109
|
+
except: row.append(v)
|
|
110
|
+
data.append(row)
|
|
111
|
+
print(json.dumps({'tables': [{'name': 'Data', 'columns': cols, 'rows': data}]}))
|
|
112
|
+
")
|
|
113
|
+
|
|
114
|
+
curl -s -X POST "$EMBERFLOW_URL/api/docs" \
|
|
115
|
+
-H 'Content-Type: application/json' \
|
|
116
|
+
-H "Authorization: Bearer $TOKEN" \
|
|
117
|
+
-d "$(jq -n --arg slug "$SLUG" --arg title "$TITLE" --arg content "$CONTENT" \
|
|
118
|
+
'{slug: $slug, title: $title, content: $content, content_type: "dataset", visibility: "public"}')"
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
The response JSON includes a `url` field. The dataset is viewable at:
|
|
122
|
+
- `https://emberflow.ai/d/<shortId>/<slug>`
|
|
123
|
+
|
|
124
|
+
To **update** an existing dataset, publish again with the same slug — the API upserts for the same author.
|
|
125
|
+
|
|
126
|
+
### Other operations
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
# List all your documents
|
|
130
|
+
curl -s -H "Authorization: Bearer $TOKEN" "$EMBERFLOW_URL/api/docs"
|
|
131
|
+
|
|
132
|
+
# Delete a dataset
|
|
133
|
+
curl -s -X DELETE -H "Authorization: Bearer $TOKEN" "$EMBERFLOW_URL/api/docs/SLUG_HERE"
|
|
134
|
+
```
|
|
@@ -10,6 +10,8 @@ Generate a **structured JSON** explainer that the Emberflow platform renders as
|
|
|
10
10
|
|
|
11
11
|
You choose the best visualization type for each slide. A single explainer can mix different viz types across slides — a timeline on slide 2, a data table on slide 3, a chart on slide 4.
|
|
12
12
|
|
|
13
|
+
**Visual diversity is critical.** Every explainer should feel bespoke. Don't fall back on "grid of stat cards" or "list of bordered boxes" as the default — those are just one option among many. Think about what shape the data actually has: is it a flow? Use a Sankey or funnel. A ranking? Use a horizontal bar chart or podium layout. A distribution? Use a heatmap or scatter. A before/after? Use a split comparison. A hierarchy? Use a tree or nested rings. A process? Use a swimlane or animated pipeline. Match the visualization to the shape of the information, not the other way around.
|
|
14
|
+
|
|
13
15
|
## Output
|
|
14
16
|
|
|
15
17
|
Write a single JSON file to `{topic-slug}-explainer.json` in the current working directory. Then publish it to Emberflow.
|
|
@@ -163,32 +165,47 @@ No `script` field is needed for diagram slides — the platform handles all anim
|
|
|
163
165
|
|
|
164
166
|
## B. Visualization Primitive Catalog
|
|
165
167
|
|
|
166
|
-
|
|
168
|
+
This is a **starting point, not a menu to order from**. The best explainers invent visualizations that fit the content perfectly. Combine, adapt, or create entirely new primitives. If none of these fit — build something original.
|
|
167
169
|
|
|
168
170
|
### Data Display
|
|
169
171
|
|
|
170
|
-
- **KPI / stat cards** — Grid of boxes with label, large value, subtitle.
|
|
171
|
-
- **Data table** — `<table>` with muted uppercase headers, monospace numbers, color-coded values
|
|
172
|
-
- **Comparison matrix** — Table with checkmark/cross SVG icons per cell.
|
|
172
|
+
- **KPI / stat cards** — Grid of boxes with label, large value, subtitle. Good for overview slides, but don't default to this for everything.
|
|
173
|
+
- **Data table** — `<table>` with muted uppercase headers, monospace numbers, color-coded values. Category dots on row labels.
|
|
174
|
+
- **Comparison matrix** — Table with checkmark/cross SVG icons per cell. Active column highlighted with accent border.
|
|
175
|
+
- **Heatmap grid** — CSS grid where cell background intensity maps to a value. Use color gradients (green→yellow→red or cool→warm). Great for showing density, coverage, or correlation.
|
|
176
|
+
- **Scorecard** — Single large metric in a ring or gauge, with trend sparkline below. Different from KPI cards — more visual weight, fewer items.
|
|
173
177
|
|
|
174
178
|
### Charts
|
|
175
179
|
|
|
176
|
-
- **Vertical bar chart** — Flex row of bars, height as percentage of max. Color-code by threshold
|
|
177
|
-
- **Horizontal bar chart** — Rows with label left, bar extending right. Good for ranked lists
|
|
178
|
-
- **Donut / ring chart** — SVG circle with `stroke-dasharray`/`stroke-dashoffset`. Percentage label centered
|
|
179
|
-
- **Funnel diagram** — Stacked horizontal bars decreasing in width, centered.
|
|
180
|
+
- **Vertical bar chart** — Flex row of bars, height as percentage of max. Color-code by threshold. Animate height on entrance.
|
|
181
|
+
- **Horizontal bar chart** — Rows with label left, bar extending right. Good for ranked lists, leaderboards, survey results.
|
|
182
|
+
- **Donut / ring chart** — SVG circle with `stroke-dasharray`/`stroke-dashoffset`. Percentage label centered.
|
|
183
|
+
- **Funnel diagram** — Stacked horizontal bars decreasing in width, centered. Show conversion rates between stages.
|
|
184
|
+
- **Waterfall chart** — Bars that float from the previous bar's end. Show how values build up or break down (budget additions/subtractions, funnel drop-offs).
|
|
185
|
+
- **Sparklines / mini line charts** — SVG polylines inside cards or rows. Show trends without axes. Great for time-series context within other layouts.
|
|
186
|
+
- **Stacked bar / segmented bar** — Single horizontal bar divided into colored segments. Show composition at a glance (e.g., traffic sources, time allocation).
|
|
187
|
+
- **Scatter / bubble plot** — SVG circles positioned by x/y data. Size encodes a third dimension. Good for correlation or distribution.
|
|
188
|
+
|
|
189
|
+
### Flows & Processes
|
|
190
|
+
|
|
191
|
+
- **Sankey diagram** — SVG paths flowing from left to right with varying widths. Show how quantities split and merge across stages (budget allocation, user flow, data pipeline).
|
|
192
|
+
- **Swimlane diagram** — Horizontal lanes (rows) representing actors/systems. Steps flow left-to-right across lanes showing handoffs. Great for cross-team processes.
|
|
193
|
+
- **Animated pipeline** — Horizontal or vertical sequence of stages with animated dots/particles moving through. Show data or request flow in real-time feel.
|
|
194
|
+
- **Decision tree** — Binary branching layout. Each node is a question, branches lead to outcomes. Clickable to explore paths.
|
|
180
195
|
|
|
181
196
|
### Timelines & Sequences
|
|
182
197
|
|
|
183
198
|
- **Vertical timeline** — Left border line with dot markers. Each event has date, title, description, optional progress bar and status badge.
|
|
184
199
|
- **Horizontal timeline** — Flex row of connected nodes along a horizontal line. Good for fewer items (3-6).
|
|
185
200
|
- **Progress stepper** — Numbered circles connected by lines. Active step highlighted, completed steps filled.
|
|
201
|
+
- **Gantt-style chart** — Rows of horizontal bars on a time axis. Show overlapping phases, dependencies, or parallel workstreams.
|
|
202
|
+
- **Spiral timeline** — Events arranged along a spiral path (SVG). Unusual and memorable for cyclical or long-spanning histories.
|
|
186
203
|
|
|
187
204
|
### Relationships & Structure (Auto-Layout Diagrams)
|
|
188
205
|
|
|
189
206
|
For any node-and-edge visualization, use a **declarative diagram object** as the `viz` field. The platform auto-positions nodes and routes edges — no manual coordinate math needed.
|
|
190
207
|
|
|
191
|
-
- **Architecture diagram** — Use `viz: { nodes, edges, groups }` with color-coded nodes, meaningful icons, and labeled groups.
|
|
208
|
+
- **Architecture diagram** — Use `viz: { nodes, edges, groups }` with color-coded nodes, meaningful icons, and labeled groups. See Section A → "Architecture diagrams".
|
|
192
209
|
- **Flowchart** — Same declarative format with a single group or no groups. Set `direction: "TB"` for top-to-bottom flow.
|
|
193
210
|
- **Org chart / hierarchy** — Use `direction: "TB"` and groups for departments. Color-code by role.
|
|
194
211
|
- **Network / data flow** — Multiple groups connected by cross-group edges. Color edges by data type.
|
|
@@ -197,17 +214,27 @@ For any node-and-edge visualization, use a **declarative diagram object** as the
|
|
|
197
214
|
|
|
198
215
|
- **Periodic table / grid** — CSS grid of cards with colored top bar per category. Hover shows detail.
|
|
199
216
|
- **Kanban board** — Columns with card items. Cards can highlight or shift between columns per slide.
|
|
217
|
+
- **Tier list** — Labeled rows (S/A/B/C/F) with items placed in each tier. Color-coded by rank. Great for evaluations and comparisons.
|
|
200
218
|
|
|
201
219
|
### Status & Indicators
|
|
202
220
|
|
|
203
221
|
- **Risk cards** — Stacked cards with severity SVG icon, title, description, colored severity badge.
|
|
204
222
|
- **Stat delta** — Large number with up/down arrow SVG and percentage change.
|
|
223
|
+
- **Gauge / meter** — SVG arc filled to a percentage. More visual impact than a number alone.
|
|
205
224
|
- **Utilization bars** — Rows with label, horizontal progress bar, percentage.
|
|
206
225
|
- **Checklist** — Items with check/cross SVG icons. Grouped by category.
|
|
207
226
|
|
|
227
|
+
### Comparisons & Layouts
|
|
228
|
+
|
|
229
|
+
- **Split comparison** — Two panels side-by-side (before/after, option A/option B). Highlight differences with color.
|
|
230
|
+
- **Podium / ranking** — Three items in a 2-1-3 podium arrangement (second, first, third). Great for top-3 results.
|
|
231
|
+
- **Concentric rings** — Nested circles or rings showing layers (e.g., onion architecture, security perimeters, impact radius).
|
|
232
|
+
- **Quadrant chart** — 2×2 grid with labeled axes. Place items as dots or cards in quadrants (e.g., effort/impact, urgency/importance).
|
|
233
|
+
- **Radar / spider chart** — SVG polygon on radial axes. Compare multiple dimensions of a single entity or overlay two entities.
|
|
234
|
+
|
|
208
235
|
### Inventing New Primitives
|
|
209
236
|
|
|
210
|
-
The catalog above is a starting point. If the topic calls for a
|
|
237
|
+
The catalog above is a starting point. **The best explainers often use visualizations not on this list.** If the topic calls for something new — a custom game board, an animated state machine, a nested ring diagram, a radial burst — build it. The only constraints are the design tokens and the slide-based interaction model.
|
|
211
238
|
|
|
212
239
|
---
|
|
213
240
|
|
|
@@ -265,6 +292,8 @@ Before writing any code, plan 4-7 slides:
|
|
|
265
292
|
2. **Slides 2-6** = Each focuses on one concept or subset. Zoom in, highlight, or switch viz type.
|
|
266
293
|
3. **Last slide** (optional) = Summary or call-to-action.
|
|
267
294
|
|
|
295
|
+
**Vary the viz type across slides.** If slide 1 uses a diagram, slide 2 should use something different (a chart, timeline, table, etc.). Repeating the same viz structure on every slide makes the explainer feel static and monotonous. Each slide transition should feel like a new lens on the topic.
|
|
296
|
+
|
|
268
297
|
For each slide, define:
|
|
269
298
|
- **label**: Short uppercase label
|
|
270
299
|
- **title**: Heading text
|
|
@@ -389,14 +418,16 @@ Or use the Emberflow MCP/CLI to publish with `content_type: 'explainer'`.
|
|
|
389
418
|
|
|
390
419
|
## H. Reference Templates
|
|
391
420
|
|
|
392
|
-
|
|
421
|
+
Read **one** template before generating to understand the JSON structure, field conventions, and script patterns:
|
|
393
422
|
|
|
394
423
|
```
|
|
395
|
-
Read templates/
|
|
424
|
+
Read templates/diverse-viz-explainer.json — RECOMMENDED: showcases 5 different viz types across slides (funnel, heatmap, radar chart, waterfall, gauges). Start here if you're unsure which template to use.
|
|
425
|
+
|
|
426
|
+
Read templates/architecture-explainer.json — declarative diagram example (nodes + edges, active/dimmed states, staggered entrance).
|
|
396
427
|
|
|
397
|
-
Read templates/project-overview-explainer.json
|
|
428
|
+
Read templates/project-overview-explainer.json — mixed-viz example (KPIs, timeline, budget table, donut ring, risk cards — different viz type per slide).
|
|
398
429
|
|
|
399
|
-
Read templates/dashboard-explainer.json
|
|
430
|
+
Read templates/dashboard-explainer.json — data viz example (animated bar chart, color-coded values, dataset switching, insight callouts).
|
|
400
431
|
```
|
|
401
432
|
|
|
402
|
-
|
|
433
|
+
Use the templates for **structural reference only** — JSON shape, field naming, script scoping, and animation timing. Do NOT copy their visual style. Your explainer should look distinct from every template. Choose visualization types that match your specific content, not the template's content.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"css": ".fn { display: flex; flex-direction: column; gap: 6px; width: 100%; }\n.fn-stage { display: flex; align-items: center; gap: 12px; opacity: 0; transform: translateX(-12px); transition: all 0.5s cubic-bezier(0.4, 0, 0.2, 1); }\n.fn-stage.visible { opacity: 1; transform: translateX(0); }\n.fn-bar { height: 38px; border-radius: 6px; width: 0; transition: width 0.8s cubic-bezier(0.4, 0, 0.2, 1); flex-shrink: 0; }\n.fn-info { min-width: 90px; }\n.fn-name { font-size: 13px; font-weight: 600; color: var(--text); display: block; }\n.fn-sub { font-size: 11px; color: var(--text-muted); }\n.fn-rate { font-size: 12px; font-weight: 600; margin-left: auto; padding: 2px 10px; border-radius: 12px; white-space: nowrap; }\n.fn-drop { font-size: 10px; color: var(--text-muted); text-align: center; padding: 2px 0; opacity: 0; transition: opacity 0.4s; }\n.fn-drop.visible { opacity: 1; }\n\n.hm { width: 100%; }\n.hm-grid { display: flex; flex-direction: column; gap: 3px; }\n.hm-row { display: grid; grid-template-columns: 44px repeat(7, 1fr); gap: 3px; }\n.hm-label { font-size: 10px; color: var(--text-muted); display: flex; align-items: center; }\n.hm-head { font-size: 10px; color: var(--text-muted); text-align: center; font-weight: 600; text-transform: uppercase; }\n.hm-cell { aspect-ratio: 1.6; border-radius: 3px; opacity: 0; transition: opacity 0.3s; cursor: pointer; position: relative; }\n.hm-cell.visible { opacity: 1; }\n.hm-cell:hover { outline: 1px solid var(--text-muted); }\n.hm-cell:hover::after { content: attr(data-val); position: absolute; top: -20px; left: 50%; transform: translateX(-50%); font-size: 9px; color: var(--text); background: var(--bg); padding: 1px 5px; border-radius: 3px; border: 1px solid var(--border); white-space: nowrap; z-index: 1; }\n.hm-scale { display: flex; align-items: center; gap: 6px; margin-top: 8px; justify-content: flex-end; }\n.hm-scale-bar { width: 80px; height: 8px; border-radius: 4px; background: linear-gradient(90deg, rgba(234,88,12,0.05), rgba(234,88,12,0.9)); }\n.hm-scale span { font-size: 9px; color: var(--text-muted); }\n\n.rd { width: 100%; display: flex; flex-direction: column; align-items: center; }\n.rd-chart { position: relative; }\n.rd-axis { stroke: var(--border); stroke-width: 1; }\n.rd-ring { fill: none; stroke: var(--border); stroke-width: 0.5; stroke-dasharray: 4 4; }\n.rd-poly { fill-opacity: 0; stroke-width: 2; stroke-dasharray: 500; stroke-dashoffset: 500; transition: stroke-dashoffset 1.2s cubic-bezier(0.4, 0, 0.2, 1), fill-opacity 0.6s 0.8s; }\n.rd-poly.visible { stroke-dashoffset: 0; fill-opacity: 0.12; }\n.rd-poly.cur { stroke: #ea580c; fill: #ea580c; }\n.rd-poly.prev { stroke: #3b82f6; fill: #3b82f6; }\n.rd-dot { r: 3; fill: var(--bg); stroke-width: 2; opacity: 0; transition: opacity 0.3s 1s; }\n.rd-dot.visible { opacity: 1; }\n.rd-dot.cur { stroke: #ea580c; }\n.rd-dot.prev { stroke: #3b82f6; }\n.rd-lbl { font-size: 10px; fill: var(--text-muted); text-anchor: middle; }\n.rd-legend { display: flex; gap: 20px; justify-content: center; margin-top: 16px; font-size: 12px; color: var(--text-muted); }\n.rd-legend-dot { width: 10px; height: 10px; border-radius: 50%; display: inline-block; margin-right: 6px; vertical-align: middle; }\n\n.wf { width: 100%; }\n.wf-chart { display: flex; align-items: flex-end; gap: 4px; height: 240px; padding-bottom: 24px; position: relative; }\n.wf-chart::before { content: ''; position: absolute; left: 0; right: 0; bottom: 24px; height: 1px; background: var(--border); }\n.wf-col { flex: 1; position: relative; height: 100%; display: flex; flex-direction: column; justify-content: flex-end; align-items: center; }\n.wf-block { width: 65%; border-radius: 3px; position: relative; opacity: 0; transform: scaleY(0); transform-origin: bottom; transition: all 0.5s cubic-bezier(0.4, 0, 0.2, 1); }\n.wf-block.visible { opacity: 1; transform: scaleY(1); }\n.wf-block.add { background: #22c55e; }\n.wf-block.sub { background: #ef4444; transform-origin: top; }\n.wf-block.total { background: var(--link); }\n.wf-val { font-size: 10px; font-weight: 600; color: var(--text); text-align: center; margin-bottom: 4px; opacity: 0; transition: opacity 0.3s 0.4s; white-space: nowrap; }\n.wf-val.visible { opacity: 1; }\n.wf-lbl { font-size: 9px; color: var(--text-muted); text-align: center; white-space: nowrap; position: absolute; bottom: -18px; }\n.wf-connector { position: absolute; right: -4px; width: 8px; border-top: 1px dashed var(--text-muted); opacity: 0.4; }\n\n.gg-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 24px; width: 100%; max-width: 360px; margin: 0 auto; }\n.gg-item { display: flex; flex-direction: column; align-items: center; gap: 6px; opacity: 0; transform: scale(0.9); transition: all 0.5s cubic-bezier(0.4, 0, 0.2, 1); }\n.gg-item.visible { opacity: 1; transform: scale(1); }\n.gg-track { fill: none; stroke: var(--border); stroke-width: 10; stroke-linecap: round; }\n.gg-fill { fill: none; stroke-width: 10; stroke-linecap: round; transition: stroke-dashoffset 1.2s cubic-bezier(0.4, 0, 0.2, 1); }\n.gg-val { font-size: 18px; font-weight: 700; text-anchor: middle; fill: var(--text); }\n.gg-lbl { font-size: 12px; color: var(--text-muted); text-align: center; font-weight: 600; }\n.gg-sub { font-size: 10px; color: var(--text-muted); text-align: center; }",
|
|
3
|
+
"slides": [
|
|
4
|
+
{
|
|
5
|
+
"label": "Funnel",
|
|
6
|
+
"title": "Signup Conversion Funnel",
|
|
7
|
+
"prose": "<p>The signup funnel tracks users from first visit through to paid conversion. Each stage represents a meaningful commitment — from anonymous browsing to entering payment details.</p><p>The <strong>activation step</strong> (completing onboarding) is the biggest single drop-off at 44% loss. Users who activate convert to paid at a strong 36% rate, suggesting the product delivers value once users get past initial setup.</p><p>Overall visitor-to-paid conversion sits at <strong>6.5%</strong>, competitive for a self-serve SaaS product.</p>",
|
|
8
|
+
"viz": "<div class=\"fn\"><div class=\"fn-stage\"><div class=\"fn-bar\" style=\"background: #3b82f6;\" data-w=\"100%\"></div><div class=\"fn-info\"><span class=\"fn-name\">Visitors</span><span class=\"fn-sub\">24,500 unique</span></div><span class=\"fn-rate\" style=\"background: rgba(59,130,246,0.12); color: #3b82f6;\">100%</span></div><div class=\"fn-drop\" data-text=\"\u2193 68% drop-off\"></div><div class=\"fn-stage\"><div class=\"fn-bar\" style=\"background: #8b5cf6;\" data-w=\"32%\"></div><div class=\"fn-info\"><span class=\"fn-name\">Signups</span><span class=\"fn-sub\">7,840 accounts</span></div><span class=\"fn-rate\" style=\"background: rgba(139,92,246,0.12); color: #8b5cf6;\">32%</span></div><div class=\"fn-drop\" data-text=\"\u2193 44% drop-off\"></div><div class=\"fn-stage\"><div class=\"fn-bar\" style=\"background: #ea580c;\" data-w=\"18%\"></div><div class=\"fn-info\"><span class=\"fn-name\">Activated</span><span class=\"fn-sub\">4,410 onboarded</span></div><span class=\"fn-rate\" style=\"background: rgba(234,88,12,0.12); color: #ea580c;\">18%</span></div><div class=\"fn-drop\" data-text=\"\u2193 64% drop-off\"></div><div class=\"fn-stage\"><div class=\"fn-bar\" style=\"background: #22c55e;\" data-w=\"6.5%\"></div><div class=\"fn-info\"><span class=\"fn-name\">Paying</span><span class=\"fn-sub\">1,590 customers</span></div><span class=\"fn-rate\" style=\"background: rgba(34,197,94,0.12); color: #22c55e;\">6.5%</span></div></div>",
|
|
9
|
+
"script": "var stages = container.querySelectorAll('.fn-stage');\nvar drops = container.querySelectorAll('.fn-drop');\nstages.forEach(function(s, i) {\n setTimeout(function() {\n s.classList.add('visible');\n var bar = s.querySelector('.fn-bar');\n bar.style.width = bar.getAttribute('data-w');\n if (i > 0 && drops[i - 1]) {\n drops[i - 1].textContent = drops[i - 1].getAttribute('data-text');\n drops[i - 1].classList.add('visible');\n }\n }, 200 + i * 180);\n});"
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"label": "Activity",
|
|
13
|
+
"title": "Signup Heatmap",
|
|
14
|
+
"prose": "<p>Signups cluster heavily around weekday lunchtimes and early evenings. <strong>Tuesday 12\u2013\u20131pm</strong> is the single hottest slot, likely driven by users discovering the product during work breaks.</p><p>Weekend activity is uniformly low, suggesting a B2B-leaning user base. Late-night signups (10pm+) are near zero across all days.</p><p>This pattern informs ad scheduling — concentrating spend on weekday 11am\u20132pm captures peak intent with minimal waste.</p>",
|
|
15
|
+
"viz": "<div class=\"hm\"><div class=\"hm-grid\"><div class=\"hm-row\"><span></span><span class=\"hm-head\">Mon</span><span class=\"hm-head\">Tue</span><span class=\"hm-head\">Wed</span><span class=\"hm-head\">Thu</span><span class=\"hm-head\">Fri</span><span class=\"hm-head\">Sat</span><span class=\"hm-head\">Sun</span></div><div class=\"hm-row\"><span class=\"hm-label\">6\u2013\u20139am</span><span class=\"hm-cell\" data-val=\"42\" style=\"background:rgba(234,88,12,0.12)\"></span><span class=\"hm-cell\" data-val=\"38\" style=\"background:rgba(234,88,12,0.10)\"></span><span class=\"hm-cell\" data-val=\"45\" style=\"background:rgba(234,88,12,0.13)\"></span><span class=\"hm-cell\" data-val=\"40\" style=\"background:rgba(234,88,12,0.11)\"></span><span class=\"hm-cell\" data-val=\"35\" style=\"background:rgba(234,88,12,0.09)\"></span><span class=\"hm-cell\" data-val=\"12\" style=\"background:rgba(234,88,12,0.03)\"></span><span class=\"hm-cell\" data-val=\"8\" style=\"background:rgba(234,88,12,0.02)\"></span></div><div class=\"hm-row\"><span class=\"hm-label\">9\u2013\u201312pm</span><span class=\"hm-cell\" data-val=\"120\" style=\"background:rgba(234,88,12,0.45)\"></span><span class=\"hm-cell\" data-val=\"135\" style=\"background:rgba(234,88,12,0.52)\"></span><span class=\"hm-cell\" data-val=\"118\" style=\"background:rgba(234,88,12,0.44)\"></span><span class=\"hm-cell\" data-val=\"125\" style=\"background:rgba(234,88,12,0.48)\"></span><span class=\"hm-cell\" data-val=\"110\" style=\"background:rgba(234,88,12,0.40)\"></span><span class=\"hm-cell\" data-val=\"28\" style=\"background:rgba(234,88,12,0.07)\"></span><span class=\"hm-cell\" data-val=\"20\" style=\"background:rgba(234,88,12,0.05)\"></span></div><div class=\"hm-row\"><span class=\"hm-label\">12\u2013\u20133pm</span><span class=\"hm-cell\" data-val=\"180\" style=\"background:rgba(234,88,12,0.72)\"></span><span class=\"hm-cell\" data-val=\"210\" style=\"background:rgba(234,88,12,0.90)\"></span><span class=\"hm-cell\" data-val=\"175\" style=\"background:rgba(234,88,12,0.70)\"></span><span class=\"hm-cell\" data-val=\"190\" style=\"background:rgba(234,88,12,0.78)\"></span><span class=\"hm-cell\" data-val=\"160\" style=\"background:rgba(234,88,12,0.62)\"></span><span class=\"hm-cell\" data-val=\"35\" style=\"background:rgba(234,88,12,0.09)\"></span><span class=\"hm-cell\" data-val=\"22\" style=\"background:rgba(234,88,12,0.06)\"></span></div><div class=\"hm-row\"><span class=\"hm-label\">3\u2013\u20136pm</span><span class=\"hm-cell\" data-val=\"145\" style=\"background:rgba(234,88,12,0.56)\"></span><span class=\"hm-cell\" data-val=\"155\" style=\"background:rgba(234,88,12,0.60)\"></span><span class=\"hm-cell\" data-val=\"140\" style=\"background:rgba(234,88,12,0.54)\"></span><span class=\"hm-cell\" data-val=\"150\" style=\"background:rgba(234,88,12,0.58)\"></span><span class=\"hm-cell\" data-val=\"130\" style=\"background:rgba(234,88,12,0.50)\"></span><span class=\"hm-cell\" data-val=\"30\" style=\"background:rgba(234,88,12,0.08)\"></span><span class=\"hm-cell\" data-val=\"18\" style=\"background:rgba(234,88,12,0.04)\"></span></div><div class=\"hm-row\"><span class=\"hm-label\">6\u2013\u201310pm</span><span class=\"hm-cell\" data-val=\"85\" style=\"background:rgba(234,88,12,0.28)\"></span><span class=\"hm-cell\" data-val=\"92\" style=\"background:rgba(234,88,12,0.32)\"></span><span class=\"hm-cell\" data-val=\"78\" style=\"background:rgba(234,88,12,0.25)\"></span><span class=\"hm-cell\" data-val=\"88\" style=\"background:rgba(234,88,12,0.30)\"></span><span class=\"hm-cell\" data-val=\"95\" style=\"background:rgba(234,88,12,0.34)\"></span><span class=\"hm-cell\" data-val=\"45\" style=\"background:rgba(234,88,12,0.13)\"></span><span class=\"hm-cell\" data-val=\"40\" style=\"background:rgba(234,88,12,0.11)\"></span></div></div><div class=\"hm-scale\"><span>Low</span><div class=\"hm-scale-bar\"></div><span>High</span></div></div>",
|
|
16
|
+
"script": "var cells = container.querySelectorAll('.hm-cell');\ncells.forEach(function(c, i) {\n setTimeout(function() {\n c.classList.add('visible');\n }, 50 + i * 20);\n});"
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"label": "Channels",
|
|
20
|
+
"title": "Channel Quality Radar",
|
|
21
|
+
"prose": "<p>Not all acquisition channels are equal. The radar chart compares six dimensions across Q1 (orange) vs the previous quarter (blue).</p><p><strong>Organic search</strong> improved dramatically in volume and retention, driven by new SEO landing pages. <strong>Paid social</strong> scores high on volume but low on retention — users from Instagram ads churn 3x faster than organic users.</p><p>The ideal channel scores high on all axes. Organic search and referrals are closest to that ideal, while paid channels trade retention for volume.</p>",
|
|
22
|
+
"viz": "<div class=\"rd\"><div class=\"rd-chart\"><svg viewBox=\"0 0 300 300\" width=\"280\" height=\"280\"><g transform=\"translate(150,150)\"><circle class=\"rd-ring\" r=\"36\" /><circle class=\"rd-ring\" r=\"72\" /><circle class=\"rd-ring\" r=\"108\" /><line class=\"rd-axis\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"-115\" /><line class=\"rd-axis\" x1=\"0\" y1=\"0\" x2=\"99.6\" y2=\"-57.5\" /><line class=\"rd-axis\" x1=\"0\" y1=\"0\" x2=\"99.6\" y2=\"57.5\" /><line class=\"rd-axis\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"115\" /><line class=\"rd-axis\" x1=\"0\" y1=\"0\" x2=\"-99.6\" y2=\"57.5\" /><line class=\"rd-axis\" x1=\"0\" y1=\"0\" x2=\"-99.6\" y2=\"-57.5\" /><polygon class=\"rd-poly prev\" points=\"0,-65 69,-44 58,47 0,75 -43,52 -58,-37\" /><polygon class=\"rd-poly cur\" points=\"0,-97 76,-35 52,62 0,54 -86,50 -81,-29\" /><circle class=\"rd-dot prev\" cx=\"0\" cy=\"-65\" /><circle class=\"rd-dot prev\" cx=\"69\" cy=\"-44\" /><circle class=\"rd-dot prev\" cx=\"58\" cy=\"47\" /><circle class=\"rd-dot prev\" cx=\"0\" cy=\"75\" /><circle class=\"rd-dot prev\" cx=\"-43\" cy=\"52\" /><circle class=\"rd-dot prev\" cx=\"-58\" cy=\"-37\" /><circle class=\"rd-dot cur\" cx=\"0\" cy=\"-97\" /><circle class=\"rd-dot cur\" cx=\"76\" cy=\"-35\" /><circle class=\"rd-dot cur\" cx=\"52\" cy=\"62\" /><circle class=\"rd-dot cur\" cx=\"0\" cy=\"54\" /><circle class=\"rd-dot cur\" cx=\"-86\" cy=\"50\" /><circle class=\"rd-dot cur\" cx=\"-81\" cy=\"-29\" /><text class=\"rd-lbl\" x=\"0\" y=\"-122\">Volume</text><text class=\"rd-lbl\" x=\"115\" y=\"-57\">Speed</text><text class=\"rd-lbl\" x=\"115\" y=\"68\">Cost Eff.</text><text class=\"rd-lbl\" x=\"0\" y=\"132\">Retention</text><text class=\"rd-lbl\" x=\"-115\" y=\"68\">Quality</text><text class=\"rd-lbl\" x=\"-115\" y=\"-57\">LTV</text></g></svg></div><div class=\"rd-legend\"><span><span class=\"rd-legend-dot\" style=\"background:#ea580c\"></span>Q1 (current)</span><span><span class=\"rd-legend-dot\" style=\"background:#3b82f6\"></span>Q4 (previous)</span></div></div>",
|
|
23
|
+
"script": "var polys = container.querySelectorAll('.rd-poly');\nvar dots = container.querySelectorAll('.rd-dot');\npolys.forEach(function(p, i) {\n setTimeout(function() { p.classList.add('visible'); }, 200 + i * 300);\n});\ndots.forEach(function(d, i) {\n setTimeout(function() { d.classList.add('visible'); }, 400);\n});"
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"label": "Revenue",
|
|
27
|
+
"title": "Revenue Waterfall",
|
|
28
|
+
"prose": "<p>Revenue grew from <strong>$180k</strong> base to <strong>$200k</strong> net — a healthy 11% increase. The waterfall breaks down each contributing factor.</p><p>New customer acquisition added <strong>$40k</strong>, and upsells from existing accounts contributed another <strong>$15k</strong>. These gains were partially offset by <strong>$25k</strong> in churn and <strong>$10k</strong> in downgrades.</p><p>The churn figure is the primary concern — it erodes 63% of new revenue. Reducing churn by even 20% would shift net growth from 11% to 14%.</p>",
|
|
29
|
+
"viz": "<div class=\"wf\"><div class=\"wf-chart\"><div class=\"wf-col\"><div class=\"wf-val\" data-v=\"$180k\"></div><div class=\"wf-block total\" style=\"height: 76.6%;\" data-h=\"76.6%\"></div><span class=\"wf-lbl\">Base</span></div><div class=\"wf-col\"><div class=\"wf-val\" data-v=\"+$40k\"></div><div class=\"wf-block add\" style=\"height: 17%; margin-bottom: 76.6%;\" data-h=\"17%\"></div><span class=\"wf-lbl\">New</span></div><div class=\"wf-col\"><div class=\"wf-val\" data-v=\"+$15k\"></div><div class=\"wf-block add\" style=\"height: 6.4%; margin-bottom: 93.6%;\" data-h=\"6.4%\"></div><span class=\"wf-lbl\">Upsell</span></div><div class=\"wf-col\"><div class=\"wf-val\" data-v=\"-$25k\"></div><div class=\"wf-block sub\" style=\"height: 10.6%; margin-bottom: 85.1%;\" data-h=\"10.6%\"></div><span class=\"wf-lbl\">Churn</span></div><div class=\"wf-col\"><div class=\"wf-val\" data-v=\"-$10k\"></div><div class=\"wf-block sub\" style=\"height: 4.3%; margin-bottom: 85.1%;\" data-h=\"4.3%\"></div><span class=\"wf-lbl\">Down</span></div><div class=\"wf-col\"><div class=\"wf-val\" data-v=\"$200k\"></div><div class=\"wf-block total\" style=\"height: 85.1%;\" data-h=\"85.1%\"></div><span class=\"wf-lbl\">Net</span></div></div></div>",
|
|
30
|
+
"script": "var blocks = container.querySelectorAll('.wf-block');\nvar vals = container.querySelectorAll('.wf-val');\nblocks.forEach(function(b, i) {\n var h = b.style.height;\n b.style.height = '0';\n setTimeout(function() {\n b.style.height = h;\n b.classList.add('visible');\n }, 150 + i * 120);\n});\nvals.forEach(function(v, i) {\n setTimeout(function() {\n v.textContent = v.getAttribute('data-v');\n v.classList.add('visible');\n }, 300 + i * 120);\n});"
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"label": "Health",
|
|
34
|
+
"title": "System Health Gauges",
|
|
35
|
+
"prose": "<p>Platform health metrics show the infrastructure is holding up well under growth. <strong>Uptime</strong> at 99.7% includes two brief outages totaling 2.2 hours — both from database connection pool exhaustion during traffic spikes.</p><p><strong>Response time</strong> (P95) sits at 210ms, well within the 500ms target. <strong>Error rate</strong> is low at 0.3%, though upload-related 504s account for nearly half of all errors.</p><p><strong>Throughput headroom</strong> at 62% means the system can absorb roughly 1.6x current load before needing horizontal scaling.</p>",
|
|
36
|
+
"viz": "<div class=\"gg-grid\"><div class=\"gg-item\"><svg viewBox=\"0 0 120 75\" width=\"120\" height=\"75\"><path class=\"gg-track\" d=\"M 15 65 A 45 45 0 0 1 105 65\" /><path class=\"gg-fill\" d=\"M 15 65 A 45 45 0 0 1 105 65\" stroke=\"#22c55e\" data-pct=\"99.7\" style=\"stroke-dasharray: 141.4; stroke-dashoffset: 141.4;\" /><text class=\"gg-val\" x=\"60\" y=\"60\" data-v=\"99.7%\"></text></svg><div class=\"gg-lbl\">Uptime</div><div class=\"gg-sub\">Target: 99.9%</div></div><div class=\"gg-item\"><svg viewBox=\"0 0 120 75\" width=\"120\" height=\"75\"><path class=\"gg-track\" d=\"M 15 65 A 45 45 0 0 1 105 65\" /><path class=\"gg-fill\" d=\"M 15 65 A 45 45 0 0 1 105 65\" stroke=\"#22c55e\" data-pct=\"92\" style=\"stroke-dasharray: 141.4; stroke-dashoffset: 141.4;\" /><text class=\"gg-val\" x=\"60\" y=\"60\" data-v=\"210ms\"></text></svg><div class=\"gg-lbl\">P95 Latency</div><div class=\"gg-sub\">Target: <500ms</div></div><div class=\"gg-item\"><svg viewBox=\"0 0 120 75\" width=\"120\" height=\"75\"><path class=\"gg-track\" d=\"M 15 65 A 45 45 0 0 1 105 65\" /><path class=\"gg-fill\" d=\"M 15 65 A 45 45 0 0 1 105 65\" stroke=\"#22c55e\" data-pct=\"97\" style=\"stroke-dasharray: 141.4; stroke-dashoffset: 141.4;\" /><text class=\"gg-val\" x=\"60\" y=\"60\" data-v=\"0.3%\"></text></svg><div class=\"gg-lbl\">Error Rate</div><div class=\"gg-sub\">Target: <1%</div></div><div class=\"gg-item\"><svg viewBox=\"0 0 120 75\" width=\"120\" height=\"75\"><path class=\"gg-track\" d=\"M 15 65 A 45 45 0 0 1 105 65\" /><path class=\"gg-fill\" d=\"M 15 65 A 45 45 0 0 1 105 65\" stroke=\"#ea580c\" data-pct=\"62\" style=\"stroke-dasharray: 141.4; stroke-dashoffset: 141.4;\" /><text class=\"gg-val\" x=\"60\" y=\"60\" data-v=\"62%\"></text></svg><div class=\"gg-lbl\">Headroom</div><div class=\"gg-sub\">Scale at <30%</div></div></div>",
|
|
37
|
+
"script": "var items = container.querySelectorAll('.gg-item');\nitems.forEach(function(item, i) {\n setTimeout(function() {\n item.classList.add('visible');\n var fill = item.querySelector('.gg-fill');\n var pct = parseFloat(fill.getAttribute('data-pct')) / 100;\n var totalLen = 141.4;\n fill.style.strokeDashoffset = totalLen * (1 - pct);\n var valEl = item.querySelector('.gg-val');\n valEl.textContent = valEl.getAttribute('data-v');\n }, 200 + i * 150);\n});"
|
|
38
|
+
}
|
|
39
|
+
]
|
|
40
|
+
}
|