@seriousgaming/deck 0.0.0-stage → 1.0.0-alpha.2
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/LICENSE +202 -0
- package/NOTICE +1 -0
- package/README.md +20 -2
- package/lib/Deck.d.ts +49 -0
- package/lib/Deck.js +148 -0
- package/lib/Slide.d.ts +27 -0
- package/lib/Slide.js +20 -0
- package/lib/Step.d.ts +25 -0
- package/lib/Step.js +24 -0
- package/lib/brand.d.ts +39 -0
- package/lib/brand.js +75 -0
- package/lib/context.d.ts +54 -0
- package/lib/context.js +32 -0
- package/lib/controls.d.ts +18 -0
- package/lib/controls.js +15 -0
- package/lib/export/collect.d.ts +32 -0
- package/lib/export/collect.js +312 -0
- package/lib/export/index.d.ts +28 -0
- package/lib/export/index.js +92 -0
- package/lib/export/ir.d.ts +85 -0
- package/lib/export/ir.js +1 -0
- package/lib/export/pptx.d.ts +12 -0
- package/lib/export/pptx.js +125 -0
- package/lib/export/use-deck-export.d.ts +19 -0
- package/lib/export/use-deck-export.js +97 -0
- package/lib/index.d.ts +17 -0
- package/lib/index.js +15 -0
- package/lib/keyboard.d.ts +18 -0
- package/lib/keyboard.js +59 -0
- package/lib/memory.d.ts +31 -0
- package/lib/memory.js +60 -0
- package/lib/navigation.d.ts +26 -0
- package/lib/navigation.js +93 -0
- package/lib/overview.d.ts +11 -0
- package/lib/overview.js +22 -0
- package/lib/primitives/chart.d.ts +40 -0
- package/lib/primitives/chart.js +116 -0
- package/lib/primitives/media.d.ts +14 -0
- package/lib/primitives/media.js +5 -0
- package/lib/primitives/table.d.ts +11 -0
- package/lib/primitives/table.js +24 -0
- package/lib/primitives/text.d.ts +33 -0
- package/lib/primitives/text.js +30 -0
- package/lib/styles.d.ts +10 -0
- package/lib/styles.js +192 -0
- package/package.json +54 -3
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useLayoutEffect, useRef, useState } from 'react';
|
|
3
|
+
import { useBrand, useSlide } from '../context.js';
|
|
4
|
+
const EXTRA_COLORS = ['#f59e0b', '#8b5cf6', '#ef4444', '#10b981', '#64748b'];
|
|
5
|
+
/**
|
|
6
|
+
* A bar or line chart drawn in the brand's colours.
|
|
7
|
+
*
|
|
8
|
+
* Sized by its container, so give it a width and height through `style` or
|
|
9
|
+
* layout. On screen it draws in when its slide arrives. In PowerPoint it is a
|
|
10
|
+
* picture of the chart as drawn, so the export always matches the screen.
|
|
11
|
+
*/
|
|
12
|
+
export function Chart({ type, categories, series, showValues = false, showLegend = series.length > 1, valueFormat = {}, locale, className, style, }) {
|
|
13
|
+
const brand = useBrand();
|
|
14
|
+
const slide = useSlide();
|
|
15
|
+
const ref = useRef(null);
|
|
16
|
+
const [size, setSize] = useState({ width: 1200, height: 600 });
|
|
17
|
+
useLayoutEffect(() => {
|
|
18
|
+
const element = ref.current;
|
|
19
|
+
if (!element)
|
|
20
|
+
return;
|
|
21
|
+
// Layout size, not the scaled on-screen size: the chart is drawn in
|
|
22
|
+
// slide pixels like everything else on the slide.
|
|
23
|
+
const measure = () => setSize({ width: element.offsetWidth, height: element.offsetHeight });
|
|
24
|
+
measure();
|
|
25
|
+
const observer = new ResizeObserver(measure);
|
|
26
|
+
observer.observe(element);
|
|
27
|
+
return () => observer.disconnect();
|
|
28
|
+
}, []);
|
|
29
|
+
const palette = [brand.colors.primary, brand.colors.secondary, ...EXTRA_COLORS];
|
|
30
|
+
const colored = series.map((s, index) => ({
|
|
31
|
+
...s,
|
|
32
|
+
color: s.color ?? palette[index % palette.length],
|
|
33
|
+
}));
|
|
34
|
+
const format = new Intl.NumberFormat(locale, {
|
|
35
|
+
style: valueFormat.style ?? 'decimal',
|
|
36
|
+
currency: valueFormat.currency,
|
|
37
|
+
maximumFractionDigits: valueFormat.maximumFractionDigits ?? 0,
|
|
38
|
+
notation: 'compact',
|
|
39
|
+
});
|
|
40
|
+
const fullFormat = new Intl.NumberFormat(locale, {
|
|
41
|
+
style: valueFormat.style ?? 'decimal',
|
|
42
|
+
currency: valueFormat.currency,
|
|
43
|
+
maximumFractionDigits: valueFormat.maximumFractionDigits ?? 0,
|
|
44
|
+
});
|
|
45
|
+
const { width, height } = size;
|
|
46
|
+
const legendHeight = showLegend ? 64 : 0;
|
|
47
|
+
const rtl = slide.dir === 'rtl';
|
|
48
|
+
// The value axis sits on the leading side, so its labels' room does too.
|
|
49
|
+
const axisRoom = 120;
|
|
50
|
+
const pad = {
|
|
51
|
+
top: 40,
|
|
52
|
+
right: rtl ? axisRoom : 24,
|
|
53
|
+
bottom: 64 + legendHeight,
|
|
54
|
+
left: rtl ? 24 : axisRoom,
|
|
55
|
+
};
|
|
56
|
+
const plotW = Math.max(1, width - pad.left - pad.right);
|
|
57
|
+
const plotH = Math.max(1, height - pad.top - pad.bottom);
|
|
58
|
+
const all = colored.flatMap((s) => s.values).filter((v) => v !== null);
|
|
59
|
+
const [min, max, step] = niceRange(Math.min(0, ...all), Math.max(0, ...all));
|
|
60
|
+
const y = (value) => pad.top + plotH - ((value - min) / (max - min || 1)) * plotH;
|
|
61
|
+
const band = plotW / Math.max(1, categories.length);
|
|
62
|
+
const ticks = Array.from({ length: Math.round((max - min) / step) + 1 }, (_, i) => min + step * i);
|
|
63
|
+
const x = (index) => rtl ? pad.left + plotW - band * (index + 1) : pad.left + band * index;
|
|
64
|
+
const animate = slide.mode === 'live';
|
|
65
|
+
return (_jsxs("div", { ref: ref, className: className, style: { position: 'relative', width: '100%', height: 600, fontSize: 28, ...style }, children: [_jsxs("svg", { width: width, height: height, style: { display: 'block', overflow: 'visible' }, children: [ticks.map((tick) => (_jsxs("g", { children: [_jsx("line", { x1: pad.left, x2: pad.left + plotW, y1: y(tick), y2: y(tick), stroke: "currentColor", strokeOpacity: tick === 0 ? 0.5 : 0.12, strokeWidth: 2 }), _jsx("text", { x: rtl ? pad.left + plotW + 16 : pad.left - 16, y: y(tick), dy: "0.35em", textAnchor: rtl ? 'start' : 'end', fill: "var(--deck-muted)", children: format.format(tick) })] }, tick))), categories.map((category, index) => (_jsx("text", { x: x(index) + band / 2, y: pad.top + plotH + 44, textAnchor: "middle", fill: "var(--deck-muted)", children: category }, category))), type === 'bar' &&
|
|
66
|
+
colored.map((s, si) => {
|
|
67
|
+
const groupW = band * 0.72;
|
|
68
|
+
const barW = groupW / colored.length;
|
|
69
|
+
return s.values.map((value, ci) => {
|
|
70
|
+
if (value === null)
|
|
71
|
+
return null;
|
|
72
|
+
const top = Math.min(y(value), y(0));
|
|
73
|
+
const barH = Math.abs(y(value) - y(0));
|
|
74
|
+
const offset = rtl ? colored.length - 1 - si : si;
|
|
75
|
+
const left = x(ci) + (band - groupW) / 2 + barW * offset;
|
|
76
|
+
return (_jsxs("g", { children: [_jsx("rect", { className: animate ? 'deck-chart-bar' : undefined, style: animate
|
|
77
|
+
? { animationDelay: `${ci * 60 + si * 30}ms` }
|
|
78
|
+
: undefined, x: left + 3, y: top, width: Math.max(1, barW - 6), height: Math.max(1, barH), rx: 6, fill: s.color }), showValues && (_jsx("text", { x: left + barW / 2, y: value >= 0 ? top - 12 : top + barH + 32, textAnchor: "middle", fill: "currentColor", fontSize: "0.85em", children: fullFormat.format(value) }))] }, `${si}-${ci}`));
|
|
79
|
+
});
|
|
80
|
+
}), type === 'line' &&
|
|
81
|
+
colored.map((s, si) => {
|
|
82
|
+
const points = s.values
|
|
83
|
+
.map((value, ci) => value === null ? null : [x(ci) + band / 2, y(value)])
|
|
84
|
+
.filter((p) => p !== null);
|
|
85
|
+
return (_jsxs("g", { children: [_jsx("path", { className: animate ? 'deck-chart-line' : undefined, d: points
|
|
86
|
+
.map(([px, py], i) => `${i ? 'L' : 'M'}${px},${py}`)
|
|
87
|
+
.join(' '), fill: "none", stroke: s.color, strokeWidth: 6, strokeLinejoin: "round", strokeLinecap: "round" }), points.map(([px, py], i) => (_jsx("circle", { cx: px, cy: py, r: 9, fill: s.color }, i))), showValues &&
|
|
88
|
+
s.values.map((value, ci) => value === null ? null : (_jsx("text", { x: x(ci) + band / 2, y: y(value) - 22, textAnchor: "middle", fill: "currentColor", fontSize: "0.85em", children: fullFormat.format(value) }, ci)))] }, si));
|
|
89
|
+
})] }), showLegend && (_jsx("div", { style: {
|
|
90
|
+
position: 'absolute',
|
|
91
|
+
insetInlineStart: axisRoom,
|
|
92
|
+
bottom: 0,
|
|
93
|
+
height: legendHeight,
|
|
94
|
+
display: 'flex',
|
|
95
|
+
alignItems: 'center',
|
|
96
|
+
gap: 48,
|
|
97
|
+
}, children: colored.map((s) => (_jsxs("span", { style: { display: 'inline-flex', alignItems: 'center', gap: 12 }, children: [_jsx("span", { style: {
|
|
98
|
+
width: 24,
|
|
99
|
+
height: 24,
|
|
100
|
+
borderRadius: 4,
|
|
101
|
+
background: s.color,
|
|
102
|
+
} }), s.name] }, s.name))) }))] }));
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* An axis a person would draw: a nice step (1, 2, 2.5 or 5 times a power of
|
|
106
|
+
* ten) that splits the range into about five, and bounds that are whole steps.
|
|
107
|
+
*/
|
|
108
|
+
export function niceRange(min, max) {
|
|
109
|
+
if (min === max)
|
|
110
|
+
return [min, min + 1, 1];
|
|
111
|
+
const rough = (max - min) / 5;
|
|
112
|
+
const magnitude = 10 ** Math.floor(Math.log10(rough));
|
|
113
|
+
const step = [1, 2, 2.5, 5, 10].map((m) => m * magnitude).find((candidate) => candidate >= rough) ??
|
|
114
|
+
magnitude * 10;
|
|
115
|
+
return [Math.floor(min / step) * step, Math.ceil(max / step) * step, step];
|
|
116
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { CSSProperties } from 'react';
|
|
2
|
+
export interface ImageProps {
|
|
3
|
+
src: string;
|
|
4
|
+
alt: string;
|
|
5
|
+
/**
|
|
6
|
+
* `contain` shows the whole picture and exports it as a picture PowerPoint
|
|
7
|
+
* can move and resize. `cover` fills the box, cropping as it must, and stays
|
|
8
|
+
* part of the slide's background in PowerPoint.
|
|
9
|
+
*/
|
|
10
|
+
fit?: 'contain' | 'cover';
|
|
11
|
+
className?: string;
|
|
12
|
+
style?: CSSProperties;
|
|
13
|
+
}
|
|
14
|
+
export declare function Image({ src, alt, fit, className, style }: ImageProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { NATIVE_ATTRIBUTE } from '../export/collect.js';
|
|
3
|
+
export function Image({ src, alt, fit = 'contain', className, style }) {
|
|
4
|
+
return (_jsx("img", { ...(fit === 'contain' ? { [NATIVE_ATTRIBUTE]: 'image' } : {}), src: src, alt: alt, className: className, draggable: false, style: { display: 'block', objectFit: fit, ...style } }));
|
|
5
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { CSSProperties, ReactNode } from 'react';
|
|
2
|
+
export interface TableProps {
|
|
3
|
+
columns: ReactNode[];
|
|
4
|
+
rows: ReactNode[][];
|
|
5
|
+
/** Text alignment per column; numbers usually read best at the end. */
|
|
6
|
+
align?: ('start' | 'center' | 'end')[];
|
|
7
|
+
className?: string;
|
|
8
|
+
style?: CSSProperties;
|
|
9
|
+
}
|
|
10
|
+
/** A table with a branded header row. Exports as an editable PowerPoint table. */
|
|
11
|
+
export declare function Table({ columns, rows, align, className, style }: TableProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { NATIVE_ATTRIBUTE } from '../export/collect.js';
|
|
3
|
+
/** A table with a branded header row. Exports as an editable PowerPoint table. */
|
|
4
|
+
export function Table({ columns, rows, align = [], className, style }) {
|
|
5
|
+
const cell = (index) => ({
|
|
6
|
+
padding: '18px 24px',
|
|
7
|
+
textAlign: align[index] ?? 'start',
|
|
8
|
+
borderBottom: '2px solid var(--deck-surface)',
|
|
9
|
+
});
|
|
10
|
+
return (_jsxs("table", { [NATIVE_ATTRIBUTE]: 'table', className: className, style: {
|
|
11
|
+
borderCollapse: 'collapse',
|
|
12
|
+
width: '100%',
|
|
13
|
+
fontSize: 36,
|
|
14
|
+
background: 'var(--deck-background)',
|
|
15
|
+
...style,
|
|
16
|
+
}, children: [_jsx("thead", { children: _jsx("tr", { children: columns.map((column, index) => (_jsx("th", { style: {
|
|
17
|
+
...cell(index),
|
|
18
|
+
background: 'var(--deck-primary)',
|
|
19
|
+
color: 'var(--deck-on-primary)',
|
|
20
|
+
fontWeight: 700,
|
|
21
|
+
}, children: column }, index))) }) }), _jsx("tbody", { children: rows.map((row, r) => (_jsx("tr", { style: {
|
|
22
|
+
background: r % 2 ? 'var(--deck-surface)' : 'var(--deck-background)',
|
|
23
|
+
}, children: row.map((value, index) => (_jsx("td", { style: cell(index), children: value }, index))) }, r))) })] }));
|
|
24
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { CSSProperties, ElementType, ReactNode } from 'react';
|
|
2
|
+
import { type StepAnimation } from '../Step.js';
|
|
3
|
+
interface TextBase {
|
|
4
|
+
children?: ReactNode;
|
|
5
|
+
className?: string;
|
|
6
|
+
style?: CSSProperties;
|
|
7
|
+
}
|
|
8
|
+
export interface HeadingProps extends TextBase {
|
|
9
|
+
level?: 1 | 2 | 3;
|
|
10
|
+
}
|
|
11
|
+
/** A heading in the brand's heading font. Exports as an editable text box. */
|
|
12
|
+
export declare function Heading({ level, children, className, style }: HeadingProps): import("react").JSX.Element;
|
|
13
|
+
export interface TextProps extends TextBase {
|
|
14
|
+
as?: ElementType;
|
|
15
|
+
/** `lead` for an introductory paragraph, `caption` for small print. */
|
|
16
|
+
variant?: 'body' | 'lead' | 'caption';
|
|
17
|
+
}
|
|
18
|
+
/** Body copy. Inline formatting (`<strong>`, `<em>`, styled spans) survives export. */
|
|
19
|
+
export declare function Text({ as: Element, variant, children, className, style, }: TextProps): import("react").JSX.Element;
|
|
20
|
+
export interface BulletsProps extends TextBase {
|
|
21
|
+
items: ReactNode[];
|
|
22
|
+
ordered?: boolean;
|
|
23
|
+
/**
|
|
24
|
+
* Reveal the items one click at a time, the first on click `stepFrom`
|
|
25
|
+
* (default 1). In print and export every item shows.
|
|
26
|
+
*/
|
|
27
|
+
stepped?: boolean;
|
|
28
|
+
stepFrom?: number;
|
|
29
|
+
animation?: StepAnimation;
|
|
30
|
+
}
|
|
31
|
+
/** A bulleted or numbered list. Exports as an editable PowerPoint list. */
|
|
32
|
+
export declare function Bullets({ items, ordered, stepped, stepFrom, animation, className, style, }: BulletsProps): import("react").JSX.Element;
|
|
33
|
+
export {};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { NATIVE_ATTRIBUTE } from '../export/collect.js';
|
|
3
|
+
import { Step } from '../Step.js';
|
|
4
|
+
const native = { [NATIVE_ATTRIBUTE]: 'text' };
|
|
5
|
+
const HEADING_SIZES = { 1: 104, 2: 72, 3: 52 };
|
|
6
|
+
/** A heading in the brand's heading font. Exports as an editable text box. */
|
|
7
|
+
export function Heading({ level = 2, children, className, style }) {
|
|
8
|
+
const Element = `h${level}`;
|
|
9
|
+
return (_jsx(Element, { ...native, className: className, style: { fontSize: HEADING_SIZES[level], fontWeight: 700, ...style }, children: children }));
|
|
10
|
+
}
|
|
11
|
+
const TEXT_STYLES = {
|
|
12
|
+
body: { fontSize: 40 },
|
|
13
|
+
lead: { fontSize: 52, lineHeight: 1.3 },
|
|
14
|
+
caption: { fontSize: 28, color: 'var(--deck-muted)' },
|
|
15
|
+
};
|
|
16
|
+
/** Body copy. Inline formatting (`<strong>`, `<em>`, styled spans) survives export. */
|
|
17
|
+
export function Text({ as: Element = 'p', variant = 'body', children, className, style, }) {
|
|
18
|
+
return (_jsx(Element, { ...native, className: className, style: { ...TEXT_STYLES[variant], ...style }, children: children }));
|
|
19
|
+
}
|
|
20
|
+
/** A bulleted or numbered list. Exports as an editable PowerPoint list. */
|
|
21
|
+
export function Bullets({ items, ordered = false, stepped = false, stepFrom = 1, animation = 'fade-left', className, style, }) {
|
|
22
|
+
const List = ordered ? 'ol' : 'ul';
|
|
23
|
+
return (_jsx(List, { ...native, className: className, style: {
|
|
24
|
+
margin: 0,
|
|
25
|
+
paddingInlineStart: '1.1em',
|
|
26
|
+
fontSize: 44,
|
|
27
|
+
lineHeight: 1.3,
|
|
28
|
+
...style,
|
|
29
|
+
}, children: items.map((item, index) => stepped ? (_jsx(Step, { as: "li", at: stepFrom + index, animation: animation, style: { marginBottom: 24 }, children: item }, index)) : (_jsx("li", { style: { marginBottom: 24 }, children: item }, index))) }));
|
|
30
|
+
}
|
package/lib/styles.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The library's whole stylesheet, injected once by `Deck`.
|
|
3
|
+
*
|
|
4
|
+
* In a string rather than a `.css` import so a game's bundler needs no CSS
|
|
5
|
+
* handling to use the deck, and so nothing about the library's styling leaks
|
|
6
|
+
* into how the game is built.
|
|
7
|
+
*/
|
|
8
|
+
export declare const DECK_CSS = "\n.deck-root {\n position: relative;\n width: 100%;\n height: 100%;\n overflow: hidden;\n background: #111;\n color: var(--deck-text);\n font-family: var(--deck-font-body);\n -webkit-font-smoothing: antialiased;\n user-select: none;\n}\n.deck-root *, .deck-root *::before, .deck-root *::after { box-sizing: border-box; }\n.deck-stage {\n position: absolute;\n top: 50%;\n left: 50%;\n width: 1920px;\n height: 1080px;\n transform-origin: center center;\n}\n.deck-slide {\n position: relative;\n width: 1920px;\n height: 1080px;\n overflow: hidden;\n background: var(--deck-background);\n color: var(--deck-text);\n font-family: var(--deck-font-body);\n font-size: 40px;\n line-height: 1.35;\n}\n.deck-slide h1, .deck-slide h2, .deck-slide h3 {\n font-family: var(--deck-font-heading);\n margin: 0;\n line-height: 1.1;\n}\n.deck-slide p { margin: 0; }\n\n/* Slide transitions: the incoming slide animates in, and the outgoing one is gone. */\n.deck-enter-fade { animation: deck-fade 450ms ease both; }\n.deck-enter-slide.deck-forward { animation: deck-slide-in-right 550ms cubic-bezier(.2,.7,.2,1) both; }\n.deck-enter-slide.deck-backward { animation: deck-slide-in-left 550ms cubic-bezier(.2,.7,.2,1) both; }\n.deck-enter-zoom { animation: deck-zoom 500ms cubic-bezier(.2,.7,.2,1) both; }\n.deck-enter-rise { animation: deck-rise 600ms cubic-bezier(.2,.7,.2,1) both; }\n@keyframes deck-fade { from { opacity: 0; } to { opacity: 1; } }\n@keyframes deck-slide-in-right { from { transform: translateX(8%); opacity: 0; } to { transform: none; opacity: 1; } }\n@keyframes deck-slide-in-left { from { transform: translateX(-8%); opacity: 0; } to { transform: none; opacity: 1; } }\n@keyframes deck-zoom { from { transform: scale(.92); opacity: 0; } to { transform: none; opacity: 1; } }\n@keyframes deck-rise { from { transform: translateY(6%); opacity: 0; } to { transform: none; opacity: 1; } }\n\n/* Steps keep their space while hidden, so revealing one never reflows the slide. */\n.deck-step { transition: opacity 450ms ease, transform 550ms cubic-bezier(.2,.7,.2,1), filter 450ms ease; }\n.deck-step[data-visible=\"false\"] { opacity: 0; pointer-events: none; }\n.deck-step[data-visible=\"false\"][data-animation=\"fade-up\"] { transform: translateY(40px); }\n.deck-step[data-visible=\"false\"][data-animation=\"fade-left\"] { transform: translateX(-40px); }\n.deck-step[data-visible=\"false\"][data-animation=\"zoom\"] { transform: scale(.85); }\n.deck-step[data-visible=\"false\"][data-animation=\"blur\"] { filter: blur(12px); }\n.deck-step[data-animation=\"none\"] { transition: none; }\n.deck-static .deck-step { transition: none; }\n\n/* Drawing in a chart's bars and lines. */\n.deck-chart-bar { transform-box: fill-box; transform-origin: bottom; animation: deck-grow 700ms cubic-bezier(.2,.7,.2,1) both; }\n.deck-chart-line { stroke-dasharray: 4000; stroke-dashoffset: 4000; animation: deck-draw 1200ms ease forwards; }\n.deck-static .deck-chart-bar, .deck-static .deck-chart-line { animation: none; stroke-dashoffset: 0; }\n@keyframes deck-grow { from { transform: scaleY(0); } to { transform: none; } }\n@keyframes deck-draw { to { stroke-dashoffset: 0; } }\n\n@media (prefers-reduced-motion: reduce) {\n .deck-root *, .deck-root *::before, .deck-root *::after {\n animation-duration: 1ms !important;\n transition-duration: 1ms !important;\n }\n}\n\n/* Controls fade in when the pointer moves, and out again when it rests. */\n.deck-controls {\n position: absolute;\n left: 50%;\n bottom: 16px;\n transform: translateX(-50%);\n display: flex;\n gap: 4px;\n align-items: center;\n padding: 6px;\n border-radius: 999px;\n background: rgba(20, 22, 28, .82);\n color: #fff;\n font: 14px/1 system-ui, sans-serif;\n opacity: 0;\n transition: opacity 250ms ease;\n z-index: 2;\n}\n.deck-root[data-controls=\"shown\"] .deck-controls, .deck-controls:focus-within { opacity: 1; }\n.deck-controls button {\n all: unset;\n cursor: pointer;\n padding: 8px 12px;\n border-radius: 999px;\n}\n.deck-controls button:hover, .deck-controls button:focus-visible { background: rgba(255,255,255,.16); }\n.deck-controls button:disabled { opacity: .4; cursor: default; }\n.deck-controls .deck-position { padding: 0 8px; font-variant-numeric: tabular-nums; }\n.deck-menu {\n position: absolute;\n bottom: 52px;\n right: 0;\n display: flex;\n flex-direction: column;\n min-width: 220px;\n padding: 6px;\n border-radius: 12px;\n background: rgba(20, 22, 28, .95);\n}\n.deck-menu button { border-radius: 8px; }\n.deck-busy {\n position: absolute;\n inset: 0;\n display: grid;\n place-items: center;\n background: rgba(0,0,0,.55);\n color: #fff;\n font: 18px system-ui, sans-serif;\n z-index: 3;\n}\n\n/* The overview grid. */\n.deck-overview {\n position: absolute;\n inset: 0;\n overflow: auto;\n padding: 24px;\n display: grid;\n grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));\n gap: 16px;\n background: #111;\n z-index: 1;\n}\n.deck-thumb { all: unset; cursor: pointer; display: block; position: relative; aspect-ratio: 16 / 9; overflow: hidden; border-radius: 6px; outline: 2px solid transparent; }\n.deck-thumb[aria-current=\"true\"] { outline-color: var(--deck-primary); }\n.deck-thumb > div { position: absolute; top: 0; left: 0; transform-origin: top left; }\n.deck-thumb-number { position: absolute; left: 8px; bottom: 6px; color: #fff; font: 12px system-ui; text-shadow: 0 1px 2px #000; }\n\n/* Static renders, laid out off screen for export and on paper for print. */\n.deck-static-stack { position: fixed; top: 0; left: -100000px; }\n/*\n * While a slide's background picture is taken, native elements step aside so\n * PowerPoint can lay them back on top as real objects. Text hides its glyphs\n * only, so a pill or a card it is drawn on stays in the picture; the rest hide\n * whole, because PowerPoint redraws their fills itself. One the exporter could\n * not describe stays in the picture instead.\n */\n.deck-exporting [data-deck-native]:not([data-deck-native=\"text\"]):not([data-deck-native-failed]) {\n visibility: hidden !important;\n}\n.deck-exporting [data-deck-native=\"text\"]:not([data-deck-native-failed]),\n.deck-exporting [data-deck-native=\"text\"]:not([data-deck-native-failed]) * {\n color: transparent !important;\n -webkit-text-fill-color: transparent !important;\n text-shadow: none !important;\n text-decoration-color: transparent !important;\n}\n\n@media print {\n @page { size: 1920px 1080px; margin: 0; }\n html, body { margin: 0 !important; padding: 0 !important; background: #fff !important; }\n body > *:not(.deck-static-stack) { display: none !important; }\n .deck-static-stack { position: static; }\n .deck-static-stack .deck-slide { break-after: page; print-color-adjust: exact; -webkit-print-color-adjust: exact; }\n}\n";
|
|
9
|
+
/** Put {@link DECK_CSS} into the document once, however many decks mount. */
|
|
10
|
+
export declare function injectDeckStyles(doc?: Document): void;
|
package/lib/styles.js
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { SLIDE_HEIGHT, SLIDE_WIDTH } from './context.js';
|
|
2
|
+
/**
|
|
3
|
+
* The library's whole stylesheet, injected once by `Deck`.
|
|
4
|
+
*
|
|
5
|
+
* In a string rather than a `.css` import so a game's bundler needs no CSS
|
|
6
|
+
* handling to use the deck, and so nothing about the library's styling leaks
|
|
7
|
+
* into how the game is built.
|
|
8
|
+
*/
|
|
9
|
+
export const DECK_CSS = `
|
|
10
|
+
.deck-root {
|
|
11
|
+
position: relative;
|
|
12
|
+
width: 100%;
|
|
13
|
+
height: 100%;
|
|
14
|
+
overflow: hidden;
|
|
15
|
+
background: #111;
|
|
16
|
+
color: var(--deck-text);
|
|
17
|
+
font-family: var(--deck-font-body);
|
|
18
|
+
-webkit-font-smoothing: antialiased;
|
|
19
|
+
user-select: none;
|
|
20
|
+
}
|
|
21
|
+
.deck-root *, .deck-root *::before, .deck-root *::after { box-sizing: border-box; }
|
|
22
|
+
.deck-stage {
|
|
23
|
+
position: absolute;
|
|
24
|
+
top: 50%;
|
|
25
|
+
left: 50%;
|
|
26
|
+
width: ${SLIDE_WIDTH}px;
|
|
27
|
+
height: ${SLIDE_HEIGHT}px;
|
|
28
|
+
transform-origin: center center;
|
|
29
|
+
}
|
|
30
|
+
.deck-slide {
|
|
31
|
+
position: relative;
|
|
32
|
+
width: ${SLIDE_WIDTH}px;
|
|
33
|
+
height: ${SLIDE_HEIGHT}px;
|
|
34
|
+
overflow: hidden;
|
|
35
|
+
background: var(--deck-background);
|
|
36
|
+
color: var(--deck-text);
|
|
37
|
+
font-family: var(--deck-font-body);
|
|
38
|
+
font-size: 40px;
|
|
39
|
+
line-height: 1.35;
|
|
40
|
+
}
|
|
41
|
+
.deck-slide h1, .deck-slide h2, .deck-slide h3 {
|
|
42
|
+
font-family: var(--deck-font-heading);
|
|
43
|
+
margin: 0;
|
|
44
|
+
line-height: 1.1;
|
|
45
|
+
}
|
|
46
|
+
.deck-slide p { margin: 0; }
|
|
47
|
+
|
|
48
|
+
/* Slide transitions: the incoming slide animates in, and the outgoing one is gone. */
|
|
49
|
+
.deck-enter-fade { animation: deck-fade 450ms ease both; }
|
|
50
|
+
.deck-enter-slide.deck-forward { animation: deck-slide-in-right 550ms cubic-bezier(.2,.7,.2,1) both; }
|
|
51
|
+
.deck-enter-slide.deck-backward { animation: deck-slide-in-left 550ms cubic-bezier(.2,.7,.2,1) both; }
|
|
52
|
+
.deck-enter-zoom { animation: deck-zoom 500ms cubic-bezier(.2,.7,.2,1) both; }
|
|
53
|
+
.deck-enter-rise { animation: deck-rise 600ms cubic-bezier(.2,.7,.2,1) both; }
|
|
54
|
+
@keyframes deck-fade { from { opacity: 0; } to { opacity: 1; } }
|
|
55
|
+
@keyframes deck-slide-in-right { from { transform: translateX(8%); opacity: 0; } to { transform: none; opacity: 1; } }
|
|
56
|
+
@keyframes deck-slide-in-left { from { transform: translateX(-8%); opacity: 0; } to { transform: none; opacity: 1; } }
|
|
57
|
+
@keyframes deck-zoom { from { transform: scale(.92); opacity: 0; } to { transform: none; opacity: 1; } }
|
|
58
|
+
@keyframes deck-rise { from { transform: translateY(6%); opacity: 0; } to { transform: none; opacity: 1; } }
|
|
59
|
+
|
|
60
|
+
/* Steps keep their space while hidden, so revealing one never reflows the slide. */
|
|
61
|
+
.deck-step { transition: opacity 450ms ease, transform 550ms cubic-bezier(.2,.7,.2,1), filter 450ms ease; }
|
|
62
|
+
.deck-step[data-visible="false"] { opacity: 0; pointer-events: none; }
|
|
63
|
+
.deck-step[data-visible="false"][data-animation="fade-up"] { transform: translateY(40px); }
|
|
64
|
+
.deck-step[data-visible="false"][data-animation="fade-left"] { transform: translateX(-40px); }
|
|
65
|
+
.deck-step[data-visible="false"][data-animation="zoom"] { transform: scale(.85); }
|
|
66
|
+
.deck-step[data-visible="false"][data-animation="blur"] { filter: blur(12px); }
|
|
67
|
+
.deck-step[data-animation="none"] { transition: none; }
|
|
68
|
+
.deck-static .deck-step { transition: none; }
|
|
69
|
+
|
|
70
|
+
/* Drawing in a chart's bars and lines. */
|
|
71
|
+
.deck-chart-bar { transform-box: fill-box; transform-origin: bottom; animation: deck-grow 700ms cubic-bezier(.2,.7,.2,1) both; }
|
|
72
|
+
.deck-chart-line { stroke-dasharray: 4000; stroke-dashoffset: 4000; animation: deck-draw 1200ms ease forwards; }
|
|
73
|
+
.deck-static .deck-chart-bar, .deck-static .deck-chart-line { animation: none; stroke-dashoffset: 0; }
|
|
74
|
+
@keyframes deck-grow { from { transform: scaleY(0); } to { transform: none; } }
|
|
75
|
+
@keyframes deck-draw { to { stroke-dashoffset: 0; } }
|
|
76
|
+
|
|
77
|
+
@media (prefers-reduced-motion: reduce) {
|
|
78
|
+
.deck-root *, .deck-root *::before, .deck-root *::after {
|
|
79
|
+
animation-duration: 1ms !important;
|
|
80
|
+
transition-duration: 1ms !important;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/* Controls fade in when the pointer moves, and out again when it rests. */
|
|
85
|
+
.deck-controls {
|
|
86
|
+
position: absolute;
|
|
87
|
+
left: 50%;
|
|
88
|
+
bottom: 16px;
|
|
89
|
+
transform: translateX(-50%);
|
|
90
|
+
display: flex;
|
|
91
|
+
gap: 4px;
|
|
92
|
+
align-items: center;
|
|
93
|
+
padding: 6px;
|
|
94
|
+
border-radius: 999px;
|
|
95
|
+
background: rgba(20, 22, 28, .82);
|
|
96
|
+
color: #fff;
|
|
97
|
+
font: 14px/1 system-ui, sans-serif;
|
|
98
|
+
opacity: 0;
|
|
99
|
+
transition: opacity 250ms ease;
|
|
100
|
+
z-index: 2;
|
|
101
|
+
}
|
|
102
|
+
.deck-root[data-controls="shown"] .deck-controls, .deck-controls:focus-within { opacity: 1; }
|
|
103
|
+
.deck-controls button {
|
|
104
|
+
all: unset;
|
|
105
|
+
cursor: pointer;
|
|
106
|
+
padding: 8px 12px;
|
|
107
|
+
border-radius: 999px;
|
|
108
|
+
}
|
|
109
|
+
.deck-controls button:hover, .deck-controls button:focus-visible { background: rgba(255,255,255,.16); }
|
|
110
|
+
.deck-controls button:disabled { opacity: .4; cursor: default; }
|
|
111
|
+
.deck-controls .deck-position { padding: 0 8px; font-variant-numeric: tabular-nums; }
|
|
112
|
+
.deck-menu {
|
|
113
|
+
position: absolute;
|
|
114
|
+
bottom: 52px;
|
|
115
|
+
right: 0;
|
|
116
|
+
display: flex;
|
|
117
|
+
flex-direction: column;
|
|
118
|
+
min-width: 220px;
|
|
119
|
+
padding: 6px;
|
|
120
|
+
border-radius: 12px;
|
|
121
|
+
background: rgba(20, 22, 28, .95);
|
|
122
|
+
}
|
|
123
|
+
.deck-menu button { border-radius: 8px; }
|
|
124
|
+
.deck-busy {
|
|
125
|
+
position: absolute;
|
|
126
|
+
inset: 0;
|
|
127
|
+
display: grid;
|
|
128
|
+
place-items: center;
|
|
129
|
+
background: rgba(0,0,0,.55);
|
|
130
|
+
color: #fff;
|
|
131
|
+
font: 18px system-ui, sans-serif;
|
|
132
|
+
z-index: 3;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/* The overview grid. */
|
|
136
|
+
.deck-overview {
|
|
137
|
+
position: absolute;
|
|
138
|
+
inset: 0;
|
|
139
|
+
overflow: auto;
|
|
140
|
+
padding: 24px;
|
|
141
|
+
display: grid;
|
|
142
|
+
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
|
|
143
|
+
gap: 16px;
|
|
144
|
+
background: #111;
|
|
145
|
+
z-index: 1;
|
|
146
|
+
}
|
|
147
|
+
.deck-thumb { all: unset; cursor: pointer; display: block; position: relative; aspect-ratio: 16 / 9; overflow: hidden; border-radius: 6px; outline: 2px solid transparent; }
|
|
148
|
+
.deck-thumb[aria-current="true"] { outline-color: var(--deck-primary); }
|
|
149
|
+
.deck-thumb > div { position: absolute; top: 0; left: 0; transform-origin: top left; }
|
|
150
|
+
.deck-thumb-number { position: absolute; left: 8px; bottom: 6px; color: #fff; font: 12px system-ui; text-shadow: 0 1px 2px #000; }
|
|
151
|
+
|
|
152
|
+
/* Static renders, laid out off screen for export and on paper for print. */
|
|
153
|
+
.deck-static-stack { position: fixed; top: 0; left: -100000px; }
|
|
154
|
+
/*
|
|
155
|
+
* While a slide's background picture is taken, native elements step aside so
|
|
156
|
+
* PowerPoint can lay them back on top as real objects. Text hides its glyphs
|
|
157
|
+
* only, so a pill or a card it is drawn on stays in the picture; the rest hide
|
|
158
|
+
* whole, because PowerPoint redraws their fills itself. One the exporter could
|
|
159
|
+
* not describe stays in the picture instead.
|
|
160
|
+
*/
|
|
161
|
+
.deck-exporting [data-deck-native]:not([data-deck-native="text"]):not([data-deck-native-failed]) {
|
|
162
|
+
visibility: hidden !important;
|
|
163
|
+
}
|
|
164
|
+
.deck-exporting [data-deck-native="text"]:not([data-deck-native-failed]),
|
|
165
|
+
.deck-exporting [data-deck-native="text"]:not([data-deck-native-failed]) * {
|
|
166
|
+
color: transparent !important;
|
|
167
|
+
-webkit-text-fill-color: transparent !important;
|
|
168
|
+
text-shadow: none !important;
|
|
169
|
+
text-decoration-color: transparent !important;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
@media print {
|
|
173
|
+
@page { size: ${SLIDE_WIDTH}px ${SLIDE_HEIGHT}px; margin: 0; }
|
|
174
|
+
html, body { margin: 0 !important; padding: 0 !important; background: #fff !important; }
|
|
175
|
+
body > *:not(.deck-static-stack) { display: none !important; }
|
|
176
|
+
.deck-static-stack { position: static; }
|
|
177
|
+
.deck-static-stack .deck-slide { break-after: page; print-color-adjust: exact; -webkit-print-color-adjust: exact; }
|
|
178
|
+
}
|
|
179
|
+
`;
|
|
180
|
+
let injected = false;
|
|
181
|
+
/** Put {@link DECK_CSS} into the document once, however many decks mount. */
|
|
182
|
+
export function injectDeckStyles(doc = document) {
|
|
183
|
+
if (injected || doc.getElementById('deck-styles')) {
|
|
184
|
+
injected = true;
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
const style = doc.createElement('style');
|
|
188
|
+
style.id = 'deck-styles';
|
|
189
|
+
style.textContent = DECK_CSS;
|
|
190
|
+
doc.head.appendChild(style);
|
|
191
|
+
injected = true;
|
|
192
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,57 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@seriousgaming/deck",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "1.0.0-alpha.2",
|
|
4
|
+
"description": "Presentation decks for Serious Gaming games, with PDF, PowerPoint and PNG export",
|
|
5
|
+
"homepage": "https://sdk.seriousgaming.com/",
|
|
6
|
+
"license": "Apache-2.0",
|
|
7
|
+
"author": "Redburn Technology Ltd",
|
|
8
|
+
"files": [
|
|
9
|
+
"lib",
|
|
10
|
+
"README.md",
|
|
11
|
+
"LICENSE",
|
|
12
|
+
"NOTICE"
|
|
13
|
+
],
|
|
14
|
+
"type": "module",
|
|
15
|
+
"main": "lib/index.js",
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"types": "./lib/index.d.ts",
|
|
19
|
+
"import": "./lib/index.js"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"modern-screenshot": "^4.7.0",
|
|
24
|
+
"pptxgenjs": "^4.0.1"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@testing-library/jest-dom": "^7.0.1",
|
|
28
|
+
"@testing-library/react": "^16.3.3",
|
|
29
|
+
"@types/react": "^19.3.0",
|
|
30
|
+
"@types/react-dom": "^19.3.0",
|
|
31
|
+
"@vitejs/plugin-react": "^6.1.2",
|
|
32
|
+
"jsdom": "^30.1.2",
|
|
33
|
+
"react": "^19.3.0",
|
|
34
|
+
"react-dom": "^19.3.0",
|
|
35
|
+
"typescript": "^6.0.3",
|
|
36
|
+
"vite": "^8.3.4",
|
|
37
|
+
"vitest": "^5.0.3"
|
|
38
|
+
},
|
|
39
|
+
"peerDependencies": {
|
|
40
|
+
"react": "^19.2.6",
|
|
41
|
+
"react-dom": "^19.2.6"
|
|
42
|
+
},
|
|
43
|
+
"engines": {
|
|
44
|
+
"node": ">=24"
|
|
45
|
+
},
|
|
46
|
+
"scripts": {
|
|
47
|
+
"build": "rm -rf lib && tsc -p tsconfig.build.json",
|
|
48
|
+
"format": "oxfmt",
|
|
49
|
+
"format:check": "oxfmt --check",
|
|
50
|
+
"harness": "vite build --config harness/vite.config.ts",
|
|
51
|
+
"lint": "oxlint --max-warnings=0",
|
|
52
|
+
"lint:fix": "oxlint --fix",
|
|
53
|
+
"test": "vitest",
|
|
54
|
+
"test:run": "vitest run",
|
|
55
|
+
"typecheck": "tsc --noEmit"
|
|
56
|
+
}
|
|
6
57
|
}
|