@deckspec/theme-noir-display 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.
@@ -0,0 +1,295 @@
1
+ import { z } from "zod";
2
+ import { SlideHeader } from "../../components/index.js";
3
+
4
+ const nodeSchema = z.object({
5
+ id: z.string().min(1).max(3).describe("Node ID (A, B, C)"),
6
+ title: z.string().min(1).max(30).describe("Node title"),
7
+ });
8
+
9
+ const explanationSchema = z.object({
10
+ id: z.string().min(1).max(3).describe("Explanation ID matching node"),
11
+ title: z.string().min(1).max(30).describe("Explanation title"),
12
+ items: z.array(z.string()).min(1).max(5).describe("Detail points"),
13
+ });
14
+
15
+ export const schema = z.object({
16
+ label: z.string().min(1).max(30).optional().describe("Accent eyebrow"),
17
+ heading: z.string().min(1).max(60).describe("Section heading"),
18
+ center: z.string().min(1).max(40).describe("Central concept"),
19
+ nodes: z.array(nodeSchema).min(2).max(5).describe("Surrounding nodes"),
20
+ explanations: z.array(explanationSchema).min(2).max(5).describe("Explanations for each node"),
21
+ footer: z.string().min(1).max(100).optional().describe("Footer message"),
22
+ });
23
+
24
+ type Props = z.infer<typeof schema>;
25
+
26
+ export default function BodyMessage({
27
+ label,
28
+ heading,
29
+ center,
30
+ nodes,
31
+ explanations,
32
+ footer,
33
+ }: Props) {
34
+ // Position nodes around the center concept in a radial layout
35
+ const nodePositions = getNodePositions(nodes.length);
36
+
37
+ return (
38
+ <div
39
+ className="slide"
40
+ style={{
41
+ display: "flex",
42
+ flexDirection: "column",
43
+ background: "var(--color-card-background)",
44
+ padding: "40px 60px",
45
+ gap: 20,
46
+ }}
47
+ >
48
+ {/* Header */}
49
+ <SlideHeader label={label} heading={heading} />
50
+
51
+ {/* Main content: diagram + explanations */}
52
+ <div style={{ display: "flex", gap: 32, flex: 1, minHeight: 0 }}>
53
+ {/* Left: diagram (60%) */}
54
+ <div
55
+ style={{
56
+ width: "58%",
57
+ position: "relative",
58
+ display: "flex",
59
+ alignItems: "center",
60
+ justifyContent: "center",
61
+ }}
62
+ >
63
+ {/* Center concept card */}
64
+ <div
65
+ style={{
66
+ position: "absolute",
67
+ left: "50%",
68
+ top: "50%",
69
+ transform: "translate(-50%, -50%)",
70
+ backgroundColor: "var(--color-card-background)",
71
+ border: "2px solid var(--color-primary)",
72
+ borderRadius: 16,
73
+ padding: "20px 28px",
74
+ textAlign: "center",
75
+ zIndex: 2,
76
+ boxShadow: "4px 4px 12px rgba(0,0,0,0.06)",
77
+ }}
78
+ >
79
+ <span
80
+ style={{
81
+ fontSize: 19,
82
+ fontWeight: 600,
83
+ fontFamily: "var(--font-heading)",
84
+ letterSpacing: "-0.022em",
85
+ color: "var(--color-foreground)",
86
+ lineHeight: 1.3,
87
+ }}
88
+ >
89
+ {center}
90
+ </span>
91
+ </div>
92
+
93
+ {/* Surrounding node cards */}
94
+ {nodes.map((node, i) => {
95
+ const pos = nodePositions[i];
96
+ return (
97
+ <div
98
+ key={i}
99
+ style={{
100
+ position: "absolute",
101
+ left: `${pos.x}%`,
102
+ top: `${pos.y}%`,
103
+ transform: "translate(-50%, -50%)",
104
+ backgroundColor: "var(--color-muted)",
105
+ borderRadius: 12,
106
+ padding: "12px 18px",
107
+ textAlign: "center",
108
+ zIndex: 1,
109
+ minWidth: 100,
110
+ }}
111
+ >
112
+ <span
113
+ style={{
114
+ fontSize: 12,
115
+ fontWeight: 600,
116
+ fontFamily: "var(--font-heading)",
117
+ color: "var(--color-primary)",
118
+ display: "block",
119
+ marginBottom: 2,
120
+ }}
121
+ >
122
+ {node.id}
123
+ </span>
124
+ <span
125
+ style={{
126
+ fontSize: 14,
127
+ fontWeight: 600,
128
+ fontFamily: "var(--font-heading)",
129
+ letterSpacing: "-0.016em",
130
+ color: "var(--color-foreground)",
131
+ lineHeight: 1.3,
132
+ }}
133
+ >
134
+ {node.title}
135
+ </span>
136
+ </div>
137
+ );
138
+ })}
139
+
140
+ {/* Connecting lines (SVG) */}
141
+ <svg
142
+ style={{
143
+ position: "absolute",
144
+ left: 0,
145
+ top: 0,
146
+ width: "100%",
147
+ height: "100%",
148
+ zIndex: 0,
149
+ overflow: "visible",
150
+ }}
151
+ viewBox="0 0 100 100"
152
+ preserveAspectRatio="none"
153
+ >
154
+ {nodes.map((_, i) => {
155
+ const pos = nodePositions[i];
156
+ return (
157
+ <line
158
+ key={i}
159
+ x1="50"
160
+ y1="50"
161
+ x2={pos.x}
162
+ y2={pos.y}
163
+ stroke="var(--color-border)"
164
+ strokeWidth="0.4"
165
+ strokeDasharray="1,1"
166
+ />
167
+ );
168
+ })}
169
+ </svg>
170
+ </div>
171
+
172
+ {/* Right: explanations (40%) */}
173
+ <div
174
+ style={{
175
+ width: "42%",
176
+ display: "flex",
177
+ flexDirection: "column",
178
+ justifyContent: "center",
179
+ gap: 16,
180
+ }}
181
+ >
182
+ {explanations.map((exp, i) => (
183
+ <div
184
+ key={i}
185
+ style={{
186
+ display: "flex",
187
+ flexDirection: "column",
188
+ gap: 6,
189
+ }}
190
+ >
191
+ <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
192
+ <span
193
+ style={{
194
+ fontSize: 13,
195
+ fontWeight: 600,
196
+ fontFamily: "var(--font-heading)",
197
+ color: "var(--color-primary)",
198
+ backgroundColor: "var(--color-background)",
199
+ borderRadius: 6,
200
+ padding: "2px 8px",
201
+ lineHeight: 1.4,
202
+ }}
203
+ >
204
+ {exp.id}
205
+ </span>
206
+ <span
207
+ style={{
208
+ fontSize: 17,
209
+ fontWeight: 600,
210
+ fontFamily: "var(--font-heading)",
211
+ letterSpacing: "-0.022em",
212
+ color: "var(--color-foreground)",
213
+ lineHeight: 1.3,
214
+ }}
215
+ >
216
+ {exp.title}
217
+ </span>
218
+ </div>
219
+ <ul
220
+ style={{
221
+ listStyle: "none",
222
+ margin: 0,
223
+ padding: 0,
224
+ display: "flex",
225
+ flexDirection: "column",
226
+ gap: 4,
227
+ paddingLeft: 4,
228
+ }}
229
+ >
230
+ {exp.items.map((item, j) => (
231
+ <li
232
+ key={j}
233
+ style={{
234
+ fontSize: 14,
235
+ fontWeight: 400,
236
+ fontFamily: "var(--font-body)",
237
+ letterSpacing: "-0.016em",
238
+ lineHeight: 1.43,
239
+ color: "var(--color-muted-foreground)",
240
+ paddingLeft: 14,
241
+ position: "relative",
242
+ }}
243
+ >
244
+ <span
245
+ style={{
246
+ position: "absolute",
247
+ left: 0,
248
+ top: 0,
249
+ color: "var(--color-muted-foreground)",
250
+ }}
251
+ >
252
+ &#x2022;
253
+ </span>
254
+ {item}
255
+ </li>
256
+ ))}
257
+ </ul>
258
+ </div>
259
+ ))}
260
+ </div>
261
+ </div>
262
+
263
+ {/* Footer */}
264
+ {footer && (
265
+ <div style={{ textAlign: "center" }}>
266
+ <span
267
+ style={{
268
+ fontSize: 14,
269
+ fontWeight: 400,
270
+ fontFamily: "var(--font-body)",
271
+ letterSpacing: "-0.016em",
272
+ color: "var(--color-muted-foreground)",
273
+ lineHeight: 1.43,
274
+ }}
275
+ >
276
+ {footer}
277
+ </span>
278
+ </div>
279
+ )}
280
+ </div>
281
+ );
282
+ }
283
+
284
+ /** Compute radial positions (%) for nodes around center (50,50) */
285
+ function getNodePositions(count: number): Array<{ x: number; y: number }> {
286
+ const radius = 36;
287
+ const startAngle = -Math.PI / 2; // Start from top
288
+ return Array.from({ length: count }, (_, i) => {
289
+ const angle = startAngle + (2 * Math.PI * i) / count;
290
+ return {
291
+ x: 50 + radius * Math.cos(angle),
292
+ y: 50 + radius * Math.sin(angle),
293
+ };
294
+ });
295
+ }
@@ -0,0 +1,132 @@
1
+ import { z } from "zod";
2
+
3
+ const itemSchema = z.object({
4
+ title: z.string().min(1).describe("Item title"),
5
+ description: z.string().optional().describe("Item description"),
6
+ });
7
+
8
+ export const schema = z.object({
9
+ label: z.string().min(1).max(30).optional().describe("Accent eyebrow"),
10
+ heading: z.string().min(1).max(80).describe("Section heading"),
11
+ items: z.array(itemSchema).min(2).max(6).describe("List items"),
12
+ });
13
+
14
+ type Props = z.infer<typeof schema>;
15
+
16
+ export default function BulletList({ label, heading, items }: Props) {
17
+ return (
18
+ <div
19
+ className="slide"
20
+ style={{
21
+ display: "flex",
22
+ background: "#ffffff",
23
+ overflow: "hidden",
24
+ }}
25
+ >
26
+ {/* Left: heading area */}
27
+ <div
28
+ style={{
29
+ width: "40%",
30
+ padding: "60px 48px 60px 80px",
31
+ display: "flex",
32
+ flexDirection: "column",
33
+ justifyContent: "center",
34
+ gap: 8,
35
+ }}
36
+ >
37
+ {label && (
38
+ <span
39
+ style={{
40
+ fontSize: 17,
41
+ fontWeight: 600,
42
+ letterSpacing: "-0.022em",
43
+ color: "var(--color-primary)",
44
+ }}
45
+ >
46
+ {label}
47
+ </span>
48
+ )}
49
+ <h2
50
+ style={{
51
+ fontSize: 40,
52
+ fontWeight: 600,
53
+ lineHeight: 1.1,
54
+ letterSpacing: "-0.009em",
55
+ }}
56
+ >
57
+ {heading}
58
+ </h2>
59
+ </div>
60
+
61
+ {/* Right: list items */}
62
+ <div
63
+ style={{
64
+ flex: 1,
65
+ padding: "60px 80px 60px 0",
66
+ display: "flex",
67
+ flexDirection: "column",
68
+ justifyContent: "center",
69
+ }}
70
+ >
71
+ {items.map((item, i) => (
72
+ <div
73
+ key={i}
74
+ style={{
75
+ display: "flex",
76
+ alignItems: "baseline",
77
+ gap: 16,
78
+ padding: "14px 0",
79
+ borderBottom:
80
+ i < items.length - 1
81
+ ? "1px solid rgba(0,0,0,0.08)"
82
+ : "none",
83
+ }}
84
+ >
85
+ <span
86
+ style={{
87
+ fontSize: 14,
88
+ fontWeight: 600,
89
+ fontFamily: "var(--font-heading)",
90
+ color: "var(--color-muted-foreground)",
91
+ letterSpacing: "-0.016em",
92
+ minWidth: 24,
93
+ flexShrink: 0,
94
+ }}
95
+ >
96
+ {String(i + 1).padStart(2, "0")}
97
+ </span>
98
+
99
+ <div style={{ display: "flex", flexDirection: "column", gap: 4 }}>
100
+ <span
101
+ style={{
102
+ fontSize: 19,
103
+ fontWeight: 600,
104
+ fontFamily: "var(--font-heading)",
105
+ letterSpacing: "0.012em",
106
+ color: "var(--color-foreground)",
107
+ lineHeight: 1.37,
108
+ }}
109
+ >
110
+ {item.title}
111
+ </span>
112
+ {item.description && (
113
+ <span
114
+ style={{
115
+ fontSize: 15,
116
+ fontWeight: 400,
117
+ fontFamily: "var(--font-body)",
118
+ letterSpacing: "-0.022em",
119
+ color: "var(--color-muted-foreground)",
120
+ lineHeight: 1.47,
121
+ }}
122
+ >
123
+ {item.description}
124
+ </span>
125
+ )}
126
+ </div>
127
+ </div>
128
+ ))}
129
+ </div>
130
+ </div>
131
+ );
132
+ }
@@ -0,0 +1,112 @@
1
+ import { z } from "zod";
2
+ import { Icon } from "../_lib/icon.js";
3
+ import { SlideHeader } from "../../components/index.js";
4
+
5
+ const challengeSchema = z.object({
6
+ title: z.string().min(1).max(30).describe("Challenge card title"),
7
+ icon: z.string().min(1).describe("Lucide icon name"),
8
+ items: z.array(z.string()).min(2).max(5).describe("Bullet points"),
9
+ });
10
+
11
+ export const schema = z.object({
12
+ label: z.string().min(1).max(30).optional().describe("Accent eyebrow"),
13
+ heading: z.string().min(1).max(60).describe("Section heading"),
14
+ challenges: z.array(challengeSchema).min(2).max(3).describe("Challenge cards (2-3)"),
15
+ });
16
+
17
+ type Props = z.infer<typeof schema>;
18
+
19
+ export default function ChallengeCards({ label, heading, challenges }: Props) {
20
+ return (
21
+ <div
22
+ className="slide-stack"
23
+ style={{ gap: 32, justifyContent: "center" }}
24
+ >
25
+ {/* Header */}
26
+ <SlideHeader label={label} heading={heading} />
27
+
28
+ {/* Cards */}
29
+ <div
30
+ style={{
31
+ display: "grid",
32
+ gridTemplateColumns: `repeat(${challenges.length}, 1fr)`,
33
+ gap: 20,
34
+ }}
35
+ >
36
+ {challenges.map((c, i) => (
37
+ <div
38
+ key={i}
39
+ style={{
40
+ backgroundColor: "var(--color-card-background)",
41
+ borderRadius: 18,
42
+ padding: 0,
43
+ boxShadow: "4px 4px 12px rgba(0,0,0,0.06)",
44
+ display: "flex",
45
+ flexDirection: "column",
46
+ overflow: "hidden",
47
+ }}
48
+ >
49
+ {/* Content */}
50
+ <div style={{ padding: "24px 24px 28px", display: "flex", flexDirection: "column", gap: 16 }}>
51
+ {/* Title */}
52
+ <span
53
+ style={{
54
+ fontSize: 21,
55
+ fontWeight: 600,
56
+ fontFamily: "var(--font-heading)",
57
+ letterSpacing: "-0.022em",
58
+ color: "var(--color-foreground)",
59
+ lineHeight: 1.24,
60
+ }}
61
+ >
62
+ {c.title}
63
+ </span>
64
+ <div style={{ color: "var(--color-muted-foreground)" }}>
65
+ <Icon name={c.icon} size={48} />
66
+ </div>
67
+
68
+ <ul
69
+ style={{
70
+ listStyle: "none",
71
+ margin: 0,
72
+ padding: 0,
73
+ display: "flex",
74
+ flexDirection: "column",
75
+ gap: 8,
76
+ }}
77
+ >
78
+ {c.items.map((item, j) => (
79
+ <li
80
+ key={j}
81
+ style={{
82
+ fontSize: 14,
83
+ fontWeight: 400,
84
+ fontFamily: "var(--font-body)",
85
+ letterSpacing: "-0.016em",
86
+ lineHeight: 1.43,
87
+ color: "var(--color-foreground)",
88
+ paddingLeft: 14,
89
+ position: "relative",
90
+ }}
91
+ >
92
+ <span
93
+ style={{
94
+ position: "absolute",
95
+ left: 0,
96
+ top: 0,
97
+ color: "var(--color-muted-foreground)",
98
+ }}
99
+ >
100
+ &#x2022;
101
+ </span>
102
+ {item}
103
+ </li>
104
+ ))}
105
+ </ul>
106
+ </div>
107
+ </div>
108
+ ))}
109
+ </div>
110
+ </div>
111
+ );
112
+ }
@@ -0,0 +1,107 @@
1
+ import { z } from "zod";
2
+ import { BarChart, Bar, CartesianGrid, XAxis, YAxis, Tooltip } from "recharts";
3
+
4
+ const dataSchema = z.object({
5
+ label: z.string().min(1).describe("Category label"),
6
+ value: z.number().describe("Bar value"),
7
+ value2: z.number().optional().describe("Second bar value"),
8
+ });
9
+
10
+ export const schema = z.object({
11
+ label: z.string().min(1).max(30).optional().describe("Accent eyebrow"),
12
+ heading: z.string().min(1).max(60).describe("Chart heading"),
13
+ data: z.array(dataSchema).min(2).max(8).describe("Bar data"),
14
+ series1Name: z.string().optional().describe("First series name"),
15
+ series2Name: z.string().optional().describe("Second series name"),
16
+ });
17
+
18
+ type Props = z.infer<typeof schema>;
19
+
20
+ export default function ChartBarPattern({ label, heading, data, series1Name, series2Name }: Props) {
21
+ const hasSecond = data.some((d) => d.value2 !== undefined);
22
+
23
+ return (
24
+ <div
25
+ className="slide"
26
+ style={{
27
+ display: "flex",
28
+ flexDirection: "column",
29
+ background: "#ffffff",
30
+ padding: "48px 80px",
31
+ gap: 32,
32
+ }}
33
+ >
34
+ {/* Header */}
35
+ <div>
36
+ {label && (
37
+ <span
38
+ style={{
39
+ fontSize: 17,
40
+ fontWeight: 600,
41
+ letterSpacing: "-0.022em",
42
+ color: "var(--color-primary)",
43
+ display: "block",
44
+ marginBottom: 8,
45
+ }}
46
+ >
47
+ {label}
48
+ </span>
49
+ )}
50
+ <h2
51
+ style={{
52
+ fontSize: 40,
53
+ fontWeight: 600,
54
+ lineHeight: 1.1,
55
+ letterSpacing: "-0.009em",
56
+ }}
57
+ >
58
+ {heading}
59
+ </h2>
60
+ </div>
61
+
62
+ {/* Legend */}
63
+ {hasSecond && (
64
+ <div style={{ display: "flex", gap: 24 }}>
65
+ <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
66
+ <div style={{ width: 12, height: 12, borderRadius: 3, backgroundColor: "#0071e3" }} />
67
+ <span style={{ fontSize: 14, color: "#6e6e73" }}>{series1Name ?? "Series 1"}</span>
68
+ </div>
69
+ <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
70
+ <div style={{ width: 12, height: 12, borderRadius: 3, backgroundColor: "#1d1d1f" }} />
71
+ <span style={{ fontSize: 14, color: "#6e6e73" }}>{series2Name ?? "Series 2"}</span>
72
+ </div>
73
+ </div>
74
+ )}
75
+
76
+ {/* Chart — fixed size for SSR */}
77
+ <div style={{ flex: 1, display: "flex", alignItems: "center", justifyContent: "center" }}>
78
+ <BarChart width={1020} height={380} data={data} margin={{ top: 8, right: 8, bottom: 8, left: 0 }}>
79
+ <CartesianGrid strokeDasharray="3 3" stroke="rgba(0,0,0,0.06)" vertical={false} />
80
+ <XAxis
81
+ dataKey="label"
82
+ tick={{ fontSize: 13, fill: "#6e6e73" }}
83
+ axisLine={{ stroke: "rgba(0,0,0,0.08)" }}
84
+ tickLine={false}
85
+ />
86
+ <YAxis
87
+ tick={{ fontSize: 13, fill: "#6e6e73" }}
88
+ axisLine={false}
89
+ tickLine={false}
90
+ />
91
+ <Tooltip
92
+ contentStyle={{
93
+ backgroundColor: "#fff",
94
+ border: "1px solid rgba(0,0,0,0.08)",
95
+ borderRadius: 8,
96
+ fontSize: 13,
97
+ }}
98
+ />
99
+ <Bar dataKey="value" fill="#0071e3" radius={[4, 4, 0, 0]} name={series1Name} />
100
+ {hasSecond && (
101
+ <Bar dataKey="value2" fill="#1d1d1f" radius={[4, 4, 0, 0]} name={series2Name} />
102
+ )}
103
+ </BarChart>
104
+ </div>
105
+ </div>
106
+ );
107
+ }