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
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
import { CANVAS, MIN_FONT } from "../core/presets.ts";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Reusable SVG chart primitives.
|
|
7
|
+
* Each is a plain function returning an SVG sub-tree (not a full component).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export interface HorizontalBarItem {
|
|
11
|
+
label: string;
|
|
12
|
+
value: number;
|
|
13
|
+
maxValue?: number;
|
|
14
|
+
color?: string;
|
|
15
|
+
bold?: boolean;
|
|
16
|
+
extra?: ReactNode;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface HorizontalBarsProps {
|
|
20
|
+
items?: HorizontalBarItem[];
|
|
21
|
+
x?: number;
|
|
22
|
+
y?: number;
|
|
23
|
+
barX?: number;
|
|
24
|
+
barMaxW?: number;
|
|
25
|
+
rowH?: number;
|
|
26
|
+
labelFont?: number;
|
|
27
|
+
valueFont?: number;
|
|
28
|
+
fontFamily?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface BubbleMatrixRow {
|
|
32
|
+
label: string;
|
|
33
|
+
values?: number[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface BubbleMatrixProps {
|
|
37
|
+
rows?: BubbleMatrixRow[];
|
|
38
|
+
columns?: string[];
|
|
39
|
+
x?: number;
|
|
40
|
+
y?: number;
|
|
41
|
+
colW?: number;
|
|
42
|
+
rowH?: number;
|
|
43
|
+
maxR?: number;
|
|
44
|
+
colorFn?: (value: number) => string;
|
|
45
|
+
fontFamily?: string;
|
|
46
|
+
headerFont?: number;
|
|
47
|
+
labelFont?: number;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface StatStackItem {
|
|
51
|
+
value: string | number;
|
|
52
|
+
label: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface StatStackProps {
|
|
56
|
+
items?: StatStackItem[];
|
|
57
|
+
cx?: number;
|
|
58
|
+
y?: number;
|
|
59
|
+
gap?: number;
|
|
60
|
+
valueFont?: number;
|
|
61
|
+
labelFont?: number;
|
|
62
|
+
color?: string;
|
|
63
|
+
fontFamily?: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Labeled horizontal bar chart.
|
|
68
|
+
* items: { label, value, maxValue, color, bold, extra }[] (extra: optional JSX after the bar)
|
|
69
|
+
*/
|
|
70
|
+
export function HorizontalBars(props: HorizontalBarsProps): ReactNode {
|
|
71
|
+
const items = props.items || [];
|
|
72
|
+
const x = props.x || 0;
|
|
73
|
+
const y = props.y || 0;
|
|
74
|
+
const barX = props.barX != null ? props.barX : x + 120;
|
|
75
|
+
const barMaxW = props.barMaxW || 240;
|
|
76
|
+
const rowH = props.rowH || 32;
|
|
77
|
+
const labelFont = Math.max(props.labelFont || 13, MIN_FONT - 1);
|
|
78
|
+
const valueFont = Math.max(props.valueFont || 13, MIN_FONT - 1);
|
|
79
|
+
const fontFamily = props.fontFamily || CANVAS.fontFamily;
|
|
80
|
+
|
|
81
|
+
return items.map(function (item, i) {
|
|
82
|
+
const ry = y + i * rowH;
|
|
83
|
+
const cy = ry + rowH / 2;
|
|
84
|
+
const maxValue = item.maxValue || 1;
|
|
85
|
+
const barW = (item.value / maxValue) * barMaxW;
|
|
86
|
+
const weight = item.bold ? 900 : 700;
|
|
87
|
+
|
|
88
|
+
return React.createElement(
|
|
89
|
+
"g",
|
|
90
|
+
{ key: i },
|
|
91
|
+
React.createElement(
|
|
92
|
+
"text",
|
|
93
|
+
{
|
|
94
|
+
x: barX - 8, y: cy, textAnchor: "end", dominantBaseline: "middle",
|
|
95
|
+
fontSize: labelFont, fontWeight: weight, fill: item.color || "#313131",
|
|
96
|
+
fontFamily: fontFamily,
|
|
97
|
+
},
|
|
98
|
+
item.label
|
|
99
|
+
),
|
|
100
|
+
React.createElement("rect", {
|
|
101
|
+
x: barX, y: ry + 5, width: barW, height: rowH - 12, rx: 3,
|
|
102
|
+
fill: item.color || "#00A6D6", fillOpacity: 0.18,
|
|
103
|
+
stroke: item.color || "#00A6D6", strokeWidth: item.bold ? 2 : 1,
|
|
104
|
+
}),
|
|
105
|
+
React.createElement(
|
|
106
|
+
"text",
|
|
107
|
+
{
|
|
108
|
+
x: barX + barW + 6, y: cy, dominantBaseline: "middle",
|
|
109
|
+
fontSize: valueFont, fontWeight: 900, fill: item.color || "#313131",
|
|
110
|
+
fontFamily: fontFamily,
|
|
111
|
+
},
|
|
112
|
+
item.value
|
|
113
|
+
),
|
|
114
|
+
item.extra != null
|
|
115
|
+
? React.createElement("g", { transform: "translate(0," + cy + ")" }, item.extra)
|
|
116
|
+
: null
|
|
117
|
+
);
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Grid of sized/colored bubbles.
|
|
123
|
+
* rows: { label, values: number[] }[]
|
|
124
|
+
* columns: string[] headers
|
|
125
|
+
*/
|
|
126
|
+
export function BubbleMatrix(props: BubbleMatrixProps): ReactNode {
|
|
127
|
+
const rows = props.rows || [];
|
|
128
|
+
const columns = props.columns || [];
|
|
129
|
+
const x = props.x || 0;
|
|
130
|
+
const y = props.y || 0;
|
|
131
|
+
const colW = props.colW || 50;
|
|
132
|
+
const rowH = props.rowH || 17;
|
|
133
|
+
const maxR = props.maxR || 7;
|
|
134
|
+
const colorFn = props.colorFn || function () { return "#00A6D6"; };
|
|
135
|
+
const fontFamily = props.fontFamily || CANVAS.fontFamily;
|
|
136
|
+
const headerFont = Math.max(props.headerFont || 12, MIN_FONT - 2);
|
|
137
|
+
const labelFont = Math.max(props.labelFont || 12.5, MIN_FONT - 2);
|
|
138
|
+
|
|
139
|
+
function colX(i: number) { return x + colW * (i + 0.5); }
|
|
140
|
+
|
|
141
|
+
const headers = columns.map(function (c, i) {
|
|
142
|
+
return React.createElement(
|
|
143
|
+
"text",
|
|
144
|
+
{
|
|
145
|
+
key: "h" + i, x: colX(i), y: y - 10, textAnchor: "middle",
|
|
146
|
+
fontSize: headerFont, fontWeight: 800, fill: "#313131", fontFamily: fontFamily,
|
|
147
|
+
},
|
|
148
|
+
c
|
|
149
|
+
);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
const body = rows.map(function (r, ri) {
|
|
153
|
+
const ry = y + ri * rowH;
|
|
154
|
+
const cy = ry + rowH / 2;
|
|
155
|
+
const label = React.createElement(
|
|
156
|
+
"text",
|
|
157
|
+
{
|
|
158
|
+
key: "l", x: x - 14, y: cy, textAnchor: "end", dominantBaseline: "middle",
|
|
159
|
+
fontSize: labelFont, fontWeight: 700, fill: "#313131", fontFamily: fontFamily,
|
|
160
|
+
},
|
|
161
|
+
r.label
|
|
162
|
+
);
|
|
163
|
+
const bubbles = (r.values || []).map(function (v, ci) {
|
|
164
|
+
const rad = v <= 0 ? 3 : 4 + v * (maxR - 4);
|
|
165
|
+
return React.createElement("circle", {
|
|
166
|
+
key: ci, cx: colX(ci), cy: cy, r: rad,
|
|
167
|
+
fill: colorFn(v), fillOpacity: v <= 0 ? 0.5 : 0.9,
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
return React.createElement("g", { key: ri }, label, bubbles);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
return React.createElement("g", null, headers, body);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Large stat numbers stacked vertically.
|
|
178
|
+
* items: { value, label }[]
|
|
179
|
+
*/
|
|
180
|
+
export function StatStack(props: StatStackProps): ReactNode {
|
|
181
|
+
const items = props.items || [];
|
|
182
|
+
const cx = props.cx || 0;
|
|
183
|
+
const y = props.y || 0;
|
|
184
|
+
const gap = props.gap || 62;
|
|
185
|
+
const valueFont = props.valueFont || 36;
|
|
186
|
+
const labelFont = Math.max(props.labelFont || 16, MIN_FONT);
|
|
187
|
+
const color = props.color || "#76B82A";
|
|
188
|
+
const fontFamily = props.fontFamily || CANVAS.fontFamily;
|
|
189
|
+
|
|
190
|
+
return items.map(function (item, i) {
|
|
191
|
+
const iy = y + i * gap;
|
|
192
|
+
return React.createElement(
|
|
193
|
+
"g",
|
|
194
|
+
{ key: i },
|
|
195
|
+
React.createElement(
|
|
196
|
+
"text",
|
|
197
|
+
{
|
|
198
|
+
x: cx, y: iy, textAnchor: "middle",
|
|
199
|
+
fontSize: valueFont, fontWeight: 900, fill: color, fontFamily: fontFamily,
|
|
200
|
+
},
|
|
201
|
+
item.value
|
|
202
|
+
),
|
|
203
|
+
React.createElement(
|
|
204
|
+
"text",
|
|
205
|
+
{
|
|
206
|
+
x: cx, y: iy + labelFont + 4, textAnchor: "middle",
|
|
207
|
+
fontSize: labelFont, fontWeight: 700, fill: "#555", fontFamily: fontFamily,
|
|
208
|
+
},
|
|
209
|
+
item.label
|
|
210
|
+
)
|
|
211
|
+
);
|
|
212
|
+
});
|
|
213
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import type { Block, BlockOptions, Slide, SlideOptions, StepNote, LayerName } from "./types.ts";
|
|
2
|
+
import { attrsToHtml, esc, style } from "./design.ts";
|
|
3
|
+
|
|
4
|
+
const LAYER_Z: Record<LayerName, number> = {
|
|
5
|
+
background: 0,
|
|
6
|
+
connector: 1,
|
|
7
|
+
content: 2,
|
|
8
|
+
overlay: 3,
|
|
9
|
+
chrome: 4,
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export function fragmentMeta(
|
|
13
|
+
fragment: boolean | number | null | undefined,
|
|
14
|
+
animation = "fade-in",
|
|
15
|
+
): { classes: string[]; attrs: Record<string, number | undefined> } {
|
|
16
|
+
if (fragment === null || fragment === undefined || fragment === false) {
|
|
17
|
+
return { classes: [], attrs: {} };
|
|
18
|
+
}
|
|
19
|
+
const classes = ["fragment", animation].filter(Boolean);
|
|
20
|
+
const attrs = Number.isInteger(fragment) ? { "data-fragment-index": fragment as number } : {};
|
|
21
|
+
return { classes, attrs };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function createBlock({
|
|
25
|
+
id,
|
|
26
|
+
kind = "html",
|
|
27
|
+
html = "",
|
|
28
|
+
src,
|
|
29
|
+
alt = "",
|
|
30
|
+
x,
|
|
31
|
+
y,
|
|
32
|
+
w,
|
|
33
|
+
h,
|
|
34
|
+
layer = "content",
|
|
35
|
+
fragment = null,
|
|
36
|
+
animation = "fade-in",
|
|
37
|
+
className = "",
|
|
38
|
+
attrs = {},
|
|
39
|
+
css = {},
|
|
40
|
+
styleText = "",
|
|
41
|
+
flow = false,
|
|
42
|
+
mount = null,
|
|
43
|
+
}: BlockOptions = {}): Block {
|
|
44
|
+
return {
|
|
45
|
+
id,
|
|
46
|
+
kind,
|
|
47
|
+
html,
|
|
48
|
+
src,
|
|
49
|
+
alt,
|
|
50
|
+
x,
|
|
51
|
+
y,
|
|
52
|
+
w,
|
|
53
|
+
h,
|
|
54
|
+
layer,
|
|
55
|
+
fragment,
|
|
56
|
+
animation,
|
|
57
|
+
className,
|
|
58
|
+
attrs,
|
|
59
|
+
css,
|
|
60
|
+
styleText,
|
|
61
|
+
flow,
|
|
62
|
+
mount,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function htmlBlock(options: BlockOptions = {}): Block {
|
|
67
|
+
return createBlock({ kind: "html", ...options });
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function svgBlock(options: BlockOptions = {}): Block {
|
|
71
|
+
return createBlock({ kind: "svg", ...options });
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function imageBlock(options: BlockOptions = {}): Block {
|
|
75
|
+
return createBlock({ kind: "image", ...options });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function mountBlock(options: BlockOptions = {}): Block {
|
|
79
|
+
return createBlock({ kind: "mount", ...options });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function blockInner(block: Block): string {
|
|
83
|
+
if (block.kind === "image") {
|
|
84
|
+
return `<img class="block-image" src="${esc(block.src ?? "")}" alt="${esc(block.alt ?? "")}">`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (block.kind === "mount") {
|
|
88
|
+
const mountAttrs = attrsToHtml({
|
|
89
|
+
"data-mount-library": block.mount?.library,
|
|
90
|
+
"data-mount-export": block.mount?.exportName,
|
|
91
|
+
"data-mount-props": block.mount?.props ? JSON.stringify(block.mount.props) : undefined,
|
|
92
|
+
});
|
|
93
|
+
return `<div class="block-mount" ${mountAttrs}>${block.html ?? ""}</div>`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return block.html ?? "";
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function renderBlock(block: Block | string): string {
|
|
100
|
+
if (typeof block === "string") return block;
|
|
101
|
+
|
|
102
|
+
const fragment = fragmentMeta(block.fragment, block.animation);
|
|
103
|
+
const classes = [
|
|
104
|
+
block.flow ? "slide-flow-block" : "slide-positioned-block",
|
|
105
|
+
"slide-block",
|
|
106
|
+
`block-${block.kind}`,
|
|
107
|
+
`layer-${block.layer}`,
|
|
108
|
+
...fragment.classes,
|
|
109
|
+
block.className,
|
|
110
|
+
].filter(Boolean);
|
|
111
|
+
|
|
112
|
+
const positionStyle: Record<string, string | number | undefined> = block.flow
|
|
113
|
+
? {}
|
|
114
|
+
: {
|
|
115
|
+
left: block.x,
|
|
116
|
+
top: block.y,
|
|
117
|
+
width: block.w,
|
|
118
|
+
height: block.h,
|
|
119
|
+
"z-index": LAYER_Z[block.layer] ?? LAYER_Z.content,
|
|
120
|
+
};
|
|
121
|
+
const styleAttr = [style({ ...positionStyle, ...block.css }), block.styleText]
|
|
122
|
+
.filter(Boolean)
|
|
123
|
+
.join(";");
|
|
124
|
+
|
|
125
|
+
const attrs = attrsToHtml({
|
|
126
|
+
"data-block-id": block.id,
|
|
127
|
+
"data-block-kind": block.kind,
|
|
128
|
+
"data-layer": block.layer,
|
|
129
|
+
...block.attrs,
|
|
130
|
+
...fragment.attrs,
|
|
131
|
+
});
|
|
132
|
+
const attrText = attrs ? ` ${attrs}` : "";
|
|
133
|
+
const styleHtml = styleAttr ? ` style="${esc(styleAttr)}"` : "";
|
|
134
|
+
|
|
135
|
+
return `<div class="${classes.map(esc).join(" ")}"${attrText}${styleHtml}>${blockInner(block)}</div>`;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function renderBlocks(blocks: (Block | string)[] = []): string {
|
|
139
|
+
return blocks.flat().filter(Boolean).map(renderBlock).join("\n");
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function stepNote({ step, visible, say, why, next }: StepNote): string {
|
|
143
|
+
return [
|
|
144
|
+
`Step: ${step}`,
|
|
145
|
+
`Visible: ${visible}`,
|
|
146
|
+
`Say: ${say}`,
|
|
147
|
+
why ? `Why: ${why}` : "",
|
|
148
|
+
next ? `Next: ${next}` : "",
|
|
149
|
+
].filter(Boolean).join("\n");
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function renderNotes(notes: string | (string | StepNote)[] = ""): string {
|
|
153
|
+
if (Array.isArray(notes)) {
|
|
154
|
+
return notes.map((note) => (typeof note === "string" ? note : stepNote(note))).join("\n\n");
|
|
155
|
+
}
|
|
156
|
+
return notes;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function defineSlide({
|
|
160
|
+
className = "native-slide",
|
|
161
|
+
attrs = {},
|
|
162
|
+
html = "",
|
|
163
|
+
blocks = [],
|
|
164
|
+
notes = "",
|
|
165
|
+
}: SlideOptions = {}): Slide {
|
|
166
|
+
return {
|
|
167
|
+
className,
|
|
168
|
+
attrs,
|
|
169
|
+
html: [html, renderBlocks(blocks)].filter(Boolean).join("\n"),
|
|
170
|
+
notes: renderNotes(notes),
|
|
171
|
+
};
|
|
172
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import Reveal from "reveal.js";
|
|
2
|
+
import RevealNotes from "reveal.js/plugin/notes";
|
|
3
|
+
import "reveal.js/reveal.css";
|
|
4
|
+
import * as anime from "animejs";
|
|
5
|
+
import "../theme.css";
|
|
6
|
+
import { installRuntime } from "./runtime.ts";
|
|
7
|
+
import type { Slide, ComponentRegistry, DeckConfig } from "./types.ts";
|
|
8
|
+
|
|
9
|
+
function injectSlides(slides: Slide[]): void {
|
|
10
|
+
const container = document.querySelector(".slides");
|
|
11
|
+
if (!container) return;
|
|
12
|
+
slides.forEach((slide) => {
|
|
13
|
+
const section = document.createElement("section");
|
|
14
|
+
if (slide.className) section.className = slide.className;
|
|
15
|
+
if (slide.attrs) {
|
|
16
|
+
Object.keys(slide.attrs).forEach((k) => {
|
|
17
|
+
const v = slide.attrs![k];
|
|
18
|
+
if (v !== undefined && v !== null) section.setAttribute(k, String(v));
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
section.innerHTML = slide.html;
|
|
22
|
+
if (slide.notes) {
|
|
23
|
+
const aside = document.createElement("aside");
|
|
24
|
+
aside.className = "notes";
|
|
25
|
+
aside.textContent = slide.notes;
|
|
26
|
+
section.appendChild(aside);
|
|
27
|
+
}
|
|
28
|
+
container.appendChild(section);
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function initDeck<C extends ComponentRegistry>(config: DeckConfig<C>): Promise<void> {
|
|
33
|
+
const slides = typeof config.buildSlides === "function"
|
|
34
|
+
? await config.buildSlides()
|
|
35
|
+
: config.buildSlides;
|
|
36
|
+
injectSlides(slides);
|
|
37
|
+
|
|
38
|
+
const deck = new (Reveal as any)({
|
|
39
|
+
width: 1280,
|
|
40
|
+
height: 720,
|
|
41
|
+
margin: 0.04,
|
|
42
|
+
minScale: 0.2,
|
|
43
|
+
maxScale: 2.0,
|
|
44
|
+
controls: true,
|
|
45
|
+
controlsTutorial: true,
|
|
46
|
+
progress: true,
|
|
47
|
+
slideNumber: "c/t",
|
|
48
|
+
overview: true,
|
|
49
|
+
keyboard: true,
|
|
50
|
+
hash: true,
|
|
51
|
+
fragmentInURL: true,
|
|
52
|
+
center: false,
|
|
53
|
+
transition: "none",
|
|
54
|
+
backgroundTransition: "none",
|
|
55
|
+
plugins: [RevealNotes],
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
await deck.initialize();
|
|
59
|
+
installRuntime(deck, anime, config.components);
|
|
60
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export const C = {
|
|
2
|
+
white: "#FFFFFF",
|
|
3
|
+
black: "#000000",
|
|
4
|
+
body: "#313131",
|
|
5
|
+
muted: "#767676",
|
|
6
|
+
faint: "#F7F7F7",
|
|
7
|
+
line: "#BFBFBF",
|
|
8
|
+
soft: "#E7E6E6",
|
|
9
|
+
red: "#C00000",
|
|
10
|
+
redSoft: "#FFF3F3",
|
|
11
|
+
blue: "#00A6D6",
|
|
12
|
+
green: "#76B82A",
|
|
13
|
+
yellow: "#FFC000",
|
|
14
|
+
teal: "#1B556B",
|
|
15
|
+
} as const;
|
|
16
|
+
|
|
17
|
+
export function esc(value: string | number): string {
|
|
18
|
+
return String(value)
|
|
19
|
+
.replaceAll("&", "&")
|
|
20
|
+
.replaceAll("<", "<")
|
|
21
|
+
.replaceAll(">", ">")
|
|
22
|
+
.replaceAll('"', """);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const UNITLESS = new Set([
|
|
26
|
+
"opacity", "z-index", "font-weight", "line-height", "flex-grow",
|
|
27
|
+
"flex-shrink", "order", "zoom", "column-count", "fill-opacity",
|
|
28
|
+
"stroke-opacity", "stroke-miterlimit",
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
export function style(obj: Record<string, string | number | undefined | null>): string {
|
|
32
|
+
return Object.entries(obj)
|
|
33
|
+
.filter((e): e is [string, string | number] => e[1] !== undefined && e[1] !== null)
|
|
34
|
+
.map(([key, value]) => {
|
|
35
|
+
if (typeof value === "number" && !UNITLESS.has(key)) return `${key}:${value}px`;
|
|
36
|
+
return `${key}:${value}`;
|
|
37
|
+
})
|
|
38
|
+
.join(";");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function attrsToHtml(attrs: Record<string, string | number | boolean | undefined | null> = {}): string {
|
|
42
|
+
return Object.entries(attrs)
|
|
43
|
+
.filter((e): e is [string, string | number | boolean] => e[1] !== undefined && e[1] !== null && e[1] !== false)
|
|
44
|
+
.map(([key, value]) => (value === true ? esc(key) : `${esc(key)}="${esc(value as string | number)}"`))
|
|
45
|
+
.join(" ");
|
|
46
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import type { Block, Slide, BlockSlideOptions, ReactSlideOptions } from "./types.ts";
|
|
2
|
+
import { C, esc } from "./design.ts";
|
|
3
|
+
import { defineSlide, htmlBlock, mountBlock, renderBlocks } from "./blocks.ts";
|
|
4
|
+
|
|
5
|
+
export function placedHtml(
|
|
6
|
+
inner: string,
|
|
7
|
+
x: number,
|
|
8
|
+
y: number,
|
|
9
|
+
w: number,
|
|
10
|
+
h: number,
|
|
11
|
+
{
|
|
12
|
+
id,
|
|
13
|
+
className = "",
|
|
14
|
+
fragment = true,
|
|
15
|
+
layer = "content",
|
|
16
|
+
css = {},
|
|
17
|
+
}: {
|
|
18
|
+
id?: string;
|
|
19
|
+
className?: string;
|
|
20
|
+
fragment?: boolean | number | null;
|
|
21
|
+
layer?: Block["layer"];
|
|
22
|
+
css?: Record<string, string | number>;
|
|
23
|
+
} = {},
|
|
24
|
+
): Block {
|
|
25
|
+
return htmlBlock({
|
|
26
|
+
id,
|
|
27
|
+
html: inner,
|
|
28
|
+
x,
|
|
29
|
+
y,
|
|
30
|
+
w,
|
|
31
|
+
h,
|
|
32
|
+
className,
|
|
33
|
+
fragment,
|
|
34
|
+
layer,
|
|
35
|
+
css,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function title(text: string): string {
|
|
40
|
+
return `<h1 class="slide-title">${esc(text)}</h1>`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function pageNo(n: number): string {
|
|
44
|
+
return `<div class="page-no">${String(n).padStart(2, "0")}</div>`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function svg(
|
|
48
|
+
width: number,
|
|
49
|
+
height: number,
|
|
50
|
+
body: string,
|
|
51
|
+
attrs: string = "",
|
|
52
|
+
): string {
|
|
53
|
+
return `<svg class="native-svg" viewBox="0 0 ${width} ${height}" ${attrs}>${body}</svg>`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function blockSlide({
|
|
57
|
+
title: titleText,
|
|
58
|
+
blocks = [],
|
|
59
|
+
page,
|
|
60
|
+
notes = "",
|
|
61
|
+
html = "",
|
|
62
|
+
className = "native-slide",
|
|
63
|
+
attrs = {},
|
|
64
|
+
}: BlockSlideOptions = {}): Slide {
|
|
65
|
+
const chrome = [
|
|
66
|
+
titleText ? title(titleText) : "",
|
|
67
|
+
html,
|
|
68
|
+
renderBlocks(blocks),
|
|
69
|
+
page !== undefined && page !== null ? pageNo(page) : "",
|
|
70
|
+
].filter(Boolean).join("\n");
|
|
71
|
+
return defineSlide({ className, attrs, html: chrome, notes });
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function tag(text: string, color: string): string {
|
|
75
|
+
return `<span class="tag" style="border-color:${color};color:${color}">${esc(text)}</span>`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function at(
|
|
79
|
+
html: string,
|
|
80
|
+
x: number,
|
|
81
|
+
y: number,
|
|
82
|
+
w: number,
|
|
83
|
+
h: number,
|
|
84
|
+
fragment: boolean | number | null = true,
|
|
85
|
+
className: string = "",
|
|
86
|
+
): Block {
|
|
87
|
+
return placedHtml(html, x, y, w, h, { fragment, className });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function claim(
|
|
91
|
+
text: string,
|
|
92
|
+
x: number,
|
|
93
|
+
y: number,
|
|
94
|
+
w: number,
|
|
95
|
+
color: string = C.red,
|
|
96
|
+
step: boolean | number | null = null,
|
|
97
|
+
): Block {
|
|
98
|
+
return placedHtml(
|
|
99
|
+
`<div class="claim" style="color:${color}">${esc(text)}</div>`,
|
|
100
|
+
x,
|
|
101
|
+
y,
|
|
102
|
+
w,
|
|
103
|
+
80,
|
|
104
|
+
{ fragment: step ?? true },
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Shortcut for slides backed by a React mount component.
|
|
110
|
+
* Generates mountBlock + invisible fragment triggers automatically.
|
|
111
|
+
*
|
|
112
|
+
* reactSlide({ title: "...", component: "FaultHeatmap", steps: 7, notes: "..." })
|
|
113
|
+
*/
|
|
114
|
+
export function reactSlide<C extends string = string>(
|
|
115
|
+
{
|
|
116
|
+
title: titleText,
|
|
117
|
+
component,
|
|
118
|
+
steps = 0,
|
|
119
|
+
notes = "",
|
|
120
|
+
page,
|
|
121
|
+
x = 20, y = 70, w = 1240, h = 640,
|
|
122
|
+
}: ReactSlideOptions<C>,
|
|
123
|
+
): Slide {
|
|
124
|
+
return blockSlide({
|
|
125
|
+
title: titleText,
|
|
126
|
+
page,
|
|
127
|
+
blocks: [
|
|
128
|
+
mountBlock({
|
|
129
|
+
x, y, w, h,
|
|
130
|
+
fragment: false,
|
|
131
|
+
mount: { library: "react", exportName: component },
|
|
132
|
+
}),
|
|
133
|
+
...Array.from({ length: steps }, function (_, i): Block {
|
|
134
|
+
return placedHtml(
|
|
135
|
+
'<span class="cascade-trigger"></span>',
|
|
136
|
+
0, 0, 1, 1,
|
|
137
|
+
{ fragment: i }
|
|
138
|
+
);
|
|
139
|
+
}),
|
|
140
|
+
],
|
|
141
|
+
notes,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function statCard(
|
|
146
|
+
value: string,
|
|
147
|
+
description: string,
|
|
148
|
+
{
|
|
149
|
+
variant = "large-stat",
|
|
150
|
+
centered = false,
|
|
151
|
+
extra = "",
|
|
152
|
+
}: { variant?: string; centered?: boolean; extra?: string } = {},
|
|
153
|
+
): string {
|
|
154
|
+
const cls = [variant, centered ? "centered" : ""].filter(Boolean).join(" ");
|
|
155
|
+
return `<div class="${cls}"><strong>${value}</strong><span>${esc(description)}</span>${extra}</div>`;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function table(
|
|
159
|
+
headers: string[],
|
|
160
|
+
rows: string[][],
|
|
161
|
+
{
|
|
162
|
+
title: titleText = "",
|
|
163
|
+
highlightCol = -1,
|
|
164
|
+
}: { title?: string; highlightCol?: number } = {},
|
|
165
|
+
): string {
|
|
166
|
+
const headCells = headers.map((h) => `<th>${esc(h)}</th>`).join("");
|
|
167
|
+
const bodyRows = rows.map((cells) =>
|
|
168
|
+
`<tr>${cells.map((c, i) =>
|
|
169
|
+
`<td${i === highlightCol ? ' class="red-cell"' : ""}>${esc(c)}</td>`
|
|
170
|
+
).join("")}</tr>`
|
|
171
|
+
).join("");
|
|
172
|
+
return `${titleText ? `<div class="table-title">${esc(titleText)}</div>` : ""}
|
|
173
|
+
<table class="native-table">
|
|
174
|
+
<thead><tr>${headCells}</tr></thead>
|
|
175
|
+
<tbody>${bodyRows}</tbody>
|
|
176
|
+
</table>`;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function dotList(
|
|
180
|
+
titleText: string,
|
|
181
|
+
items: { label: string; color: string }[],
|
|
182
|
+
): string {
|
|
183
|
+
const rows = items.map(({ label, color }) =>
|
|
184
|
+
`<div class="fault-dot-row"><span>${esc(label)}</span><b style="background:${color}"></b></div>`
|
|
185
|
+
).join("");
|
|
186
|
+
return `<div class="fault-panel"><h3>${esc(titleText)}</h3>${rows}</div>`;
|
|
187
|
+
}
|