@classic-homes/slides 0.2.1 → 0.4.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/README.md +80 -23
- package/dist/chunk-JFUJ47TP.js +107 -0
- package/dist/chunk-JFUJ47TP.js.map +1 -0
- package/dist/{chunk-MYY5GZC2.js → chunk-L7G3VH7K.js} +37 -7
- package/dist/chunk-L7G3VH7K.js.map +1 -0
- package/dist/{chunk-7FROOJCY.js → chunk-PTTLL6PQ.js} +2503 -300
- package/dist/chunk-PTTLL6PQ.js.map +1 -0
- package/dist/cli.js +4 -4
- package/dist/echarts-render-2QVSBZ6V.js +50 -0
- package/dist/echarts-render-2QVSBZ6V.js.map +1 -0
- package/dist/index.d.ts +154 -4
- package/dist/index.js +66 -12
- package/dist/index.js.map +1 -1
- package/dist/{mcp-DGMQTIQI.js → mcp-UX32IP2J.js} +17 -14
- package/dist/mcp-UX32IP2J.js.map +1 -0
- package/dist/node.d.ts +4 -2
- package/dist/node.js +2 -2
- package/dist/render-CU0sXO2y.d.ts +604 -0
- package/package.json +3 -1
- package/dist/chunk-33EPZMT6.js +0 -69
- package/dist/chunk-33EPZMT6.js.map +0 -1
- package/dist/chunk-7FROOJCY.js.map +0 -1
- package/dist/chunk-MYY5GZC2.js.map +0 -1
- package/dist/chunk-VONVGYRL.js +0 -52
- package/dist/chunk-VONVGYRL.js.map +0 -1
- package/dist/mcp-DGMQTIQI.js.map +0 -1
- package/dist/render-DTz7tLKM.d.ts +0 -149
|
@@ -1,56 +1,154 @@
|
|
|
1
|
-
// src/
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
1
|
+
// src/brand.ts
|
|
2
|
+
import { colors } from "@classic-homes/theme-tokens";
|
|
3
|
+
import { getContrastRatio } from "@classic-homes/theme-tokens/contrast";
|
|
4
|
+
import { z as z2 } from "zod";
|
|
5
|
+
|
|
6
|
+
// src/schema/common.ts
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
var Text = z.string().describe(
|
|
9
|
+
"Inline text. Supports **bold**, *highlight* (brand accent), `code`, [label](url) and newlines."
|
|
10
|
+
);
|
|
11
|
+
var Track = z.enum(["value", "build", "neutral"]).describe("Accent colour for the slide: value = red, build = teal, neutral = navy.");
|
|
12
|
+
var Background = z.enum(["paper", "navy", "slate", "red", "teal", "plum", "grey", "stone"]).describe(
|
|
13
|
+
'Slide ground. "paper" is the default light grid; the others are solid brand colours with white text.'
|
|
14
|
+
);
|
|
15
|
+
var Image = z.preprocess(
|
|
16
|
+
(v) => typeof v === "string" ? { src: v } : v,
|
|
17
|
+
z.object({
|
|
18
|
+
src: z.string().min(1).describe(
|
|
19
|
+
"Relative file path (resolved from the deck file), https URL, data: URI, or asset:<id> in hosted decks."
|
|
20
|
+
),
|
|
21
|
+
alt: z.string().default("").describe("Describe the image for screen readers. Empty only for pure decoration."),
|
|
22
|
+
fit: z.enum(["cover", "contain"]).default("cover").describe("cover crops to fill; contain shows the whole image."),
|
|
23
|
+
size: z.enum(["fill", "natural"]).default("fill").describe(
|
|
24
|
+
"fill stretches to the layout slot; natural keeps small images (logos, badges) at their own size."
|
|
25
|
+
),
|
|
26
|
+
focus: z.string().optional().describe('CSS object-position used when cropping, e.g. "top", "center 30%".')
|
|
27
|
+
})
|
|
28
|
+
);
|
|
29
|
+
var Item = z.preprocess(
|
|
30
|
+
(v) => typeof v === "string" ? { text: v } : v,
|
|
31
|
+
z.object({
|
|
32
|
+
text: Text,
|
|
33
|
+
sub: Text.optional().describe("Smaller secondary line under the item.")
|
|
34
|
+
})
|
|
35
|
+
);
|
|
36
|
+
var Marker = z.enum(["square", "check"]).default("square").describe("Bullet style.");
|
|
37
|
+
var baseSlide = {
|
|
38
|
+
id: z.string().regex(/^[a-z0-9][a-z0-9-]*$/).optional().describe("Stable slug for this slide (used by editors and agents to address it)."),
|
|
39
|
+
track: Track.optional(),
|
|
40
|
+
background: Background.optional(),
|
|
41
|
+
notes: z.string().optional().describe(
|
|
42
|
+
'Speaker notes. Blank lines split paragraphs; "- " lines become bullets. Put detail here, not on the slide.'
|
|
43
|
+
),
|
|
44
|
+
time: z.string().optional().describe('Timing cue shown in the notes panel, e.g. "4:00 \xB7 2 min".'),
|
|
45
|
+
step: z.number().int().positive().optional().describe(
|
|
46
|
+
"Journey step this slide belongs to. Draws the vertical STEP rail. Requires deck.journey."
|
|
47
|
+
),
|
|
48
|
+
stepDone: z.boolean().optional().describe("Show the step rail with a completed check mark."),
|
|
49
|
+
showJourney: z.boolean().optional().describe(
|
|
50
|
+
'Show the compact "you are here" journey strip at the top (uses deck.journey and step).'
|
|
51
|
+
),
|
|
52
|
+
hidden: z.boolean().optional().describe("Skip this slide when presenting and exporting (kept in the editor)."),
|
|
53
|
+
backgroundImage: Image.optional().describe(
|
|
54
|
+
"Full-bleed photo behind the slide. Best on title, section and statement slides."
|
|
55
|
+
),
|
|
56
|
+
overlay: z.enum(["dark", "light", "brand"]).optional().describe("Tint over the background image so text stays readable (default dark).")
|
|
8
57
|
};
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
)
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
58
|
+
|
|
59
|
+
// src/brand.ts
|
|
60
|
+
var Hex = z2.string().regex(/^#[0-9a-fA-F]{6}$/, "Use a 6-digit hex colour like #a91c22.");
|
|
61
|
+
var BrandPaletteSchema = z2.object({
|
|
62
|
+
primary: Hex.describe('Main accent: rails, bullets, highlights, "value" slides.'),
|
|
63
|
+
accent: Hex.describe('Second accent: "build" slides and secondary highlights.'),
|
|
64
|
+
dark: Hex.describe('Headings, navy grounds and "neutral" slides.')
|
|
65
|
+
}).strict();
|
|
66
|
+
var BrandProfileSchema = z2.object({
|
|
67
|
+
key: z2.string().regex(/^[a-z0-9][a-z0-9-]{1,40}$/, "Lowercase letters, numbers and dashes.").describe("Stable id used by decks (brand.profile)."),
|
|
68
|
+
name: z2.string().min(1).max(60).describe("Company or division name shown in the logo and tab title."),
|
|
69
|
+
tagline: z2.string().max(80).optional(),
|
|
70
|
+
logo: Image.optional().describe(
|
|
71
|
+
"Logo for light grounds. Omit to use the Classic Homes mark + name."
|
|
72
|
+
),
|
|
73
|
+
logoOnDark: Image.optional().describe("Logo for dark/solid grounds (white version)."),
|
|
74
|
+
favicon: Image.optional().describe("Square icon for the browser tab."),
|
|
75
|
+
palette: BrandPaletteSchema.optional().describe(
|
|
76
|
+
"Replaces the Classic Homes colours for this brand."
|
|
77
|
+
),
|
|
78
|
+
footer: z2.string().max(120).optional().describe("Default footer text on every slide.")
|
|
79
|
+
}).strict();
|
|
80
|
+
var DeckBrandSchema = z2.object({
|
|
81
|
+
profile: z2.string().optional().describe("Brand profile key; defaults to classic-homes."),
|
|
82
|
+
name: z2.string().min(1).max(60).optional().describe("Override the company name."),
|
|
83
|
+
tagline: z2.string().max(80).optional(),
|
|
84
|
+
logo: Image.optional().describe("Override the logo (light grounds)."),
|
|
85
|
+
logoOnDark: Image.optional().describe("Override the logo for dark grounds."),
|
|
86
|
+
favicon: Image.optional().describe("Override the browser-tab icon."),
|
|
87
|
+
footer: z2.string().max(120).optional().describe("Footer text on every slide.")
|
|
88
|
+
}).strict();
|
|
89
|
+
var CLASSIC_HOMES_PALETTE = {
|
|
90
|
+
primary: colors.red[700],
|
|
91
|
+
accent: colors.teal[700],
|
|
92
|
+
dark: colors.navy[700]
|
|
93
|
+
};
|
|
94
|
+
var CLASSIC_HOMES_BRAND = {
|
|
95
|
+
key: "classic-homes",
|
|
96
|
+
name: "Classic Homes",
|
|
97
|
+
palette: CLASSIC_HOMES_PALETTE
|
|
98
|
+
};
|
|
99
|
+
function resolveBrand(deckBrand, brands = []) {
|
|
100
|
+
const all = [CLASSIC_HOMES_BRAND, ...brands];
|
|
101
|
+
const key = deckBrand?.profile ?? CLASSIC_HOMES_BRAND.key;
|
|
102
|
+
const profile = [...all].reverse().find((b) => b.key === key) ?? CLASSIC_HOMES_BRAND;
|
|
103
|
+
const palette = profile.palette ?? CLASSIC_HOMES_PALETTE;
|
|
104
|
+
return {
|
|
105
|
+
key: profile.key,
|
|
106
|
+
name: deckBrand?.name ?? profile.name,
|
|
107
|
+
tagline: deckBrand?.tagline ?? profile.tagline,
|
|
108
|
+
footer: deckBrand?.footer ?? profile.footer,
|
|
109
|
+
logo: deckBrand?.logo ?? profile.logo,
|
|
110
|
+
logoOnDark: deckBrand?.logoOnDark ?? profile.logoOnDark,
|
|
111
|
+
favicon: deckBrand?.favicon ?? profile.favicon,
|
|
112
|
+
palette,
|
|
113
|
+
stock: palette.primary === CLASSIC_HOMES_PALETTE.primary && palette.accent === CLASSIC_HOMES_PALETTE.accent && palette.dark === CLASSIC_HOMES_PALETTE.dark
|
|
114
|
+
};
|
|
40
115
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
116
|
+
var PAPER = "#faf9f6";
|
|
117
|
+
function checkPalette(palette) {
|
|
118
|
+
const issues = [];
|
|
119
|
+
for (const field of ["primary", "accent", "dark"]) {
|
|
120
|
+
const c = palette[field];
|
|
121
|
+
const onPaper = getContrastRatio(c, PAPER);
|
|
122
|
+
const withWhite = getContrastRatio("#ffffff", c);
|
|
123
|
+
if (onPaper < 4.5 || withWhite < 4.5) {
|
|
124
|
+
issues.push({
|
|
125
|
+
field,
|
|
126
|
+
message: `${field} ${c} is too light for text (${onPaper.toFixed(1)}:1 on paper, ${withWhite.toFixed(1)}:1 under white text; both need 4.5:1). Pick a darker shade.`
|
|
127
|
+
});
|
|
47
128
|
}
|
|
48
|
-
|
|
49
|
-
|
|
129
|
+
}
|
|
130
|
+
return issues;
|
|
131
|
+
}
|
|
132
|
+
function validateBrandProfile(input) {
|
|
133
|
+
const parsed = BrandProfileSchema.safeParse(input);
|
|
134
|
+
if (!parsed.success) {
|
|
135
|
+
return {
|
|
136
|
+
ok: false,
|
|
137
|
+
issues: parsed.error.issues.map((i) => ({ path: i.path, message: i.message }))
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
const contrast = parsed.data.palette ? checkPalette(parsed.data.palette) : [];
|
|
141
|
+
if (contrast.length) {
|
|
142
|
+
return {
|
|
143
|
+
ok: false,
|
|
144
|
+
issues: contrast.map((c) => ({ path: ["palette", c.field], message: c.message }))
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
return { ok: true, profile: parsed.data };
|
|
50
148
|
}
|
|
51
149
|
|
|
52
150
|
// src/theme.ts
|
|
53
|
-
import { colors } from "@classic-homes/theme-tokens";
|
|
151
|
+
import { colors as colors2 } from "@classic-homes/theme-tokens";
|
|
54
152
|
|
|
55
153
|
// src/generated/fonts.ts
|
|
56
154
|
var FONTS = {
|
|
@@ -65,29 +163,33 @@ var FONTS = {
|
|
|
65
163
|
|
|
66
164
|
// src/theme.ts
|
|
67
165
|
var PALETTE = {
|
|
68
|
-
red:
|
|
69
|
-
redLogo:
|
|
70
|
-
teal:
|
|
166
|
+
red: colors2.red[700],
|
|
167
|
+
redLogo: colors2.red[500],
|
|
168
|
+
teal: colors2.teal[700],
|
|
71
169
|
// brand teal 500 fails AA as small text on paper; 700 passes
|
|
72
|
-
tealFill:
|
|
73
|
-
navy:
|
|
74
|
-
plum:
|
|
75
|
-
sage:
|
|
76
|
-
stone:
|
|
77
|
-
grey:
|
|
78
|
-
ink:
|
|
79
|
-
inkSoft:
|
|
80
|
-
inkFaint:
|
|
170
|
+
tealFill: colors2.teal[700],
|
|
171
|
+
navy: colors2.navy[700],
|
|
172
|
+
plum: colors2.plum[700],
|
|
173
|
+
sage: colors2.sage[600],
|
|
174
|
+
stone: colors2.stone[700],
|
|
175
|
+
grey: colors2.gray[600],
|
|
176
|
+
ink: colors2.navy[700],
|
|
177
|
+
inkSoft: colors2.gray[600],
|
|
178
|
+
inkFaint: colors2.gray[500]
|
|
81
179
|
};
|
|
82
|
-
function tokensCss() {
|
|
180
|
+
function tokensCss(palette) {
|
|
83
181
|
const p = PALETTE;
|
|
182
|
+
const primary = palette?.primary ?? p.red;
|
|
183
|
+
const accent = palette?.accent ?? p.teal;
|
|
184
|
+
const dark = palette?.dark ?? p.navy;
|
|
185
|
+
const custom = !!palette && palette.primary !== p.red;
|
|
84
186
|
return `:root{color-scheme:light;
|
|
85
187
|
--paper:hsl(40 20% 97.5%);--paper-2:#fff;--paper-3:hsl(60 11% 96%);
|
|
86
|
-
--ink:${
|
|
188
|
+
--ink:${dark};--ink-soft:${p.inkSoft};--ink-faint:${p.inkFaint};
|
|
87
189
|
--line:hsl(210 12% 80%);--line-strong:hsl(200 14% 55%);--line-faint:hsl(210 14% 90%);
|
|
88
|
-
--red:${
|
|
89
|
-
--teal:${
|
|
90
|
-
--navy:${
|
|
190
|
+
--red:${primary};--red-logo:${custom ? primary : p.redLogo};--red-soft:${custom ? `color-mix(in srgb,${primary} 8%,#fff)` : colors2.red[50]};
|
|
191
|
+
--teal:${accent};--teal-fill:${accent};--teal-soft:${custom ? `color-mix(in srgb,${accent} 8%,#fff)` : colors2.teal[50]};
|
|
192
|
+
--navy:${dark};--slate:${palette ? `color-mix(in srgb,${dark} 75%,#fff)` : colors2.navy[600]};--plum:${p.plum};--sage:${p.sage};--stone:${p.stone};--grey:${p.grey};
|
|
91
193
|
--grid:hsl(196 30% 20% / .03);
|
|
92
194
|
--shadow:0 1px 2px hsl(196 25% 20% / .04),0 6px 18px -10px hsl(196 25% 20% / .12);
|
|
93
195
|
--font-display:"Spectral",Georgia,"Times New Roman",serif;
|
|
@@ -114,60 +216,457 @@ var LOGO_PATHS = [
|
|
|
114
216
|
"M14.202 22.5c-.1-.3-.5-1.4-.6-2.3 0-.2.6-.5.7.1.1.6.4 1.6.6 1.9.3.5-.4.9-.7.3z",
|
|
115
217
|
"M10.302 22.5c.1-.3.5-1.4.6-2.2 0-.3-.5-.6-.7 0-.1.6-.4 1.6-.6 1.9-.3.5.4.9.7.3z"
|
|
116
218
|
];
|
|
117
|
-
var
|
|
219
|
+
var LOGO_MARK_SVG = `<svg viewBox="0 0 25 25" aria-hidden="true"><g class="logo-mark">${LOGO_PATHS.map(
|
|
118
220
|
(d) => `<path d="${d}"/>`
|
|
119
|
-
).join("")}</g></svg
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
221
|
+
).join("")}</g></svg>`;
|
|
222
|
+
var LOGO_SVG = logoLockup("Classic Homes");
|
|
223
|
+
function logoLockup(name, withMark = true) {
|
|
224
|
+
const safe = name.replace(/[&<>"']/g, (c) => `&#${c.charCodeAt(0)};`);
|
|
225
|
+
return `<span class="logo${withMark ? "" : " logo--word"}" role="img" aria-label="${safe}">${withMark ? LOGO_MARK_SVG : ""}<span class="logo-word" aria-hidden="true">${safe}</span></span>`;
|
|
226
|
+
}
|
|
123
227
|
|
|
124
|
-
// src/
|
|
125
|
-
import { z } from "zod";
|
|
126
|
-
var
|
|
127
|
-
"
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
);
|
|
147
|
-
var Item = z.preprocess(
|
|
148
|
-
(v) => typeof v === "string" ? { text: v } : v,
|
|
149
|
-
z.object({
|
|
150
|
-
text: Text,
|
|
151
|
-
sub: Text.optional().describe("Smaller secondary line under the item.")
|
|
152
|
-
})
|
|
153
|
-
);
|
|
154
|
-
var Marker = z.enum(["square", "check"]).default("square").describe("Bullet style.");
|
|
155
|
-
var baseSlide = {
|
|
156
|
-
id: z.string().regex(/^[a-z0-9][a-z0-9-]*$/).optional().describe("Stable slug for this slide (used by editors and agents to address it)."),
|
|
157
|
-
track: Track.optional(),
|
|
158
|
-
background: Background.optional(),
|
|
159
|
-
notes: z.string().optional().describe(
|
|
160
|
-
'Speaker notes. Blank lines split paragraphs; "- " lines become bullets. Put detail here, not on the slide.'
|
|
228
|
+
// src/charts.ts
|
|
229
|
+
import { z as z3 } from "zod";
|
|
230
|
+
var CHART_TYPES = [
|
|
231
|
+
"column",
|
|
232
|
+
"bar",
|
|
233
|
+
"stacked-column",
|
|
234
|
+
"stacked-bar",
|
|
235
|
+
"line",
|
|
236
|
+
"area",
|
|
237
|
+
"pie",
|
|
238
|
+
"donut",
|
|
239
|
+
"funnel",
|
|
240
|
+
"gauge",
|
|
241
|
+
"scatter",
|
|
242
|
+
"radar"
|
|
243
|
+
];
|
|
244
|
+
var ChartSeries = z3.object({
|
|
245
|
+
name: z3.string().min(1).max(40),
|
|
246
|
+
values: z3.array(z3.number()).min(1).max(40)
|
|
247
|
+
});
|
|
248
|
+
var ChartSpec = z3.object({
|
|
249
|
+
type: z3.enum(CHART_TYPES).describe(
|
|
250
|
+
"column/bar compare categories; stacked-* show parts of a whole per category; line/area show change over time; pie/donut show shares (one series); funnel shows drop-off; gauge shows one value against a max; scatter pairs two series; radar compares profiles."
|
|
161
251
|
),
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
"
|
|
252
|
+
labels: z3.array(z3.string().max(40)).min(1).max(40).describe("Category labels (x-axis, slices, stages). One per value."),
|
|
253
|
+
series: z3.array(ChartSeries).min(1).max(6).describe(
|
|
254
|
+
"Data series; each has one value per label. Pie, donut, funnel and gauge use the first series."
|
|
165
255
|
),
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
)
|
|
256
|
+
format: z3.enum(["number", "percent", "currency", "compact"]).default("number").describe("How values are shown. percent expects 0\u2013100."),
|
|
257
|
+
decimals: z3.number().int().min(0).max(3).default(0),
|
|
258
|
+
unit: z3.string().max(16).optional().describe('Suffix after values, e.g. "homes", "days".'),
|
|
259
|
+
legend: z3.enum(["auto", "show", "hide"]).default("auto"),
|
|
260
|
+
labelsOnData: z3.boolean().default(true).describe("Print values on bars, slices and points."),
|
|
261
|
+
highlight: z3.string().optional().describe("A label to emphasise; other categories are muted."),
|
|
262
|
+
max: z3.number().optional().describe("Axis or gauge maximum.")
|
|
263
|
+
}).strict();
|
|
264
|
+
function chartIssues(spec) {
|
|
265
|
+
const out = [];
|
|
266
|
+
spec.series.forEach((s) => {
|
|
267
|
+
if (s.values.length !== spec.labels.length) {
|
|
268
|
+
out.push(
|
|
269
|
+
`Series "${s.name}" has ${s.values.length} values but there are ${spec.labels.length} labels.`
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
if (spec.type === "scatter" && spec.series.length < 2)
|
|
274
|
+
out.push("Scatter needs two series (x and y).");
|
|
275
|
+
if (spec.type === "gauge" && spec.max === void 0) out.push("Gauge needs a max.");
|
|
276
|
+
return out;
|
|
277
|
+
}
|
|
278
|
+
function chartWarnings(spec) {
|
|
279
|
+
const out = [];
|
|
280
|
+
if (["pie", "donut"].includes(spec.type) && spec.labels.length > 7) {
|
|
281
|
+
out.push(
|
|
282
|
+
`${spec.labels.length} slices is hard to read; group small ones into "Other" or use a bar chart.`
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
if (["pie", "donut", "funnel", "gauge"].includes(spec.type) && spec.series.length > 1) {
|
|
286
|
+
out.push(`${spec.type} charts only show the first series.`);
|
|
287
|
+
}
|
|
288
|
+
if (spec.labels.length > 16 && ["column", "bar", "stacked-column", "stacked-bar"].includes(spec.type)) {
|
|
289
|
+
out.push("More than 16 categories crowds the axis; consider a line chart or fewer categories.");
|
|
290
|
+
}
|
|
291
|
+
return out;
|
|
292
|
+
}
|
|
293
|
+
function formatValue(v, spec) {
|
|
294
|
+
const opts = { minimumFractionDigits: spec.decimals, maximumFractionDigits: spec.decimals };
|
|
295
|
+
let s;
|
|
296
|
+
switch (spec.format) {
|
|
297
|
+
case "percent":
|
|
298
|
+
s = `${v.toLocaleString("en-US", opts)}%`;
|
|
299
|
+
break;
|
|
300
|
+
case "currency":
|
|
301
|
+
s = v.toLocaleString("en-US", { style: "currency", currency: "USD", ...opts });
|
|
302
|
+
break;
|
|
303
|
+
case "compact":
|
|
304
|
+
s = v.toLocaleString("en-US", {
|
|
305
|
+
notation: "compact",
|
|
306
|
+
maximumFractionDigits: Math.max(1, spec.decimals)
|
|
307
|
+
});
|
|
308
|
+
break;
|
|
309
|
+
default:
|
|
310
|
+
s = v.toLocaleString("en-US", opts);
|
|
311
|
+
}
|
|
312
|
+
return spec.unit ? `${s} ${spec.unit}` : s;
|
|
313
|
+
}
|
|
314
|
+
function chartColors(palette) {
|
|
315
|
+
const p = palette ?? { primary: PALETTE.red, accent: PALETTE.teal, dark: PALETTE.navy };
|
|
316
|
+
return [p.primary, p.accent, p.dark, PALETTE.sage, PALETTE.plum, PALETTE.stone];
|
|
317
|
+
}
|
|
318
|
+
var MUTED = "#c9ccc6";
|
|
319
|
+
var TEXT = "#4f4f4f";
|
|
320
|
+
var AXIS = "#d1d5db";
|
|
321
|
+
function chartOption(spec, palette) {
|
|
322
|
+
const colors3 = chartColors(palette);
|
|
323
|
+
const fmt = (v) => formatValue(v, spec);
|
|
324
|
+
const muted = (label, color) => spec.highlight && spec.highlight !== label ? MUTED : color;
|
|
325
|
+
const showLegend = spec.legend === "show" || spec.legend === "auto" && spec.series.length > 1;
|
|
326
|
+
const textStyle = { fontFamily: "Figtree, system-ui, sans-serif", color: TEXT, fontSize: 15 };
|
|
327
|
+
const base = {
|
|
328
|
+
animation: false,
|
|
329
|
+
color: colors3,
|
|
330
|
+
textStyle,
|
|
331
|
+
legend: showLegend ? { top: 0, textStyle: { ...textStyle, fontSize: 14 }, itemGap: 18 } : { show: false }
|
|
332
|
+
};
|
|
333
|
+
const first = spec.series[0];
|
|
334
|
+
const labelStyle = {
|
|
335
|
+
show: spec.labelsOnData,
|
|
336
|
+
color: TEXT,
|
|
337
|
+
fontSize: 13,
|
|
338
|
+
formatter: (p) => fmt(p.value)
|
|
339
|
+
};
|
|
340
|
+
if (spec.type === "pie" || spec.type === "donut") {
|
|
341
|
+
return {
|
|
342
|
+
...base,
|
|
343
|
+
legend: spec.legend === "show" ? base.legend : { show: false },
|
|
344
|
+
series: [
|
|
345
|
+
{
|
|
346
|
+
type: "pie",
|
|
347
|
+
radius: spec.type === "donut" ? ["46%", "72%"] : [0, "72%"],
|
|
348
|
+
center: ["50%", "54%"],
|
|
349
|
+
data: spec.labels.map((name, i) => ({
|
|
350
|
+
name,
|
|
351
|
+
value: first.values[i],
|
|
352
|
+
itemStyle: { color: muted(name, colors3[i % colors3.length]) }
|
|
353
|
+
})),
|
|
354
|
+
label: {
|
|
355
|
+
show: true,
|
|
356
|
+
color: TEXT,
|
|
357
|
+
fontSize: 14,
|
|
358
|
+
formatter: (p) => spec.labelsOnData ? `${p.name}
|
|
359
|
+
${fmt(p.value)}` : p.name
|
|
360
|
+
},
|
|
361
|
+
itemStyle: { borderColor: "#fff", borderWidth: 2 }
|
|
362
|
+
}
|
|
363
|
+
]
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
if (spec.type === "funnel") {
|
|
367
|
+
return {
|
|
368
|
+
...base,
|
|
369
|
+
legend: { show: false },
|
|
370
|
+
series: [
|
|
371
|
+
{
|
|
372
|
+
type: "funnel",
|
|
373
|
+
left: "12%",
|
|
374
|
+
width: "76%",
|
|
375
|
+
top: 16,
|
|
376
|
+
bottom: 16,
|
|
377
|
+
sort: "descending",
|
|
378
|
+
gap: 3,
|
|
379
|
+
data: spec.labels.map((name, i) => ({
|
|
380
|
+
name,
|
|
381
|
+
value: first.values[i],
|
|
382
|
+
itemStyle: { color: muted(name, colors3[i % colors3.length]) }
|
|
383
|
+
})),
|
|
384
|
+
label: {
|
|
385
|
+
show: true,
|
|
386
|
+
position: "inside",
|
|
387
|
+
color: "#fff",
|
|
388
|
+
fontSize: 14,
|
|
389
|
+
formatter: (p) => spec.labelsOnData ? `${p.name} ${fmt(p.value)}` : p.name
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
]
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
if (spec.type === "gauge") {
|
|
396
|
+
return {
|
|
397
|
+
...base,
|
|
398
|
+
series: [
|
|
399
|
+
{
|
|
400
|
+
type: "gauge",
|
|
401
|
+
min: 0,
|
|
402
|
+
max: spec.max ?? 100,
|
|
403
|
+
progress: { show: true, width: 22, itemStyle: { color: colors3[0] } },
|
|
404
|
+
axisLine: { lineStyle: { width: 22, color: [[1, "#e5e7eb"]] } },
|
|
405
|
+
axisTick: { show: false },
|
|
406
|
+
splitLine: { show: false },
|
|
407
|
+
axisLabel: { show: false },
|
|
408
|
+
pointer: { show: false },
|
|
409
|
+
anchor: { show: false },
|
|
410
|
+
title: { offsetCenter: [0, "30%"], fontSize: 16, color: TEXT },
|
|
411
|
+
detail: {
|
|
412
|
+
valueAnimation: false,
|
|
413
|
+
offsetCenter: [0, "-5%"],
|
|
414
|
+
fontSize: 44,
|
|
415
|
+
fontWeight: 600,
|
|
416
|
+
color: colors3[2],
|
|
417
|
+
formatter: (v) => fmt(v)
|
|
418
|
+
},
|
|
419
|
+
data: [{ value: first.values[0], name: spec.labels[0] }]
|
|
420
|
+
}
|
|
421
|
+
]
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
if (spec.type === "radar") {
|
|
425
|
+
const max = spec.max ?? Math.max(...spec.series.flatMap((s) => s.values)) * 1.1;
|
|
426
|
+
return {
|
|
427
|
+
...base,
|
|
428
|
+
radar: {
|
|
429
|
+
indicator: spec.labels.map((name) => ({ name, max })),
|
|
430
|
+
axisName: { color: TEXT, fontSize: 14 },
|
|
431
|
+
splitLine: { lineStyle: { color: AXIS } },
|
|
432
|
+
splitArea: { show: false },
|
|
433
|
+
axisLine: { lineStyle: { color: AXIS } }
|
|
434
|
+
},
|
|
435
|
+
series: [
|
|
436
|
+
{
|
|
437
|
+
type: "radar",
|
|
438
|
+
data: spec.series.map((s, i) => ({
|
|
439
|
+
name: s.name,
|
|
440
|
+
value: s.values,
|
|
441
|
+
areaStyle: { opacity: 0.12 },
|
|
442
|
+
lineStyle: { width: 2.5, color: colors3[i % colors3.length] },
|
|
443
|
+
itemStyle: { color: colors3[i % colors3.length] }
|
|
444
|
+
}))
|
|
445
|
+
}
|
|
446
|
+
]
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
if (spec.type === "scatter") {
|
|
450
|
+
const [xs, ys] = spec.series;
|
|
451
|
+
return {
|
|
452
|
+
...base,
|
|
453
|
+
legend: { show: false },
|
|
454
|
+
grid: { left: 70, right: 30, top: 30, bottom: 60, containLabel: true },
|
|
455
|
+
xAxis: {
|
|
456
|
+
type: "value",
|
|
457
|
+
name: xs.name,
|
|
458
|
+
nameLocation: "middle",
|
|
459
|
+
nameGap: 34,
|
|
460
|
+
axisLine: { lineStyle: { color: AXIS } },
|
|
461
|
+
splitLine: { lineStyle: { color: "#eef0ec" } }
|
|
462
|
+
},
|
|
463
|
+
yAxis: {
|
|
464
|
+
type: "value",
|
|
465
|
+
name: ys.name,
|
|
466
|
+
nameLocation: "middle",
|
|
467
|
+
nameGap: 50,
|
|
468
|
+
axisLine: { lineStyle: { color: AXIS } },
|
|
469
|
+
splitLine: { lineStyle: { color: "#eef0ec" } }
|
|
470
|
+
},
|
|
471
|
+
series: [
|
|
472
|
+
{
|
|
473
|
+
type: "scatter",
|
|
474
|
+
symbolSize: 16,
|
|
475
|
+
data: spec.labels.map((name, i) => ({
|
|
476
|
+
name,
|
|
477
|
+
value: [xs.values[i], ys.values[i]],
|
|
478
|
+
itemStyle: { color: muted(name, colors3[0]) }
|
|
479
|
+
})),
|
|
480
|
+
label: {
|
|
481
|
+
show: spec.labelsOnData,
|
|
482
|
+
formatter: (p) => p.name,
|
|
483
|
+
position: "top",
|
|
484
|
+
color: TEXT,
|
|
485
|
+
fontSize: 12
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
]
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
const horizontal = spec.type === "bar" || spec.type === "stacked-bar";
|
|
492
|
+
const stacked = spec.type.startsWith("stacked");
|
|
493
|
+
const isLine = spec.type === "line" || spec.type === "area";
|
|
494
|
+
const category = {
|
|
495
|
+
type: "category",
|
|
496
|
+
data: spec.labels,
|
|
497
|
+
axisLine: { lineStyle: { color: AXIS } },
|
|
498
|
+
axisTick: { show: false },
|
|
499
|
+
axisLabel: { color: TEXT, fontSize: 14, interval: 0, hideOverlap: true },
|
|
500
|
+
boundaryGap: !isLine
|
|
501
|
+
};
|
|
502
|
+
const value = {
|
|
503
|
+
type: "value",
|
|
504
|
+
max: spec.max,
|
|
505
|
+
axisLine: { show: false },
|
|
506
|
+
splitLine: { lineStyle: { color: "#eef0ec" } },
|
|
507
|
+
axisLabel: {
|
|
508
|
+
color: "#7a7a7a",
|
|
509
|
+
fontSize: 13,
|
|
510
|
+
formatter: (v) => formatValue(v, { ...spec, unit: void 0 })
|
|
511
|
+
}
|
|
512
|
+
};
|
|
513
|
+
return {
|
|
514
|
+
...base,
|
|
515
|
+
grid: { left: 16, right: 24, top: showLegend ? 48 : 20, bottom: 12, containLabel: true },
|
|
516
|
+
xAxis: horizontal ? value : category,
|
|
517
|
+
yAxis: horizontal ? { ...category, inverse: true } : value,
|
|
518
|
+
series: spec.series.map((s, si) => {
|
|
519
|
+
const color = colors3[si % colors3.length];
|
|
520
|
+
return {
|
|
521
|
+
type: isLine ? "line" : "bar",
|
|
522
|
+
name: s.name,
|
|
523
|
+
stack: stacked ? "total" : void 0,
|
|
524
|
+
smooth: false,
|
|
525
|
+
symbolSize: 8,
|
|
526
|
+
lineStyle: isLine ? { width: 3 } : void 0,
|
|
527
|
+
areaStyle: spec.type === "area" ? { opacity: 0.18 } : void 0,
|
|
528
|
+
barMaxWidth: 56,
|
|
529
|
+
itemStyle: stacked || isLine ? { color } : void 0,
|
|
530
|
+
data: s.values.map((v, i) => ({
|
|
531
|
+
value: v,
|
|
532
|
+
itemStyle: stacked || isLine ? void 0 : {
|
|
533
|
+
color: muted(spec.labels[i], color),
|
|
534
|
+
borderRadius: horizontal ? [0, 4, 4, 0] : [4, 4, 0, 0]
|
|
535
|
+
}
|
|
536
|
+
})),
|
|
537
|
+
label: {
|
|
538
|
+
...labelStyle,
|
|
539
|
+
show: spec.labelsOnData && (!stacked || spec.series.length <= 3),
|
|
540
|
+
position: stacked ? "inside" : horizontal ? "right" : "top",
|
|
541
|
+
color: stacked ? "#fff" : TEXT
|
|
542
|
+
}
|
|
543
|
+
};
|
|
544
|
+
})
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
var renderer;
|
|
548
|
+
function loadChartRenderer() {
|
|
549
|
+
renderer ??= import("./echarts-render-2QVSBZ6V.js").then((m) => m.createRenderer());
|
|
550
|
+
return renderer;
|
|
551
|
+
}
|
|
552
|
+
async function renderChartSvg(spec, opts = {}) {
|
|
553
|
+
const width = opts.width ?? 960;
|
|
554
|
+
const height = opts.height ?? 520;
|
|
555
|
+
const r = await loadChartRenderer();
|
|
556
|
+
const svg = r.render(chartOption(spec, opts.palette), width, height);
|
|
557
|
+
const label = `${opts.title ? `${opts.title}: ` : ""}${describeChart(spec)}`.replace(
|
|
558
|
+
/[&<>"]/g,
|
|
559
|
+
(c) => `&#${c.charCodeAt(0)};`
|
|
560
|
+
);
|
|
561
|
+
return svg.replace(/^<svg([^>]*?) width="\d+" height="\d+"/, "<svg$1").replace(/^<svg/, `<svg role="img" aria-label="${label}" preserveAspectRatio="xMidYMid meet"`);
|
|
562
|
+
}
|
|
563
|
+
function describeChart(spec) {
|
|
564
|
+
const parts = spec.series.slice(0, ["pie", "donut", "funnel", "gauge"].includes(spec.type) ? 1 : 6).map(
|
|
565
|
+
(s) => `${s.name}: ${spec.labels.map((l, i) => `${l} ${formatValue(s.values[i] ?? 0, spec)}`).join(", ")}`
|
|
566
|
+
);
|
|
567
|
+
return `${spec.type.replace("-", " ")} chart. ${parts.join(". ")}.`;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// src/inline.ts
|
|
571
|
+
var ESCAPES = {
|
|
572
|
+
"&": "&",
|
|
573
|
+
"<": "<",
|
|
574
|
+
">": ">",
|
|
575
|
+
'"': """,
|
|
576
|
+
"'": "'"
|
|
170
577
|
};
|
|
578
|
+
function escapeHtml(value) {
|
|
579
|
+
return value.replace(/[&<>"']/g, (c) => ESCAPES[c]);
|
|
580
|
+
}
|
|
581
|
+
function escapeAttr(value) {
|
|
582
|
+
return escapeHtml(value);
|
|
583
|
+
}
|
|
584
|
+
var SAFE_URL = /^(https?:|mailto:|tel:|#|\/)/i;
|
|
585
|
+
function isSafeLinkUrl(url) {
|
|
586
|
+
return SAFE_URL.test(url);
|
|
587
|
+
}
|
|
588
|
+
function inline(text) {
|
|
589
|
+
if (!text) return "";
|
|
590
|
+
const codes = [];
|
|
591
|
+
let s = text.replace(/`([^`]+)`/g, (_, code) => {
|
|
592
|
+
codes.push(code);
|
|
593
|
+
return `\uE000${codes.length - 1}\uE000`;
|
|
594
|
+
});
|
|
595
|
+
const links = [];
|
|
596
|
+
s = s.replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, (match, label, url) => {
|
|
597
|
+
if (!isSafeLinkUrl(url)) return label;
|
|
598
|
+
links.push(`<a href="${escapeAttr(url)}">${formatEmphasis(escapeHtml(label))}</a>`);
|
|
599
|
+
return `\uE001${links.length - 1}\uE001`;
|
|
600
|
+
});
|
|
601
|
+
s = formatEmphasis(escapeHtml(s));
|
|
602
|
+
s = s.replace(/\r?\n/g, "<br>");
|
|
603
|
+
s = s.replace(/\uE001(\d+)\uE001/g, (_, i) => links[Number(i)]);
|
|
604
|
+
s = s.replace(
|
|
605
|
+
/\uE000(\d+)\uE000/g,
|
|
606
|
+
(_, i) => `<code>${escapeHtml(codes[Number(i)])}</code>`
|
|
607
|
+
);
|
|
608
|
+
return s;
|
|
609
|
+
}
|
|
610
|
+
function formatEmphasis(escaped) {
|
|
611
|
+
return escaped.replace(/\*\*(.+?)\*\*/g, "<b>$1</b>").replace(/(^|[^*])\*(?!\s)(.+?)(?<!\s)\*(?!\*)/g, '$1<em class="hl">$2</em>');
|
|
612
|
+
}
|
|
613
|
+
function prose(text) {
|
|
614
|
+
if (!text) return "";
|
|
615
|
+
return text.trim().split(/\n\s*\n/).map((para) => {
|
|
616
|
+
const lines = para.split(/\n/);
|
|
617
|
+
if (lines.every((l) => /^\s*[-*]\s+/.test(l))) {
|
|
618
|
+
return `<ul>${lines.map((l) => `<li>${inline(l.replace(/^\s*[-*]\s+/, ""))}</li>`).join("")}</ul>`;
|
|
619
|
+
}
|
|
620
|
+
return `<p>${inline(para)}</p>`;
|
|
621
|
+
}).join("");
|
|
622
|
+
}
|
|
623
|
+
function plain(text) {
|
|
624
|
+
if (!text) return "";
|
|
625
|
+
return text.replace(/`([^`]+)`/g, "$1").replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").replace(/\*\*(.+?)\*\*/g, "$1").replace(/\*(.+?)\*/g, "$1");
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// src/video.ts
|
|
629
|
+
var YT = /^(?:https:\/\/)?(?:www\.|m\.)?(?:youtube\.com\/(?:watch\?(?:.*&)?v=|embed\/|shorts\/|live\/)|youtu\.be\/)([\w-]{11})/;
|
|
630
|
+
var VIMEO = /^(?:https:\/\/)?(?:www\.|player\.)?vimeo\.com\/(?:video\/)?(\d{6,12})/;
|
|
631
|
+
function parseVideoUrl(url) {
|
|
632
|
+
const yt = YT.exec(url);
|
|
633
|
+
if (yt) {
|
|
634
|
+
return {
|
|
635
|
+
provider: "youtube",
|
|
636
|
+
id: yt[1],
|
|
637
|
+
watchUrl: `https://www.youtube.com/watch?v=${yt[1]}`,
|
|
638
|
+
poster: `https://i.ytimg.com/vi/${yt[1]}/hqdefault.jpg`
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
const vm = VIMEO.exec(url);
|
|
642
|
+
if (vm) return { provider: "vimeo", id: vm[1], watchUrl: `https://vimeo.com/${vm[1]}` };
|
|
643
|
+
return void 0;
|
|
644
|
+
}
|
|
645
|
+
function videoEmbedUrl(ref, o) {
|
|
646
|
+
if (ref.provider === "youtube") {
|
|
647
|
+
const q2 = new URLSearchParams({
|
|
648
|
+
rel: "0",
|
|
649
|
+
modestbranding: "1",
|
|
650
|
+
playsinline: "1",
|
|
651
|
+
enablejsapi: "1"
|
|
652
|
+
});
|
|
653
|
+
if (o.start) q2.set("start", String(o.start));
|
|
654
|
+
if (o.end) q2.set("end", String(o.end));
|
|
655
|
+
if (o.muted) q2.set("mute", "1");
|
|
656
|
+
if (o.captions) q2.set("cc_load_policy", "1");
|
|
657
|
+
if (o.autoplay) q2.set("autoplay", "1");
|
|
658
|
+
return `https://www.youtube-nocookie.com/embed/${ref.id}?${q2}`;
|
|
659
|
+
}
|
|
660
|
+
const q = new URLSearchParams({ dnt: "1", title: "0", byline: "0", portrait: "0" });
|
|
661
|
+
if (o.muted) q.set("muted", "1");
|
|
662
|
+
if (o.autoplay) q.set("autoplay", "1");
|
|
663
|
+
if (o.captions) q.set("texttrack", "en");
|
|
664
|
+
const hash = o.start ? `#t=${o.start}s` : "";
|
|
665
|
+
return `https://player.vimeo.com/video/${ref.id}?${q}${hash}`;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// src/layouts/text.ts
|
|
669
|
+
import { z as z4 } from "zod";
|
|
171
670
|
|
|
172
671
|
// src/layouts/parts.ts
|
|
173
672
|
function eyebrow(text, sub) {
|
|
@@ -188,7 +687,7 @@ function items(list, marker = "square") {
|
|
|
188
687
|
if (!list?.length) return "";
|
|
189
688
|
const cls = marker === "check" ? "clean check" : "clean";
|
|
190
689
|
return `<ul class="${cls}">${list.map(
|
|
191
|
-
(i) => `<li>${inline(i.text)}${i.sub ? `<span class="sub">${inline(i.sub)}</span>` : ""}</li>`
|
|
690
|
+
(i) => `<li class="b-item">${inline(i.text)}${i.sub ? `<span class="sub">${inline(i.sub)}</span>` : ""}</li>`
|
|
192
691
|
).join("")}</ul>`;
|
|
193
692
|
}
|
|
194
693
|
function img(image2, ctx, cls = "") {
|
|
@@ -249,9 +748,9 @@ var title = defineLayout({
|
|
|
249
748
|
title: Text,
|
|
250
749
|
lead: Text.optional(),
|
|
251
750
|
image: Image.optional().describe("Hero photo under the title."),
|
|
252
|
-
meta:
|
|
253
|
-
logo:
|
|
254
|
-
align:
|
|
751
|
+
meta: z4.array(Text).max(4).optional().describe("Short facts on one line, e.g. URL, date, presenter."),
|
|
752
|
+
logo: z4.boolean().default(true).describe("Show the Classic Homes logo."),
|
|
753
|
+
align: z4.enum(["left", "center"]).default("left")
|
|
255
754
|
},
|
|
256
755
|
defaults: { track: "neutral" },
|
|
257
756
|
example: {
|
|
@@ -267,7 +766,7 @@ var title = defineLayout({
|
|
|
267
766
|
],
|
|
268
767
|
render: (s, ctx) => ({
|
|
269
768
|
classes: ["title-wrap", `align-${s.align}`],
|
|
270
|
-
html: `${s.logo ? `<div class="brand-logo">${
|
|
769
|
+
html: `${s.logo ? `<div class="brand-logo">${ctx.logo()}</div>` : ""}${s.kicker ? `<p class="kicker">${inline(s.kicker)}</p>` : ""}${heading(s.title, 1)}${lead(s.lead)}${s.image ? `<div class="hero">${img(s.image, ctx)}</div>` : ""}${s.meta?.length ? `<div class="title-meta">${s.meta.map((m) => `<span>${inline(m)}</span>`).join("")}</div>` : ""}`
|
|
271
770
|
})
|
|
272
771
|
});
|
|
273
772
|
var section = defineLayout({
|
|
@@ -278,10 +777,10 @@ var section = defineLayout({
|
|
|
278
777
|
kicker: Text.optional(),
|
|
279
778
|
title: Text,
|
|
280
779
|
lead: Text.optional(),
|
|
281
|
-
images:
|
|
780
|
+
images: z4.array(Image).max(12).optional().describe(
|
|
282
781
|
"Optional photos under the title (e.g. the team for this step). Rows wrap after 6."
|
|
283
782
|
),
|
|
284
|
-
imageAspect:
|
|
783
|
+
imageAspect: z4.enum(["portrait", "landscape", "native"]).default("portrait").describe(
|
|
285
784
|
"portrait suits headshots; landscape/native suit a single scene photo (shown wide)."
|
|
286
785
|
)
|
|
287
786
|
},
|
|
@@ -313,8 +812,8 @@ var statement = defineLayout({
|
|
|
313
812
|
eyebrowSub: Text.optional(),
|
|
314
813
|
title: Text,
|
|
315
814
|
lead: Text.optional(),
|
|
316
|
-
style:
|
|
317
|
-
logo:
|
|
815
|
+
style: z4.enum(["headline", "display"]).default("headline").describe("display = large italic serif."),
|
|
816
|
+
logo: z4.boolean().default(false)
|
|
318
817
|
},
|
|
319
818
|
example: {
|
|
320
819
|
layout: "statement",
|
|
@@ -326,8 +825,8 @@ var statement = defineLayout({
|
|
|
326
825
|
...maxLen(s.title, 110, ["title"], "Headline"),
|
|
327
826
|
...maxLen(s.lead, 240, ["lead"], "Lead")
|
|
328
827
|
],
|
|
329
|
-
render: (s) => ({
|
|
330
|
-
html: `${eyebrow(s.eyebrow, s.eyebrowSub)}${heading(s.title, s.style === "display" ? 1 : 2)}${lead(s.lead)}${s.logo ? `<div class="brand-logo end">${
|
|
828
|
+
render: (s, ctx) => ({
|
|
829
|
+
html: `${eyebrow(s.eyebrow, s.eyebrowSub)}${heading(s.title, s.style === "display" ? 1 : 2)}${lead(s.lead)}${s.logo ? `<div class="brand-logo end">${ctx.logo()}</div>` : ""}`
|
|
331
830
|
})
|
|
332
831
|
});
|
|
333
832
|
var bullets = defineLayout({
|
|
@@ -339,10 +838,11 @@ var bullets = defineLayout({
|
|
|
339
838
|
eyebrowSub: Text.optional(),
|
|
340
839
|
title: Text,
|
|
341
840
|
lead: Text.optional(),
|
|
342
|
-
items:
|
|
841
|
+
items: z4.array(Item).min(1),
|
|
343
842
|
marker: Marker,
|
|
344
|
-
columns:
|
|
345
|
-
footnote: Text.optional().describe("Closing line under the list.")
|
|
843
|
+
columns: z4.union([z4.literal(1), z4.literal(2)]).default(1),
|
|
844
|
+
footnote: Text.optional().describe("Closing line under the list."),
|
|
845
|
+
build: z4.boolean().default(false).describe("Reveal one bullet per click while presenting.")
|
|
346
846
|
},
|
|
347
847
|
example: {
|
|
348
848
|
layout: "bullets",
|
|
@@ -368,7 +868,7 @@ var bullets = defineLayout({
|
|
|
368
868
|
const half = Math.ceil(s.items.length / 2);
|
|
369
869
|
const list = s.columns === 2 ? `<div class="cols even">${items(s.items.slice(0, half), s.marker)}${items(s.items.slice(half), s.marker)}</div>` : items(s.items, s.marker);
|
|
370
870
|
return {
|
|
371
|
-
html: `${eyebrow(s.eyebrow, s.eyebrowSub)}${heading(s.title)}${lead(s.lead)}${list}${footnote(s.footnote)}`
|
|
871
|
+
html: `${eyebrow(s.eyebrow, s.eyebrowSub)}${heading(s.title)}${lead(s.lead)}${s.build ? `<div data-build>${list}</div>` : list}${footnote(s.footnote)}`
|
|
372
872
|
};
|
|
373
873
|
}
|
|
374
874
|
});
|
|
@@ -381,17 +881,18 @@ var cards = defineLayout({
|
|
|
381
881
|
eyebrowSub: Text.optional(),
|
|
382
882
|
title: Text,
|
|
383
883
|
lead: Text.optional(),
|
|
384
|
-
columns:
|
|
385
|
-
style:
|
|
386
|
-
cards:
|
|
387
|
-
|
|
884
|
+
columns: z4.number().int().min(2).max(4).default(3),
|
|
885
|
+
style: z4.enum(["card", "banner"]).default("card").describe("banner = coloured header band per card."),
|
|
886
|
+
cards: z4.array(
|
|
887
|
+
z4.object({
|
|
388
888
|
tag: Text.optional(),
|
|
389
889
|
title: Text,
|
|
390
890
|
body: Text.optional(),
|
|
391
891
|
image: Image.optional()
|
|
392
892
|
})
|
|
393
893
|
).min(1),
|
|
394
|
-
footnote: Text.optional().describe("Closing line under the cards.")
|
|
894
|
+
footnote: Text.optional().describe("Closing line under the cards."),
|
|
895
|
+
build: z4.boolean().default(false).describe("Reveal one card per click while presenting.")
|
|
395
896
|
},
|
|
396
897
|
example: {
|
|
397
898
|
layout: "cards",
|
|
@@ -416,8 +917,8 @@ var cards = defineLayout({
|
|
|
416
917
|
])
|
|
417
918
|
],
|
|
418
919
|
render: (s, ctx) => ({
|
|
419
|
-
html: `${eyebrow(s.eyebrow, s.eyebrowSub)}${heading(s.title)}${lead(s.lead)}<div class="cardgrid cards--${s.style}" style="--cols:${s.columns}">${s.cards.map(
|
|
420
|
-
(c) => `<div class="card">${c.image ? img(c.image, ctx, "card-img") : ""}${s.style === "banner" ? `<div class="card-band">${inline(c.tag ?? c.title)}</div><div class="card-b">${c.tag ? `<h3>${inline(c.title)}</h3>` : ""}${c.body ? `<p>${inline(c.body)}</p>` : ""}</div>` : `<div class="card-b">${c.tag ? `<span class="tag">${inline(c.tag)}</span>` : ""}<h3>${inline(
|
|
920
|
+
html: `${eyebrow(s.eyebrow, s.eyebrowSub)}${heading(s.title)}${lead(s.lead)}<div class="cardgrid cards--${s.style}"${s.build ? " data-build" : ""} style="--cols:${s.columns}">${s.cards.map(
|
|
921
|
+
(c) => `<div class="card b-item">${c.image ? img(c.image, ctx, "card-img") : ""}${s.style === "banner" ? `<div class="card-band">${inline(c.tag ?? c.title)}</div><div class="card-b">${c.tag ? `<h3>${inline(c.title)}</h3>` : ""}${c.body ? `<p>${inline(c.body)}</p>` : ""}</div>` : `<div class="card-b">${c.tag ? `<span class="tag">${inline(c.tag)}</span>` : ""}<h3>${inline(
|
|
421
922
|
c.title
|
|
422
923
|
)}</h3>${c.body ? `<p>${inline(c.body)}</p>` : ""}</div>`}</div>`
|
|
423
924
|
).join("")}</div>${footnote(s.footnote)}`
|
|
@@ -432,8 +933,15 @@ var stats = defineLayout({
|
|
|
432
933
|
eyebrowSub: Text.optional(),
|
|
433
934
|
title: Text,
|
|
434
935
|
lead: Text.optional(),
|
|
435
|
-
stats:
|
|
436
|
-
|
|
936
|
+
stats: z4.array(
|
|
937
|
+
z4.object({
|
|
938
|
+
value: z4.string().max(12).describe('Short: "60", "2 wks", "3\xD7".'),
|
|
939
|
+
label: Text,
|
|
940
|
+
trend: z4.enum(["up", "down", "flat"]).optional().describe("Arrow next to the value."),
|
|
941
|
+
delta: z4.string().max(16).optional().describe('Change text beside the arrow, e.g. "+12%".'),
|
|
942
|
+
good: z4.enum(["up", "down"]).default("up").describe("Which direction is good news (colours the arrow)."),
|
|
943
|
+
spark: z4.array(z4.number()).min(2).max(40).optional().describe("Recent values drawn as a tiny line under the number.")
|
|
944
|
+
})
|
|
437
945
|
).min(1)
|
|
438
946
|
},
|
|
439
947
|
example: {
|
|
@@ -443,7 +951,14 @@ var stats = defineLayout({
|
|
|
443
951
|
stats: [
|
|
444
952
|
{ value: "60", label: "days from contract to final selections" },
|
|
445
953
|
{ value: "14", label: "construction stages" },
|
|
446
|
-
{ value: "2", label: "site walkthroughs with your superintendent" }
|
|
954
|
+
{ value: "2", label: "site walkthroughs with your superintendent" },
|
|
955
|
+
{
|
|
956
|
+
value: "4.8",
|
|
957
|
+
label: "average homeowner rating",
|
|
958
|
+
trend: "up",
|
|
959
|
+
delta: "+0.3",
|
|
960
|
+
spark: [4.2, 4.4, 4.5, 4.8]
|
|
961
|
+
}
|
|
447
962
|
]
|
|
448
963
|
},
|
|
449
964
|
lint: (s) => [
|
|
@@ -452,7 +967,7 @@ var stats = defineLayout({
|
|
|
452
967
|
],
|
|
453
968
|
render: (s) => ({
|
|
454
969
|
html: `${eyebrow(s.eyebrow, s.eyebrowSub)}${heading(s.title)}${lead(s.lead)}<div class="stat-row">${s.stats.map(
|
|
455
|
-
(st) => `<div class="stat"><b>${inline(st.value)}</b
|
|
970
|
+
(st) => `<div class="stat"><b>${inline(st.value)}</b>${st.trend ? `<em class="trend trend--${st.trend === "flat" ? "flat" : st.trend === st.good ? "good" : "bad"}">${st.trend === "up" ? "\u25B2" : st.trend === "down" ? "\u25BC" : "\u25B6"}${st.delta ? ` ${inline(st.delta)}` : ""}</em>` : ""}${st.spark ? sparkline(st.spark) : ""}<span>${inline(st.label)}</span></div>`
|
|
456
971
|
).join("")}</div>`
|
|
457
972
|
})
|
|
458
973
|
});
|
|
@@ -465,12 +980,12 @@ var comparison = defineLayout({
|
|
|
465
980
|
eyebrowSub: Text.optional(),
|
|
466
981
|
title: Text,
|
|
467
982
|
lead: Text.optional(),
|
|
468
|
-
columns:
|
|
469
|
-
|
|
983
|
+
columns: z4.array(
|
|
984
|
+
z4.object({
|
|
470
985
|
heading: Text,
|
|
471
986
|
tone: Track.default("value"),
|
|
472
987
|
text: Text.optional(),
|
|
473
|
-
items:
|
|
988
|
+
items: z4.array(Item).optional(),
|
|
474
989
|
marker: Marker
|
|
475
990
|
})
|
|
476
991
|
).min(2).max(3)
|
|
@@ -513,32 +1028,528 @@ var quote = defineLayout({
|
|
|
513
1028
|
whenToUse: "A homeowner testimonial, a value statement, a line from leadership.",
|
|
514
1029
|
shape: {
|
|
515
1030
|
eyebrow: Text.optional(),
|
|
516
|
-
quote: Text,
|
|
517
|
-
cite: Text.optional(),
|
|
518
|
-
image: Image.optional().describe("Portrait of the person quoted.")
|
|
1031
|
+
quote: Text,
|
|
1032
|
+
cite: Text.optional(),
|
|
1033
|
+
image: Image.optional().describe("Portrait of the person quoted.")
|
|
1034
|
+
},
|
|
1035
|
+
example: {
|
|
1036
|
+
layout: "quote",
|
|
1037
|
+
quote: "They walked us through every step. We always knew what came next.",
|
|
1038
|
+
cite: "The Martinez family \xB7 Wolf Ranch"
|
|
1039
|
+
},
|
|
1040
|
+
lint: (s) => [
|
|
1041
|
+
...maxLen(s.quote, 240, ["quote"], "Quote"),
|
|
1042
|
+
...missingAlt(s.image ? [s.image] : [], ["image"])
|
|
1043
|
+
],
|
|
1044
|
+
render: (s, ctx) => ({
|
|
1045
|
+
html: `${eyebrow(s.eyebrow)}<div class="${s.image ? "cols quote-cols" : ""}"><blockquote class="quote">${inline(
|
|
1046
|
+
s.quote
|
|
1047
|
+
)}${s.cite ? `<cite>${inline(s.cite)}</cite>` : ""}</blockquote>${s.image ? `<div class="portrait">${img(s.image, ctx)}</div>` : ""}</div>`
|
|
1048
|
+
})
|
|
1049
|
+
});
|
|
1050
|
+
function sparkline(values) {
|
|
1051
|
+
const w = 120;
|
|
1052
|
+
const h = 32;
|
|
1053
|
+
const min = Math.min(...values);
|
|
1054
|
+
const span = Math.max(...values) - min || 1;
|
|
1055
|
+
const pts = values.map(
|
|
1056
|
+
(v, i) => `${(i / (values.length - 1) * w).toFixed(1)},${(h - 3 - (v - min) / span * (h - 6)).toFixed(1)}`
|
|
1057
|
+
).join(" ");
|
|
1058
|
+
const last = pts.split(" ").pop().split(",");
|
|
1059
|
+
return `<svg class="spark" viewBox="0 0 ${w} ${h}" aria-hidden="true"><polyline points="${pts}"/><circle cx="${last[0]}" cy="${last[1]}" r="3"/></svg>`;
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
// src/layouts/data.ts
|
|
1063
|
+
import { z as z5 } from "zod";
|
|
1064
|
+
var CHART_SIZE = { wide: { width: 1040, height: 500 }, side: { width: 720, height: 500 } };
|
|
1065
|
+
var chart = defineLayout({
|
|
1066
|
+
name: "chart",
|
|
1067
|
+
description: "A branded chart (column, bar, stacked, line, area, pie, donut, funnel, gauge, scatter, radar) with an optional takeaway column.",
|
|
1068
|
+
whenToUse: "Numbers that tell a story: sales by quarter, timelines, shares, survey scores. State the takeaway in the title.",
|
|
1069
|
+
shape: {
|
|
1070
|
+
eyebrow: Text.optional(),
|
|
1071
|
+
eyebrowSub: Text.optional(),
|
|
1072
|
+
title: Text.describe('Say what the chart shows, e.g. "Closings doubled since spring".'),
|
|
1073
|
+
lead: Text.optional(),
|
|
1074
|
+
chart: ChartSpec,
|
|
1075
|
+
insights: z5.array(Item).max(4).optional().describe("Up to 4 takeaways shown beside the chart."),
|
|
1076
|
+
source: Text.optional().describe("Where the numbers come from.")
|
|
1077
|
+
},
|
|
1078
|
+
example: {
|
|
1079
|
+
layout: "chart",
|
|
1080
|
+
eyebrow: "Sales",
|
|
1081
|
+
title: "Closings *doubled* since spring",
|
|
1082
|
+
chart: {
|
|
1083
|
+
type: "column",
|
|
1084
|
+
labels: ["Q1", "Q2", "Q3", "Q4"],
|
|
1085
|
+
series: [{ name: "Closings", values: [42, 61, 88, 94] }],
|
|
1086
|
+
unit: "homes",
|
|
1087
|
+
highlight: "Q4"
|
|
1088
|
+
},
|
|
1089
|
+
insights: ["Q4 best quarter on record", "**+124%** vs Q1"],
|
|
1090
|
+
source: "Sales ops, closed contracts"
|
|
1091
|
+
},
|
|
1092
|
+
prepare: (s) => ({
|
|
1093
|
+
charts: [
|
|
1094
|
+
{
|
|
1095
|
+
key: JSON.stringify(s.chart),
|
|
1096
|
+
spec: s.chart,
|
|
1097
|
+
...s.insights?.length ? CHART_SIZE.side : CHART_SIZE.wide
|
|
1098
|
+
}
|
|
1099
|
+
]
|
|
1100
|
+
}),
|
|
1101
|
+
lint: (s) => [
|
|
1102
|
+
...maxLen(s.title, 80, ["title"], "Title"),
|
|
1103
|
+
...chartIssues(s.chart).map(
|
|
1104
|
+
(message) => ({ level: "error", path: ["chart"], message })
|
|
1105
|
+
),
|
|
1106
|
+
...chartWarnings(s.chart).map(
|
|
1107
|
+
(message) => ({ level: "warning", path: ["chart"], message })
|
|
1108
|
+
)
|
|
1109
|
+
],
|
|
1110
|
+
render: (s, ctx) => {
|
|
1111
|
+
const svg = `<figure class="chart-fig">${ctx.chart(JSON.stringify(s.chart))}${s.source ? `<figcaption class="source">Source: ${inline(s.source)}</figcaption>` : ""}</figure>`;
|
|
1112
|
+
return {
|
|
1113
|
+
html: `${eyebrow(s.eyebrow, s.eyebrowSub)}${heading(s.title)}${lead(s.lead)}${s.insights?.length ? `<div class="chart-split">${svg}<div class="insights">${items(s.insights)}</div></div>` : svg}`
|
|
1114
|
+
};
|
|
1115
|
+
}
|
|
1116
|
+
});
|
|
1117
|
+
var Cell = z5.union([z5.string().max(80), z5.number()]);
|
|
1118
|
+
var table = defineLayout({
|
|
1119
|
+
name: "table",
|
|
1120
|
+
description: "A clean data table: header row, aligned numbers, optional highlighted row/column and totals row.",
|
|
1121
|
+
whenToUse: "Exact figures people will compare: pricing, schedules, specs, option matrices.",
|
|
1122
|
+
shape: {
|
|
1123
|
+
eyebrow: Text.optional(),
|
|
1124
|
+
eyebrowSub: Text.optional(),
|
|
1125
|
+
title: Text,
|
|
1126
|
+
lead: Text.optional(),
|
|
1127
|
+
columns: z5.array(
|
|
1128
|
+
z5.object({
|
|
1129
|
+
label: Text,
|
|
1130
|
+
align: z5.enum(["left", "center", "right"]).optional().describe("Defaults to right for numbers."),
|
|
1131
|
+
format: z5.enum(["text", "number", "percent", "currency"]).default("text")
|
|
1132
|
+
})
|
|
1133
|
+
).min(1).max(8),
|
|
1134
|
+
rows: z5.array(z5.array(Cell)).min(1).max(14).describe("One array per row, one cell per column. Text cells support inline formatting."),
|
|
1135
|
+
totals: z5.array(Cell).optional().describe("Optional bold totals row."),
|
|
1136
|
+
highlightRow: z5.number().int().positive().optional().describe("1-based row to emphasise."),
|
|
1137
|
+
highlightColumn: z5.number().int().positive().optional().describe("1-based column to emphasise."),
|
|
1138
|
+
footnote: Text.optional()
|
|
1139
|
+
},
|
|
1140
|
+
example: {
|
|
1141
|
+
layout: "table",
|
|
1142
|
+
eyebrow: "Options",
|
|
1143
|
+
title: "Smart home packages",
|
|
1144
|
+
columns: [
|
|
1145
|
+
{ label: "Package" },
|
|
1146
|
+
{ label: "Devices", format: "number" },
|
|
1147
|
+
{ label: "Price", format: "currency" }
|
|
1148
|
+
],
|
|
1149
|
+
rows: [
|
|
1150
|
+
["Essentials", 6, 1450],
|
|
1151
|
+
["Connected", 11, 2900],
|
|
1152
|
+
["Premier", 18, 4800]
|
|
1153
|
+
],
|
|
1154
|
+
highlightRow: 2,
|
|
1155
|
+
footnote: "Prices per home; installed before closing."
|
|
1156
|
+
},
|
|
1157
|
+
lint: (s) => {
|
|
1158
|
+
const out = [...maxLen(s.title, 80, ["title"], "Title")];
|
|
1159
|
+
s.rows.forEach((r, i) => {
|
|
1160
|
+
if (r.length !== s.columns.length) {
|
|
1161
|
+
out.push({
|
|
1162
|
+
level: "error",
|
|
1163
|
+
path: ["rows", i],
|
|
1164
|
+
message: `Row has ${r.length} cells but there are ${s.columns.length} columns.`
|
|
1165
|
+
});
|
|
1166
|
+
}
|
|
1167
|
+
});
|
|
1168
|
+
if (s.totals && s.totals.length !== s.columns.length) {
|
|
1169
|
+
out.push({
|
|
1170
|
+
level: "error",
|
|
1171
|
+
path: ["totals"],
|
|
1172
|
+
message: `Totals has ${s.totals.length} cells but there are ${s.columns.length} columns.`
|
|
1173
|
+
});
|
|
1174
|
+
}
|
|
1175
|
+
if (s.rows.length > 10) {
|
|
1176
|
+
out.push({
|
|
1177
|
+
level: "warning",
|
|
1178
|
+
path: ["rows"],
|
|
1179
|
+
message: `${s.rows.length} rows is dense for a slide; split or summarise.`
|
|
1180
|
+
});
|
|
1181
|
+
}
|
|
1182
|
+
return out;
|
|
1183
|
+
},
|
|
1184
|
+
render: (s) => {
|
|
1185
|
+
const cell = (v, ci) => {
|
|
1186
|
+
const col = s.columns[ci];
|
|
1187
|
+
if (typeof v === "number" && col.format !== "text") {
|
|
1188
|
+
return formatValue(v, {
|
|
1189
|
+
format: col.format === "currency" ? "currency" : col.format === "percent" ? "percent" : "number",
|
|
1190
|
+
decimals: 0
|
|
1191
|
+
});
|
|
1192
|
+
}
|
|
1193
|
+
return typeof v === "number" ? escapeHtml(String(v)) : inline(v);
|
|
1194
|
+
};
|
|
1195
|
+
const align = (ci) => s.columns[ci].align ?? (s.columns[ci].format === "text" ? "left" : "right");
|
|
1196
|
+
const td = (tag, v, ci) => `<${tag} class="al-${align(ci)}${s.highlightColumn === ci + 1 ? " hl-col" : ""}">${cell(v, ci)}</${tag}>`;
|
|
1197
|
+
return {
|
|
1198
|
+
html: `${eyebrow(s.eyebrow, s.eyebrowSub)}${heading(s.title)}${lead(s.lead)}<div class="table-wrap"><table class="data-table"><thead><tr>${s.columns.map(
|
|
1199
|
+
(c, ci) => `<th scope="col" class="al-${align(ci)}${s.highlightColumn === ci + 1 ? " hl-col" : ""}">${inline(c.label)}</th>`
|
|
1200
|
+
).join("")}</tr></thead><tbody>${s.rows.map(
|
|
1201
|
+
(r, ri) => `<tr${s.highlightRow === ri + 1 ? ' class="hl-row"' : ""}>${r.map((v, ci) => td(ci === 0 ? "th" : "td", v, ci)).join("")}</tr>`
|
|
1202
|
+
).join("")}</tbody>${s.totals ? `<tfoot><tr>${s.totals.map((v, ci) => td(ci === 0 ? "th" : "td", v, ci)).join("")}</tr></tfoot>` : ""}</table></div>${footnote(s.footnote)}`
|
|
1203
|
+
};
|
|
1204
|
+
}
|
|
1205
|
+
});
|
|
1206
|
+
|
|
1207
|
+
// src/layouts/diagrams.ts
|
|
1208
|
+
import { z as z6 } from "zod";
|
|
1209
|
+
var Build = z6.boolean().default(false).describe("Reveal one item per click while presenting.");
|
|
1210
|
+
var Step = z6.object({
|
|
1211
|
+
title: Text,
|
|
1212
|
+
text: Text.optional()
|
|
1213
|
+
});
|
|
1214
|
+
var buildAttr = (on) => on ? " data-build" : "";
|
|
1215
|
+
var flow = defineLayout({
|
|
1216
|
+
name: "flow",
|
|
1217
|
+
description: "A process: 2\u20137 steps joined by arrows, left-to-right or top-to-bottom, with an optional end pill.",
|
|
1218
|
+
whenToUse: "How something happens in order: approval paths, what comes next, hand-offs.",
|
|
1219
|
+
shape: {
|
|
1220
|
+
eyebrow: Text.optional(),
|
|
1221
|
+
eyebrowSub: Text.optional(),
|
|
1222
|
+
title: Text,
|
|
1223
|
+
lead: Text.optional(),
|
|
1224
|
+
steps: z6.array(Step).min(2).max(7),
|
|
1225
|
+
direction: z6.enum(["horizontal", "vertical"]).default("horizontal"),
|
|
1226
|
+
numbered: z6.boolean().default(true),
|
|
1227
|
+
end: Text.optional().describe('Pill after the last step, e.g. "Step 3" or "Keys!".'),
|
|
1228
|
+
build: Build,
|
|
1229
|
+
footnote: Text.optional()
|
|
1230
|
+
},
|
|
1231
|
+
example: {
|
|
1232
|
+
layout: "flow",
|
|
1233
|
+
eyebrow: "Step 2",
|
|
1234
|
+
title: "What comes next",
|
|
1235
|
+
steps: [
|
|
1236
|
+
{ title: "Personalized Color Chart", text: "Design timeline \u2014 60 days from contract" },
|
|
1237
|
+
{ title: "New Home Specialists", text: "Home Start Review" }
|
|
1238
|
+
],
|
|
1239
|
+
end: "Step 3",
|
|
1240
|
+
direction: "vertical"
|
|
1241
|
+
},
|
|
1242
|
+
lint: (s) => [
|
|
1243
|
+
...maxLen(s.title, 80, ["title"], "Title"),
|
|
1244
|
+
...s.steps.flatMap((st, i) => maxLen(st.text, 110, ["steps", i, "text"], "Step text")),
|
|
1245
|
+
...s.direction === "horizontal" && s.steps.length > 5 ? [
|
|
1246
|
+
{
|
|
1247
|
+
level: "warning",
|
|
1248
|
+
path: ["steps"],
|
|
1249
|
+
message: "More than 5 steps side by side gets cramped; try direction: vertical."
|
|
1250
|
+
}
|
|
1251
|
+
] : []
|
|
1252
|
+
],
|
|
1253
|
+
render: (s) => ({
|
|
1254
|
+
html: `${eyebrow(s.eyebrow, s.eyebrowSub)}${heading(s.title)}${lead(s.lead)}<ol class="flow flow--${s.direction}"${buildAttr(
|
|
1255
|
+
s.build
|
|
1256
|
+
)} style="--n:${s.steps.length + (s.end ? 1 : 0)}">${s.steps.map(
|
|
1257
|
+
(st, i) => `<li class="flow-step b-item">${s.numbered ? `<span class="flow-n">${i + 1}</span>` : ""}<div><h3>${inline(
|
|
1258
|
+
st.title
|
|
1259
|
+
)}</h3>${st.text ? `<p>${inline(st.text)}</p>` : ""}</div></li>`
|
|
1260
|
+
).join(
|
|
1261
|
+
""
|
|
1262
|
+
)}${s.end ? `<li class="flow-end b-item"><span>${inline(s.end)}</span></li>` : ""}</ol>${footnote(s.footnote)}`
|
|
1263
|
+
})
|
|
1264
|
+
});
|
|
1265
|
+
var timeline = defineLayout({
|
|
1266
|
+
name: "timeline",
|
|
1267
|
+
description: "Dated milestones along a line, optionally grouped into tinted phases, with the current point marked.",
|
|
1268
|
+
whenToUse: "Schedules and histories: build timeline, closing countdown, company milestones.",
|
|
1269
|
+
shape: {
|
|
1270
|
+
eyebrow: Text.optional(),
|
|
1271
|
+
eyebrowSub: Text.optional(),
|
|
1272
|
+
title: Text,
|
|
1273
|
+
lead: Text.optional(),
|
|
1274
|
+
items: z6.array(
|
|
1275
|
+
z6.object({
|
|
1276
|
+
date: z6.string().max(24).describe('Short: "Week 1", "Mar 2026", "Day 60".'),
|
|
1277
|
+
title: Text,
|
|
1278
|
+
text: Text.optional()
|
|
1279
|
+
})
|
|
1280
|
+
).min(2).max(10),
|
|
1281
|
+
phases: z6.array(
|
|
1282
|
+
z6.object({
|
|
1283
|
+
label: Text,
|
|
1284
|
+
from: z6.number().int().positive().describe("First item in the phase (1-based)."),
|
|
1285
|
+
to: z6.number().int().positive().describe("Last item in the phase (1-based).")
|
|
1286
|
+
})
|
|
1287
|
+
).max(4).optional(),
|
|
1288
|
+
current: z6.number().int().positive().optional().describe('Item to mark as "you are here" (1-based).'),
|
|
1289
|
+
build: Build
|
|
1290
|
+
},
|
|
1291
|
+
example: {
|
|
1292
|
+
layout: "timeline",
|
|
1293
|
+
eyebrow: "Your guide",
|
|
1294
|
+
title: "The whole path \u2014 from contract to year eight",
|
|
1295
|
+
items: [
|
|
1296
|
+
{ date: "01", title: "Introduction" },
|
|
1297
|
+
{ date: "02", title: "Purchasing" },
|
|
1298
|
+
{ date: "03", title: "Design" },
|
|
1299
|
+
{ date: "04", title: "Construction" },
|
|
1300
|
+
{ date: "05", title: "Closing" },
|
|
1301
|
+
{ date: "06", title: "Warranty" },
|
|
1302
|
+
{ date: "07", title: "Use & Care" },
|
|
1303
|
+
{ date: "08", title: "HOA & Metro" }
|
|
1304
|
+
],
|
|
1305
|
+
phases: [
|
|
1306
|
+
{ label: "Getting to your home", from: 1, to: 5 },
|
|
1307
|
+
{ label: "Living in your home", from: 6, to: 8 }
|
|
1308
|
+
],
|
|
1309
|
+
current: 5
|
|
1310
|
+
},
|
|
1311
|
+
lint: (s) => {
|
|
1312
|
+
const out = [...maxLen(s.title, 80, ["title"], "Title")];
|
|
1313
|
+
(s.phases ?? []).forEach((p, i) => {
|
|
1314
|
+
if (p.from > p.to || p.to > s.items.length) {
|
|
1315
|
+
out.push({
|
|
1316
|
+
level: "error",
|
|
1317
|
+
path: ["phases", i],
|
|
1318
|
+
message: `Phase covers items ${p.from}\u2013${p.to} but there are ${s.items.length} items.`
|
|
1319
|
+
});
|
|
1320
|
+
}
|
|
1321
|
+
});
|
|
1322
|
+
if (s.current && s.current > s.items.length) {
|
|
1323
|
+
out.push({
|
|
1324
|
+
level: "error",
|
|
1325
|
+
path: ["current"],
|
|
1326
|
+
message: `current is ${s.current} but there are ${s.items.length} items.`
|
|
1327
|
+
});
|
|
1328
|
+
}
|
|
1329
|
+
return out;
|
|
1330
|
+
},
|
|
1331
|
+
render: (s) => {
|
|
1332
|
+
const phaseOf = (i) => (s.phases ?? []).findIndex((p) => i + 1 >= p.from && i + 1 <= p.to);
|
|
1333
|
+
const bands = (s.phases ?? []).map(
|
|
1334
|
+
(p, pi) => `<div class="tl-phase tl-phase--${pi % 2}" style="grid-column:${p.from} / ${p.to + 1}"><span>${inline(p.label)}</span></div>`
|
|
1335
|
+
).join("");
|
|
1336
|
+
return {
|
|
1337
|
+
html: `${eyebrow(s.eyebrow, s.eyebrowSub)}${heading(s.title)}${lead(s.lead)}<div class="timeline"${buildAttr(
|
|
1338
|
+
s.build
|
|
1339
|
+
)} style="--n:${s.items.length}">${bands ? `<div class="tl-phases">${bands}</div>` : ""}<ol class="tl-items">${s.items.map((it, i) => {
|
|
1340
|
+
const cls = [
|
|
1341
|
+
"tl-item",
|
|
1342
|
+
"b-item",
|
|
1343
|
+
phaseOf(i) >= 0 ? `tl-in-${phaseOf(i) % 2}` : "",
|
|
1344
|
+
s.current === i + 1 ? "tl-current" : "",
|
|
1345
|
+
s.current && i + 1 < s.current ? "tl-done" : ""
|
|
1346
|
+
].filter(Boolean).join(" ");
|
|
1347
|
+
return `<li class="${cls}"><span class="tl-dot"></span><span class="tl-date">${escapeHtml(it.date)}</span><h3>${inline(
|
|
1348
|
+
it.title
|
|
1349
|
+
)}</h3>${it.text ? `<p>${inline(it.text)}</p>` : ""}</li>`;
|
|
1350
|
+
}).join("")}</ol></div>`
|
|
1351
|
+
};
|
|
1352
|
+
}
|
|
1353
|
+
});
|
|
1354
|
+
var cycle = defineLayout({
|
|
1355
|
+
name: "cycle",
|
|
1356
|
+
description: "A repeating process: 3\u20136 steps arranged around a circle with arrows, optional centre label.",
|
|
1357
|
+
whenToUse: "Loops that repeat: seasonal maintenance, feedback cycles, plan-do-review.",
|
|
1358
|
+
shape: {
|
|
1359
|
+
eyebrow: Text.optional(),
|
|
1360
|
+
title: Text,
|
|
1361
|
+
lead: Text.optional(),
|
|
1362
|
+
steps: z6.array(Step).min(3).max(6),
|
|
1363
|
+
center: Text.optional().describe("Word in the middle of the circle.")
|
|
1364
|
+
},
|
|
1365
|
+
example: {
|
|
1366
|
+
layout: "cycle",
|
|
1367
|
+
eyebrow: "Use & care",
|
|
1368
|
+
title: "Seasonal maintenance, every year",
|
|
1369
|
+
center: "Your home",
|
|
1370
|
+
steps: [
|
|
1371
|
+
{ title: "Spring", text: "Check grading and gutters" },
|
|
1372
|
+
{ title: "Summer", text: "Service AC, reseal decks" },
|
|
1373
|
+
{ title: "Fall", text: "Blow out sprinklers" },
|
|
1374
|
+
{ title: "Winter", text: "Replace furnace filter" }
|
|
1375
|
+
]
|
|
1376
|
+
},
|
|
1377
|
+
lint: (s) => s.steps.flatMap((st, i) => maxLen(st.text, 70, ["steps", i, "text"], "Step text")),
|
|
1378
|
+
render: (s) => {
|
|
1379
|
+
const n = s.steps.length;
|
|
1380
|
+
const pos = s.steps.map((_, i) => {
|
|
1381
|
+
const a = i / n * Math.PI * 2 - Math.PI / 2;
|
|
1382
|
+
return { x: 50 + 38 * Math.cos(a), y: 50 + 38 * Math.sin(a) };
|
|
1383
|
+
});
|
|
1384
|
+
const arcs = pos.map((_, i) => {
|
|
1385
|
+
const a0 = i / n * Math.PI * 2 - Math.PI / 2 + 0.34;
|
|
1386
|
+
const a1 = (i + 1) / n * Math.PI * 2 - Math.PI / 2 - 0.34;
|
|
1387
|
+
const p = (a) => `${(50 + 38 * Math.cos(a)).toFixed(2)} ${(50 + 38 * Math.sin(a)).toFixed(2)}`;
|
|
1388
|
+
return `<path d="M ${p(a0)} A 38 38 0 0 1 ${p(a1)}" marker-end="url(#cy-arrow)"/>`;
|
|
1389
|
+
}).join("");
|
|
1390
|
+
return {
|
|
1391
|
+
html: `${eyebrow(s.eyebrow)}${heading(s.title)}${lead(s.lead)}<div class="cycle"><svg class="cycle-ring" viewBox="0 0 100 100" aria-hidden="true"><defs><marker id="cy-arrow" viewBox="0 0 10 10" refX="7" refY="5" markerWidth="4" markerHeight="4" orient="auto-start-reverse"><path d="M0 0 L10 5 L0 10 z"/></marker></defs>${arcs}</svg>${s.center ? `<div class="cycle-center">${inline(s.center)}</div>` : ""}${s.steps.map(
|
|
1392
|
+
(st, i) => `<div class="cycle-node" style="left:${pos[i].x.toFixed(2)}%;top:${pos[i].y.toFixed(2)}%"><h3>${inline(st.title)}</h3>${st.text ? `<p>${inline(st.text)}</p>` : ""}</div>`
|
|
1393
|
+
).join("")}</div>`
|
|
1394
|
+
};
|
|
1395
|
+
}
|
|
1396
|
+
});
|
|
1397
|
+
var hierarchy = defineLayout({
|
|
1398
|
+
name: "hierarchy",
|
|
1399
|
+
description: "An org chart or tree, up to 3 levels, with optional photos.",
|
|
1400
|
+
whenToUse: "Who reports to whom, how a team is organised, how a topic breaks down.",
|
|
1401
|
+
shape: {
|
|
1402
|
+
eyebrow: Text.optional(),
|
|
1403
|
+
title: Text,
|
|
1404
|
+
lead: Text.optional(),
|
|
1405
|
+
nodes: z6.array(
|
|
1406
|
+
z6.object({
|
|
1407
|
+
id: z6.string().regex(/^[a-z0-9-]+$/).describe("Short slug, referenced by children."),
|
|
1408
|
+
parent: z6.string().optional().describe("id of the parent; omit for the top node."),
|
|
1409
|
+
name: Text,
|
|
1410
|
+
role: Text.optional(),
|
|
1411
|
+
photo: Image.optional()
|
|
1412
|
+
})
|
|
1413
|
+
).min(1).max(16)
|
|
1414
|
+
},
|
|
1415
|
+
example: {
|
|
1416
|
+
layout: "hierarchy",
|
|
1417
|
+
eyebrow: "Your team",
|
|
1418
|
+
title: "Who builds your home",
|
|
1419
|
+
nodes: [
|
|
1420
|
+
{ id: "pm", name: "Project Manager", role: "Oversees your community" },
|
|
1421
|
+
{ id: "super", parent: "pm", name: "Superintendent", role: "Runs your site day to day" },
|
|
1422
|
+
{ id: "cs", parent: "pm", name: "Customer Service", role: "After you move in" },
|
|
1423
|
+
{
|
|
1424
|
+
id: "trades",
|
|
1425
|
+
parent: "super",
|
|
1426
|
+
name: "Trade partners",
|
|
1427
|
+
role: "Framing, plumbing, electrical\u2026"
|
|
1428
|
+
}
|
|
1429
|
+
]
|
|
1430
|
+
},
|
|
1431
|
+
lint: (s) => {
|
|
1432
|
+
const out = [
|
|
1433
|
+
...missingAlt(
|
|
1434
|
+
s.nodes.flatMap((n) => n.photo ? [n.photo] : []),
|
|
1435
|
+
["nodes"]
|
|
1436
|
+
)
|
|
1437
|
+
];
|
|
1438
|
+
const ids = new Set(s.nodes.map((n) => n.id));
|
|
1439
|
+
const roots = s.nodes.filter((n) => !n.parent);
|
|
1440
|
+
if (roots.length !== 1)
|
|
1441
|
+
out.push({
|
|
1442
|
+
level: "error",
|
|
1443
|
+
path: ["nodes"],
|
|
1444
|
+
message: `Needs exactly one top node (no parent); found ${roots.length}.`
|
|
1445
|
+
});
|
|
1446
|
+
s.nodes.forEach((n, i) => {
|
|
1447
|
+
if (n.parent && !ids.has(n.parent))
|
|
1448
|
+
out.push({
|
|
1449
|
+
level: "error",
|
|
1450
|
+
path: ["nodes", i, "parent"],
|
|
1451
|
+
message: `No node with id "${n.parent}".`
|
|
1452
|
+
});
|
|
1453
|
+
});
|
|
1454
|
+
const depth = (id, seen = /* @__PURE__ */ new Set()) => {
|
|
1455
|
+
const node = s.nodes.find((n) => n.id === id);
|
|
1456
|
+
if (!node?.parent || seen.has(id)) return 1;
|
|
1457
|
+
seen.add(id);
|
|
1458
|
+
return 1 + depth(node.parent, seen);
|
|
1459
|
+
};
|
|
1460
|
+
if (s.nodes.some((n) => depth(n.id) > 3))
|
|
1461
|
+
out.push({ level: "error", path: ["nodes"], message: "Keep the tree to 3 levels." });
|
|
1462
|
+
return out;
|
|
1463
|
+
},
|
|
1464
|
+
render: (s, ctx) => {
|
|
1465
|
+
const kids = (id) => s.nodes.filter((n) => n.parent === id);
|
|
1466
|
+
const node = (n) => {
|
|
1467
|
+
const children = kids(n.id);
|
|
1468
|
+
return `<li><div class="h-node">${n.photo ? img(n.photo, ctx, "avatar") : ""}<div><h3>${inline(n.name)}</h3>${n.role ? `<p>${inline(n.role)}</p>` : ""}</div></div>${children.length ? `<ul>${children.map(node).join("")}</ul>` : ""}</li>`;
|
|
1469
|
+
};
|
|
1470
|
+
const root = s.nodes.find((n) => !n.parent);
|
|
1471
|
+
return {
|
|
1472
|
+
html: `${eyebrow(s.eyebrow)}${heading(s.title)}${lead(s.lead)}<div class="hierarchy"><ul>${root ? node(root) : ""}</ul></div>`
|
|
1473
|
+
};
|
|
1474
|
+
}
|
|
1475
|
+
});
|
|
1476
|
+
var matrix = defineLayout({
|
|
1477
|
+
name: "matrix",
|
|
1478
|
+
description: "A 2\xD72 grid with labelled axes and a few items in each quadrant.",
|
|
1479
|
+
whenToUse: "Sorting options by two factors: effort vs impact, cost vs value, who handles what when.",
|
|
1480
|
+
shape: {
|
|
1481
|
+
eyebrow: Text.optional(),
|
|
1482
|
+
title: Text,
|
|
1483
|
+
lead: Text.optional(),
|
|
1484
|
+
xAxis: z6.object({ low: Text, high: Text, label: Text.optional() }),
|
|
1485
|
+
yAxis: z6.object({ low: Text, high: Text, label: Text.optional() }),
|
|
1486
|
+
quadrants: z6.array(z6.object({ title: Text, items: z6.array(Text).max(4).optional() })).length(4).describe("In reading order: top-left, top-right, bottom-left, bottom-right."),
|
|
1487
|
+
highlight: z6.number().int().min(1).max(4).optional().describe("Quadrant to emphasise (1\u20134).")
|
|
1488
|
+
},
|
|
1489
|
+
example: {
|
|
1490
|
+
layout: "matrix",
|
|
1491
|
+
title: "Where to spend your upgrade budget",
|
|
1492
|
+
xAxis: { low: "Easy to add later", high: "Hard to add later", label: "After move-in" },
|
|
1493
|
+
yAxis: { low: "Nice to have", high: "High value", label: "Value" },
|
|
1494
|
+
quadrants: [
|
|
1495
|
+
{ title: "Add later", items: ["Paint colours", "Light fixtures"] },
|
|
1496
|
+
{ title: "Choose now", items: ["Structural options", "Electrical & low volt"] },
|
|
1497
|
+
{ title: "Skip", items: ["Trend finishes"] },
|
|
1498
|
+
{ title: "Consider", items: ["Flooring upgrades"] }
|
|
1499
|
+
],
|
|
1500
|
+
highlight: 2
|
|
1501
|
+
},
|
|
1502
|
+
render: (s) => ({
|
|
1503
|
+
html: `${eyebrow(s.eyebrow)}${heading(s.title)}${lead(s.lead)}<div class="matrix"><div class="mx-y"><span>${inline(s.yAxis.high)}</span>${s.yAxis.label ? `<b>${inline(s.yAxis.label)}</b>` : ""}<span>${inline(s.yAxis.low)}</span></div><div class="mx-grid">${s.quadrants.map(
|
|
1504
|
+
(q, i) => `<div class="mx-q${s.highlight === i + 1 ? " mx-hl" : ""}"><h3>${inline(q.title)}</h3>${q.items?.length ? `<ul>${q.items.map((t) => `<li>${inline(t)}</li>`).join("")}</ul>` : ""}</div>`
|
|
1505
|
+
).join(
|
|
1506
|
+
""
|
|
1507
|
+
)}</div><div class="mx-x"><span>${inline(s.xAxis.low)}</span>${s.xAxis.label ? `<b>${inline(s.xAxis.label)}</b>` : ""}<span>${inline(
|
|
1508
|
+
s.xAxis.high
|
|
1509
|
+
)}</span></div></div>`
|
|
1510
|
+
})
|
|
1511
|
+
});
|
|
1512
|
+
var tiers = defineLayout({
|
|
1513
|
+
name: "tiers",
|
|
1514
|
+
description: "A funnel (wide to narrow) or pyramid (narrow to wide) of 3\u20136 labelled tiers with optional values.",
|
|
1515
|
+
whenToUse: "Stages that narrow (leads \u2192 contracts \u2192 closings) or build on each other (foundation \u2192 finish).",
|
|
1516
|
+
shape: {
|
|
1517
|
+
eyebrow: Text.optional(),
|
|
1518
|
+
title: Text,
|
|
1519
|
+
lead: Text.optional(),
|
|
1520
|
+
style: z6.enum(["funnel", "pyramid"]).default("funnel"),
|
|
1521
|
+
tiers: z6.array(z6.object({ label: Text, value: z6.string().max(16).optional(), note: Text.optional() })).min(3).max(6)
|
|
519
1522
|
},
|
|
520
1523
|
example: {
|
|
521
|
-
layout: "
|
|
522
|
-
|
|
523
|
-
|
|
1524
|
+
layout: "tiers",
|
|
1525
|
+
eyebrow: "Warranty",
|
|
1526
|
+
title: "Coverage that lasts",
|
|
1527
|
+
style: "pyramid",
|
|
1528
|
+
tiers: [
|
|
1529
|
+
{ label: "Structural", value: "10 yrs", note: "Load-bearing elements" },
|
|
1530
|
+
{ label: "Systems", value: "2 yrs", note: "Plumbing, electrical, HVAC" },
|
|
1531
|
+
{ label: "Workmanship", value: "1 yr", note: "Fit and finish" }
|
|
1532
|
+
]
|
|
524
1533
|
},
|
|
525
|
-
lint: (s) => [
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
1534
|
+
lint: (s) => maxCount(s.tiers, 6, ["tiers"], "tiers"),
|
|
1535
|
+
render: (s) => {
|
|
1536
|
+
const n = s.tiers.length;
|
|
1537
|
+
return {
|
|
1538
|
+
html: `${eyebrow(s.eyebrow)}${heading(s.title)}${lead(s.lead)}<ol class="tiers tiers--${s.style}">${s.tiers.map((t, i) => {
|
|
1539
|
+
const rank = s.style === "funnel" ? i : n - 1 - i;
|
|
1540
|
+
const width = 100 - rank * 60 / Math.max(1, n - 1);
|
|
1541
|
+
return `<li style="--w:${width.toFixed(1)}%;--i:${i}"><div class="tier-bar"><span class="tier-label">${inline(t.label)}</span>${t.value ? `<b>${escapeHtml(t.value)}</b>` : ""}</div>${t.note ? `<p class="tier-note">${inline(t.note)}</p>` : ""}</li>`;
|
|
1542
|
+
}).join("")}</ol>`
|
|
1543
|
+
};
|
|
1544
|
+
}
|
|
534
1545
|
});
|
|
535
1546
|
|
|
536
1547
|
// src/layouts/media.ts
|
|
537
|
-
import { z as
|
|
538
|
-
var Group =
|
|
1548
|
+
import { z as z7 } from "zod";
|
|
1549
|
+
var Group = z7.object({
|
|
539
1550
|
heading: Text.optional().describe("Small bold heading above this group."),
|
|
540
1551
|
text: Text.optional(),
|
|
541
|
-
items:
|
|
1552
|
+
items: z7.array(Item).optional(),
|
|
542
1553
|
marker: Marker
|
|
543
1554
|
});
|
|
544
1555
|
var split = defineLayout({
|
|
@@ -550,11 +1561,11 @@ var split = defineLayout({
|
|
|
550
1561
|
eyebrowSub: Text.optional(),
|
|
551
1562
|
title: Text,
|
|
552
1563
|
lead: Text.optional(),
|
|
553
|
-
groups:
|
|
554
|
-
images:
|
|
555
|
-
arrange:
|
|
556
|
-
mediaSide:
|
|
557
|
-
badge:
|
|
1564
|
+
groups: z7.array(Group).optional().describe("Text blocks under the title, each with an optional heading."),
|
|
1565
|
+
images: z7.array(Image).max(4).default([]).describe("Photos. May be empty when a badge carries the media side."),
|
|
1566
|
+
arrange: z7.enum(["single", "stack", "grid", "overlap"]).default("single").describe("single: one photo \xB7 stack: vertical \xB7 grid: 2\xD72 \xB7 overlap: two photos layered."),
|
|
1567
|
+
mediaSide: z7.enum(["right", "left"]).default("right"),
|
|
1568
|
+
badge: z7.object({ title: Text, text: Text.optional() }).optional().describe(
|
|
558
1569
|
'Red circle callout over the photos, e.g. "Pre-drywall meeting / about 60 days in".'
|
|
559
1570
|
),
|
|
560
1571
|
footnote: Text.optional().describe("Closing line under the text column.")
|
|
@@ -610,10 +1621,10 @@ var photos = defineLayout({
|
|
|
610
1621
|
eyebrow: Text.optional(),
|
|
611
1622
|
title: Text.optional(),
|
|
612
1623
|
lead: Text.optional(),
|
|
613
|
-
images:
|
|
614
|
-
columns:
|
|
615
|
-
aspect:
|
|
616
|
-
captions:
|
|
1624
|
+
images: z7.array(Image).min(1).max(24),
|
|
1625
|
+
columns: z7.number().int().min(1).max(8).optional().describe("Defaults to a balanced count for the number of photos."),
|
|
1626
|
+
aspect: z7.enum(["portrait", "landscape", "square", "native"]).default("landscape").describe("portrait suits headshots; native keeps each photo uncropped."),
|
|
1627
|
+
captions: z7.boolean().default(false).describe("Show each image alt text as a caption.")
|
|
617
1628
|
},
|
|
618
1629
|
example: {
|
|
619
1630
|
layout: "photos",
|
|
@@ -648,7 +1659,15 @@ var image = defineLayout({
|
|
|
648
1659
|
title: Text.optional(),
|
|
649
1660
|
image: Image,
|
|
650
1661
|
caption: Text.optional(),
|
|
651
|
-
frame:
|
|
1662
|
+
frame: z7.boolean().default(true).describe("White card frame; turn off for full-bleed photos."),
|
|
1663
|
+
hotspots: z7.array(
|
|
1664
|
+
z7.object({
|
|
1665
|
+
x: z7.number().min(0).max(100).describe("Horizontal position, % from the left."),
|
|
1666
|
+
y: z7.number().min(0).max(100).describe("Vertical position, % from the top."),
|
|
1667
|
+
label: Text,
|
|
1668
|
+
text: Text.optional()
|
|
1669
|
+
})
|
|
1670
|
+
).max(12).optional().describe("Numbered markers on the image with a legend, e.g. rooms on a floor plan.")
|
|
652
1671
|
},
|
|
653
1672
|
example: {
|
|
654
1673
|
layout: "image",
|
|
@@ -657,12 +1676,143 @@ var image = defineLayout({
|
|
|
657
1676
|
caption: "Model homes are open daily 10\u20135."
|
|
658
1677
|
},
|
|
659
1678
|
lint: (s) => missingAlt([s.image], ["image"]).map((i) => ({ ...i, path: ["image", "alt"] })),
|
|
1679
|
+
render: (s, ctx) => {
|
|
1680
|
+
if (s.hotspots?.length) {
|
|
1681
|
+
const marks = s.hotspots.map(
|
|
1682
|
+
(h, i) => `<span class="hs-mark" style="left:${h.x}%;top:${h.y}%" aria-hidden="true">${i + 1}</span>`
|
|
1683
|
+
).join("");
|
|
1684
|
+
const legend = s.hotspots.map(
|
|
1685
|
+
(h) => `<li><b>${inline(h.label)}</b>${h.text ? `<span>${inline(h.text)}</span>` : ""}</li>`
|
|
1686
|
+
).join("");
|
|
1687
|
+
return {
|
|
1688
|
+
classes: ["has-hotspots"],
|
|
1689
|
+
html: `${eyebrow(s.eyebrow)}${s.title ? heading(s.title) : ""}<div class="hs-layout"><figure class="diagram-shot"><div class="hs-wrap">${img(
|
|
1690
|
+
{ ...s.image, fit: "contain" },
|
|
1691
|
+
ctx
|
|
1692
|
+
)}${marks}</div>${s.caption ? `<figcaption class="shot-cap">${inline(s.caption)}</figcaption>` : ""}</figure><ol class="hs-legend">${legend}</ol></div>`
|
|
1693
|
+
};
|
|
1694
|
+
}
|
|
1695
|
+
return {
|
|
1696
|
+
classes: s.frame ? [] : ["bleed"],
|
|
1697
|
+
html: `${eyebrow(s.eyebrow)}${s.title ? heading(s.title) : ""}<figure class="${s.frame ? "diagram-shot" : "bleed-img"}">${img(
|
|
1698
|
+
{ ...s.image, fit: s.frame ? "contain" : s.image.fit },
|
|
1699
|
+
ctx
|
|
1700
|
+
)}${s.caption ? `<figcaption class="shot-cap">${inline(s.caption)}</figcaption>` : ""}</figure>`
|
|
1701
|
+
};
|
|
1702
|
+
}
|
|
1703
|
+
});
|
|
1704
|
+
var video = defineLayout({
|
|
1705
|
+
name: "video",
|
|
1706
|
+
description: "A YouTube or Vimeo video that loads when the slide is shown, with a poster + QR fallback offline and in print.",
|
|
1707
|
+
whenToUse: "Walkthrough tours, testimonials, how-to clips. Keep them short; needs internet to play.",
|
|
1708
|
+
shape: {
|
|
1709
|
+
eyebrow: Text.optional(),
|
|
1710
|
+
title: Text.optional(),
|
|
1711
|
+
url: z7.string().url().refine((u) => !!parseVideoUrl(u), "Use a YouTube or Vimeo link.").describe("YouTube or Vimeo link (watch, share or embed URL)."),
|
|
1712
|
+
poster: Image.optional().describe(
|
|
1713
|
+
"Still shown before play and offline. YouTube posters are fetched automatically."
|
|
1714
|
+
),
|
|
1715
|
+
start: z7.number().int().min(0).optional().describe("Start at this many seconds."),
|
|
1716
|
+
end: z7.number().int().min(1).optional().describe("Stop at this many seconds (YouTube)."),
|
|
1717
|
+
autoplay: z7.boolean().default(false).describe("Start playing when the slide is shown (muted by browsers)."),
|
|
1718
|
+
muted: z7.boolean().default(false),
|
|
1719
|
+
captions: z7.boolean().default(true).describe("Show captions when the video has them."),
|
|
1720
|
+
caption: Text.optional()
|
|
1721
|
+
},
|
|
1722
|
+
example: {
|
|
1723
|
+
layout: "video",
|
|
1724
|
+
eyebrow: "Take the tour",
|
|
1725
|
+
title: "Inside the Aspen floor plan",
|
|
1726
|
+
url: "https://www.youtube.com/watch?v=aqz-KE-bpKQ",
|
|
1727
|
+
caption: "Virtual tour \xB7 2 min"
|
|
1728
|
+
},
|
|
1729
|
+
prepare: (s) => {
|
|
1730
|
+
const ref = parseVideoUrl(s.url);
|
|
1731
|
+
return { qr: [ref.watchUrl], images: !s.poster && ref.poster ? [ref.poster] : [] };
|
|
1732
|
+
},
|
|
1733
|
+
render: (s, ctx) => {
|
|
1734
|
+
const ref = parseVideoUrl(s.url);
|
|
1735
|
+
const src = videoEmbedUrl(ref, s);
|
|
1736
|
+
const posterSrc = s.poster?.src ?? ref.poster;
|
|
1737
|
+
const poster = posterSrc ? `<img class="media-poster" src="${escapeAttr(ctx.image(posterSrc))}" alt="${escapeAttr(
|
|
1738
|
+
s.poster?.alt || (s.title ? `Video: ${s.title}` : "Video poster")
|
|
1739
|
+
)}" decoding="async">` : "";
|
|
1740
|
+
return {
|
|
1741
|
+
classes: ["has-media"],
|
|
1742
|
+
html: `${eyebrow(s.eyebrow)}${s.title ? heading(s.title) : ""}<figure class="media-frame" data-media-src="${escapeAttr(
|
|
1743
|
+
src
|
|
1744
|
+
)}" data-media-title="${escapeAttr(s.title ?? "Video")}" data-media-kind="video">${poster}<div class="media-fallback"><span class="play-badge" aria-hidden="true">\u25B6</span><div class="media-qr">${ctx.qr(
|
|
1745
|
+
ref.watchUrl
|
|
1746
|
+
)}<a href="${escapeAttr(ref.watchUrl)}" target="_blank" rel="noopener">Watch on ${ref.provider === "youtube" ? "YouTube" : "Vimeo"} \u2197</a></div></div></figure>${s.caption ? `<p class="shot-cap">${inline(s.caption)}</p>` : ""}`
|
|
1747
|
+
};
|
|
1748
|
+
}
|
|
1749
|
+
});
|
|
1750
|
+
var embed = defineLayout({
|
|
1751
|
+
name: "embed",
|
|
1752
|
+
description: "A live web page (dashboard, map, form, 3D tour) in a sandboxed frame that loads when the slide is shown, with an open link and QR fallback.",
|
|
1753
|
+
whenToUse: "Showing something interactive: a Matterport tour, a Google Map, a Looker dashboard, a sign-up form. Needs internet; some sites refuse to be framed.",
|
|
1754
|
+
shape: {
|
|
1755
|
+
eyebrow: Text.optional(),
|
|
1756
|
+
title: Text.optional(),
|
|
1757
|
+
url: z7.string().url().refine((u) => u.startsWith("https://"), "Embeds must use https.").describe("https address of the page to show."),
|
|
1758
|
+
aspect: z7.enum(["16:9", "4:3", "1:1", "tall"]).default("16:9").describe("Frame shape; tall suits forms and phones."),
|
|
1759
|
+
fallback: Image.optional().describe(
|
|
1760
|
+
"Screenshot shown offline, in print, or if the site refuses framing."
|
|
1761
|
+
),
|
|
1762
|
+
caption: Text.optional()
|
|
1763
|
+
},
|
|
1764
|
+
example: {
|
|
1765
|
+
layout: "embed",
|
|
1766
|
+
eyebrow: "Explore",
|
|
1767
|
+
title: "Find your community",
|
|
1768
|
+
url: "https://www.openstreetmap.org/export/embed.html?bbox=-104.95,38.78,-104.65,39.03&layer=mapnik",
|
|
1769
|
+
caption: "Pan and zoom to see every Classic Homes community."
|
|
1770
|
+
},
|
|
1771
|
+
prepare: (s) => ({ qr: [s.url] }),
|
|
1772
|
+
render: (s, ctx) => ({
|
|
1773
|
+
classes: ["has-media"],
|
|
1774
|
+
html: `${eyebrow(s.eyebrow)}${s.title ? heading(s.title) : ""}<figure class="media-frame aspect-${s.aspect.replace(
|
|
1775
|
+
":",
|
|
1776
|
+
"x"
|
|
1777
|
+
)}" data-media-src="${escapeAttr(s.url)}" data-media-title="${escapeAttr(s.title ?? "Embedded page")}" data-media-kind="embed">${s.fallback ? img(s.fallback, ctx, "media-poster") : ""}<div class="media-fallback"><div class="media-qr">${ctx.qr(s.url)}<a href="${escapeAttr(
|
|
1778
|
+
s.url
|
|
1779
|
+
)}" target="_blank" rel="noopener">${escapeHtml(s.url.replace(/^https:\/\//, "").split("/")[0])} \u2197</a></div></div><a class="media-open" href="${escapeAttr(
|
|
1780
|
+
s.url
|
|
1781
|
+
)}" target="_blank" rel="noopener">Open \u2197</a></figure>${s.caption ? `<p class="shot-cap">${inline(s.caption)}</p>` : ""}`
|
|
1782
|
+
})
|
|
1783
|
+
});
|
|
1784
|
+
var compare = defineLayout({
|
|
1785
|
+
name: "compare",
|
|
1786
|
+
description: "Before/after: two photos in one frame with a draggable divider (side by side in print).",
|
|
1787
|
+
whenToUse: "Showing change: framing vs finished, model vs your home, standard vs upgraded finish.",
|
|
1788
|
+
shape: {
|
|
1789
|
+
eyebrow: Text.optional(),
|
|
1790
|
+
title: Text.optional(),
|
|
1791
|
+
before: Image,
|
|
1792
|
+
after: Image,
|
|
1793
|
+
beforeLabel: z7.string().max(24).default("Before"),
|
|
1794
|
+
afterLabel: z7.string().max(24).default("After"),
|
|
1795
|
+
caption: Text.optional()
|
|
1796
|
+
},
|
|
1797
|
+
example: {
|
|
1798
|
+
layout: "compare",
|
|
1799
|
+
title: "Framing to finished",
|
|
1800
|
+
before: { src: "images/framing.jpg", alt: "Kitchen during framing" },
|
|
1801
|
+
after: { src: "images/finished.jpg", alt: "Finished kitchen" },
|
|
1802
|
+
beforeLabel: "Week 6",
|
|
1803
|
+
afterLabel: "Move-in"
|
|
1804
|
+
},
|
|
1805
|
+
lint: (s) => missingAlt([s.before, s.after], ["images"]),
|
|
660
1806
|
render: (s, ctx) => ({
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
)}
|
|
1807
|
+
html: `${eyebrow(s.eyebrow)}${s.title ? heading(s.title) : ""}<figure class="compare" style="--pos:50%">${img(
|
|
1808
|
+
s.after,
|
|
1809
|
+
ctx,
|
|
1810
|
+
"cmp-after"
|
|
1811
|
+
)}<div class="cmp-before-clip">${img(s.before, ctx, "cmp-before")}</div><span class="cmp-label cmp-label--before">${escapeHtml(
|
|
1812
|
+
s.beforeLabel
|
|
1813
|
+
)}</span><span class="cmp-label cmp-label--after">${escapeHtml(
|
|
1814
|
+
s.afterLabel
|
|
1815
|
+
)}</span><input class="cmp-range" type="range" min="0" max="100" value="50" aria-label="Reveal before or after"><span class="cmp-handle" aria-hidden="true"></span></figure>${s.caption ? `<p class="shot-cap">${inline(s.caption)}</p>` : ""}`
|
|
666
1816
|
})
|
|
667
1817
|
});
|
|
668
1818
|
var walkthrough = defineLayout({
|
|
@@ -672,23 +1822,23 @@ var walkthrough = defineLayout({
|
|
|
672
1822
|
shape: {
|
|
673
1823
|
eyebrow: Text.optional(),
|
|
674
1824
|
title: Text,
|
|
675
|
-
label:
|
|
676
|
-
start:
|
|
1825
|
+
label: z7.string().default("STAGE").describe("Word on the badge, e.g. STAGE, PHASE, DAY."),
|
|
1826
|
+
start: z7.number().int().positive().default(1).describe(
|
|
677
1827
|
"Position of the first stage in the whole sequence (1-based). Use to continue a sequence split by other slides."
|
|
678
1828
|
),
|
|
679
|
-
numberFrom:
|
|
680
|
-
total:
|
|
1829
|
+
numberFrom: z7.number().int().default(1).describe("Number shown on the badge of the first stage in the sequence, e.g. 0."),
|
|
1830
|
+
total: z7.number().int().positive().optional().describe(
|
|
681
1831
|
'Stages in the whole sequence when it spans several walkthroughs (for "n of N" and the rail).'
|
|
682
1832
|
),
|
|
683
|
-
stages:
|
|
684
|
-
|
|
1833
|
+
stages: z7.array(
|
|
1834
|
+
z7.object({
|
|
685
1835
|
image: Image,
|
|
686
|
-
badge:
|
|
687
|
-
number:
|
|
1836
|
+
badge: z7.string().max(10).optional().describe('Replaces the badge for this stage, e.g. "PERMIT" (label is hidden).'),
|
|
1837
|
+
number: z7.union([z7.string().max(4), z7.number().int()]).optional().describe("Number shown under the label for this stage; defaults to its position."),
|
|
688
1838
|
eyebrow: Text.optional().describe("Overrides the walkthrough eyebrow for this stage."),
|
|
689
1839
|
heading: Text.optional(),
|
|
690
1840
|
text: Text,
|
|
691
|
-
notes:
|
|
1841
|
+
notes: z7.string().optional().describe("Speaker notes for this stage (falls back to the slide notes).")
|
|
692
1842
|
})
|
|
693
1843
|
).min(2).max(20)
|
|
694
1844
|
},
|
|
@@ -752,11 +1902,11 @@ var qr = defineLayout({
|
|
|
752
1902
|
eyebrowSub: Text.optional(),
|
|
753
1903
|
title: Text,
|
|
754
1904
|
lead: Text.optional(),
|
|
755
|
-
items:
|
|
756
|
-
url:
|
|
1905
|
+
items: z7.array(Item).optional(),
|
|
1906
|
+
url: z7.string().url().refine(isSafeLinkUrl, "Use an http, https, mailto or tel URL.").optional().describe("Where the code points. Required unless image is set."),
|
|
757
1907
|
image: Image.optional().describe("Use a pre-made QR/image instead of generating one from url."),
|
|
758
|
-
logo:
|
|
759
|
-
label:
|
|
1908
|
+
logo: z7.boolean().default(false).describe("Show the Classic Homes logo under the text."),
|
|
1909
|
+
label: z7.string().optional().describe("URL text shown under the code; defaults to the URL without https://.")
|
|
760
1910
|
},
|
|
761
1911
|
example: {
|
|
762
1912
|
layout: "qr",
|
|
@@ -765,6 +1915,7 @@ var qr = defineLayout({
|
|
|
765
1915
|
lead: "Scan to open the Homeowner Guide on your phone.",
|
|
766
1916
|
url: "https://guide.classichomes.com"
|
|
767
1917
|
},
|
|
1918
|
+
prepare: (s) => ({ qr: !s.image && s.url ? [s.url] : [] }),
|
|
768
1919
|
lint: (s) => [
|
|
769
1920
|
...maxLen(s.title, 70, ["title"], "Title"),
|
|
770
1921
|
...s.url || s.image ? [] : [
|
|
@@ -780,7 +1931,7 @@ var qr = defineLayout({
|
|
|
780
1931
|
const label = s.label ?? s.url?.replace(/^https?:\/\//, "").replace(/\/$/, "");
|
|
781
1932
|
const inner = `${code}${label ? `<div class="u">${escapeHtml(label)}</div>` : ""}`;
|
|
782
1933
|
return {
|
|
783
|
-
html: `<div class="cols"><div>${eyebrow(s.eyebrow, s.eyebrowSub)}${heading(s.title)}${lead(s.lead)}${items(s.items)}${s.logo ? `<div class="brand-logo end">${
|
|
1934
|
+
html: `<div class="cols"><div>${eyebrow(s.eyebrow, s.eyebrowSub)}${heading(s.title)}${lead(s.lead)}${items(s.items)}${s.logo ? `<div class="brand-logo end">${ctx.logo()}</div>` : ""}</div><div class="qr-wrap">${s.url ? `<a class="qr-hero" href="${escapeAttr(s.url)}">${inner}</a>` : `<div class="qr-hero">${inner}</div>`}</div></div>`
|
|
784
1935
|
};
|
|
785
1936
|
}
|
|
786
1937
|
});
|
|
@@ -792,13 +1943,13 @@ var contacts = defineLayout({
|
|
|
792
1943
|
eyebrow: Text.optional(),
|
|
793
1944
|
title: Text,
|
|
794
1945
|
lead: Text.optional(),
|
|
795
|
-
contacts:
|
|
796
|
-
|
|
1946
|
+
contacts: z7.array(
|
|
1947
|
+
z7.object({
|
|
797
1948
|
name: Text,
|
|
798
1949
|
role: Text.optional(),
|
|
799
|
-
phone:
|
|
800
|
-
email:
|
|
801
|
-
url:
|
|
1950
|
+
phone: z7.string().optional(),
|
|
1951
|
+
email: z7.string().email().optional(),
|
|
1952
|
+
url: z7.string().url().refine(isSafeLinkUrl, "Use an http, https, mailto or tel URL.").optional(),
|
|
802
1953
|
photo: Image.optional()
|
|
803
1954
|
})
|
|
804
1955
|
).min(1).max(9)
|
|
@@ -836,7 +1987,7 @@ var journey = defineLayout({
|
|
|
836
1987
|
eyebrowSub: Text.optional(),
|
|
837
1988
|
title: Text,
|
|
838
1989
|
lead: Text.optional(),
|
|
839
|
-
active:
|
|
1990
|
+
active: z7.number().int().positive().optional().describe("Step to highlight; defaults to the slide step.")
|
|
840
1991
|
},
|
|
841
1992
|
example: {
|
|
842
1993
|
layout: "journey",
|
|
@@ -874,15 +2025,169 @@ var LAYOUTS = [
|
|
|
874
2025
|
walkthrough,
|
|
875
2026
|
quote,
|
|
876
2027
|
qr,
|
|
877
|
-
contacts
|
|
2028
|
+
contacts,
|
|
2029
|
+
chart,
|
|
2030
|
+
table,
|
|
2031
|
+
flow,
|
|
2032
|
+
timeline,
|
|
2033
|
+
cycle,
|
|
2034
|
+
hierarchy,
|
|
2035
|
+
matrix,
|
|
2036
|
+
tiers,
|
|
2037
|
+
video,
|
|
2038
|
+
embed,
|
|
2039
|
+
compare
|
|
878
2040
|
];
|
|
879
2041
|
var byName = new Map(LAYOUTS.map((l) => [l.name, l]));
|
|
880
2042
|
function getLayout(name) {
|
|
881
2043
|
return byName.get(name);
|
|
882
2044
|
}
|
|
2045
|
+
var LAYOUT_CATEGORY_ORDER = [
|
|
2046
|
+
"Openers & dividers",
|
|
2047
|
+
"Text",
|
|
2048
|
+
"Data",
|
|
2049
|
+
"Diagrams",
|
|
2050
|
+
"Media",
|
|
2051
|
+
"People & places"
|
|
2052
|
+
];
|
|
2053
|
+
var LAYOUT_CATEGORIES = {
|
|
2054
|
+
title: "Openers & dividers",
|
|
2055
|
+
section: "Openers & dividers",
|
|
2056
|
+
statement: "Openers & dividers",
|
|
2057
|
+
journey: "Openers & dividers",
|
|
2058
|
+
bullets: "Text",
|
|
2059
|
+
cards: "Text",
|
|
2060
|
+
comparison: "Text",
|
|
2061
|
+
quote: "Text",
|
|
2062
|
+
stats: "Data",
|
|
2063
|
+
chart: "Data",
|
|
2064
|
+
table: "Data",
|
|
2065
|
+
flow: "Diagrams",
|
|
2066
|
+
timeline: "Diagrams",
|
|
2067
|
+
cycle: "Diagrams",
|
|
2068
|
+
hierarchy: "Diagrams",
|
|
2069
|
+
matrix: "Diagrams",
|
|
2070
|
+
tiers: "Diagrams",
|
|
2071
|
+
split: "Media",
|
|
2072
|
+
photos: "Media",
|
|
2073
|
+
image: "Media",
|
|
2074
|
+
compare: "Media",
|
|
2075
|
+
walkthrough: "Media",
|
|
2076
|
+
video: "Media",
|
|
2077
|
+
embed: "Media",
|
|
2078
|
+
qr: "People & places",
|
|
2079
|
+
contacts: "People & places"
|
|
2080
|
+
};
|
|
2081
|
+
|
|
2082
|
+
// src/edit.ts
|
|
2083
|
+
function findSlide(deck, ref) {
|
|
2084
|
+
const index = typeof ref === "number" || /^\d+$/.test(ref) ? Number(ref) - 1 : deck.slides.findIndex((s) => s.id === ref);
|
|
2085
|
+
if (index < 0 || index >= deck.slides.length) {
|
|
2086
|
+
throw new Error(
|
|
2087
|
+
`No slide "${ref}". The deck has ${deck.slides.length} slides; use a 1-based number or a slide id.`
|
|
2088
|
+
);
|
|
2089
|
+
}
|
|
2090
|
+
return index;
|
|
2091
|
+
}
|
|
2092
|
+
function newSlideId(taken = /* @__PURE__ */ new Set()) {
|
|
2093
|
+
const alphabet = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
2094
|
+
for (; ; ) {
|
|
2095
|
+
const bytes = new Uint8Array(6);
|
|
2096
|
+
globalThis.crypto.getRandomValues(bytes);
|
|
2097
|
+
const id = `s-${Array.from(bytes, (b) => alphabet[b % alphabet.length]).join("")}`;
|
|
2098
|
+
if (!taken.has(id)) return id;
|
|
2099
|
+
}
|
|
2100
|
+
}
|
|
2101
|
+
function ensureSlideIds(deck) {
|
|
2102
|
+
const taken = new Set(
|
|
2103
|
+
deck.slides.map((s) => s.id).filter((id) => typeof id === "string")
|
|
2104
|
+
);
|
|
2105
|
+
let added = 0;
|
|
2106
|
+
for (const slide of deck.slides) {
|
|
2107
|
+
if (typeof slide.id === "string" && slide.id) continue;
|
|
2108
|
+
slide.id = newSlideId(taken);
|
|
2109
|
+
taken.add(slide.id);
|
|
2110
|
+
added++;
|
|
2111
|
+
}
|
|
2112
|
+
return added;
|
|
2113
|
+
}
|
|
2114
|
+
function slideKey(slide, index) {
|
|
2115
|
+
return typeof slide.id === "string" && slide.id ? slide.id : `#${index + 1}`;
|
|
2116
|
+
}
|
|
2117
|
+
function addSlide(deck, slide, position) {
|
|
2118
|
+
if (typeof slide.id !== "string" || !slide.id) {
|
|
2119
|
+
slide = { ...slide, id: newSlideId(new Set(deck.slides.map((s) => s.id))) };
|
|
2120
|
+
}
|
|
2121
|
+
const at = position === void 0 ? deck.slides.length : Math.max(0, Math.min(deck.slides.length, position - 1));
|
|
2122
|
+
deck.slides.splice(at, 0, slide);
|
|
2123
|
+
return at + 1;
|
|
2124
|
+
}
|
|
2125
|
+
function updateSlide(deck, ref, patch) {
|
|
2126
|
+
const i = findSlide(deck, ref);
|
|
2127
|
+
const next = patch.layout && patch.layout !== deck.slides[i].layout ? { id: deck.slides[i].id } : { ...deck.slides[i] };
|
|
2128
|
+
for (const [k, v] of Object.entries(patch)) {
|
|
2129
|
+
if (v === null) delete next[k];
|
|
2130
|
+
else next[k] = v;
|
|
2131
|
+
}
|
|
2132
|
+
deck.slides[i] = next;
|
|
2133
|
+
return i + 1;
|
|
2134
|
+
}
|
|
2135
|
+
function moveSlide(deck, ref, to) {
|
|
2136
|
+
const from = findSlide(deck, ref);
|
|
2137
|
+
const [slide] = deck.slides.splice(from, 1);
|
|
2138
|
+
const at = Math.max(0, Math.min(deck.slides.length, to - 1));
|
|
2139
|
+
deck.slides.splice(at, 0, slide);
|
|
2140
|
+
return at + 1;
|
|
2141
|
+
}
|
|
2142
|
+
function removeSlide(deck, ref) {
|
|
2143
|
+
const i = findSlide(deck, ref);
|
|
2144
|
+
return deck.slides.splice(i, 1)[0];
|
|
2145
|
+
}
|
|
2146
|
+
function outline(deck) {
|
|
2147
|
+
return deck.slides.map((s, i) => {
|
|
2148
|
+
const label = s.title ?? s.quote ?? s.kicker ?? "";
|
|
2149
|
+
return `${i + 1}. [${s.layout}]${s.id ? ` #${s.id}` : ""} ${label}`.trim();
|
|
2150
|
+
});
|
|
2151
|
+
}
|
|
2152
|
+
|
|
2153
|
+
// src/schema/deck.ts
|
|
2154
|
+
import { z as z8 } from "zod";
|
|
2155
|
+
var slideSchemas = LAYOUTS.map(
|
|
2156
|
+
(layout) => z8.object({ layout: z8.literal(layout.name), ...baseSlide, ...layout.shape }).strict().describe(`${layout.description} Use for: ${layout.whenToUse}`)
|
|
2157
|
+
);
|
|
2158
|
+
var SlideSchema = z8.discriminatedUnion("layout", slideSchemas);
|
|
2159
|
+
var DeckSettingsSchema = z8.object({
|
|
2160
|
+
slideNumbers: z8.boolean().default(false).describe("Show the slide number in the footer."),
|
|
2161
|
+
logo: z8.enum(["title", "all", "none"]).default("title").describe(
|
|
2162
|
+
"Where the logo appears: title/closing slides only, a corner mark on every slide, or nowhere."
|
|
2163
|
+
),
|
|
2164
|
+
confidential: z8.string().max(40).optional().describe('Footer label such as "Internal \u2014 do not distribute".'),
|
|
2165
|
+
autoAdvance: z8.number().int().min(3).max(600).optional().describe(
|
|
2166
|
+
"Kiosk mode: seconds per slide (loops unattended). For a file or hosted URL, ?kiosk=15 also works."
|
|
2167
|
+
),
|
|
2168
|
+
loop: z8.boolean().default(false).describe("Return to the first slide after the last (kiosk).")
|
|
2169
|
+
}).strict();
|
|
2170
|
+
var DeckSchema = z8.object({
|
|
2171
|
+
title: z8.string().min(1).describe("Deck title (browser tab, file name, gallery)."),
|
|
2172
|
+
description: z8.string().optional().describe("One sentence: who the deck is for and what it covers."),
|
|
2173
|
+
author: z8.string().optional(),
|
|
2174
|
+
date: z8.string().optional(),
|
|
2175
|
+
lang: z8.string().default("en"),
|
|
2176
|
+
journey: z8.object({
|
|
2177
|
+
steps: z8.array(Text).min(2).max(10).describe("Short names of each step, in order."),
|
|
2178
|
+
label: z8.string().default("STEP").describe("Word on the vertical step rail.")
|
|
2179
|
+
}).optional().describe(
|
|
2180
|
+
'Optional numbered journey (agenda). Enables the journey layout, step rails and "you are here" strips.'
|
|
2181
|
+
),
|
|
2182
|
+
brand: DeckBrandSchema.optional().describe(
|
|
2183
|
+
"Which brand the deck speaks for (profile key) plus optional name/logo/favicon/footer overrides."
|
|
2184
|
+
),
|
|
2185
|
+
settings: DeckSettingsSchema.default({}),
|
|
2186
|
+
slides: z8.array(SlideSchema).min(1)
|
|
2187
|
+
}).strict();
|
|
883
2188
|
|
|
884
2189
|
// src/editor.ts
|
|
885
|
-
import { z as
|
|
2190
|
+
import { z as z9 } from "zod";
|
|
886
2191
|
var MULTILINE = /* @__PURE__ */ new Set(["lead", "body", "text", "quote", "footnote", "sub", "notes", "caption"]);
|
|
887
2192
|
var LABELS = {
|
|
888
2193
|
eyebrowSub: "Eyebrow subtitle",
|
|
@@ -892,8 +2197,11 @@ var LABELS = {
|
|
|
892
2197
|
imageAspect: "Photo shape",
|
|
893
2198
|
numberFrom: "First badge number",
|
|
894
2199
|
url: "URL",
|
|
895
|
-
id: "Slide id"
|
|
2200
|
+
id: "Slide id",
|
|
2201
|
+
time: "Speaking time",
|
|
2202
|
+
track: "Accent colour"
|
|
896
2203
|
};
|
|
2204
|
+
var ADVANCED = /* @__PURE__ */ new Set(["id", "time", "track"]);
|
|
897
2205
|
function labelFor(key) {
|
|
898
2206
|
if (LABELS[key]) return LABELS[key];
|
|
899
2207
|
const words = key.replace(/([A-Z])/g, " $1").toLowerCase();
|
|
@@ -906,19 +2214,21 @@ function unwrap(schema) {
|
|
|
906
2214
|
let description = schema.description;
|
|
907
2215
|
for (; ; ) {
|
|
908
2216
|
description ??= s.description;
|
|
909
|
-
if (s instanceof
|
|
2217
|
+
if (s instanceof z9.ZodOptional || s instanceof z9.ZodNullable) {
|
|
910
2218
|
required = false;
|
|
911
2219
|
s = s.unwrap();
|
|
912
|
-
} else if (s instanceof
|
|
2220
|
+
} else if (s instanceof z9.ZodDefault) {
|
|
913
2221
|
required = false;
|
|
914
2222
|
dflt = s._def.defaultValue();
|
|
915
2223
|
s = s._def.innerType;
|
|
2224
|
+
} else if (s instanceof z9.ZodEffects && s !== Image && s !== Item) {
|
|
2225
|
+
s = s.innerType();
|
|
916
2226
|
} else break;
|
|
917
2227
|
}
|
|
918
2228
|
return { inner: s, required, default: dflt, description };
|
|
919
2229
|
}
|
|
920
2230
|
function isText(s) {
|
|
921
|
-
return s === Text || s instanceof
|
|
2231
|
+
return s === Text || s instanceof z9.ZodString && s.description === Text.description;
|
|
922
2232
|
}
|
|
923
2233
|
function describeField(key, schema) {
|
|
924
2234
|
const { inner, required, default: dflt, description } = unwrap(schema);
|
|
@@ -928,16 +2238,36 @@ function describeField(key, schema) {
|
|
|
928
2238
|
required,
|
|
929
2239
|
...description ? { description } : {},
|
|
930
2240
|
...dflt !== void 0 ? { default: dflt } : {},
|
|
931
|
-
...MULTILINE.has(key) ? { multiline: true } : {}
|
|
2241
|
+
...MULTILINE.has(key) ? { multiline: true } : {},
|
|
2242
|
+
...ADVANCED.has(key) ? { advanced: true } : {}
|
|
932
2243
|
};
|
|
933
2244
|
if (inner === Image) return { ...base, kind: "image" };
|
|
2245
|
+
if (inner === ChartSpec) {
|
|
2246
|
+
return {
|
|
2247
|
+
...base,
|
|
2248
|
+
kind: "group",
|
|
2249
|
+
hint: "chart",
|
|
2250
|
+
fields: Object.entries(ChartSpec.shape).map(
|
|
2251
|
+
([k, v]) => describeField(k, v)
|
|
2252
|
+
)
|
|
2253
|
+
};
|
|
2254
|
+
}
|
|
934
2255
|
if (inner === Item) return { ...base, kind: "item" };
|
|
935
2256
|
if (isText(inner)) return { ...base, kind: "inline" };
|
|
936
|
-
if (inner instanceof
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
2257
|
+
if (inner instanceof z9.ZodString) {
|
|
2258
|
+
if (key === "url" || inner.isURL) {
|
|
2259
|
+
return {
|
|
2260
|
+
...base,
|
|
2261
|
+
kind: "string",
|
|
2262
|
+
hint: /YouTube|Vimeo/.test(description ?? "") ? "video-url" : "url"
|
|
2263
|
+
};
|
|
2264
|
+
}
|
|
2265
|
+
return { ...base, kind: "string" };
|
|
2266
|
+
}
|
|
2267
|
+
if (inner instanceof z9.ZodBoolean) return { ...base, kind: "boolean" };
|
|
2268
|
+
if (inner instanceof z9.ZodEnum) return { ...base, kind: "enum", options: [...inner.options] };
|
|
2269
|
+
if (inner instanceof z9.ZodLiteral) return { ...base, kind: "enum", options: [inner.value] };
|
|
2270
|
+
if (inner instanceof z9.ZodNumber) {
|
|
941
2271
|
return {
|
|
942
2272
|
...base,
|
|
943
2273
|
kind: "number",
|
|
@@ -946,9 +2276,9 @@ function describeField(key, schema) {
|
|
|
946
2276
|
...inner.maxValue !== null ? { max: inner.maxValue } : {}
|
|
947
2277
|
};
|
|
948
2278
|
}
|
|
949
|
-
if (inner instanceof
|
|
2279
|
+
if (inner instanceof z9.ZodUnion) {
|
|
950
2280
|
const opts = inner.options;
|
|
951
|
-
if (opts.every((o) => o instanceof
|
|
2281
|
+
if (opts.every((o) => o instanceof z9.ZodLiteral)) {
|
|
952
2282
|
return {
|
|
953
2283
|
...base,
|
|
954
2284
|
kind: "enum",
|
|
@@ -957,20 +2287,24 @@ function describeField(key, schema) {
|
|
|
957
2287
|
}
|
|
958
2288
|
return { ...base, kind: "string" };
|
|
959
2289
|
}
|
|
960
|
-
if (inner instanceof
|
|
2290
|
+
if (inner instanceof z9.ZodArray) {
|
|
961
2291
|
const el = inner.element;
|
|
962
2292
|
return {
|
|
963
2293
|
...base,
|
|
964
2294
|
kind: "list",
|
|
2295
|
+
...el instanceof z9.ZodArray ? { hint: "grid" } : {},
|
|
965
2296
|
of: { ...describeField("", el), label: labelFor(key.replace(/s$/, "")) },
|
|
966
2297
|
...inner._def.minLength ? { min: inner._def.minLength.value } : {},
|
|
967
2298
|
...inner._def.maxLength ? { max: inner._def.maxLength.value } : {}
|
|
968
2299
|
};
|
|
969
2300
|
}
|
|
970
|
-
if (inner instanceof
|
|
2301
|
+
if (inner instanceof z9.ZodObject) {
|
|
2302
|
+
const shape = inner.shape;
|
|
2303
|
+
const isPoint = shape.x instanceof z9.ZodNumber && shape.y instanceof z9.ZodNumber;
|
|
971
2304
|
return {
|
|
972
2305
|
...base,
|
|
973
2306
|
kind: "group",
|
|
2307
|
+
...isPoint ? { hint: "point" } : {},
|
|
974
2308
|
fields: Object.entries(inner.shape).map(
|
|
975
2309
|
([k, v]) => describeField(k, v)
|
|
976
2310
|
)
|
|
@@ -978,6 +2312,16 @@ function describeField(key, schema) {
|
|
|
978
2312
|
}
|
|
979
2313
|
return { ...base, kind: "string" };
|
|
980
2314
|
}
|
|
2315
|
+
function deckEditorFields() {
|
|
2316
|
+
return {
|
|
2317
|
+
brand: Object.entries(DeckBrandSchema.shape).map(
|
|
2318
|
+
([k, v]) => describeField(k, v)
|
|
2319
|
+
),
|
|
2320
|
+
settings: Object.entries(DeckSettingsSchema.shape).map(
|
|
2321
|
+
([k, v]) => describeField(k, v)
|
|
2322
|
+
)
|
|
2323
|
+
};
|
|
2324
|
+
}
|
|
981
2325
|
function commonSlideFields() {
|
|
982
2326
|
return Object.entries(baseSlide).map(([k, v]) => describeField(k, v));
|
|
983
2327
|
}
|
|
@@ -986,6 +2330,7 @@ function layoutEditorInfo(name) {
|
|
|
986
2330
|
if (!layout) return void 0;
|
|
987
2331
|
return {
|
|
988
2332
|
name: layout.name,
|
|
2333
|
+
category: LAYOUT_CATEGORIES[layout.name],
|
|
989
2334
|
description: layout.description,
|
|
990
2335
|
whenToUse: layout.whenToUse,
|
|
991
2336
|
defaults: layout.defaults ?? {},
|
|
@@ -998,6 +2343,16 @@ function layoutEditorInfo(name) {
|
|
|
998
2343
|
function allLayoutEditorInfo() {
|
|
999
2344
|
return LAYOUTS.map((l) => layoutEditorInfo(l.name));
|
|
1000
2345
|
}
|
|
2346
|
+
function layoutGalleryDeck() {
|
|
2347
|
+
const slides = [];
|
|
2348
|
+
const slideFor = {};
|
|
2349
|
+
for (const layout of LAYOUTS) {
|
|
2350
|
+
slideFor[layout.name] = slides.length;
|
|
2351
|
+
slides.push(blankSlide(layout.name));
|
|
2352
|
+
}
|
|
2353
|
+
const journey2 = { steps: ["Contract", "Design", "Build", "Close", "Move in", "Warranty"] };
|
|
2354
|
+
return { deck: { title: "Layouts", journey: journey2, slides }, slideFor };
|
|
2355
|
+
}
|
|
1001
2356
|
var PLACEHOLDER_IMAGE = "data:image/svg+xml;base64," + btoa(
|
|
1002
2357
|
'<svg xmlns="http://www.w3.org/2000/svg" width="1600" height="1000" viewBox="0 0 1600 1000"><rect width="1600" height="1000" fill="#ecebe6"/><rect x="24" y="24" width="1552" height="952" fill="none" stroke="#b9bcb4" stroke-width="6" stroke-dasharray="28 20"/><text x="800" y="520" text-anchor="middle" font-family="Figtree,Arial,sans-serif" font-size="64" fill="#7a7a7a">Add a photo</text></svg>'
|
|
1003
2358
|
);
|
|
@@ -1007,32 +2362,15 @@ function isPlaceholderImage(src) {
|
|
|
1007
2362
|
function blankSlide(name) {
|
|
1008
2363
|
const layout = getLayout(name);
|
|
1009
2364
|
if (!layout) return void 0;
|
|
1010
|
-
|
|
2365
|
+
const slide = JSON.parse(
|
|
1011
2366
|
JSON.stringify(layout.example),
|
|
1012
2367
|
(key, value) => key === "src" && typeof value === "string" && !/^(https?:|data:)/.test(value) ? PLACEHOLDER_IMAGE : value
|
|
1013
2368
|
);
|
|
2369
|
+
return { ...slide, id: newSlideId() };
|
|
1014
2370
|
}
|
|
1015
2371
|
|
|
1016
|
-
// src/
|
|
1017
|
-
import
|
|
1018
|
-
var slideSchemas = LAYOUTS.map(
|
|
1019
|
-
(layout) => z5.object({ layout: z5.literal(layout.name), ...baseSlide, ...layout.shape }).strict().describe(`${layout.description} Use for: ${layout.whenToUse}`)
|
|
1020
|
-
);
|
|
1021
|
-
var SlideSchema = z5.discriminatedUnion("layout", slideSchemas);
|
|
1022
|
-
var DeckSchema = z5.object({
|
|
1023
|
-
title: z5.string().min(1).describe("Deck title (browser tab, file name, gallery)."),
|
|
1024
|
-
description: z5.string().optional().describe("One sentence: who the deck is for and what it covers."),
|
|
1025
|
-
author: z5.string().optional(),
|
|
1026
|
-
date: z5.string().optional(),
|
|
1027
|
-
lang: z5.string().default("en"),
|
|
1028
|
-
journey: z5.object({
|
|
1029
|
-
steps: z5.array(Text).min(2).max(10).describe("Short names of each step, in order."),
|
|
1030
|
-
label: z5.string().default("STEP").describe("Word on the vertical step rail.")
|
|
1031
|
-
}).optional().describe(
|
|
1032
|
-
'Optional numbered journey (agenda). Enables the journey layout, step rails and "you are here" strips.'
|
|
1033
|
-
),
|
|
1034
|
-
slides: z5.array(SlideSchema).min(1)
|
|
1035
|
-
}).strict();
|
|
2372
|
+
// src/validate.ts
|
|
2373
|
+
import QRCode2 from "qrcode";
|
|
1036
2374
|
|
|
1037
2375
|
// src/render.ts
|
|
1038
2376
|
import QRCode from "qrcode";
|
|
@@ -1040,6 +2378,55 @@ import QRCode from "qrcode";
|
|
|
1040
2378
|
// src/runtime.ts
|
|
1041
2379
|
var RUNTIME_JS = String.raw`
|
|
1042
2380
|
(() => {
|
|
2381
|
+
// The presenter runs this exact script too, retaining the host's script hash.
|
|
2382
|
+
// A MessagePort stays attached to its document if the popup later navigates;
|
|
2383
|
+
// unlike window.postMessage('*'), it cannot leak notes to a replacement page.
|
|
2384
|
+
if (document.body.classList.contains('presenter')) {
|
|
2385
|
+
if (!window.opener) return;
|
|
2386
|
+
const channel = document.querySelector('meta[name="presenter-channel"]').content;
|
|
2387
|
+
const connection = new MessageChannel();
|
|
2388
|
+
const port = connection.port1;
|
|
2389
|
+
let started = Date.now();
|
|
2390
|
+
function scale(container, html) {
|
|
2391
|
+
container.innerHTML = html;
|
|
2392
|
+
const wrap = container.firstElementChild;
|
|
2393
|
+
if (!wrap?.classList.contains('ov-thumb')) return;
|
|
2394
|
+
const width = Math.max(200, container.clientWidth || 760);
|
|
2395
|
+
wrap.style.width = width + 'px';
|
|
2396
|
+
wrap.style.height = width * 9 / 16 + 'px';
|
|
2397
|
+
wrap.firstElementChild.style.transform = 'scale(' + width / 1280 + ')';
|
|
2398
|
+
}
|
|
2399
|
+
function tick() {
|
|
2400
|
+
const seconds = Math.floor((Date.now() - started) / 1000);
|
|
2401
|
+
document.querySelector('.pv-elapsed').textContent = Math.floor(seconds / 60) + ':' + String(seconds % 60).padStart(2, '0');
|
|
2402
|
+
document.querySelector('.pv-time').textContent = new Date().toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' });
|
|
2403
|
+
}
|
|
2404
|
+
port.onmessage = (event) => {
|
|
2405
|
+
const state = event.data;
|
|
2406
|
+
if (!state || state.type !== 'classic-slides:presenter-state') return;
|
|
2407
|
+
document.title = 'Presenter · ' + state.title;
|
|
2408
|
+
started = state.started;
|
|
2409
|
+
scale(document.querySelector('.pv-cur'), state.current);
|
|
2410
|
+
scale(document.querySelector('.pv-next'), state.next);
|
|
2411
|
+
document.querySelector('.pv-notes-body').innerHTML = state.notes;
|
|
2412
|
+
document.querySelector('.pv-cue').textContent = state.cue;
|
|
2413
|
+
document.querySelector('.pv-count').textContent = state.count;
|
|
2414
|
+
tick();
|
|
2415
|
+
};
|
|
2416
|
+
const keys = ['ArrowDown', 'ArrowRight', 'PageDown', ' ', 'Enter', 'ArrowUp', 'ArrowLeft', 'PageUp', 'Backspace', 'Home', 'End'];
|
|
2417
|
+
document.addEventListener('keydown', (event) => {
|
|
2418
|
+
if (event.metaKey || event.ctrlKey || event.altKey || !keys.includes(event.key)) return;
|
|
2419
|
+
if (event.target?.closest?.('a,button,input,textarea,select,[contenteditable]')) return;
|
|
2420
|
+
event.preventDefault();
|
|
2421
|
+
port.postMessage({ type: 'classic-slides:presenter-key', key: event.key });
|
|
2422
|
+
});
|
|
2423
|
+
window.addEventListener('resize', () => port.postMessage({ type: 'classic-slides:presenter-refresh' }));
|
|
2424
|
+
window.opener.postMessage({ type: 'classic-slides:presenter-ready', channel }, '*', [connection.port2]);
|
|
2425
|
+
const timer = setInterval(tick, 1000);
|
|
2426
|
+
window.addEventListener('beforeunload', () => { clearInterval(timer); port.close(); });
|
|
2427
|
+
return;
|
|
2428
|
+
}
|
|
2429
|
+
const runtimeSource = document.currentScript.textContent;
|
|
1043
2430
|
const NOTES = JSON.parse(document.getElementById('deck-notes').textContent || '[]');
|
|
1044
2431
|
const deck = document.getElementById('deck');
|
|
1045
2432
|
const slides = [...deck.querySelectorAll('.slide')];
|
|
@@ -1049,6 +2436,8 @@ var RUNTIME_JS = String.raw`
|
|
|
1049
2436
|
const notesTime = document.getElementById('notesTime');
|
|
1050
2437
|
const rail = document.getElementById('rail');
|
|
1051
2438
|
const counter = document.getElementById('counter');
|
|
2439
|
+
const body = document.body;
|
|
2440
|
+
const embedded = window.parent !== window;
|
|
1052
2441
|
|
|
1053
2442
|
slides.forEach((s, i) => {
|
|
1054
2443
|
const b = document.createElement('button');
|
|
@@ -1058,27 +2447,72 @@ var RUNTIME_JS = String.raw`
|
|
|
1058
2447
|
});
|
|
1059
2448
|
const dots = [...dotsWrap.children];
|
|
1060
2449
|
|
|
1061
|
-
|
|
1062
|
-
|
|
2450
|
+
// ---------- progressive reveal ----------
|
|
2451
|
+
const buildItems = slides.map((s) => [...s.querySelectorAll('[data-build] .b-item')]);
|
|
2452
|
+
const shown = slides.map((_, i) => buildItems[i].length); // start fully shown until entered forwards
|
|
2453
|
+
function paintBuild(i) {
|
|
2454
|
+
buildItems[i].forEach((el, j) => el.classList.toggle('b-hidden', j >= shown[i]));
|
|
2455
|
+
}
|
|
2456
|
+
|
|
2457
|
+
// ---------- media (video / embed) ----------
|
|
2458
|
+
function mountMedia(slide) {
|
|
2459
|
+
slide.querySelectorAll('.media-frame[data-media-src]').forEach((f) => {
|
|
2460
|
+
if (f.querySelector('iframe') || !navigator.onLine) return;
|
|
2461
|
+
const ifr = document.createElement('iframe');
|
|
2462
|
+
ifr.src = f.dataset.mediaSrc;
|
|
2463
|
+
ifr.title = f.dataset.mediaTitle || 'Embedded content';
|
|
2464
|
+
ifr.loading = 'eager';
|
|
2465
|
+
ifr.referrerPolicy = 'strict-origin-when-cross-origin';
|
|
2466
|
+
ifr.setAttribute('sandbox', 'allow-scripts allow-same-origin allow-forms allow-popups allow-presentation');
|
|
2467
|
+
if (f.dataset.mediaKind === 'video') {
|
|
2468
|
+
ifr.allow = 'autoplay; encrypted-media; picture-in-picture; fullscreen';
|
|
2469
|
+
ifr.allowFullscreen = true;
|
|
2470
|
+
}
|
|
2471
|
+
f.appendChild(ifr);
|
|
2472
|
+
f.classList.add('live');
|
|
2473
|
+
});
|
|
2474
|
+
}
|
|
2475
|
+
function unmountMedia(slide) {
|
|
2476
|
+
slide.querySelectorAll('.media-frame.live').forEach((f) => {
|
|
2477
|
+
f.querySelector('iframe')?.remove();
|
|
2478
|
+
f.classList.remove('live');
|
|
2479
|
+
});
|
|
2480
|
+
}
|
|
2481
|
+
|
|
2482
|
+
// ---------- before / after ----------
|
|
2483
|
+
deck.querySelectorAll('.compare').forEach((c) => {
|
|
2484
|
+
const r = c.querySelector('.cmp-range');
|
|
2485
|
+
r.addEventListener('input', () => c.style.setProperty('--pos', r.value + '%'));
|
|
2486
|
+
r.addEventListener('keydown', (e) => e.stopPropagation());
|
|
2487
|
+
});
|
|
2488
|
+
|
|
2489
|
+
// ---------- active slide ----------
|
|
2490
|
+
let idx = -1;
|
|
2491
|
+
function setActive(i, fromKeys) {
|
|
2492
|
+
if (i === idx) return;
|
|
2493
|
+
const prev = idx;
|
|
1063
2494
|
idx = i;
|
|
2495
|
+
if (prev >= 0) unmountMedia(slides[prev]);
|
|
2496
|
+
if (!fromKeys) shown[i] = buildItems[i].length; // arriving by scroll or jump shows everything
|
|
2497
|
+
paintBuild(i);
|
|
2498
|
+
mountMedia(slides[i]);
|
|
1064
2499
|
dots.forEach((d, j) => d.setAttribute('aria-current', j === i ? 'true' : 'false'));
|
|
1065
|
-
const
|
|
1066
|
-
const track = s.getAttribute('data-track');
|
|
2500
|
+
const track = slides[i].getAttribute('data-track');
|
|
1067
2501
|
rail.style.background = track === 'build' ? 'var(--teal)' : track === 'neutral' ? 'var(--navy)' : 'var(--red)';
|
|
1068
2502
|
counter.textContent = (i + 1) + ' / ' + slides.length;
|
|
1069
|
-
if (notes
|
|
1070
|
-
const n = NOTES[i] || {};
|
|
1071
|
-
notesBody.innerHTML = n.html || '<p><i>No notes for this slide.</i></p>';
|
|
1072
|
-
notesTime.textContent = n.time || '';
|
|
1073
|
-
}
|
|
1074
|
-
// Sandboxed or srcdoc frames may refuse history access; the deep link is optional.
|
|
2503
|
+
if (notes?.classList.contains('open')) fillNotes();
|
|
1075
2504
|
try {
|
|
1076
2505
|
if (location.hash !== '#slide-' + (i + 1)) history.replaceState(null, '', '#slide-' + (i + 1));
|
|
1077
2506
|
} catch (_) {}
|
|
1078
|
-
if (
|
|
2507
|
+
if (embedded) window.parent.postMessage({ type: 'classic-slides:slide', index: i, count: slides.length }, '*');
|
|
2508
|
+
presenterSync();
|
|
2509
|
+
}
|
|
2510
|
+
function fillNotes() {
|
|
2511
|
+
const n = NOTES[idx] || {};
|
|
2512
|
+
notesBody.innerHTML = n.html || '<p><i>No notes for this slide.</i></p>';
|
|
2513
|
+
notesTime.textContent = n.time || '';
|
|
1079
2514
|
}
|
|
1080
2515
|
|
|
1081
|
-
// Current slide comes from the scroll position, the single source of truth.
|
|
1082
2516
|
const clamp = (n) => Math.max(0, Math.min(slides.length - 1, n));
|
|
1083
2517
|
const currentIndex = () => clamp(Math.round(deck.scrollTop / deck.clientHeight));
|
|
1084
2518
|
function updateRail() {
|
|
@@ -1087,53 +2521,307 @@ var RUNTIME_JS = String.raw`
|
|
|
1087
2521
|
}
|
|
1088
2522
|
|
|
1089
2523
|
let ticking = false;
|
|
2524
|
+
let jumping = false;
|
|
1090
2525
|
deck.addEventListener('scroll', () => {
|
|
1091
2526
|
if (ticking) return;
|
|
1092
2527
|
ticking = true;
|
|
1093
2528
|
requestAnimationFrame(() => {
|
|
1094
2529
|
ticking = false;
|
|
1095
2530
|
updateRail();
|
|
1096
|
-
|
|
1097
|
-
if (i !== idx) setActive(i);
|
|
2531
|
+
if (!jumping) setActive(currentIndex());
|
|
1098
2532
|
});
|
|
1099
2533
|
}, { passive: true });
|
|
1100
2534
|
|
|
1101
2535
|
// Scroll the deck element itself; scrolling the slide into view would also move an embedding page.
|
|
1102
|
-
function go(n, instant) {
|
|
2536
|
+
function go(n, instant, fromKeys) {
|
|
1103
2537
|
const t = clamp(n);
|
|
1104
|
-
|
|
1105
|
-
|
|
2538
|
+
jumping = true;
|
|
2539
|
+
// 'auto' would inherit the CSS smooth scrolling; jumps must really be instant.
|
|
2540
|
+
deck.scrollTo({ top: t * deck.clientHeight, behavior: instant || window.matchMedia?.('(prefers-reduced-motion: reduce)').matches ? 'instant' : 'smooth' });
|
|
2541
|
+
setActive(t, fromKeys);
|
|
2542
|
+
clearTimeout(go.t);
|
|
2543
|
+
// Ignore scroll events until the programmatic scroll settles.
|
|
2544
|
+
go.t = setTimeout(() => { jumping = false; }, instant ? 80 : 1200);
|
|
2545
|
+
}
|
|
2546
|
+
deck.addEventListener('scrollend', () => {
|
|
2547
|
+
if (!jumping) return;
|
|
2548
|
+
clearTimeout(go.t);
|
|
2549
|
+
jumping = false;
|
|
2550
|
+
});
|
|
2551
|
+
|
|
2552
|
+
// next/prev step through build items first, then move between slides.
|
|
2553
|
+
// Entering a slide forwards starts its build from zero; going back shows it complete.
|
|
2554
|
+
function next() {
|
|
2555
|
+
if (idx < 0) return;
|
|
2556
|
+
if (shown[idx] < buildItems[idx].length) { shown[idx]++; paintBuild(idx); presenterSync(); return; }
|
|
2557
|
+
if (idx === slides.length - 1) { if (loop) { shown[0] = 0; go(0, true, true); } return; }
|
|
2558
|
+
shown[idx + 1] = 0;
|
|
2559
|
+
go(idx + 1, false, true);
|
|
2560
|
+
}
|
|
2561
|
+
function prev() {
|
|
2562
|
+
if (idx < 0) return;
|
|
2563
|
+
if (buildItems[idx].length && shown[idx] > 0) { shown[idx]--; paintBuild(idx); presenterSync(); return; }
|
|
2564
|
+
const t = clamp(idx - 1);
|
|
2565
|
+
shown[t] = buildItems[t].length;
|
|
2566
|
+
go(t, false, true);
|
|
1106
2567
|
}
|
|
1107
2568
|
|
|
1108
|
-
// Embedders (editors, previews) can drive the deck: postMessage({ type: 'classic-slides:go', index }).
|
|
1109
2569
|
window.addEventListener('message', (e) => {
|
|
1110
2570
|
const d = e.data;
|
|
1111
2571
|
if (!d || typeof d !== 'object') return;
|
|
2572
|
+
if (pw && e.source === pw && d.type === 'classic-slides:presenter-ready' && d.channel === presenterChannel && e.ports[0]) {
|
|
2573
|
+
presenterPort?.close();
|
|
2574
|
+
presenterPort = e.ports[0];
|
|
2575
|
+
presenterPort.onmessage = (event) => {
|
|
2576
|
+
const command = event.data;
|
|
2577
|
+
if (command?.type === 'classic-slides:presenter-refresh') presenterSync();
|
|
2578
|
+
else if (command?.type === 'classic-slides:presenter-key' && typeof command.key === 'string') {
|
|
2579
|
+
onKey({ key: command.key, preventDefault() {} });
|
|
2580
|
+
}
|
|
2581
|
+
};
|
|
2582
|
+
presenterSync();
|
|
2583
|
+
return;
|
|
2584
|
+
}
|
|
2585
|
+
if (!embedded || e.source !== window.parent) return;
|
|
1112
2586
|
if (d.type === 'classic-slides:go' && Number.isInteger(d.index)) go(d.index, true);
|
|
1113
|
-
else if (d.type === 'classic-slides:notes') { if (notes.classList.contains('open') !== !!d.open) toggleNotes(); }
|
|
2587
|
+
else if (d.type === 'classic-slides:notes') { if (notes && notes.classList.contains('open') !== !!d.open) toggleNotes(); }
|
|
2588
|
+
else if (d.type === 'classic-slides:highlight' && editor) highlight(d.index, d.path);
|
|
1114
2589
|
});
|
|
1115
2590
|
|
|
1116
|
-
|
|
2591
|
+
// ---------- editor click-to-edit ----------
|
|
2592
|
+
// Matches the clicked element to a slide field by its visible text (or an
|
|
2593
|
+
// image by its src), walking up from the target, so layouts need no markup.
|
|
2594
|
+
const editor = embedded && body.hasAttribute('data-editor');
|
|
2595
|
+
const fieldsEl = editor && document.getElementById('deck-fields');
|
|
2596
|
+
const FIELDS = fieldsEl ? JSON.parse(fieldsEl.textContent || '[]') : [];
|
|
2597
|
+
const norm = (t) => (t || '').replace(/\s+/g, ' ').trim();
|
|
2598
|
+
function ownText(el) {
|
|
2599
|
+
let t = '';
|
|
2600
|
+
el.childNodes.forEach((n) => { if (n.nodeType === 3) t += n.textContent; });
|
|
2601
|
+
return norm(t);
|
|
2602
|
+
}
|
|
2603
|
+
function fieldFor(el, candidates) {
|
|
2604
|
+
if (el.tagName === 'IMG') {
|
|
2605
|
+
const src = el.getAttribute('src');
|
|
2606
|
+
return candidates.find((c) => c.img && c.img === src);
|
|
2607
|
+
}
|
|
2608
|
+
const text = norm(el.textContent);
|
|
2609
|
+
const own = ownText(el);
|
|
2610
|
+
return candidates.find((c) => c.text && (c.text === text || (own && c.text === own)));
|
|
2611
|
+
}
|
|
2612
|
+
function locate(target) {
|
|
2613
|
+
const section = target.closest && target.closest('.slide');
|
|
2614
|
+
if (!section) return null;
|
|
2615
|
+
const index = Number(section.getAttribute('data-index'));
|
|
2616
|
+
const candidates = FIELDS[index] || [];
|
|
2617
|
+
for (let el = target; el && el !== section; el = el.parentElement) {
|
|
2618
|
+
const field = fieldFor(el, candidates);
|
|
2619
|
+
if (field) return { index, path: field.path, el };
|
|
2620
|
+
}
|
|
2621
|
+
return { index, path: null, el: section };
|
|
2622
|
+
}
|
|
2623
|
+
let hovered = null;
|
|
2624
|
+
let selected = null;
|
|
2625
|
+
function highlight(index, path) {
|
|
2626
|
+
selected?.classList.remove('cs-edit-selected');
|
|
2627
|
+
selected = null;
|
|
2628
|
+
if (!Array.isArray(path)) return;
|
|
2629
|
+
const key = JSON.stringify(path);
|
|
2630
|
+
const candidates = FIELDS[index] || [];
|
|
2631
|
+
for (const section of deck.querySelectorAll('.slide[data-index="' + index + '"]')) {
|
|
2632
|
+
for (const el of section.querySelectorAll('*')) {
|
|
2633
|
+
const field = fieldFor(el, candidates);
|
|
2634
|
+
if (field && JSON.stringify(field.path) === key) {
|
|
2635
|
+
selected = el;
|
|
2636
|
+
el.classList.add('cs-edit-selected');
|
|
2637
|
+
return;
|
|
2638
|
+
}
|
|
2639
|
+
}
|
|
2640
|
+
}
|
|
2641
|
+
}
|
|
2642
|
+
if (editor) {
|
|
2643
|
+
const style = document.createElement('style');
|
|
2644
|
+
style.textContent = '.cs-edit-hover{outline:2px dashed rgba(0,122,255,.7);outline-offset:4px;cursor:pointer}' +
|
|
2645
|
+
'.cs-edit-selected{outline:2px solid rgb(0,122,255);outline-offset:4px}';
|
|
2646
|
+
document.head.appendChild(style);
|
|
2647
|
+
deck.addEventListener('click', (e) => {
|
|
2648
|
+
const hit = locate(e.target);
|
|
2649
|
+
if (!hit) return;
|
|
2650
|
+
// Links on a slide would navigate the preview away
|
|
2651
|
+
if (e.target.closest('a[href]')) e.preventDefault();
|
|
2652
|
+
window.parent.postMessage({ type: 'classic-slides:select', index: hit.index, path: hit.path }, '*');
|
|
2653
|
+
}, true);
|
|
2654
|
+
deck.addEventListener('mouseover', (e) => {
|
|
2655
|
+
const hit = locate(e.target);
|
|
2656
|
+
const el = hit && hit.path ? hit.el : null;
|
|
2657
|
+
if (el === hovered) return;
|
|
2658
|
+
hovered?.classList.remove('cs-edit-hover');
|
|
2659
|
+
hovered = el;
|
|
2660
|
+
el?.classList.add('cs-edit-hover');
|
|
2661
|
+
});
|
|
2662
|
+
deck.addEventListener('mouseleave', () => { hovered?.classList.remove('cs-edit-hover'); hovered = null; });
|
|
2663
|
+
}
|
|
2664
|
+
|
|
2665
|
+
function onKey(e) {
|
|
1117
2666
|
if (e.metaKey || e.ctrlKey || e.altKey) return;
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
2667
|
+
const k = e.key;
|
|
2668
|
+
if (overview && k === 'Escape') { e.preventDefault(); closeOverview(); return; }
|
|
2669
|
+
if (overview && k === 'Tab') {
|
|
2670
|
+
const buttons = [...overview.querySelectorAll('button')];
|
|
2671
|
+
const first = buttons[0], last = buttons[buttons.length - 1];
|
|
2672
|
+
if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last?.focus(); }
|
|
2673
|
+
else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first?.focus(); }
|
|
2674
|
+
return;
|
|
2675
|
+
}
|
|
2676
|
+
if (e.target?.closest?.('a,button,input,textarea,select,[contenteditable]:not([contenteditable="false"]),[role="button"],[role="slider"]')) return;
|
|
2677
|
+
if (overview) return;
|
|
2678
|
+
if (['ArrowDown', 'ArrowRight', 'PageDown', ' ', 'Enter'].includes(k)) { e.preventDefault(); next(); }
|
|
2679
|
+
else if (['ArrowUp', 'ArrowLeft', 'PageUp', 'Backspace'].includes(k)) { e.preventDefault(); prev(); }
|
|
2680
|
+
else if (k === 'Home') { e.preventDefault(); go(0); }
|
|
2681
|
+
else if (k === 'End') { e.preventDefault(); go(slides.length - 1); }
|
|
2682
|
+
else if (k.toLowerCase() === 'n') toggleNotes();
|
|
2683
|
+
else if (k.toLowerCase() === 'f') toggleFs();
|
|
2684
|
+
else if (k.toLowerCase() === 'p') openPresenter();
|
|
2685
|
+
else if (k.toLowerCase() === 'o') toggleOverview();
|
|
2686
|
+
else return;
|
|
2687
|
+
wake();
|
|
2688
|
+
}
|
|
2689
|
+
document.addEventListener('keydown', onKey);
|
|
1125
2690
|
|
|
1126
|
-
|
|
1127
|
-
|
|
2691
|
+
// Shared links may render without speaker notes: no panel, no presenter window.
|
|
2692
|
+
function toggleNotes() { if (!notes) return; notes.classList.toggle('open'); if (notes.classList.contains('open')) fillNotes(); }
|
|
2693
|
+
document.getElementById('notesBtn')?.addEventListener('click', toggleNotes);
|
|
1128
2694
|
function toggleFs() {
|
|
1129
2695
|
if (!document.fullscreenElement) document.documentElement.requestFullscreen?.();
|
|
1130
2696
|
else document.exitFullscreen?.();
|
|
1131
2697
|
}
|
|
1132
2698
|
|
|
2699
|
+
// ---------- idle chrome ----------
|
|
2700
|
+
let idleT;
|
|
2701
|
+
function wake() {
|
|
2702
|
+
body.classList.remove('idle');
|
|
2703
|
+
clearTimeout(idleT);
|
|
2704
|
+
idleT = setTimeout(() => body.classList.add('idle'), 2800);
|
|
2705
|
+
}
|
|
2706
|
+
document.addEventListener('mousemove', wake, { passive: true });
|
|
2707
|
+
|
|
2708
|
+
// ---------- overview (O) ----------
|
|
2709
|
+
let overview = null;
|
|
2710
|
+
let overviewFocus = null;
|
|
2711
|
+
function scaledClone(i, width) {
|
|
2712
|
+
const wrap = document.createElement('div');
|
|
2713
|
+
wrap.className = 'ov-thumb';
|
|
2714
|
+
const inner = slides[i].cloneNode(true);
|
|
2715
|
+
inner.removeAttribute('id');
|
|
2716
|
+
inner.querySelectorAll('iframe').forEach((f) => f.remove());
|
|
2717
|
+
inner.querySelectorAll('.media-frame.live').forEach((f) => f.classList.remove('live'));
|
|
2718
|
+
inner.querySelectorAll('.b-hidden').forEach((el) => el.classList.remove('b-hidden'));
|
|
2719
|
+
inner.style.width = '1280px';
|
|
2720
|
+
inner.style.height = '720px';
|
|
2721
|
+
inner.style.transform = 'scale(' + width / 1280 + ')';
|
|
2722
|
+
inner.style.transformOrigin = '0 0';
|
|
2723
|
+
inner.style.position = 'absolute';
|
|
2724
|
+
inner.style.pointerEvents = 'none';
|
|
2725
|
+
wrap.style.width = width + 'px';
|
|
2726
|
+
wrap.style.height = (width * 9) / 16 + 'px';
|
|
2727
|
+
wrap.appendChild(inner);
|
|
2728
|
+
return wrap;
|
|
2729
|
+
}
|
|
2730
|
+
function toggleOverview() { overview ? closeOverview() : openOverview(); }
|
|
2731
|
+
function openOverview() {
|
|
2732
|
+
overviewFocus = document.activeElement;
|
|
2733
|
+
overview = document.createElement('div');
|
|
2734
|
+
overview.className = 'overview';
|
|
2735
|
+
overview.setAttribute('role', 'dialog');
|
|
2736
|
+
overview.setAttribute('aria-modal', 'true');
|
|
2737
|
+
overview.setAttribute('aria-label', 'All slides');
|
|
2738
|
+
const w = Math.min(300, Math.floor((window.innerWidth - 96) / 4) - 16);
|
|
2739
|
+
slides.forEach((_, i) => {
|
|
2740
|
+
const b = document.createElement('button');
|
|
2741
|
+
b.className = 'ov-item' + (i === idx ? ' ov-current' : '');
|
|
2742
|
+
b.setAttribute('aria-label', 'Go to slide ' + (i + 1));
|
|
2743
|
+
b.appendChild(scaledClone(i, w));
|
|
2744
|
+
const n = document.createElement('span');
|
|
2745
|
+
n.textContent = String(i + 1);
|
|
2746
|
+
b.appendChild(n);
|
|
2747
|
+
b.addEventListener('click', () => { closeOverview(); go(i, true); });
|
|
2748
|
+
overview.appendChild(b);
|
|
2749
|
+
});
|
|
2750
|
+
body.appendChild(overview);
|
|
2751
|
+
const cur = overview.querySelector('.ov-current');
|
|
2752
|
+
if (cur) { overview.scrollTop = cur.offsetTop - overview.clientHeight / 2 + cur.clientHeight / 2; cur.focus(); }
|
|
2753
|
+
}
|
|
2754
|
+
function closeOverview() { overview?.remove(); overview = null; overviewFocus?.focus(); overviewFocus = null; }
|
|
2755
|
+
|
|
2756
|
+
// ---------- presenter window (P) ----------
|
|
2757
|
+
let pw = null;
|
|
2758
|
+
let presenterPort = null;
|
|
2759
|
+
let presenterChannel = '';
|
|
2760
|
+
let presenterUrl = '';
|
|
2761
|
+
const started = Date.now();
|
|
2762
|
+
function openPresenter() {
|
|
2763
|
+
if (!notes) return;
|
|
2764
|
+
if (pw && !pw.closed) { pw.focus(); return; }
|
|
2765
|
+
presenterPort?.close();
|
|
2766
|
+
presenterPort = null;
|
|
2767
|
+
if (presenterUrl) URL.revokeObjectURL(presenterUrl);
|
|
2768
|
+
presenterChannel = Array.from(crypto.getRandomValues(new Uint8Array(24)), (n) => n.toString(16).padStart(2, '0')).join('');
|
|
2769
|
+
const styles = [...document.querySelectorAll('style')].map((s) => s.outerHTML).join('');
|
|
2770
|
+
const html = '<!doctype html><html><head><meta charset="utf-8"><meta name="presenter-channel" content="' + presenterChannel + '"><title>Presenter</title>' + styles +
|
|
2771
|
+
'</head><body class="presenter"><div class="pv-main"><div class="pv-cur"></div><div class="pv-side"><div class="pv-label">Next</div><div class="pv-next"></div>' +
|
|
2772
|
+
'<div class="pv-clock"><b class="pv-elapsed">0:00</b><span class="pv-time"></span><span class="pv-count"></span></div></div></div>' +
|
|
2773
|
+
'<div class="pv-notes"><div class="pv-label">Notes <span class="pv-cue"></span></div><div class="pv-notes-body notes__body"></div></div><script>' + runtimeSource + '<' + '/script></body></html>';
|
|
2774
|
+
presenterUrl = URL.createObjectURL(new Blob([html], { type: 'text/html' }));
|
|
2775
|
+
pw = window.open(presenterUrl, '_blank', 'width=1200,height=760');
|
|
2776
|
+
if (!pw) { URL.revokeObjectURL(presenterUrl); presenterUrl = ''; }
|
|
2777
|
+
}
|
|
2778
|
+
function presenterSync() {
|
|
2779
|
+
if (!pw || pw.closed || !presenterPort || idx < 0) return;
|
|
2780
|
+
const current = scaledClone(idx, 760);
|
|
2781
|
+
buildItems[idx].forEach((_, j) => {
|
|
2782
|
+
if (j >= shown[idx]) current.querySelectorAll('[data-build] .b-item')[j]?.classList.add('b-dim');
|
|
2783
|
+
});
|
|
2784
|
+
const n = NOTES[idx] || {};
|
|
2785
|
+
const steps = buildItems[idx].length ? ' · step ' + shown[idx] + '/' + buildItems[idx].length : '';
|
|
2786
|
+
presenterPort.postMessage({
|
|
2787
|
+
type: 'classic-slides:presenter-state', title: document.title, started,
|
|
2788
|
+
current: current.outerHTML,
|
|
2789
|
+
next: idx + 1 < slides.length ? scaledClone(idx + 1, 360).outerHTML : 'End of deck',
|
|
2790
|
+
notes: n.html || '<p><i>No notes for this slide.</i></p>', cue: n.time || '',
|
|
2791
|
+
count: 'Slide ' + (idx + 1) + ' of ' + slides.length + steps,
|
|
2792
|
+
});
|
|
2793
|
+
}
|
|
2794
|
+
window.addEventListener('beforeunload', () => {
|
|
2795
|
+
presenterPort?.close();
|
|
2796
|
+
if (pw && !pw.closed) pw.close();
|
|
2797
|
+
if (presenterUrl) URL.revokeObjectURL(presenterUrl);
|
|
2798
|
+
});
|
|
2799
|
+
|
|
2800
|
+
// ---------- kiosk ----------
|
|
2801
|
+
const km = /[?&]kiosk=(\d+)/.exec(location.search);
|
|
2802
|
+
const autoAdvance = km ? Number(km[1]) : Number(body.dataset.autoAdvance || 0);
|
|
2803
|
+
const loop = !!km || body.hasAttribute('data-loop');
|
|
2804
|
+
if (autoAdvance >= 3 && (!embedded || body.hasAttribute('data-embedded-kiosk'))) {
|
|
2805
|
+
body.classList.add('kiosk');
|
|
2806
|
+
setInterval(() => {
|
|
2807
|
+
shown[idx] = buildItems[idx].length;
|
|
2808
|
+
if (idx === slides.length - 1) { if (loop) go(0, true); }
|
|
2809
|
+
else go(idx + 1, true);
|
|
2810
|
+
}, autoAdvance * 1000);
|
|
2811
|
+
}
|
|
2812
|
+
|
|
2813
|
+
// ---------- start ----------
|
|
1133
2814
|
const m = /^#slide-(\d+)$/.exec(location.hash);
|
|
1134
|
-
const initial = m ? Number(m[1]) - 1 : Number(
|
|
1135
|
-
|
|
2815
|
+
const initial = m ? Number(m[1]) - 1 : Number(body.dataset.initialSlide || 0);
|
|
2816
|
+
// The script runs at the end of <body>, so layout exists; jump synchronously.
|
|
2817
|
+
if (initial > 0) go(initial, true);
|
|
1136
2818
|
else setActive(0);
|
|
2819
|
+
window.addEventListener('online', () => mountMedia(slides[idx]));
|
|
2820
|
+
window.addEventListener('hashchange', () => {
|
|
2821
|
+
const h = /^#slide-(\d+)$/.exec(location.hash);
|
|
2822
|
+
if (h && Number(h[1]) - 1 !== idx) go(Number(h[1]) - 1, true);
|
|
2823
|
+
});
|
|
2824
|
+
wake();
|
|
1137
2825
|
})();
|
|
1138
2826
|
`;
|
|
1139
2827
|
|
|
@@ -1141,13 +2829,14 @@ var RUNTIME_JS = String.raw`
|
|
|
1141
2829
|
var DECK_CSS = String.raw`
|
|
1142
2830
|
*{box-sizing:border-box}
|
|
1143
2831
|
html{scroll-behavior:smooth}
|
|
1144
|
-
@media (prefers-reduced-motion:reduce){html{scroll-behavior:auto}}
|
|
2832
|
+
@media (prefers-reduced-motion:reduce){html,.deck{scroll-behavior:auto}}
|
|
1145
2833
|
body{margin:0;font-family:var(--font-body);color:var(--ink);background:var(--paper);-webkit-font-smoothing:antialiased}
|
|
1146
2834
|
a{color:var(--teal)}
|
|
1147
2835
|
img{display:block;max-width:100%}
|
|
1148
2836
|
|
|
1149
2837
|
/* ---------- deck + slide frame ---------- */
|
|
1150
2838
|
.deck{height:100vh;overflow-y:scroll;scroll-snap-type:y mandatory;scroll-behavior:smooth}
|
|
2839
|
+
@media (prefers-reduced-motion:reduce){.deck{scroll-behavior:auto}}
|
|
1151
2840
|
.slide{height:100vh;scroll-snap-align:start;scroll-snap-stop:always;position:relative;display:flex;flex-direction:column;justify-content:center;
|
|
1152
2841
|
padding:clamp(24px,5.5vh,76px) clamp(28px,8vw,116px);overflow:hidden;
|
|
1153
2842
|
background:linear-gradient(var(--grid) 1px,transparent 1px) 0 0/100% 46px,linear-gradient(90deg,var(--grid) 1px,transparent 1px) 0 0/46px 100%,var(--paper)}
|
|
@@ -1446,9 +3135,292 @@ img.natural,.media img.natural,.photo-grid img.natural{width:auto;max-width:min(
|
|
|
1446
3135
|
.rail,.hud,.dots,.notes,.hint{display:none!important}
|
|
1447
3136
|
}
|
|
1448
3137
|
`;
|
|
3138
|
+
var DECK_CSS_EXTRA = String.raw`
|
|
3139
|
+
/* ---------- brand logo images ---------- */
|
|
3140
|
+
.logo--word .logo-word{font-weight:700;letter-spacing:.01em}
|
|
3141
|
+
.logo--img img{height:2.1em;width:auto;max-width:14em;object-fit:contain}
|
|
3142
|
+
.logo--img .logo-dark{display:none}
|
|
3143
|
+
.solid .logo--img .logo-light{display:none}
|
|
3144
|
+
.solid .logo--img .logo-dark{display:inline-block}
|
|
3145
|
+
|
|
3146
|
+
/* ---------- background photos ---------- */
|
|
3147
|
+
.slide.has-bg{background:var(--overlay),var(--bg-img) center/cover no-repeat}
|
|
3148
|
+
.slide.overlay-dark{--overlay:linear-gradient(100deg,hsl(196 100% 6% / .78),hsl(196 100% 8% / .45))}
|
|
3149
|
+
.slide.overlay-brand{--overlay:linear-gradient(100deg,color-mix(in srgb,var(--navy) 86%,transparent),color-mix(in srgb,var(--navy) 55%,transparent))}
|
|
3150
|
+
.slide.overlay-light{--overlay:linear-gradient(100deg,hsl(40 20% 98% / .92),hsl(40 20% 98% / .7))}
|
|
3151
|
+
.slide.has-bg.solid .lead{color:#fff}
|
|
3152
|
+
|
|
3153
|
+
/* ---------- footer, hidden ---------- */
|
|
3154
|
+
.slide-foot{position:absolute;left:clamp(28px,8vw,116px);right:clamp(28px,8vw,116px);bottom:14px;display:flex;align-items:center;justify-content:space-between;gap:16px;font-family:var(--font-mono);font-size:11px;letter-spacing:.04em;color:var(--ink-faint);pointer-events:none}
|
|
3155
|
+
.slide.has-step .slide-foot{left:clamp(92px,10vw,168px)}
|
|
3156
|
+
.slide-foot .foot-l{display:flex;align-items:center;gap:16px}
|
|
3157
|
+
.slide-foot .foot-l>span+span::before{content:"·";margin-right:16px}
|
|
3158
|
+
.slide-foot .foot-logo .logo{font-size:12px}
|
|
3159
|
+
.slide-foot .foot-n{font-weight:700;color:var(--ink-soft)}
|
|
3160
|
+
.slide.solid .slide-foot,.slide.solid .slide-foot .foot-n{color:hsl(0 0% 100% / .72)}
|
|
3161
|
+
.slide.is-hidden>.slide__inner{opacity:.45}
|
|
3162
|
+
.hidden-badge{position:absolute;top:14px;right:18px;font-family:var(--font-mono);font-size:11px;letter-spacing:.12em;text-transform:uppercase;background:var(--ink);color:#fff;padding:4px 8px;border-radius:4px}
|
|
3163
|
+
|
|
3164
|
+
/* ---------- progressive reveal ---------- */
|
|
3165
|
+
[data-build] .b-item{transition:opacity .35s ease,transform .35s ease}
|
|
3166
|
+
[data-build] .b-item.b-hidden{opacity:0;transform:translateY(10px)}
|
|
3167
|
+
.b-dim{opacity:.28!important;transform:none!important}
|
|
3168
|
+
@media (prefers-reduced-motion:reduce){[data-build] .b-item{transition:none}}
|
|
3169
|
+
|
|
3170
|
+
/* ---------- stats trends ---------- */
|
|
3171
|
+
.stat .trend{display:inline-block;margin:6px 0 0;font-family:var(--font-mono);font-style:normal;font-size:13px;font-weight:700}
|
|
3172
|
+
.trend--good{color:var(--good)}
|
|
3173
|
+
.trend--bad{color:var(--red)}
|
|
3174
|
+
.trend--flat{color:var(--ink-faint)}
|
|
3175
|
+
.stat .spark{display:block;width:100%;max-width:140px;height:32px;margin-top:8px}
|
|
3176
|
+
.spark polyline{fill:none;stroke:var(--red);stroke-width:2.5;stroke-linejoin:round;stroke-linecap:round}
|
|
3177
|
+
.spark circle{fill:var(--red)}
|
|
3178
|
+
.slide[data-track="build"] .spark polyline{stroke:var(--teal)}
|
|
3179
|
+
.slide[data-track="build"] .spark circle{fill:var(--teal)}
|
|
3180
|
+
.slide[data-track="neutral"] .spark polyline{stroke:var(--navy)}
|
|
3181
|
+
.slide[data-track="neutral"] .spark circle{fill:var(--navy)}
|
|
3182
|
+
|
|
3183
|
+
/* ---------- chart ---------- */
|
|
3184
|
+
.chart-fig{margin:8px 0 0}
|
|
3185
|
+
.chart-fig svg{display:block;width:100%;height:auto;max-height:56vh}
|
|
3186
|
+
.chart-split{display:grid;grid-template-columns:1.55fr 1fr;gap:clamp(20px,3vw,44px);align-items:center}
|
|
3187
|
+
@media (max-width:840px){.chart-split{grid-template-columns:1fr}}
|
|
3188
|
+
.insights ul.clean li{font-size:clamp(16px,1.7vw,21px)}
|
|
3189
|
+
.source{margin-top:6px;font-family:var(--font-mono);font-size:11.5px;color:var(--ink-faint)}
|
|
3190
|
+
.slide.solid .chart-fig{background:#fff;border-radius:10px;padding:14px}
|
|
3191
|
+
|
|
3192
|
+
/* ---------- table ---------- */
|
|
3193
|
+
.table-wrap{margin-top:14px;border:1px solid var(--line);border-radius:12px;overflow:hidden;background:var(--paper-2);box-shadow:var(--shadow)}
|
|
3194
|
+
.data-table{width:100%;border-collapse:collapse;font-size:clamp(14px,1.45vw,18px);font-variant-numeric:tabular-nums}
|
|
3195
|
+
.data-table th,.data-table td{padding:12px 18px;border-bottom:1px solid var(--line-faint);color:var(--ink-soft)}
|
|
3196
|
+
.data-table thead th{background:var(--paper-3);font-family:var(--font-mono);font-size:12px;font-weight:600;letter-spacing:.1em;text-transform:uppercase;color:var(--ink-faint)}
|
|
3197
|
+
.data-table tbody th{font-weight:600;color:var(--ink)}
|
|
3198
|
+
.data-table tbody tr:nth-child(even){background:hsl(60 11% 98%)}
|
|
3199
|
+
.data-table .al-left{text-align:left}.data-table .al-center{text-align:center}.data-table .al-right{text-align:right}
|
|
3200
|
+
.data-table .hl-row th,.data-table .hl-row td{background:var(--red-soft);color:var(--ink);font-weight:600}
|
|
3201
|
+
.data-table .hl-col{background:color-mix(in srgb,var(--red-soft) 70%,transparent)}
|
|
3202
|
+
.data-table thead .hl-col{color:var(--red)}
|
|
3203
|
+
.data-table tfoot th,.data-table tfoot td{font-weight:700;color:var(--ink);border-top:2px solid var(--ink);border-bottom:0}
|
|
3204
|
+
.slide.solid .data-table th,.slide.solid .data-table td{color:var(--ink-soft)}
|
|
3205
|
+
.slide.solid .data-table tbody th,.slide.solid .data-table tfoot *{color:var(--ink)}
|
|
3206
|
+
|
|
3207
|
+
/* ---------- flow ---------- */
|
|
3208
|
+
.flow{list-style:none;margin:18px 0 0;padding:0;display:grid;gap:clamp(22px,2.6vw,40px)}
|
|
3209
|
+
.flow--horizontal{grid-template-columns:repeat(var(--n),minmax(0,1fr));align-items:stretch}
|
|
3210
|
+
.flow--vertical{max-width:720px;gap:34px}
|
|
3211
|
+
.align-center .flow--vertical{margin-left:auto;margin-right:auto}
|
|
3212
|
+
.flow-step{position:relative;display:flex;gap:14px;align-items:flex-start;background:var(--paper-2);border:1px solid var(--line);border-radius:12px;padding:18px 20px;box-shadow:var(--shadow);text-align:left}
|
|
3213
|
+
.flow-step h3{margin:0 0 4px;font-size:clamp(16px,1.6vw,20px);color:var(--ink)}
|
|
3214
|
+
.flow-step p{margin:0;font-size:clamp(13.5px,1.35vw,16px);line-height:1.45;color:var(--ink-soft)}
|
|
3215
|
+
.flow-n{flex:none;width:32px;height:32px;border-radius:50%;background:var(--red);color:#fff;font-family:var(--font-mono);font-weight:700;display:flex;align-items:center;justify-content:center;font-size:14px}
|
|
3216
|
+
.slide[data-track="build"] .flow-n{background:var(--teal-fill)}
|
|
3217
|
+
.slide[data-track="neutral"] .flow-n{background:var(--navy)}
|
|
3218
|
+
.flow>li:not(:last-child)::after{content:"";position:absolute;background:var(--red);-webkit-mask:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M4 11h12.2l-4.6-4.6L13 5l7 7-7 7-1.4-1.4 4.6-4.6H4z'/%3E%3C/svg%3E") center/contain no-repeat;mask:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M4 11h12.2l-4.6-4.6L13 5l7 7-7 7-1.4-1.4 4.6-4.6H4z'/%3E%3C/svg%3E") center/contain no-repeat}
|
|
3219
|
+
.flow--horizontal>li:not(:last-child)::after{width:24px;height:24px;right:calc(-1 * clamp(22px,2.6vw,40px) / 2 - 12px);top:calc(50% - 12px)}
|
|
3220
|
+
.flow--vertical>li:not(:last-child)::after{width:26px;height:26px;left:calc(50% - 13px);bottom:-31px;transform:rotate(90deg)}
|
|
3221
|
+
.slide[data-track="build"] .flow>li::after{background:var(--teal)}
|
|
3222
|
+
.flow-end{position:relative;display:flex;align-items:center;justify-content:center}
|
|
3223
|
+
.flow-end span{background:var(--red);color:#fff;font-weight:700;letter-spacing:.12em;text-transform:uppercase;padding:12px 30px;border-radius:999px;font-size:clamp(14px,1.4vw,17px)}
|
|
3224
|
+
.slide.solid .flow-step h3{color:var(--ink)}
|
|
3225
|
+
.slide.solid .flow-step p{color:var(--ink-soft)}
|
|
3226
|
+
|
|
3227
|
+
/* ---------- timeline ---------- */
|
|
3228
|
+
.timeline{margin-top:24px;display:grid;grid-template-columns:1fr;gap:14px}
|
|
3229
|
+
.tl-phases{display:grid;grid-template-columns:repeat(var(--n),minmax(0,1fr));gap:0 10px}
|
|
3230
|
+
.tl-phase{border-radius:8px 8px 0 0;padding:8px 12px;font-family:var(--font-mono);font-size:12px;letter-spacing:.08em;text-transform:uppercase;font-weight:700;border-bottom:3px solid}
|
|
3231
|
+
.tl-phase--0{background:var(--red-soft);color:var(--red);border-color:var(--red)}
|
|
3232
|
+
.tl-phase--1{background:var(--teal-soft);color:var(--teal);border-color:var(--teal)}
|
|
3233
|
+
.tl-items{list-style:none;margin:0;padding:0;display:grid;grid-template-columns:repeat(var(--n),minmax(0,1fr));gap:10px;position:relative}
|
|
3234
|
+
.tl-items::before{content:"";position:absolute;left:0;right:0;top:9px;height:2px;background:var(--line)}
|
|
3235
|
+
.tl-item{position:relative;padding-top:30px;text-align:left}
|
|
3236
|
+
.tl-dot{position:absolute;top:2px;left:0;width:16px;height:16px;border-radius:50%;background:#fff;border:2.5px solid var(--line-strong)}
|
|
3237
|
+
.tl-in-0 .tl-dot,.tl-done .tl-dot{border-color:var(--red)}
|
|
3238
|
+
.tl-in-1 .tl-dot{border-color:var(--teal)}
|
|
3239
|
+
.tl-done .tl-dot{background:var(--red)}
|
|
3240
|
+
.tl-current .tl-dot{background:var(--red);border-color:var(--red);box-shadow:0 0 0 5px var(--red-soft);transform:scale(1.2)}
|
|
3241
|
+
.tl-date{display:block;font-family:var(--font-mono);font-size:12px;letter-spacing:.06em;color:var(--ink-faint);margin-bottom:4px}
|
|
3242
|
+
.tl-item h3{margin:0 0 4px;font-size:clamp(14px,1.45vw,18px);line-height:1.2;color:var(--ink)}
|
|
3243
|
+
.tl-item p{margin:0;font-size:clamp(12.5px,1.2vw,14.5px);line-height:1.4;color:var(--ink-soft)}
|
|
3244
|
+
.tl-current h3{color:var(--red)}
|
|
3245
|
+
.slide.solid .tl-item h3{color:#fff}
|
|
3246
|
+
.slide.solid .tl-items::before{background:hsl(0 0% 100% / .35)}
|
|
3247
|
+
|
|
3248
|
+
/* ---------- cycle ---------- */
|
|
3249
|
+
.cycle{position:relative;width:min(62vh,540px);aspect-ratio:1;margin:10px auto 0}
|
|
3250
|
+
.cycle-ring{position:absolute;inset:0;width:100%;height:100%;overflow:visible}
|
|
3251
|
+
.cycle-ring path{fill:none;stroke:var(--line-strong);stroke-width:.9}
|
|
3252
|
+
.cycle-ring marker path{fill:var(--line-strong);stroke:none}
|
|
3253
|
+
.cycle-center{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);font-family:var(--font-display);font-style:italic;font-size:clamp(20px,2.4vw,30px);color:var(--red);text-align:center;max-width:30%}
|
|
3254
|
+
.cycle-node{position:absolute;transform:translate(-50%,-50%);width:clamp(140px,15vw,200px);background:var(--paper-2);border:1px solid var(--line);border-top:3px solid var(--red);border-radius:10px;padding:12px 14px;box-shadow:var(--shadow);text-align:center}
|
|
3255
|
+
.slide[data-track="build"] .cycle-node{border-top-color:var(--teal)}
|
|
3256
|
+
.cycle-node h3{margin:0 0 3px;font-size:clamp(15px,1.5vw,18px);color:var(--ink)}
|
|
3257
|
+
.cycle-node p{margin:0;font-size:clamp(12.5px,1.2vw,14px);line-height:1.35;color:var(--ink-soft)}
|
|
3258
|
+
|
|
3259
|
+
/* ---------- hierarchy ---------- */
|
|
3260
|
+
.hierarchy{margin-top:16px;overflow:visible}
|
|
3261
|
+
.hierarchy ul{position:relative;display:flex;justify-content:center;gap:0;margin:0;padding:26px 0 0;list-style:none}
|
|
3262
|
+
.hierarchy>ul{padding-top:0}
|
|
3263
|
+
.hierarchy li{position:relative;display:flex;flex-direction:column;align-items:center;padding:26px 10px 0}
|
|
3264
|
+
.hierarchy>ul>li{padding-top:0}
|
|
3265
|
+
.hierarchy li::before,.hierarchy li::after{content:"";position:absolute;top:0;width:50%;height:26px;border-top:2px solid var(--line-strong)}
|
|
3266
|
+
.hierarchy li::before{right:50%}
|
|
3267
|
+
.hierarchy li::after{left:50%;border-left:2px solid var(--line-strong)}
|
|
3268
|
+
.hierarchy li:only-child::before,.hierarchy li:only-child::after{border-top:0}
|
|
3269
|
+
.hierarchy li:first-child::before,.hierarchy li:last-child::after{border-top:0}
|
|
3270
|
+
.hierarchy li:last-child::before{border-right:2px solid var(--line-strong)}
|
|
3271
|
+
.hierarchy li:last-child::after{border-left:0}
|
|
3272
|
+
.hierarchy>ul>li::before,.hierarchy>ul>li::after{display:none}
|
|
3273
|
+
.hierarchy ul ul::before{content:"";position:absolute;top:0;left:50%;height:26px;border-left:2px solid var(--line-strong)}
|
|
3274
|
+
.h-node{display:flex;align-items:center;gap:12px;background:var(--paper-2);border:1px solid var(--line);border-top:3px solid var(--red);border-radius:10px;padding:12px 16px;box-shadow:var(--shadow);text-align:left;min-width:150px;max-width:240px}
|
|
3275
|
+
.hierarchy>ul>li>.h-node{border-top-color:var(--navy)}
|
|
3276
|
+
.h-node .avatar{width:44px;height:44px;border-radius:50%;object-fit:cover;flex:none}
|
|
3277
|
+
.h-node h3{margin:0;font-size:clamp(14px,1.4vw,17px);color:var(--ink)}
|
|
3278
|
+
.h-node p{margin:2px 0 0;font-size:clamp(12px,1.15vw,13.5px);line-height:1.3;color:var(--ink-soft)}
|
|
3279
|
+
.slide.solid .hierarchy li::before,.slide.solid .hierarchy li::after,.slide.solid .hierarchy ul ul::before{border-color:hsl(0 0% 100% / .5)}
|
|
3280
|
+
|
|
3281
|
+
/* ---------- matrix ---------- */
|
|
3282
|
+
.matrix{display:grid;grid-template-columns:auto 1fr;grid-template-rows:1fr auto;gap:10px;margin-top:12px;max-width:980px}
|
|
3283
|
+
.mx-y{grid-row:1;display:flex;flex-direction:column-reverse;justify-content:space-between;align-items:center;writing-mode:vertical-rl;transform:rotate(180deg);font-size:13px;color:var(--ink-faint);font-family:var(--font-mono);letter-spacing:.04em}
|
|
3284
|
+
.mx-y b,.mx-x b{color:var(--ink);font-family:var(--font-body);letter-spacing:.08em;text-transform:uppercase;font-size:12px}
|
|
3285
|
+
.mx-grid{display:grid;grid-template-columns:1fr 1fr;grid-template-rows:1fr 1fr;gap:10px}
|
|
3286
|
+
.mx-x{grid-column:2;display:flex;justify-content:space-between;font-size:13px;color:var(--ink-faint);font-family:var(--font-mono);letter-spacing:.04em}
|
|
3287
|
+
.mx-q{background:var(--paper-2);border:1px solid var(--line);border-radius:12px;padding:16px 18px;box-shadow:var(--shadow);min-height:clamp(110px,17vh,170px);text-align:left}
|
|
3288
|
+
.mx-q h3{margin:0 0 8px;font-size:clamp(15px,1.5vw,19px);color:var(--ink)}
|
|
3289
|
+
.mx-q ul{margin:0;padding-left:18px;color:var(--ink-soft);font-size:clamp(13px,1.3vw,15.5px);line-height:1.5}
|
|
3290
|
+
.mx-hl{background:var(--red);border-color:var(--red)}
|
|
3291
|
+
.mx-hl h3,.mx-hl ul{color:#fff}
|
|
3292
|
+
|
|
3293
|
+
/* ---------- tiers ---------- */
|
|
3294
|
+
.tiers{list-style:none;margin:18px auto 0;padding:0;display:flex;flex-direction:column;gap:8px;max-width:880px}
|
|
3295
|
+
.tiers li{display:grid;grid-template-columns:1fr minmax(160px,.55fr);align-items:center;gap:22px}
|
|
3296
|
+
.tier-bar{width:var(--w);justify-self:center;display:flex;align-items:center;justify-content:space-between;gap:12px;padding:14px 22px;border-radius:6px;color:#fff;background:color-mix(in srgb,var(--red) calc(100% - var(--i) * 14%),var(--navy))}
|
|
3297
|
+
.slide[data-track="build"] .tier-bar{background:color-mix(in srgb,var(--teal-fill) calc(100% - var(--i) * 14%),var(--navy))}
|
|
3298
|
+
.tier-label{font-weight:700;font-size:clamp(15px,1.55vw,19px)}
|
|
3299
|
+
.tier-bar b{color:#fff;font-family:var(--font-display);font-size:clamp(18px,2vw,26px)}
|
|
3300
|
+
.tier-note{margin:0;font-size:clamp(13px,1.3vw,15.5px);color:var(--ink-soft);text-align:left}
|
|
3301
|
+
.slide.solid .tier-note{color:hsl(0 0% 100% / .85)}
|
|
3302
|
+
|
|
3303
|
+
/* ---------- image hotspots ---------- */
|
|
3304
|
+
.hs-layout{display:grid;grid-template-columns:1.5fr 1fr;gap:clamp(20px,3vw,40px);align-items:center}
|
|
3305
|
+
@media (max-width:840px){.hs-layout{grid-template-columns:1fr}}
|
|
3306
|
+
.hs-layout .diagram-shot{margin:0}
|
|
3307
|
+
.hs-wrap{position:relative;display:inline-block;max-width:100%}
|
|
3308
|
+
.hs-wrap img{display:block;max-height:62vh;width:auto;max-width:100%}
|
|
3309
|
+
.hs-mark{position:absolute;transform:translate(-50%,-50%);width:30px;height:30px;border-radius:50%;background:var(--red);color:#fff;font-family:var(--font-mono);font-weight:700;font-size:13px;display:flex;align-items:center;justify-content:center;box-shadow:0 0 0 3px #fff,0 4px 10px rgba(0,0,0,.3)}
|
|
3310
|
+
.hs-legend{margin:0;padding:0;list-style:none;counter-reset:hs;display:flex;flex-direction:column;gap:12px;text-align:left}
|
|
3311
|
+
.hs-legend li{counter-increment:hs;position:relative;padding-left:42px;font-size:clamp(14px,1.45vw,17px);line-height:1.4;color:var(--ink-soft)}
|
|
3312
|
+
.hs-legend li::before{content:counter(hs);position:absolute;left:0;top:-2px;width:28px;height:28px;border-radius:50%;background:var(--red);color:#fff;font-family:var(--font-mono);font-weight:700;font-size:12.5px;display:flex;align-items:center;justify-content:center}
|
|
3313
|
+
.hs-legend b{display:block;color:var(--ink)}
|
|
3314
|
+
.slide.solid .hs-legend li{color:hsl(0 0% 100% / .85)}
|
|
3315
|
+
.slide.solid .hs-legend b{color:#fff}
|
|
3316
|
+
|
|
3317
|
+
/* ---------- video / embed ---------- */
|
|
3318
|
+
.media-frame{position:relative;margin:8px auto 0;width:min(100%,calc(62vh * 16 / 9));aspect-ratio:16/9;border-radius:12px;overflow:hidden;background:var(--navy);box-shadow:0 18px 40px -18px rgba(0,0,0,.45)}
|
|
3319
|
+
.media-frame.aspect-4x3{aspect-ratio:4/3;width:min(100%,calc(62vh * 4 / 3))}
|
|
3320
|
+
.media-frame.aspect-1x1{aspect-ratio:1;width:min(100%,62vh)}
|
|
3321
|
+
.media-frame.aspect-tall{aspect-ratio:9/14;width:min(100%,calc(64vh * 9 / 14))}
|
|
3322
|
+
.media-poster{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}
|
|
3323
|
+
.media-frame iframe{position:absolute;inset:0;width:100%;height:100%;border:0;background:#fff;z-index:3}
|
|
3324
|
+
.media-fallback{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;gap:28px;background:linear-gradient(180deg,hsl(196 100% 8% / .25),hsl(196 100% 8% / .6));z-index:2}
|
|
3325
|
+
.media-frame.live .media-fallback{display:none}
|
|
3326
|
+
.play-badge{width:84px;height:84px;border-radius:50%;background:var(--red);color:#fff;font-size:34px;display:flex;align-items:center;justify-content:center;padding-left:6px;box-shadow:0 8px 24px rgba(0,0,0,.35)}
|
|
3327
|
+
.media-qr{background:#fff;border-radius:12px;padding:12px;text-align:center;width:150px;box-shadow:var(--shadow)}
|
|
3328
|
+
.media-qr svg{width:100%;height:auto;display:block}
|
|
3329
|
+
.media-qr a{display:block;margin-top:8px;font-family:var(--font-mono);font-size:10.5px;color:var(--ink);text-decoration:none;overflow-wrap:anywhere}
|
|
3330
|
+
.media-open{position:absolute;right:10px;top:10px;z-index:4;font-family:var(--font-mono);font-size:11.5px;background:hsl(0 0% 100% / .92);color:var(--ink);padding:5px 9px;border-radius:6px;text-decoration:none;opacity:.85}
|
|
3331
|
+
.media-open:hover{opacity:1}
|
|
3332
|
+
.has-media .shot-cap{text-align:center}
|
|
3333
|
+
|
|
3334
|
+
/* ---------- compare ---------- */
|
|
3335
|
+
.compare{position:relative;margin:8px auto 0;width:min(100%,calc(62vh * 16 / 10));aspect-ratio:16/10;border-radius:12px;overflow:hidden;box-shadow:var(--shadow);user-select:none}
|
|
3336
|
+
.compare img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}
|
|
3337
|
+
.cmp-before-clip{position:absolute;inset:0;clip-path:inset(0 calc(100% - var(--pos)) 0 0)}
|
|
3338
|
+
.cmp-handle{position:absolute;top:0;bottom:0;left:var(--pos);width:3px;margin-left:-1.5px;background:#fff;box-shadow:0 0 12px rgba(0,0,0,.4);pointer-events:none}
|
|
3339
|
+
.cmp-handle::after{content:"◀ ▶";position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);background:#fff;color:var(--ink);font-size:11px;padding:8px 10px;border-radius:999px;white-space:nowrap;box-shadow:var(--shadow)}
|
|
3340
|
+
.cmp-range{position:absolute;inset:0;width:100%;height:100%;opacity:0;cursor:ew-resize;margin:0}
|
|
3341
|
+
.cmp-label{position:absolute;bottom:12px;z-index:2;font-family:var(--font-mono);font-size:12px;letter-spacing:.1em;text-transform:uppercase;background:hsl(196 100% 8% / .72);color:#fff;padding:5px 10px;border-radius:5px}
|
|
3342
|
+
.cmp-label--before{left:12px}
|
|
3343
|
+
.cmp-label--after{right:12px}
|
|
3344
|
+
|
|
3345
|
+
/* ---------- chrome: idle, kiosk, overview, presenter ---------- */
|
|
3346
|
+
body.has-foot .hud,body.has-foot .hint{bottom:40px}
|
|
3347
|
+
.hud,.hint,.dots,.rail{transition:opacity .4s}
|
|
3348
|
+
body.idle .hud,body.idle .hint,body.idle .dots{opacity:0}
|
|
3349
|
+
body.kiosk .hud,body.kiosk .hint,body.kiosk .dots{display:none}
|
|
3350
|
+
body.kiosk .deck{cursor:none}
|
|
3351
|
+
.overview{position:fixed;inset:0;z-index:80;background:hsl(196 100% 6% / .94);display:flex;flex-wrap:wrap;align-content:flex-start;gap:16px;padding:40px 48px;overflow-y:auto}
|
|
3352
|
+
.ov-item{position:relative;padding:0;border:2px solid transparent;border-radius:8px;background:none;cursor:pointer}
|
|
3353
|
+
.ov-item:hover,.ov-item:focus-visible{border-color:#fff;outline:0}
|
|
3354
|
+
.ov-current{border-color:var(--red)}
|
|
3355
|
+
.ov-thumb{position:relative;overflow:hidden;border-radius:6px;background:var(--paper)}
|
|
3356
|
+
.ov-item>span{position:absolute;left:6px;bottom:6px;font-family:var(--font-mono);font-size:11px;background:hsl(196 100% 6% / .8);color:#fff;padding:2px 6px;border-radius:4px}
|
|
3357
|
+
body.presenter{margin:0;height:100vh;display:grid;grid-template-rows:1fr auto;background:#0b1d24;color:#fff;font-family:var(--font-body)}
|
|
3358
|
+
.pv-main{display:grid;grid-template-columns:2fr 1fr;gap:20px;padding:20px;min-height:0}
|
|
3359
|
+
.pv-cur,.pv-next{position:relative;min-width:0}
|
|
3360
|
+
.pv-cur .ov-thumb,.pv-next .ov-thumb{box-shadow:0 10px 30px rgba(0,0,0,.4)}
|
|
3361
|
+
.pv-side{display:flex;flex-direction:column;gap:12px;min-width:0}
|
|
3362
|
+
.pv-label{font-family:var(--font-mono);font-size:11px;letter-spacing:.16em;text-transform:uppercase;color:hsl(0 0% 100% / .6)}
|
|
3363
|
+
.pv-clock{margin-top:auto;display:flex;flex-direction:column;gap:4px;font-family:var(--font-mono)}
|
|
3364
|
+
.pv-elapsed{font-size:46px;font-weight:700;color:#fff}
|
|
3365
|
+
.pv-time,.pv-count{color:hsl(0 0% 100% / .7);font-size:14px}
|
|
3366
|
+
.pv-notes{border-top:3px solid var(--red);padding:16px 24px 22px;max-height:34vh;overflow-y:auto;background:var(--navy)}
|
|
3367
|
+
.pv-notes .notes__body{font-size:19px;max-width:none}
|
|
3368
|
+
.pv-cue{margin-left:12px;color:hsl(0 0% 100% / .5)}
|
|
3369
|
+
|
|
3370
|
+
@media print{
|
|
3371
|
+
.slide-foot{position:absolute}
|
|
3372
|
+
[data-build] .b-item.b-hidden{opacity:1;transform:none}
|
|
3373
|
+
.media-frame iframe{display:none}
|
|
3374
|
+
.media-frame .media-fallback{display:flex!important}
|
|
3375
|
+
.cmp-before-clip{clip-path:inset(0 50% 0 0)}
|
|
3376
|
+
.overview,.hidden-badge{display:none}
|
|
3377
|
+
}
|
|
3378
|
+
`;
|
|
1449
3379
|
|
|
1450
3380
|
// src/render.ts
|
|
1451
3381
|
var PREVIEW_SIZE = { width: 1280, height: 720 };
|
|
3382
|
+
var NON_VISUAL_KEYS = /* @__PURE__ */ new Set([
|
|
3383
|
+
"layout",
|
|
3384
|
+
"id",
|
|
3385
|
+
"notes",
|
|
3386
|
+
"time",
|
|
3387
|
+
"hidden",
|
|
3388
|
+
"background",
|
|
3389
|
+
"backgroundImage",
|
|
3390
|
+
"overlay",
|
|
3391
|
+
"track",
|
|
3392
|
+
"step",
|
|
3393
|
+
"stepDone",
|
|
3394
|
+
"showJourney",
|
|
3395
|
+
"href",
|
|
3396
|
+
"url",
|
|
3397
|
+
"fit",
|
|
3398
|
+
"focus",
|
|
3399
|
+
"size",
|
|
3400
|
+
"alt"
|
|
3401
|
+
]);
|
|
3402
|
+
function fieldMarkers(slide, resolveImage = (src) => src) {
|
|
3403
|
+
const out = [];
|
|
3404
|
+
const walk = (value, path) => {
|
|
3405
|
+
if (typeof value === "string") {
|
|
3406
|
+
const text = plain(value).replace(/\s+/g, " ").trim();
|
|
3407
|
+
if (text) out.push({ path, text });
|
|
3408
|
+
} else if (Array.isArray(value)) {
|
|
3409
|
+
value.forEach((v, i) => walk(v, [...path, i]));
|
|
3410
|
+
} else if (value && typeof value === "object") {
|
|
3411
|
+
const obj = value;
|
|
3412
|
+
if (typeof obj.src === "string") {
|
|
3413
|
+
out.push({ path, img: resolveImage(obj.src) });
|
|
3414
|
+
return;
|
|
3415
|
+
}
|
|
3416
|
+
for (const [key, v] of Object.entries(obj)) {
|
|
3417
|
+
if (!NON_VISUAL_KEYS.has(key)) walk(v, [...path, key]);
|
|
3418
|
+
}
|
|
3419
|
+
}
|
|
3420
|
+
};
|
|
3421
|
+
walk(slide, []);
|
|
3422
|
+
return out;
|
|
3423
|
+
}
|
|
1452
3424
|
var passThrough = (src) => {
|
|
1453
3425
|
if (/^(data:|https?:)/.test(src)) return src;
|
|
1454
3426
|
throw new Error(
|
|
@@ -1466,39 +3438,146 @@ function collectImageSources(value, out = /* @__PURE__ */ new Set()) {
|
|
|
1466
3438
|
}
|
|
1467
3439
|
var CHECK_SVG = '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 12.5 L10 17.5 L19 7"/></svg>';
|
|
1468
3440
|
async function renderDeck(input, options = {}) {
|
|
1469
|
-
const result = validateDeck(input);
|
|
3441
|
+
const result = validateDeck(input, { brands: options.brands });
|
|
1470
3442
|
if (!result.ok || !result.deck) throw new DeckError(result.issues);
|
|
1471
3443
|
const deck = result.deck;
|
|
3444
|
+
const brand = resolveBrand(deck.brand, options.brands);
|
|
3445
|
+
const visible = deck.slides.filter((sl) => options.includeHidden || !sl.hidden);
|
|
3446
|
+
const qrUrls = /* @__PURE__ */ new Set();
|
|
3447
|
+
const chartReqs = /* @__PURE__ */ new Map();
|
|
3448
|
+
const extraImages = /* @__PURE__ */ new Set();
|
|
3449
|
+
for (const slide of visible) {
|
|
3450
|
+
const prep = getLayout(slide.layout).prepare?.(slide);
|
|
3451
|
+
prep?.qr?.forEach((u) => qrUrls.add(u));
|
|
3452
|
+
prep?.charts?.forEach((c) => chartReqs.set(c.key, c));
|
|
3453
|
+
prep?.images?.forEach((u) => extraImages.add(u));
|
|
3454
|
+
}
|
|
1472
3455
|
const resolve = options.resolveImage ?? passThrough;
|
|
1473
3456
|
const images = /* @__PURE__ */ new Map();
|
|
3457
|
+
const brandImages = [brand.logo, brand.logoOnDark, brand.favicon].flatMap(
|
|
3458
|
+
(i) => i ? [i.src] : []
|
|
3459
|
+
);
|
|
3460
|
+
const requiredImages = /* @__PURE__ */ new Set([...collectImageSources(visible), ...brandImages]);
|
|
3461
|
+
const pendingImages = [.../* @__PURE__ */ new Set([...requiredImages, ...extraImages])];
|
|
3462
|
+
let nextImage = 0;
|
|
3463
|
+
let imageError;
|
|
3464
|
+
let imageFailed = false;
|
|
1474
3465
|
await Promise.all(
|
|
1475
|
-
|
|
3466
|
+
Array.from({ length: Math.min(4, pendingImages.length) }, async () => {
|
|
3467
|
+
while (!imageFailed && nextImage < pendingImages.length) {
|
|
3468
|
+
const src = pendingImages[nextImage++];
|
|
3469
|
+
try {
|
|
3470
|
+
images.set(src, await resolve(src));
|
|
3471
|
+
} catch (error) {
|
|
3472
|
+
if (requiredImages.has(src)) {
|
|
3473
|
+
imageFailed = true;
|
|
3474
|
+
imageError = error;
|
|
3475
|
+
return;
|
|
3476
|
+
}
|
|
3477
|
+
images.set(src, src);
|
|
3478
|
+
}
|
|
3479
|
+
}
|
|
3480
|
+
})
|
|
1476
3481
|
);
|
|
3482
|
+
if (imageFailed) throw imageError;
|
|
1477
3483
|
const qrs = /* @__PURE__ */ new Map();
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
3484
|
+
await Promise.all(
|
|
3485
|
+
[...qrUrls].map(async (url) => {
|
|
3486
|
+
try {
|
|
3487
|
+
qrs.set(
|
|
3488
|
+
url,
|
|
3489
|
+
await QRCode.toString(url, {
|
|
3490
|
+
type: "svg",
|
|
3491
|
+
margin: 0,
|
|
3492
|
+
errorCorrectionLevel: "M",
|
|
3493
|
+
color: { dark: "#002e3fff", light: "#ffffffff" }
|
|
3494
|
+
})
|
|
3495
|
+
);
|
|
3496
|
+
} catch {
|
|
3497
|
+
throw new DeckError([
|
|
3498
|
+
{
|
|
3499
|
+
level: "error",
|
|
3500
|
+
path: ["slides"],
|
|
3501
|
+
message: "A QR code could not be generated. Shorten its URL and try again."
|
|
3502
|
+
}
|
|
3503
|
+
]);
|
|
3504
|
+
}
|
|
3505
|
+
})
|
|
3506
|
+
);
|
|
3507
|
+
const charts = /* @__PURE__ */ new Map();
|
|
3508
|
+
for (const [key, req] of chartReqs) {
|
|
3509
|
+
charts.set(
|
|
3510
|
+
key,
|
|
3511
|
+
await renderChartSvg(req.spec, {
|
|
3512
|
+
width: req.width,
|
|
3513
|
+
height: req.height,
|
|
3514
|
+
palette: brand.stock ? void 0 : brand.palette
|
|
3515
|
+
})
|
|
3516
|
+
);
|
|
1492
3517
|
}
|
|
3518
|
+
const imageBudget = options.maxInlineImageBytes;
|
|
3519
|
+
if (imageBudget !== void 0 && (!Number.isFinite(imageBudget) || imageBudget < 0)) {
|
|
3520
|
+
throw new RangeError("maxInlineImageBytes must be a finite nonnegative number.");
|
|
3521
|
+
}
|
|
3522
|
+
let emittedImageBytes = 0;
|
|
3523
|
+
let imagePath = ["brand"];
|
|
3524
|
+
const imageSizes = /* @__PURE__ */ new Map();
|
|
3525
|
+
const warnedImages = /* @__PURE__ */ new Set();
|
|
3526
|
+
const image2 = (src, path = imagePath) => {
|
|
3527
|
+
const resolved = images.get(src) ?? src;
|
|
3528
|
+
if (imageBudget === void 0 || !/^data:/i.test(resolved)) return resolved;
|
|
3529
|
+
let bytes = imageSizes.get(resolved);
|
|
3530
|
+
if (bytes === void 0) {
|
|
3531
|
+
bytes = escapedImageBytes(resolved);
|
|
3532
|
+
imageSizes.set(resolved, bytes);
|
|
3533
|
+
}
|
|
3534
|
+
if (emittedImageBytes + bytes <= imageBudget) {
|
|
3535
|
+
emittedImageBytes += bytes;
|
|
3536
|
+
return resolved;
|
|
3537
|
+
}
|
|
3538
|
+
if (/^https:\/\//i.test(src)) {
|
|
3539
|
+
if (!warnedImages.has(src)) {
|
|
3540
|
+
warnedImages.add(src);
|
|
3541
|
+
result.issues.push({
|
|
3542
|
+
level: "warning",
|
|
3543
|
+
path,
|
|
3544
|
+
message: "An image remains at its original HTTPS URL because the inline image budget was reached."
|
|
3545
|
+
});
|
|
3546
|
+
}
|
|
3547
|
+
return src;
|
|
3548
|
+
}
|
|
3549
|
+
throw new DeckError([
|
|
3550
|
+
{
|
|
3551
|
+
level: "error",
|
|
3552
|
+
path,
|
|
3553
|
+
message: "This embedded image exceeds the rendered inline image budget. Use smaller images or an HTTPS image URL."
|
|
3554
|
+
}
|
|
3555
|
+
]);
|
|
3556
|
+
};
|
|
3557
|
+
const icon = brand.favicon ? image2(brand.favicon.src, ["brand", "favicon"]) : image2(
|
|
3558
|
+
favicon(brand, (src) => images.get(src) ?? src),
|
|
3559
|
+
["brand", "favicon"]
|
|
3560
|
+
);
|
|
3561
|
+
const logo = () => deck.settings.logo === "none" ? "" : brandLogo(brand, image2);
|
|
1493
3562
|
const ctx = {
|
|
1494
3563
|
deck,
|
|
1495
|
-
image:
|
|
1496
|
-
qr: (url) => qrs.get(url) ?? ""
|
|
3564
|
+
image: image2,
|
|
3565
|
+
qr: (url) => qrs.get(url) ?? "",
|
|
3566
|
+
chart: (key) => charts.get(key) ?? "",
|
|
3567
|
+
logo,
|
|
3568
|
+
brandName: brand.name
|
|
1497
3569
|
};
|
|
1498
3570
|
const sections = [];
|
|
1499
3571
|
const notes = [];
|
|
1500
3572
|
const slideMap = [];
|
|
3573
|
+
const footerText = [brand.footer, deck.settings.confidential].filter(Boolean);
|
|
3574
|
+
const wantFooter = deck.settings.slideNumbers || footerText.length > 0 || deck.settings.logo === "all";
|
|
1501
3575
|
deck.slides.forEach((slide, i) => {
|
|
3576
|
+
if (slide.hidden && !options.includeHidden) {
|
|
3577
|
+
slideMap.push([]);
|
|
3578
|
+
return;
|
|
3579
|
+
}
|
|
3580
|
+
imagePath = ["slides", i];
|
|
1502
3581
|
const layout = getLayout(slide.layout);
|
|
1503
3582
|
const rendered = layout.render(slide, ctx);
|
|
1504
3583
|
const list = Array.isArray(rendered) ? rendered : [rendered];
|
|
@@ -1508,6 +3587,14 @@ async function renderDeck(input, options = {}) {
|
|
|
1508
3587
|
list.forEach((r, j) => {
|
|
1509
3588
|
const classes = ["slide", `layout-${layout.name}`, ...r.classes ?? []];
|
|
1510
3589
|
if (bg !== "paper") classes.push("solid", `bg-${bg}`);
|
|
3590
|
+
let style = "";
|
|
3591
|
+
if (slide.backgroundImage) {
|
|
3592
|
+
const overlay = slide.overlay ?? "dark";
|
|
3593
|
+
classes.push("has-bg", `overlay-${overlay}`);
|
|
3594
|
+
if (overlay !== "light" && !classes.includes("solid")) classes.push("solid");
|
|
3595
|
+
style = ` style="--bg-img:url("${escapeAttr(ctx.image(slide.backgroundImage.src))}")"`;
|
|
3596
|
+
}
|
|
3597
|
+
if (slide.hidden) classes.push("is-hidden");
|
|
1511
3598
|
let chrome = "";
|
|
1512
3599
|
if (slide.step && deck.journey) {
|
|
1513
3600
|
classes.push("has-step");
|
|
@@ -1515,14 +3602,23 @@ async function renderDeck(input, options = {}) {
|
|
|
1515
3602
|
}
|
|
1516
3603
|
const strip = slide.showJourney && deck.journey ? roadmap(deck.journey.steps, slide.step, true) : "";
|
|
1517
3604
|
const id = slide.id ? list.length > 1 ? `${slide.id}-${j + 1}` : slide.id : "";
|
|
3605
|
+
const number = sections.length + 1;
|
|
3606
|
+
const foot = wantFooter && layout.name !== "title" ? `<footer class="slide-foot" aria-hidden="true"><span class="foot-l">${deck.settings.logo === "all" ? `<span class="foot-logo">${logo()}</span>` : ""}${footerText.map((t) => `<span>${escapeHtml(t)}</span>`).join("")}</span>${deck.settings.slideNumbers ? `<span class="foot-n">${number}</span>` : ""}</footer>` : "";
|
|
1518
3607
|
sections.push(
|
|
1519
|
-
`<section class="${classes.join(" ")}" data-track="${escapeAttr(track)}" data-index="${i}"${id ? ` id="${escapeAttr(id)}"` : ""} aria-label="Slide ${
|
|
3608
|
+
`<section class="${classes.join(" ")}" data-track="${escapeAttr(track)}" data-index="${i}"${id ? ` id="${escapeAttr(id)}"` : ""}${style} aria-label="Slide ${number}">${chrome}<div class="slide__inner">${strip}${r.html}</div>${foot}${slide.hidden ? '<span class="hidden-badge">Hidden</span>' : ""}</section>`
|
|
1520
3609
|
);
|
|
1521
3610
|
notes.push({ html: prose(r.notes ?? slide.notes), time: r.time ?? slide.time });
|
|
1522
3611
|
});
|
|
1523
3612
|
});
|
|
3613
|
+
const withNotes = options.speakerNotes !== false;
|
|
3614
|
+
const fields = options.editorMarkers ? `<script type="application/json" id="deck-fields">${JSON.stringify(
|
|
3615
|
+
deck.slides.map(
|
|
3616
|
+
(slide) => fieldMarkers(slide, (src) => images.get(src) ?? src)
|
|
3617
|
+
)
|
|
3618
|
+
).replace(/</g, "\\u003c")}</script>
|
|
3619
|
+
` : "";
|
|
1524
3620
|
const json = (v) => JSON.stringify(v).replace(/</g, "\\u003c");
|
|
1525
|
-
const stripData = (
|
|
3621
|
+
const stripData = (key, v) => !withNotes && key === "notes" ? void 0 : typeof v === "string" && /^data:/i.test(v) ? "(embedded)" : v;
|
|
1526
3622
|
const source = options.embedSource === false ? "" : `<script type="application/json" id="deck-source">${JSON.stringify(deck, stripData).replace(/</g, "\\u003c")}</script>
|
|
1527
3623
|
`;
|
|
1528
3624
|
const html = `<!doctype html>
|
|
@@ -1530,27 +3626,74 @@ async function renderDeck(input, options = {}) {
|
|
|
1530
3626
|
<head>
|
|
1531
3627
|
<meta charset="utf-8">
|
|
1532
3628
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
1533
|
-
<title>${escapeHtml(deck.title)} ·
|
|
3629
|
+
<title>${escapeHtml(deck.title)} · ${escapeHtml(brand.name)}</title>
|
|
3630
|
+
<link rel="icon" href="${escapeAttr(icon)}">
|
|
1534
3631
|
${deck.description ? `<meta name="description" content="${escapeAttr(deck.description)}">
|
|
1535
3632
|
` : ""}<meta name="generator" content="@classic-homes/slides">
|
|
1536
3633
|
<style id="brand-fonts">${fontsCss()}</style>
|
|
1537
|
-
<style>${tokensCss()}${DECK_CSS}</style>
|
|
3634
|
+
<style>${tokensCss(brand.stock ? void 0 : brand.palette)}${DECK_CSS}${DECK_CSS_EXTRA}</style>
|
|
1538
3635
|
</head>
|
|
1539
|
-
<body${options.initialSlide ? ` data-initial-slide="${Math.max(0, Math.floor(options.initialSlide))}"` : ""}>
|
|
3636
|
+
<body${wantFooter ? ' class="has-foot"' : ""}${options.initialSlide ? ` data-initial-slide="${Math.max(0, Math.floor(options.initialSlide))}"` : ""}${deck.settings.autoAdvance ? ` data-auto-advance="${deck.settings.autoAdvance}"` : ""}${deck.settings.loop ? " data-loop" : ""}${options.allowEmbeddedKiosk ? " data-embedded-kiosk" : ""}${options.editorMarkers ? " data-editor" : ""}>
|
|
1540
3637
|
<div class="rail" id="rail"></div>
|
|
1541
3638
|
<main class="deck" id="deck">
|
|
1542
3639
|
${sections.join("\n")}
|
|
1543
3640
|
</main>
|
|
1544
3641
|
<nav class="dots" id="dots" aria-label="Slide navigation"></nav>
|
|
1545
|
-
|
|
1546
|
-
<div class="hint"><kbd>↑</kbd><kbd>↓</kbd> or <kbd>Space</kbd> navigate · <kbd>N</kbd> notes · <kbd>F</kbd> fullscreen</div>
|
|
1547
|
-
<aside class="notes" id="notes" aria-live="polite"><div class="notes__head"><span class="tag">Speaker notes</span><span class="time" id="notesTime"></span></div><div class="notes__body" id="notesBody"></div></aside>
|
|
1548
|
-
<
|
|
1549
|
-
|
|
3642
|
+
${withNotes ? `<div class="hud"><span class="counter" id="counter" aria-live="polite"></span><button class="btn" id="notesBtn" aria-label="Toggle speaker notes">▤ notes (N)</button></div>
|
|
3643
|
+
<div class="hint"><kbd>↑</kbd><kbd>↓</kbd> or <kbd>Space</kbd> navigate · <kbd>N</kbd> notes · <kbd>P</kbd> presenter · <kbd>O</kbd> overview · <kbd>F</kbd> fullscreen</div>
|
|
3644
|
+
<aside class="notes" id="notes" aria-live="polite"><div class="notes__head"><span class="tag">Speaker notes</span><span class="time" id="notesTime"></span></div><div class="notes__body" id="notesBody"></div></aside>` : `<div class="hud"><span class="counter" id="counter" aria-live="polite"></span></div>
|
|
3645
|
+
<div class="hint"><kbd>↑</kbd><kbd>↓</kbd> or <kbd>Space</kbd> navigate · <kbd>O</kbd> overview · <kbd>F</kbd> fullscreen</div>`}
|
|
3646
|
+
<script type="application/json" id="deck-notes">${json(withNotes ? notes : [])}</script>
|
|
3647
|
+
${source}${fields}<script>${RUNTIME_JS}</script>
|
|
1550
3648
|
</body>
|
|
1551
3649
|
</html>
|
|
1552
3650
|
`;
|
|
1553
|
-
return { html, deck, slideCount: sections.length, slideMap, warnings: result.issues };
|
|
3651
|
+
return { html, deck, slideCount: sections.length, slideMap, warnings: result.issues, brand };
|
|
3652
|
+
}
|
|
3653
|
+
function escapedImageBytes(value) {
|
|
3654
|
+
let bytes = 0;
|
|
3655
|
+
for (let i = 0; i < value.length; i++) {
|
|
3656
|
+
const code = value.codePointAt(i);
|
|
3657
|
+
if (code === 38 || code === 39)
|
|
3658
|
+
bytes += 5;
|
|
3659
|
+
else if (code === 34)
|
|
3660
|
+
bytes += 6;
|
|
3661
|
+
else if (code === 60 || code === 62)
|
|
3662
|
+
bytes += 4;
|
|
3663
|
+
else if (code < 128) bytes++;
|
|
3664
|
+
else if (code < 2048) bytes += 2;
|
|
3665
|
+
else if (code <= 65535) bytes += 3;
|
|
3666
|
+
else {
|
|
3667
|
+
bytes += 4;
|
|
3668
|
+
i++;
|
|
3669
|
+
}
|
|
3670
|
+
}
|
|
3671
|
+
return bytes;
|
|
3672
|
+
}
|
|
3673
|
+
function brandLogo(brand, image2) {
|
|
3674
|
+
if (brand.logo) {
|
|
3675
|
+
const light = image2(brand.logo.src);
|
|
3676
|
+
const dark = image2((brand.logoOnDark ?? brand.logo).src);
|
|
3677
|
+
return `<span class="logo logo--img" role="img" aria-label="${escapeAttr(brand.name)}"><img class="logo-light" src="${escapeAttr(
|
|
3678
|
+
light
|
|
3679
|
+
)}" alt=""><img class="logo-dark" src="${escapeAttr(dark)}" alt=""></span>`;
|
|
3680
|
+
}
|
|
3681
|
+
if (brand.name === "Classic Homes") return LOGO_SVG;
|
|
3682
|
+
return logoLockup(brand.name, brand.key === "classic-homes");
|
|
3683
|
+
}
|
|
3684
|
+
function favicon(brand, image2) {
|
|
3685
|
+
if (brand.favicon) return image2(brand.favicon.src);
|
|
3686
|
+
if (brand.key !== "classic-homes") {
|
|
3687
|
+
const letter = (brand.name.match(/[A-Za-z0-9]/)?.[0] ?? "\u2022").toUpperCase();
|
|
3688
|
+
const mono = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><rect width="32" height="32" rx="7" fill="${brand.palette.primary}"/><text x="16" y="22.5" text-anchor="middle" font-family="Figtree,Arial,sans-serif" font-size="19" font-weight="700" fill="#fff">${letter}</text></svg>`;
|
|
3689
|
+
return `data:image/svg+xml,${encodeURIComponent(mono)}`;
|
|
3690
|
+
}
|
|
3691
|
+
const colour = brand.stock ? "#c50f22" : brand.palette.primary;
|
|
3692
|
+
const svg = LOGO_MARK_SVG.replace(
|
|
3693
|
+
"<svg ",
|
|
3694
|
+
`<svg xmlns="http://www.w3.org/2000/svg" fill="${colour}" `
|
|
3695
|
+
).replace(' aria-hidden="true"', "");
|
|
3696
|
+
return `data:image/svg+xml,${encodeURIComponent(svg)}`;
|
|
1554
3697
|
}
|
|
1555
3698
|
function extractDeckSource(html) {
|
|
1556
3699
|
const m = /<script type="application\/json" id="deck-source">([\s\S]*?)<\/script>/.exec(html);
|
|
@@ -1567,7 +3710,7 @@ function fromZod(issue) {
|
|
|
1567
3710
|
}
|
|
1568
3711
|
return { level: "error", path: issue.path, message };
|
|
1569
3712
|
}
|
|
1570
|
-
function validateDeck(input) {
|
|
3713
|
+
function validateDeck(input, options = {}) {
|
|
1571
3714
|
const parsed = DeckSchema.safeParse(input);
|
|
1572
3715
|
if (!parsed.success) {
|
|
1573
3716
|
return { ok: false, issues: parsed.error.issues.map(fromZod) };
|
|
@@ -1578,6 +3721,18 @@ function validateDeck(input) {
|
|
|
1578
3721
|
const seen = /* @__PURE__ */ new Set();
|
|
1579
3722
|
deck.slides.forEach((slide, i) => {
|
|
1580
3723
|
const layout = getLayout(slide.layout);
|
|
3724
|
+
for (const url of layout.prepare?.(slide)?.qr ?? []) {
|
|
3725
|
+
try {
|
|
3726
|
+
if (url.length > 1e4) throw new Error("URL too long");
|
|
3727
|
+
QRCode2.create(url, { errorCorrectionLevel: "M" });
|
|
3728
|
+
} catch {
|
|
3729
|
+
issues.push({
|
|
3730
|
+
level: "error",
|
|
3731
|
+
path: ["slides", i, "url"],
|
|
3732
|
+
message: "This URL is too long for a QR code. Use a shorter URL."
|
|
3733
|
+
});
|
|
3734
|
+
}
|
|
3735
|
+
}
|
|
1581
3736
|
for (const issue of layout.lint?.(slide) ?? []) {
|
|
1582
3737
|
issues.push({ ...issue, path: ["slides", i, ...issue.path] });
|
|
1583
3738
|
}
|
|
@@ -1612,6 +3767,22 @@ function validateDeck(input) {
|
|
|
1612
3767
|
});
|
|
1613
3768
|
}
|
|
1614
3769
|
});
|
|
3770
|
+
const profile = deck.brand?.profile;
|
|
3771
|
+
if (profile && ![CLASSIC_HOMES_BRAND, ...options.brands ?? []].some((b) => b.key === profile)) {
|
|
3772
|
+
const known = [CLASSIC_HOMES_BRAND, ...options.brands ?? []].map((b) => b.key).join(", ");
|
|
3773
|
+
issues.push({
|
|
3774
|
+
level: "error",
|
|
3775
|
+
path: ["brand", "profile"],
|
|
3776
|
+
message: `Unknown brand profile "${profile}". Available: ${known}.`
|
|
3777
|
+
});
|
|
3778
|
+
}
|
|
3779
|
+
if (deck.slides.every((sl) => sl.hidden)) {
|
|
3780
|
+
issues.push({
|
|
3781
|
+
level: "error",
|
|
3782
|
+
path: ["slides"],
|
|
3783
|
+
message: "Every slide is hidden; unhide at least one."
|
|
3784
|
+
});
|
|
3785
|
+
}
|
|
1615
3786
|
if (deck.slides[0]?.layout !== "title") {
|
|
1616
3787
|
issues.push({
|
|
1617
3788
|
level: "warning",
|
|
@@ -1654,31 +3825,63 @@ function stringifyDeck(deck) {
|
|
|
1654
3825
|
}
|
|
1655
3826
|
|
|
1656
3827
|
export {
|
|
1657
|
-
escapeHtml,
|
|
1658
|
-
inline,
|
|
1659
|
-
prose,
|
|
1660
3828
|
baseSlide,
|
|
3829
|
+
BrandPaletteSchema,
|
|
3830
|
+
BrandProfileSchema,
|
|
3831
|
+
DeckBrandSchema,
|
|
3832
|
+
CLASSIC_HOMES_PALETTE,
|
|
3833
|
+
CLASSIC_HOMES_BRAND,
|
|
3834
|
+
resolveBrand,
|
|
3835
|
+
checkPalette,
|
|
3836
|
+
validateBrandProfile,
|
|
1661
3837
|
PALETTE,
|
|
1662
3838
|
LOGO_SVG,
|
|
3839
|
+
logoLockup,
|
|
3840
|
+
CHART_TYPES,
|
|
3841
|
+
ChartSpec,
|
|
3842
|
+
formatValue,
|
|
3843
|
+
chartOption,
|
|
3844
|
+
renderChartSvg,
|
|
3845
|
+
describeChart,
|
|
3846
|
+
escapeHtml,
|
|
3847
|
+
inline,
|
|
3848
|
+
prose,
|
|
3849
|
+
parseVideoUrl,
|
|
3850
|
+
videoEmbedUrl,
|
|
1663
3851
|
LAYOUTS,
|
|
1664
3852
|
getLayout,
|
|
3853
|
+
LAYOUT_CATEGORY_ORDER,
|
|
3854
|
+
LAYOUT_CATEGORIES,
|
|
3855
|
+
findSlide,
|
|
3856
|
+
newSlideId,
|
|
3857
|
+
ensureSlideIds,
|
|
3858
|
+
slideKey,
|
|
3859
|
+
addSlide,
|
|
3860
|
+
updateSlide,
|
|
3861
|
+
moveSlide,
|
|
3862
|
+
removeSlide,
|
|
3863
|
+
outline,
|
|
3864
|
+
SlideSchema,
|
|
3865
|
+
DeckSettingsSchema,
|
|
3866
|
+
DeckSchema,
|
|
3867
|
+
deckEditorFields,
|
|
1665
3868
|
commonSlideFields,
|
|
1666
3869
|
layoutEditorInfo,
|
|
1667
3870
|
allLayoutEditorInfo,
|
|
3871
|
+
layoutGalleryDeck,
|
|
1668
3872
|
PLACEHOLDER_IMAGE,
|
|
1669
3873
|
isPlaceholderImage,
|
|
1670
3874
|
blankSlide,
|
|
1671
|
-
SlideSchema,
|
|
1672
|
-
DeckSchema,
|
|
1673
3875
|
validateDeck,
|
|
1674
3876
|
formatPath,
|
|
1675
3877
|
formatIssues,
|
|
1676
3878
|
DeckError,
|
|
1677
3879
|
PREVIEW_SIZE,
|
|
3880
|
+
fieldMarkers,
|
|
1678
3881
|
collectImageSources,
|
|
1679
3882
|
renderDeck,
|
|
1680
3883
|
extractDeckSource,
|
|
1681
3884
|
parseDeckSource,
|
|
1682
3885
|
stringifyDeck
|
|
1683
3886
|
};
|
|
1684
|
-
//# sourceMappingURL=chunk-
|
|
3887
|
+
//# sourceMappingURL=chunk-PTTLL6PQ.js.map
|