create-slide-deck 0.1.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/dist/index.js +119 -0
- package/package.json +36 -0
- package/template-full/README.md +99 -0
- package/template-full/package.json +47 -0
- package/template-full/src/reveal/components/auto-layout.ts +229 -0
- package/template-full/src/reveal/components/charts.tsx +213 -0
- package/template-full/src/reveal/core/blocks.ts +172 -0
- package/template-full/src/reveal/core/deck-init.ts +60 -0
- package/template-full/src/reveal/core/design.ts +46 -0
- package/template-full/src/reveal/core/layout.ts +187 -0
- package/template-full/src/reveal/core/mount-registry.ts +41 -0
- package/template-full/src/reveal/core/presets.ts +189 -0
- package/template-full/src/reveal/core/runtime.ts +141 -0
- package/template-full/src/reveal/core/types.ts +114 -0
- package/template-full/src/reveal/data/algorithms.ts +78 -0
- package/template-full/src/reveal/data/benchmark.ts +79 -0
- package/template-full/src/reveal/decks/demo-showcase/components/demo-arc-progress.tsx +153 -0
- package/template-full/src/reveal/decks/demo-showcase/components/demo-before-after.tsx +164 -0
- package/template-full/src/reveal/decks/demo-showcase/components/demo-bigtext.tsx +70 -0
- package/template-full/src/reveal/decks/demo-showcase/components/demo-card-flip.tsx +118 -0
- package/template-full/src/reveal/decks/demo-showcase/components/demo-chat-bubbles.tsx +257 -0
- package/template-full/src/reveal/decks/demo-showcase/components/demo-code.tsx +136 -0
- package/template-full/src/reveal/decks/demo-showcase/components/demo-concept-map.tsx +336 -0
- package/template-full/src/reveal/decks/demo-showcase/components/demo-counter.tsx +194 -0
- package/template-full/src/reveal/decks/demo-showcase/components/demo-cover.tsx +188 -0
- package/template-full/src/reveal/decks/demo-showcase/components/demo-dark-dashboard.tsx +166 -0
- package/template-full/src/reveal/decks/demo-showcase/components/demo-eval-matrix.tsx +191 -0
- package/template-full/src/reveal/decks/demo-showcase/components/demo-force-graph.tsx +169 -0
- package/template-full/src/reveal/decks/demo-showcase/components/demo-fullbleed-bars.tsx +109 -0
- package/template-full/src/reveal/decks/demo-showcase/components/demo-fullbleed-flow.tsx +177 -0
- package/template-full/src/reveal/decks/demo-showcase/components/demo-heatmap.tsx +135 -0
- package/template-full/src/reveal/decks/demo-showcase/components/demo-icon-wall.tsx +143 -0
- package/template-full/src/reveal/decks/demo-showcase/components/demo-math.tsx +103 -0
- package/template-full/src/reveal/decks/demo-showcase/components/demo-number-morph.tsx +126 -0
- package/template-full/src/reveal/decks/demo-showcase/components/demo-path.tsx +185 -0
- package/template-full/src/reveal/decks/demo-showcase/components/demo-radar.tsx +124 -0
- package/template-full/src/reveal/decks/demo-showcase/components/demo-rough.tsx +169 -0
- package/template-full/src/reveal/decks/demo-showcase/components/demo-sankey.tsx +144 -0
- package/template-full/src/reveal/decks/demo-showcase/components/demo-screenshot-annotate.tsx +181 -0
- package/template-full/src/reveal/decks/demo-showcase/components/demo-stacked-cards.tsx +159 -0
- package/template-full/src/reveal/decks/demo-showcase/components/demo-tabs.tsx +206 -0
- package/template-full/src/reveal/decks/demo-showcase/components/demo-timeline.tsx +162 -0
- package/template-full/src/reveal/decks/demo-showcase/components/demo-treemap.tsx +161 -0
- package/template-full/src/reveal/decks/demo-showcase/components/demo-zoom-focus.tsx +223 -0
- package/template-full/src/reveal/decks/demo-showcase/components/registry.ts +63 -0
- package/template-full/src/reveal/decks/demo-showcase/demo.css +237 -0
- package/template-full/src/reveal/decks/demo-showcase/index.html +24 -0
- package/template-full/src/reveal/decks/demo-showcase/main.ts +7 -0
- package/template-full/src/reveal/decks/demo-showcase/slides.ts +271 -0
- package/template-full/src/reveal/decks/fse26-rca/components/aws-cascade.tsx +295 -0
- package/template-full/src/reveal/decks/fse26-rca/components/bench-compare.tsx +64 -0
- package/template-full/src/reveal/decks/fse26-rca/components/bench-deficiency.tsx +104 -0
- package/template-full/src/reveal/decks/fse26-rca/components/bench-loop.tsx +402 -0
- package/template-full/src/reveal/decks/fse26-rca/components/bench-needs.tsx +78 -0
- package/template-full/src/reveal/decks/fse26-rca/components/closing-takeaway.tsx +165 -0
- package/template-full/src/reveal/decks/fse26-rca/components/cloud-incidents.tsx +88 -0
- package/template-full/src/reveal/decks/fse26-rca/components/failure-modes.tsx +59 -0
- package/template-full/src/reveal/decks/fse26-rca/components/fault-heatmap.tsx +85 -0
- package/template-full/src/reveal/decks/fse26-rca/components/hierarchy-tree.tsx +93 -0
- package/template-full/src/reveal/decks/fse26-rca/components/incident-hard.tsx +72 -0
- package/template-full/src/reveal/decks/fse26-rca/components/rca-pipeline.tsx +193 -0
- package/template-full/src/reveal/decks/fse26-rca/components/registry.ts +37 -0
- package/template-full/src/reveal/decks/fse26-rca/components/simple-rca.tsx +216 -0
- package/template-full/src/reveal/decks/fse26-rca/components/sota-collapse.tsx +63 -0
- package/template-full/src/reveal/decks/fse26-rca/components/srca-results.tsx +115 -0
- package/template-full/src/reveal/decks/fse26-rca/images/aws-outage-2025-deployflow.png +0 -0
- package/template-full/src/reveal/decks/fse26-rca/images/aws-post-event-summary.png +0 -0
- package/template-full/src/reveal/decks/fse26-rca/images/bbc-crowdstrike.png +0 -0
- package/template-full/src/reveal/decks/fse26-rca/images/cnn-meta-outage-2021.png +0 -0
- package/template-full/src/reveal/decks/fse26-rca/images/cover.png +0 -0
- package/template-full/src/reveal/decks/fse26-rca/images/nyt-facebook-2021.png +0 -0
- package/template-full/src/reveal/decks/fse26-rca/images/qr-repo.png +0 -0
- package/template-full/src/reveal/decks/fse26-rca/images/verge-crowdstrike-2024.png +0 -0
- package/template-full/src/reveal/decks/fse26-rca/images/wiki-meta-outage-2021.png +0 -0
- package/template-full/src/reveal/decks/fse26-rca/index.html +30 -0
- package/template-full/src/reveal/decks/fse26-rca/main.ts +8 -0
- package/template-full/src/reveal/decks/fse26-rca/slides.ts +175 -0
- package/template-full/src/reveal/env.d.ts +38 -0
- package/template-full/src/reveal/theme.css +762 -0
- package/template-full/src/reveal/tools/dev.mjs +120 -0
- package/template-full/src/reveal/tools/export-pdf.mjs +86 -0
- package/template-full/src/reveal/tools/preview.mjs +132 -0
- package/template-full/tsconfig.json +19 -0
- package/template-full/vite.config.ts +95 -0
- package/template-minimal/package.json +42 -0
- package/template-minimal/src/reveal/components/auto-layout.ts +229 -0
- package/template-minimal/src/reveal/components/charts.tsx +213 -0
- package/template-minimal/src/reveal/core/blocks.ts +172 -0
- package/template-minimal/src/reveal/core/deck-init.ts +60 -0
- package/template-minimal/src/reveal/core/design.ts +46 -0
- package/template-minimal/src/reveal/core/layout.ts +187 -0
- package/template-minimal/src/reveal/core/mount-registry.ts +41 -0
- package/template-minimal/src/reveal/core/presets.ts +189 -0
- package/template-minimal/src/reveal/core/runtime.ts +141 -0
- package/template-minimal/src/reveal/core/types.ts +114 -0
- package/template-minimal/src/reveal/data/.gitkeep +0 -0
- package/template-minimal/src/reveal/decks/my-deck/components/example-component.tsx +28 -0
- package/template-minimal/src/reveal/decks/my-deck/components/registry.ts +9 -0
- package/template-minimal/src/reveal/decks/my-deck/index.html +14 -0
- package/template-minimal/src/reveal/decks/my-deck/main.ts +5 -0
- package/template-minimal/src/reveal/decks/my-deck/slides.ts +34 -0
- package/template-minimal/src/reveal/env.d.ts +38 -0
- package/template-minimal/src/reveal/theme.css +762 -0
- package/template-minimal/tsconfig.json +19 -0
- package/template-minimal/vite.config.ts +95 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import readline from "node:readline";
|
|
6
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
function ask(question) {
|
|
8
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
9
|
+
return new Promise((resolve) => {
|
|
10
|
+
rl.question(question, (answer) => {
|
|
11
|
+
rl.close();
|
|
12
|
+
resolve(answer.trim());
|
|
13
|
+
});
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
function copyDir(src, dest) {
|
|
17
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
18
|
+
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
19
|
+
const srcPath = path.join(src, entry.name);
|
|
20
|
+
const destPath = path.join(dest, entry.name);
|
|
21
|
+
if (entry.isDirectory()) {
|
|
22
|
+
copyDir(srcPath, destPath);
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
fs.copyFileSync(srcPath, destPath);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function replaceInFile(filePath, replacements) {
|
|
30
|
+
if (!fs.existsSync(filePath))
|
|
31
|
+
return;
|
|
32
|
+
let content = fs.readFileSync(filePath, "utf-8");
|
|
33
|
+
for (const [search, replace] of Object.entries(replacements)) {
|
|
34
|
+
content = content.replaceAll(search, replace);
|
|
35
|
+
}
|
|
36
|
+
fs.writeFileSync(filePath, content);
|
|
37
|
+
}
|
|
38
|
+
function* walkFiles(dir) {
|
|
39
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
40
|
+
const full = path.join(dir, entry.name);
|
|
41
|
+
if (entry.isDirectory()) {
|
|
42
|
+
yield* walkFiles(full);
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
yield full;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
async function main() {
|
|
50
|
+
console.log();
|
|
51
|
+
console.log(" create-slide-deck");
|
|
52
|
+
console.log(" Scaffold a Reveal.js + React + TypeScript slide deck");
|
|
53
|
+
console.log();
|
|
54
|
+
let projectName = process.argv[2];
|
|
55
|
+
if (!projectName) {
|
|
56
|
+
projectName = await ask(" Project name: ");
|
|
57
|
+
}
|
|
58
|
+
if (!projectName) {
|
|
59
|
+
console.error(" Error: project name is required.");
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
const targetDir = path.resolve(process.cwd(), projectName);
|
|
63
|
+
if (fs.existsSync(targetDir) && fs.readdirSync(targetDir).length > 0) {
|
|
64
|
+
console.error(` Error: directory "${projectName}" already exists and is not empty.`);
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
console.log();
|
|
68
|
+
console.log(" Templates:");
|
|
69
|
+
console.log(" 1) minimal — empty deck with one cover slide");
|
|
70
|
+
console.log(" 2) full — includes 28-page demo showcase for reference");
|
|
71
|
+
console.log();
|
|
72
|
+
let choice = await ask(" Choose template (1/2) [1]: ");
|
|
73
|
+
choice = choice || "1";
|
|
74
|
+
const templateName = choice === "2" ? "template-full" : "template-minimal";
|
|
75
|
+
// Templates are siblings of the compiled dist/ output
|
|
76
|
+
const templateDir = path.join(__dirname, "..", templateName);
|
|
77
|
+
if (!fs.existsSync(templateDir)) {
|
|
78
|
+
console.error(` Error: template "${templateName}" not found.`);
|
|
79
|
+
process.exit(1);
|
|
80
|
+
}
|
|
81
|
+
const deckName = projectName
|
|
82
|
+
.replace(/[^a-zA-Z0-9-_]/g, "-")
|
|
83
|
+
.replace(/-+/g, "-")
|
|
84
|
+
.replace(/^-|-$/g, "")
|
|
85
|
+
.toLowerCase();
|
|
86
|
+
console.log();
|
|
87
|
+
console.log(` Scaffolding project in ${targetDir}...`);
|
|
88
|
+
copyDir(templateDir, targetDir);
|
|
89
|
+
const replacements = {
|
|
90
|
+
"{{PROJECT_NAME}}": projectName,
|
|
91
|
+
"{{DECK_NAME}}": deckName,
|
|
92
|
+
};
|
|
93
|
+
replaceInFile(path.join(targetDir, "package.json"), replacements);
|
|
94
|
+
replaceInFile(path.join(targetDir, "vite.config.ts"), replacements);
|
|
95
|
+
const placeholderDeck = path.join(targetDir, "src", "reveal", "decks", "my-deck");
|
|
96
|
+
const actualDeck = path.join(targetDir, "src", "reveal", "decks", deckName);
|
|
97
|
+
if (fs.existsSync(placeholderDeck) && placeholderDeck !== actualDeck) {
|
|
98
|
+
fs.renameSync(placeholderDeck, actualDeck);
|
|
99
|
+
}
|
|
100
|
+
const deckDir = fs.existsSync(actualDeck) ? actualDeck : placeholderDeck;
|
|
101
|
+
if (fs.existsSync(deckDir)) {
|
|
102
|
+
for (const file of walkFiles(deckDir)) {
|
|
103
|
+
replaceInFile(file, replacements);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
console.log();
|
|
107
|
+
console.log(" Done! Next steps:");
|
|
108
|
+
console.log();
|
|
109
|
+
console.log(` cd ${projectName}`);
|
|
110
|
+
console.log(" npm install");
|
|
111
|
+
console.log(" npm run dev");
|
|
112
|
+
console.log();
|
|
113
|
+
console.log(` Open http://localhost:8766/${deckName}/ to see your deck.`);
|
|
114
|
+
console.log();
|
|
115
|
+
}
|
|
116
|
+
main().catch((err) => {
|
|
117
|
+
console.error(err);
|
|
118
|
+
process.exit(1);
|
|
119
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-slide-deck",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Scaffold a Reveal.js slide deck with React components and TypeScript",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"create-slide-deck": "dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "tsc",
|
|
11
|
+
"prepublishOnly": "npm run build"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist",
|
|
15
|
+
"template-minimal",
|
|
16
|
+
"template-full"
|
|
17
|
+
],
|
|
18
|
+
"keywords": [
|
|
19
|
+
"create",
|
|
20
|
+
"scaffold",
|
|
21
|
+
"reveal.js",
|
|
22
|
+
"slides",
|
|
23
|
+
"presentation",
|
|
24
|
+
"react",
|
|
25
|
+
"typescript"
|
|
26
|
+
],
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "https://github.com/Lincyaw/slide-deck"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@types/node": "^22.20.0",
|
|
34
|
+
"typescript": "^5.9.3"
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# Slide Template Workspace
|
|
2
|
+
|
|
3
|
+
这个仓库现在采用 reveal.js-first 的 slide-as-code 工作流:slide 源码放在 `src/reveal/`,共享素材放在 `assets/`,浏览器/PDF 输出放在 `examples/`。
|
|
4
|
+
|
|
5
|
+
## Quick Map
|
|
6
|
+
|
|
7
|
+
| Path | Role |
|
|
8
|
+
| --- | --- |
|
|
9
|
+
| `SKILL.md` | 模板的主要视觉规范、版式规则和执行约束。 |
|
|
10
|
+
| `docs/ppt-implementation-spec.md` | Reveal.js-first 的实现规范:Block、Layer、动画、UI/图表库接入方式。 |
|
|
11
|
+
| `src/reveal/` | reveal.js HTML/PDF 输出链路,承载 HTML、SVG、D3 和 JS 动画 slide。 |
|
|
12
|
+
| `src/reveal/decks/` | 每套 deck 的源码、讲稿 notes、节点位置和 reveal 顺序。 |
|
|
13
|
+
| `src/reveal/shared/` | 共享 slide primitives:Block 渲染、设计 token、布局、图标、连接线、图表。 |
|
|
14
|
+
| `examples/microservice-rca/fse26-paper-talk/reveal-summary/` | 当前 FSE26 RCA paper talk 的可浏览 HTML 输出。 |
|
|
15
|
+
| `assets/icons/` | 全项目共享的软件工程图标库,构建时会内联到 slide。 |
|
|
16
|
+
| `assets/` | 共享素材根目录;deck 专属素材优先放在对应 deck 目录下。 |
|
|
17
|
+
|
|
18
|
+
## Build Commands
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm run build:reveal:fse26-rca
|
|
22
|
+
npm run pdf:reveal:fse26-rca
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
`src/reveal/` 先生成 HTML,再通过 Playwright/Chrome 打印成 PDF。当前 FSE26 deck 是原生 reveal.js HTML,页面由 HTML、CSS、内联 SVG 图标、SVG 图表、D3 连接线和 animejs fragment 动画组成。实现上采用通用 Block 模型,通过 mount block + MountRegistry 可接入 React、ECharts、Cytoscape、Web Components 或任意 JS 库。
|
|
26
|
+
|
|
27
|
+
## Mount System
|
|
28
|
+
|
|
29
|
+
Mount block 允许在 slide 中嵌入需要 JS 初始化的交互式组件。运行时由 `MountRegistry` 统一管理生命周期。
|
|
30
|
+
|
|
31
|
+
### 使用方式
|
|
32
|
+
|
|
33
|
+
在 slide 定义中使用 `mountBlock`:
|
|
34
|
+
|
|
35
|
+
```js
|
|
36
|
+
import { mountBlock } from "../../shared/index.mjs";
|
|
37
|
+
|
|
38
|
+
// React 组件
|
|
39
|
+
mountBlock({
|
|
40
|
+
mount: { library: "react", exportName: "MyChart", props: { data: [1, 2, 3] } },
|
|
41
|
+
x: 100, y: 200, w: 500, h: 300,
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
// animejs 自定义动画
|
|
45
|
+
mountBlock({
|
|
46
|
+
mount: { library: "anim", props: { translateX: [0, 200], duration: 800, ease: "outQuad" } },
|
|
47
|
+
html: '<div class="animated-box">Hello</div>',
|
|
48
|
+
x: 100, y: 200, w: 300, h: 200,
|
|
49
|
+
})
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
在 build 脚本中通过 `scripts` 传入 mount adapter 和组件脚本:
|
|
53
|
+
|
|
54
|
+
```js
|
|
55
|
+
import { writeRevealDeck, mountAdapterPaths } from "./reveal-html.mjs";
|
|
56
|
+
|
|
57
|
+
await writeRevealDeck({
|
|
58
|
+
rootDir,
|
|
59
|
+
outPath,
|
|
60
|
+
title: "My Deck",
|
|
61
|
+
slides,
|
|
62
|
+
scripts: [
|
|
63
|
+
// React (从 CDN 或 node_modules 加载)
|
|
64
|
+
"https://unpkg.com/react@18/umd/react.production.min.js",
|
|
65
|
+
"https://unpkg.com/react-dom@18/umd/react-dom.production.min.js",
|
|
66
|
+
// Mount adapters
|
|
67
|
+
...mountAdapterPaths(rootDir, ["react", "anim"]).map(p => rel(outDir, p)),
|
|
68
|
+
// 自定义组件(注册到 window.SlideComponents)
|
|
69
|
+
"components.js",
|
|
70
|
+
],
|
|
71
|
+
});
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### 自定义 Mount Adapter
|
|
75
|
+
|
|
76
|
+
```js
|
|
77
|
+
// my-adapter.js (browser-side)
|
|
78
|
+
MountRegistry.register("echarts", function (el, ctx) {
|
|
79
|
+
var chart = echarts.init(el);
|
|
80
|
+
chart.setOption(ctx.props);
|
|
81
|
+
return function () { chart.dispose(); };
|
|
82
|
+
});
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### 内置 Adapter
|
|
86
|
+
|
|
87
|
+
| Adapter | 文件 | 用途 |
|
|
88
|
+
| --- | --- | --- |
|
|
89
|
+
| `react` | `src/reveal/mounts/react-mount.js` | 渲染 `window.SlideComponents[exportName]` 为 React 组件 |
|
|
90
|
+
| `anim` | `src/reveal/mounts/anim-mount.js` | 对 block 内元素执行 animejs 动画 |
|
|
91
|
+
|
|
92
|
+
## Conventions
|
|
93
|
+
|
|
94
|
+
- Slide 页面内可见文字保持英文。
|
|
95
|
+
- 新增模板规则优先写入 `SKILL.md`。
|
|
96
|
+
- 新增 slide 内容必须优先遵守 `SKILL.md` 的 icon-led progressive storytelling:减少方块化文字模块,避免纯文字页,把长段说明转成图标、证据对象、连接线和可逐步 reveal 的对象组。
|
|
97
|
+
- 新增 deck 源码放到 `src/reveal/decks/<deck-id>/`,可浏览输出放到对应 `examples/<deck-name>/`。
|
|
98
|
+
- 图标优先从 `assets/icons/lucide_core/` 选择,不够时再用 `tabler_extended/` 或 `simpleicons_tech/`。
|
|
99
|
+
- 共享层只放通用 Block、布局、动画和渲染适配;业务语义组件放在具体 deck 目录。
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{PROJECT_NAME}}",
|
|
3
|
+
"private": true,
|
|
4
|
+
"type": "module",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"dev": "vite",
|
|
7
|
+
"build": "vite build",
|
|
8
|
+
"preview": "vite preview",
|
|
9
|
+
"pdf": "node src/reveal/tools/export-pdf.mjs",
|
|
10
|
+
"typecheck": "tsc --noEmit"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@xyflow/react": "^12.11.0",
|
|
14
|
+
"animejs": "^4.4.1",
|
|
15
|
+
"d3-force": "^3.0.0",
|
|
16
|
+
"d3-hierarchy": "^3.1.2",
|
|
17
|
+
"d3-interpolate": "^3.0.1",
|
|
18
|
+
"d3-sankey": "^0.12.3",
|
|
19
|
+
"d3-scale": "^4.0.2",
|
|
20
|
+
"d3-shape": "^3.2.0",
|
|
21
|
+
"framer-motion": "^12.40.0",
|
|
22
|
+
"katex": "^0.17.0",
|
|
23
|
+
"lucide-react": "^1.21.0",
|
|
24
|
+
"pdf-lib": "^1.17.1",
|
|
25
|
+
"playwright": "^1.61.0",
|
|
26
|
+
"prismjs": "^1.30.0",
|
|
27
|
+
"react": "^19.2.7",
|
|
28
|
+
"react-dom": "^19.2.7",
|
|
29
|
+
"reveal.js": "^6.0.1",
|
|
30
|
+
"roughjs": "^4.6.6"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@types/d3-force": "^3.0.10",
|
|
34
|
+
"@types/d3-hierarchy": "^3.1.7",
|
|
35
|
+
"@types/d3-interpolate": "^3.0.4",
|
|
36
|
+
"@types/d3-sankey": "^0.12.5",
|
|
37
|
+
"@types/d3-scale": "^4.0.9",
|
|
38
|
+
"@types/d3-shape": "^3.1.8",
|
|
39
|
+
"@types/node": "^26.0.0",
|
|
40
|
+
"@types/react": "^19.2.17",
|
|
41
|
+
"@types/react-dom": "^19.2.3",
|
|
42
|
+
"@vitejs/plugin-react": "^6.0.2",
|
|
43
|
+
"chokidar": "^5.0.0",
|
|
44
|
+
"typescript": "^6.0.3",
|
|
45
|
+
"vite": "^8.0.16"
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic empty-slot finder for slide annotations.
|
|
3
|
+
*
|
|
4
|
+
* Given a canvas size, a list of occupied rectangles, and a desired slot size,
|
|
5
|
+
* finds the best position that:
|
|
6
|
+
* 1. Does not overlap any occupied rect (with margin)
|
|
7
|
+
* 2. Maximizes distance from occupied rects (use empty space well)
|
|
8
|
+
* 3. Optionally stays close to a "near" point (e.g. the active node)
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* const pos = findEmptySlot({
|
|
12
|
+
* canvasW: 1200, canvasH: 700,
|
|
13
|
+
* occupied: [ {x:10, y:10, w:200, h:150}, ... ],
|
|
14
|
+
* slotW: 500, slotH: 160,
|
|
15
|
+
* margin: 16,
|
|
16
|
+
* near: { x: 300, y: 100 }, // optional: prefer positions near this point
|
|
17
|
+
* });
|
|
18
|
+
* // pos = { x, y }
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
export interface Rect {
|
|
22
|
+
x: number;
|
|
23
|
+
y: number;
|
|
24
|
+
w: number;
|
|
25
|
+
h: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface Point {
|
|
29
|
+
x: number;
|
|
30
|
+
y: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface FindEmptySlotOpts {
|
|
34
|
+
canvasW: number;
|
|
35
|
+
canvasH: number;
|
|
36
|
+
occupied: Rect[];
|
|
37
|
+
slotW: number;
|
|
38
|
+
slotH: number;
|
|
39
|
+
margin?: number;
|
|
40
|
+
near?: Point | null;
|
|
41
|
+
step?: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface FindLargestSlotOpts {
|
|
45
|
+
canvasW: number;
|
|
46
|
+
canvasH: number;
|
|
47
|
+
occupied: Rect[];
|
|
48
|
+
maxW?: number;
|
|
49
|
+
maxH?: number;
|
|
50
|
+
margin?: number;
|
|
51
|
+
step?: number;
|
|
52
|
+
near?: Point | null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
interface CascadeNode {
|
|
56
|
+
step: number;
|
|
57
|
+
id: string;
|
|
58
|
+
col: number;
|
|
59
|
+
row: number;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface NodeOccupiedRectsOpts {
|
|
63
|
+
nx: (col: number) => number;
|
|
64
|
+
ny: (row: number) => number;
|
|
65
|
+
iconR: number;
|
|
66
|
+
labelH?: number;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function rectsOverlap(a: Rect, b: Rect, gap: number): boolean {
|
|
70
|
+
return !(a.x + a.w + gap <= b.x || b.x + b.w + gap <= a.x ||
|
|
71
|
+
a.y + a.h + gap <= b.y || b.y + b.h + gap <= a.y);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function rectMinDist(slot: Rect, rect: Rect): number {
|
|
75
|
+
const dx = Math.max(rect.x - (slot.x + slot.w), slot.x - (rect.x + rect.w), 0);
|
|
76
|
+
const dy = Math.max(rect.y - (slot.y + slot.h), slot.y - (rect.y + rect.h), 0);
|
|
77
|
+
return Math.sqrt(dx * dx + dy * dy);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function findEmptySlot({
|
|
81
|
+
canvasW,
|
|
82
|
+
canvasH,
|
|
83
|
+
occupied,
|
|
84
|
+
slotW,
|
|
85
|
+
slotH,
|
|
86
|
+
margin = 16,
|
|
87
|
+
near = null,
|
|
88
|
+
step = 16,
|
|
89
|
+
}: FindEmptySlotOpts): Point {
|
|
90
|
+
let bestPos: Point = { x: margin, y: canvasH - slotH - margin };
|
|
91
|
+
let bestScore = -Infinity;
|
|
92
|
+
|
|
93
|
+
for (let y = margin; y + slotH <= canvasH - margin; y += step) {
|
|
94
|
+
for (let x = margin; x + slotW <= canvasW - margin; x += step) {
|
|
95
|
+
const slot: Rect = { x: x, y: y, w: slotW, h: slotH };
|
|
96
|
+
|
|
97
|
+
let overlaps = false;
|
|
98
|
+
for (let i = 0; i < occupied.length; i++) {
|
|
99
|
+
if (rectsOverlap(slot, occupied[i], margin)) {
|
|
100
|
+
overlaps = true;
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (overlaps) continue;
|
|
105
|
+
|
|
106
|
+
let minDist = Infinity;
|
|
107
|
+
for (let j = 0; j < occupied.length; j++) {
|
|
108
|
+
const d = rectMinDist(slot, occupied[j]);
|
|
109
|
+
if (d < minDist) minDist = d;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Small bonus for not being jammed against a node, but cap it low
|
|
113
|
+
let score = Math.min(minDist, 30) * 0.5;
|
|
114
|
+
|
|
115
|
+
// Primary: stay as close as possible to the active node
|
|
116
|
+
if (near) {
|
|
117
|
+
const dx = (x + slotW / 2) - near.x;
|
|
118
|
+
const dy = (y + slotH / 2) - near.y;
|
|
119
|
+
score -= Math.sqrt(dx * dx + dy * dy) * 0.8;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Slight preference for horizontally centered positions
|
|
123
|
+
const cxDist = Math.abs(x + slotW / 2 - canvasW / 2);
|
|
124
|
+
score -= cxDist * 0.02;
|
|
125
|
+
|
|
126
|
+
if (score > bestScore) {
|
|
127
|
+
bestScore = score;
|
|
128
|
+
bestPos = { x: x, y: y };
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return bestPos;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Find the largest empty rectangle in the canvas that doesn't overlap any occupied rect.
|
|
138
|
+
* Returns { x, y, w, h }. If maxW/maxH are provided, the result is capped.
|
|
139
|
+
*/
|
|
140
|
+
export function findLargestSlot({
|
|
141
|
+
canvasW, canvasH, occupied,
|
|
142
|
+
maxW, maxH,
|
|
143
|
+
margin = 14,
|
|
144
|
+
step = 18,
|
|
145
|
+
near = null,
|
|
146
|
+
}: FindLargestSlotOpts): Rect {
|
|
147
|
+
maxW = maxW || (canvasW - 2 * margin);
|
|
148
|
+
maxH = maxH || (canvasH - 2 * margin);
|
|
149
|
+
|
|
150
|
+
let best: Rect | null = null;
|
|
151
|
+
let bestScore = -Infinity;
|
|
152
|
+
|
|
153
|
+
for (let y = margin; y < canvasH - margin - 60; y += step) {
|
|
154
|
+
for (let x = margin; x < canvasW - margin - 60; x += step) {
|
|
155
|
+
let inside = false;
|
|
156
|
+
for (let i = 0; i < occupied.length; i++) {
|
|
157
|
+
const r = occupied[i];
|
|
158
|
+
if (x >= r.x - margin && x < r.x + r.w + margin &&
|
|
159
|
+
y >= r.y - margin && y < r.y + r.h + margin) {
|
|
160
|
+
inside = true; break;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
if (inside) continue;
|
|
164
|
+
|
|
165
|
+
let w = Math.min(canvasW - margin - x, maxW);
|
|
166
|
+
for (let i = 0; i < occupied.length; i++) {
|
|
167
|
+
const r = occupied[i];
|
|
168
|
+
const rl = r.x - margin;
|
|
169
|
+
if (rl > x && rl < x + w &&
|
|
170
|
+
y < r.y + r.h + margin && y + maxH > r.y - margin) {
|
|
171
|
+
w = Math.min(w, rl - x);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
let h = Math.min(canvasH - margin - y, maxH);
|
|
176
|
+
for (let i = 0; i < occupied.length; i++) {
|
|
177
|
+
const r = occupied[i];
|
|
178
|
+
const rt = r.y - margin;
|
|
179
|
+
if (rt > y && rt < y + h &&
|
|
180
|
+
x < r.x + r.w + margin && x + w > r.x - margin) {
|
|
181
|
+
h = Math.min(h, rt - y);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (w < 60 || h < 60) continue;
|
|
186
|
+
|
|
187
|
+
const area = w * h;
|
|
188
|
+
let score = area;
|
|
189
|
+
if (near) {
|
|
190
|
+
const dx = (x + w / 2) - near.x;
|
|
191
|
+
const dy = (y + h / 2) - near.y;
|
|
192
|
+
score -= Math.sqrt(dx * dx + dy * dy) * 50;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (score > bestScore) {
|
|
196
|
+
bestScore = score;
|
|
197
|
+
best = { x: x, y: y, w: w, h: h };
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return best || { x: margin, y: margin, w: maxW, h: maxH };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Build occupied rectangles from cascade nodes that are visible at a given step.
|
|
207
|
+
* Each node becomes a bounding rect covering its icon circle + label area.
|
|
208
|
+
*/
|
|
209
|
+
export function nodeOccupiedRects(
|
|
210
|
+
nodes: CascadeNode[],
|
|
211
|
+
visibleStep: number,
|
|
212
|
+
{ nx, ny, iconR, labelH = 50 }: NodeOccupiedRectsOpts,
|
|
213
|
+
): Rect[] {
|
|
214
|
+
const rects: Rect[] = [];
|
|
215
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
216
|
+
const n = nodes[i];
|
|
217
|
+
if (n.step > visibleStep) continue;
|
|
218
|
+
const cx = nx(n.col);
|
|
219
|
+
const cy = ny(n.row);
|
|
220
|
+
const r = (n.id === "dns" || n.id === "users") ? iconR + 4 : iconR;
|
|
221
|
+
rects.push({
|
|
222
|
+
x: cx - r - 12,
|
|
223
|
+
y: cy - r - 30,
|
|
224
|
+
w: 2 * (r + 12),
|
|
225
|
+
h: 2 * r + 30 + labelH,
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
return rects;
|
|
229
|
+
}
|