@codename/deck 0.2.0 → 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 +4 -4
- package/dist/deck.bundle.mjs +95 -89
- package/dist/deck.bundle.mjs.map +4 -4
- package/dist/deck.css +118 -0
- package/dist/deck.js +215 -91
- package/dist/deck.js.map +4 -4
- package/dist/deck.mjs +43 -37
- package/dist/deck.mjs.map +4 -4
- package/dist/template.mjs +3 -1
- package/dist/template.mjs.map +4 -4
- package/dist/types/blocks.d.ts +6 -4
- package/dist/types/figures.d.ts +68 -0
- package/dist/types/index.d.ts +1 -1
- package/dist/types/kit.d.ts +27 -4
- package/dist/types/look.d.ts +6 -2
- package/dist/types/mindmap.d.ts +69 -0
- package/dist/types/pptx.d.ts +3 -1
- package/dist/types/render.d.ts +21 -3
- package/dist/types/template.d.ts +35 -0
- package/dist/types/version.d.ts +1 -1
- package/docs/format.md +85 -4
- package/docs/host.md +4 -4
- package/docs/kits.md +21 -4
- package/package.json +1 -1
- package/src/blocks.js +26 -9
- package/src/deck.css +118 -0
- package/src/deck.js +13 -3
- package/src/figures.js +255 -0
- package/src/index.js +1 -1
- package/src/kit.js +59 -15
- package/src/look.js +22 -16
- package/src/mindmap.js +208 -0
- package/src/pptx.js +157 -26
- package/src/render.js +126 -20
- package/src/template.js +451 -26
- package/src/version.js +1 -1
package/src/blocks.js
CHANGED
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
// from `base` (a CDN by default) the first time a slide needs it, never before. A chart is data,
|
|
3
3
|
// read with JSON.parse; it takes the deck's colours and type, light or dark, and is drawn as SVG.
|
|
4
4
|
// A diagram is drawn by Pele from Mermaid's language as SVG, then shown as a picture, so nothing it
|
|
5
|
-
// makes runs or joins the page; its words are its picture's text for whoever can't see it
|
|
6
|
-
//
|
|
5
|
+
// makes runs or joins the page; its words are its picture's text for whoever can't see it; its colours
|
|
6
|
+
// (a pie's, a chart's too) are the deck's, again when the scheme changes. Options after a fence's
|
|
7
|
+
// language, as Slidev writes them (```mermaid {scale: 0.8}), reach the drawer. A host gives its own
|
|
8
|
+
// drawers instead.
|
|
7
9
|
|
|
8
10
|
/** @typedef {{ base: string, root: HTMLElement, versions?: { echarts?: string, pele?: string } }} Where */
|
|
9
11
|
|
|
@@ -34,12 +36,22 @@ export function defaultBlocks({ base, root, versions = {} }) {
|
|
|
34
36
|
const { echarts: chartsAt = '6.1.0', pele: diagramsAt = '1.10.0' } = versions
|
|
35
37
|
/** @type {Set<{ chart: any, option: any, box: HTMLElement }>} */
|
|
36
38
|
const charts = new Set()
|
|
39
|
+
/** @type {Set<{ picture: HTMLImageElement, svg: string }>} */
|
|
40
|
+
const diagrams = new Set()
|
|
37
41
|
/** @type {any} */
|
|
38
42
|
let colours = null
|
|
39
43
|
root.addEventListener('deck-scheme', (event) => {
|
|
40
44
|
colours = /** @type {CustomEvent} */ (event).detail.colours
|
|
41
45
|
for (const one of charts) one.chart.setOption(themed(one.option, one.box), true)
|
|
46
|
+
for (const one of diagrams) one.picture.src = pictured(one.svg)
|
|
42
47
|
})
|
|
48
|
+
// A diagram's SVG as a picture in the deck's colours, set as Pele's variables inside it.
|
|
49
|
+
const pictured = (/** @type {string} */ svg) => {
|
|
50
|
+
const font = getComputedStyle(root).getPropertyValue('--deck-body') || 'system-ui, sans-serif'
|
|
51
|
+
const series = Array.from({ length: 8 }, (_, n) => `--pele-series-${n + 1}:${colours?.series?.[n % (colours?.series?.length || 1)]}`).join(';')
|
|
52
|
+
const look = `--pele-bg:transparent;--pele-fg:${colours?.ink};--pele-muted:${colours?.muted};--pele-line:${colours?.muted};--pele-surface:${colours?.soft};--pele-surface-alt:${colours?.line};--pele-border:${colours?.primary};--pele-accent:${colours?.primary};${series};--pele-font:${font.replace(/[<>"]/g, '')}`
|
|
53
|
+
return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg.replace(/^(<svg\b[^>]*>)/, `$1<style>svg{${look}}</style>`))}`
|
|
54
|
+
}
|
|
43
55
|
// Its type sized with the slide's, its category axis read from the right in a language written so.
|
|
44
56
|
const themed = (/** @type {any} */ option, /** @type {HTMLElement} */ box) => {
|
|
45
57
|
const size = parseFloat(getComputedStyle(box).fontSize) || 16
|
|
@@ -61,7 +73,7 @@ export function defaultBlocks({ base, root, versions = {} }) {
|
|
|
61
73
|
}
|
|
62
74
|
}
|
|
63
75
|
return {
|
|
64
|
-
/** @param {HTMLElement} box @param {string} source */
|
|
76
|
+
/** @param {HTMLElement} box @param {string} source the chart's option, as JSON */
|
|
65
77
|
async echarts(box, source) {
|
|
66
78
|
const option = JSON.parse(source)
|
|
67
79
|
const echarts = await script(libraryAt(base, 'echarts', chartsAt, 'dist/echarts.min.js'), 'echarts')
|
|
@@ -79,17 +91,22 @@ export function defaultBlocks({ base, root, versions = {} }) {
|
|
|
79
91
|
},
|
|
80
92
|
/**
|
|
81
93
|
* A diagram written in Mermaid's language, drawn by Pele (Obsidian's, MIT): every kind Mermaid
|
|
82
|
-
* 12.1 has, no DOM needed, an SVG with no script or HTML in it
|
|
83
|
-
*
|
|
94
|
+
* 12.1 has, no DOM needed, an SVG with no script or HTML in it; its own settings (a `---` config
|
|
95
|
+
* block, `%%{init: …}%%`) read by Pele, the fence's options too, `scale` the share of its box it
|
|
96
|
+
* takes. Shown as a picture, its colours the deck's. @param {HTMLElement} box @param {string} source
|
|
97
|
+
* @param {Record<string, any>} [options]
|
|
84
98
|
*/
|
|
85
|
-
async mermaid(box, source) {
|
|
99
|
+
async mermaid(box, source, options = {}) {
|
|
86
100
|
const pele = await script(libraryAt(base, 'pele', diagramsAt, 'dist/pele.global.js'), 'pele')
|
|
87
101
|
const font = getComputedStyle(root).getPropertyValue('--deck-body') || 'system-ui, sans-serif'
|
|
88
|
-
const {
|
|
89
|
-
const
|
|
102
|
+
const { scale, theme, ...config } = options
|
|
103
|
+
const { svg } = pele.render(source, { fontFamily: font, links: false, images: false, autoDirection: true, maxWidth: Math.max(320, box.clientWidth * 1.4), ...(Object.keys(config).length && { config }) })
|
|
90
104
|
const picture = document.createElement('img')
|
|
91
105
|
picture.alt = source
|
|
92
|
-
picture.src =
|
|
106
|
+
picture.src = pictured(svg)
|
|
107
|
+
const share = Number(scale)
|
|
108
|
+
if (share > 0 && share < 1) for (const side of ['max-inline-size', 'max-block-size']) picture.style.setProperty(side, `${share * 100}%`)
|
|
109
|
+
diagrams.add({ picture, svg })
|
|
93
110
|
const place = document.createElement('div')
|
|
94
111
|
place.className = 'deck-diagram'
|
|
95
112
|
place.append(picture)
|
package/src/deck.css
CHANGED
|
@@ -43,6 +43,8 @@
|
|
|
43
43
|
.deck-slide [data-align="center"] { text-align: center; }
|
|
44
44
|
.deck-block { position: relative; flex: 1 1 auto; min-block-size: 12cqi; overflow: hidden; }
|
|
45
45
|
.deck-block .deck-chart, .deck-block .deck-diagram { position: absolute; inset: 0; display: grid; place-items: center; }
|
|
46
|
+
/* A diagram's picture held to its box, its track sized so its share of it can be. */
|
|
47
|
+
.deck-block .deck-diagram { grid-template: minmax(0, 1fr) / minmax(0, 1fr); }
|
|
46
48
|
.deck-block .deck-diagram img { max-inline-size: 100%; max-block-size: 100%; }
|
|
47
49
|
.deck-block .deck-block-source { display: none; }
|
|
48
50
|
.deck-block.is-failed .deck-block-source { display: block; }
|
|
@@ -58,6 +60,13 @@
|
|
|
58
60
|
:is(.deck[data-scheme="light"] :is(.deck-slide[data-paper="ink"], .deck-class-invert), .deck[data-scheme="dark"] .deck-slide:is([data-paper="paper"], [data-paper="soft"]):not(.deck-class-invert), .deck[data-fill="dark"] .deck-slide[data-paper="fill"]) .deck-logo.is-dark { display: block; }
|
|
59
61
|
:is(.deck[data-scheme="light"] :is(.deck-slide[data-paper="ink"], .deck-class-invert), .deck[data-scheme="dark"] .deck-slide:is([data-paper="paper"], [data-paper="soft"]):not(.deck-class-invert), .deck[data-fill="dark"] .deck-slide[data-paper="fill"]) .deck-logo:not(.has-dark, .is-dark) { background: #fff; padding: 0.5cqi; border-radius: 0.5cqi; }
|
|
60
62
|
|
|
63
|
+
/* A layout's art (docs/kits.md), as its template draws it, behind words that sit where the template has them: its grid edge to edge, the slide laid out left to right, its number too. */
|
|
64
|
+
.deck-art { position: absolute; inset: 0; inline-size: 100%; block-size: 100%; pointer-events: none; }
|
|
65
|
+
.deck-slide.has-art { direction: ltr; }
|
|
66
|
+
.has-art .deck-canvas { padding: 0; gap: 0; }
|
|
67
|
+
.has-art .deck-slot { padding: 0.5cqi 1cqi; }
|
|
68
|
+
.deck[dir="rtl"] .has-art .deck-slot { direction: rtl; }
|
|
69
|
+
|
|
61
70
|
/* Layouts from their kit's definition: the paper they are on, where their words sit, how big their heading is. */
|
|
62
71
|
.deck-slide[data-paper="fill"] { background: var(--deck-fill); color: var(--deck-on-fill); }
|
|
63
72
|
.deck-slide[data-paper="ink"] { background: var(--deck-ink); color: var(--deck-paper); }
|
|
@@ -72,9 +81,113 @@
|
|
|
72
81
|
.deck-slide[data-title="huge"] :is(h2, .deck-h2) { font-size: 2.4em; }
|
|
73
82
|
/* What the usual layouts add to their definition: Slidev's looks for them. */
|
|
74
83
|
.deck-layout-section .deck-slot::before { content: ""; display: block; inline-size: 3em; block-size: 0.3em; background: var(--deck-fill); margin-block-end: 0.8em; border-radius: 0.15em; }
|
|
84
|
+
.deck-layout-subsection .deck-slot::before { content: ""; display: block; inline-size: 2em; block-size: 0.2em; background: var(--deck-primary); margin-block-end: 0.7em; border-radius: 0.1em; }
|
|
75
85
|
.deck-layout-statement :is(h1, h2, p, .deck-h1, .deck-h2) { font-size: 2.4em; font-family: var(--deck-heading); font-weight: 700; line-height: 1.2; }
|
|
76
86
|
.deck-slide.deck-layout-fact :is(h1, .deck-h1) { font-size: 5em; color: var(--deck-primary); margin-block-end: 0.15em; }
|
|
77
87
|
.deck-layout-quote blockquote { font-size: 1.7em; border: 0; padding: 0; font-family: var(--deck-heading); }
|
|
88
|
+
/* A mind map (mindmap.js): its words where its layout puts them, in em of its own type, the whole shrunk to its box; its
|
|
89
|
+
root on the fill, each branch in a colour of the charts', its lines and its first words' rim. */
|
|
90
|
+
.deck-mindmap { position: relative; flex: 1 1 auto; align-self: stretch; min-block-size: 12cqi; overflow: hidden; }
|
|
91
|
+
.deck-map-plane { position: absolute; top: 50%; left: 50%; translate: -50% -50%; }
|
|
92
|
+
.deck-map-lines { position: absolute; inset: 0; inline-size: 100%; block-size: 100%; overflow: visible; }
|
|
93
|
+
.deck-map-lines path { fill: none; stroke: var(--deck-map-colour); stroke-width: 0.07; stroke-linecap: round; }
|
|
94
|
+
.deck-map-lines path[data-depth="1"] { stroke-width: 0.15; }
|
|
95
|
+
.deck-slide .deck-map-tree, .deck-slide .deck-map-tree :is(ul, li) { margin: 0; padding: 0; list-style: none; }
|
|
96
|
+
.deck-slide .deck-mindmap .deck-map-node { position: absolute; inline-size: max-content; max-inline-size: 11em; margin: 0; padding: 0.15em 0.35em; font-family: var(--deck-body); font-size: 0.85em; font-weight: 400; line-height: 1.2; letter-spacing: 0; text-align: center; text-wrap: balance; color: inherit; }
|
|
97
|
+
/* A chart or a diagram in a node, under its words, at a size of its own. */
|
|
98
|
+
.deck-slide .deck-mindmap .deck-map-node.has-block[data-depth] { display: flex; flex-direction: column; align-items: center; gap: 0.2em; max-inline-size: none; }
|
|
99
|
+
.deck-slide .deck-mindmap .deck-map-node .deck-block { flex: none; inline-size: 13em; block-size: 7.5em; min-block-size: 0; margin: 0; font-size: 0.85em; }
|
|
100
|
+
/* Words wrapped beyond the first branches keep to the side their line comes from. */
|
|
101
|
+
.deck-slide .deck-mindmap .deck-map-node[data-side="right"]:not([data-depth="1"]) { text-align: left; }
|
|
102
|
+
.deck-slide .deck-mindmap .deck-map-node[data-side="left"]:not([data-depth="1"]) { text-align: right; }
|
|
103
|
+
.deck-slide .deck-mindmap .deck-map-node[data-depth="0"] { max-inline-size: 9em; padding: 0.55em 1em; font-family: var(--deck-heading); font-size: 1.35em; font-weight: 700; background: var(--deck-fill); color: var(--deck-on-fill); border-radius: 1.2em; }
|
|
104
|
+
.deck-slide .deck-mindmap .deck-map-node[data-depth="0"]:empty { inline-size: 0.8em; block-size: 0.8em; padding: 0; border-radius: 50%; }
|
|
105
|
+
.deck-slide .deck-mindmap .deck-map-node[data-depth="1"] { max-inline-size: 9em; padding: 0.3em 0.8em; font-size: 1em; font-weight: 600; background: color-mix(in srgb, var(--deck-map-colour) 14%, transparent); border: 0.12em solid var(--deck-map-colour); border-radius: 1em; }
|
|
106
|
+
.deck-mindmap [data-branch="1"] { --deck-map-colour: var(--deck-series-1); }
|
|
107
|
+
.deck-mindmap [data-branch="2"] { --deck-map-colour: var(--deck-series-2); }
|
|
108
|
+
.deck-mindmap [data-branch="3"] { --deck-map-colour: var(--deck-series-3); }
|
|
109
|
+
.deck-mindmap [data-branch="4"] { --deck-map-colour: var(--deck-series-4); }
|
|
110
|
+
.deck-mindmap [data-branch="5"] { --deck-map-colour: var(--deck-series-5); }
|
|
111
|
+
.deck-mindmap [data-branch="6"] { --deck-map-colour: var(--deck-series-6); }
|
|
112
|
+
/* A branch shown a click fades in where it is: moved, its words would be placed from it. */
|
|
113
|
+
.deck-mindmap .deck-step:not(.is-on) { translate: none; }
|
|
114
|
+
/* Figures (figures.js): a slot's lists as cards, key figures, a timeline, chevrons, a programme, a pyramid or a bento;
|
|
115
|
+
its parts face to face or in a matrix; the deck's contents. In em of the slot's type, so they shrink with it. */
|
|
116
|
+
.deck-figure { flex: 1 1 auto; min-block-size: 0; display: grid; gap: 1em; align-content: center; line-break: strict; }
|
|
117
|
+
.deck-slide .deck-figure :is(p, ul, ol) { margin-block: 0 0.4em; }
|
|
118
|
+
.deck-slide .deck-figure :is(p, ul, ol):last-child { margin-block-end: 0; }
|
|
119
|
+
.deck-words { display: block; }
|
|
120
|
+
.deck-figure .deck-lead { font-family: var(--deck-heading); font-weight: 700; line-height: 1.2; }
|
|
121
|
+
.deck-figure .deck-text { line-height: 1.35; }
|
|
122
|
+
.deck-figure :is(ul, ol).deck-words { font-size: 0.85em; }
|
|
123
|
+
/* On the fill, words are on the fill's ink, their bold and links too. */
|
|
124
|
+
:is(.deck-tile.is-big, .deck-part.is-highlight, .deck-chevron, .deck-level) :is(strong, a, h3, .deck-h3) { color: inherit; }
|
|
125
|
+
:is(.deck-tile.is-big, .deck-part.is-highlight) li::marker { color: inherit; }
|
|
126
|
+
/* A chart or a diagram on the fill would lose its first colour, the fill's: what holds one is on the soft fill, ringed with it. */
|
|
127
|
+
:is(.deck-tile.is-big, .deck-part.is-highlight):has(.deck-block) { background: var(--deck-soft); color: var(--deck-ink); border: 0.15em solid var(--deck-fill); }
|
|
128
|
+
/* Steps: a row of cards, each with its number. */
|
|
129
|
+
.deck-steps { align-items: stretch; }
|
|
130
|
+
.deck-card { display: flex; flex-direction: column; gap: 0.45em; padding: 1.1em 1em; background: var(--deck-soft); border-radius: var(--deck-radius); }
|
|
131
|
+
.deck-badge { display: grid; place-items: center; text-align: center; inline-size: 1.9em; block-size: 1.9em; border-radius: 50%; background: var(--deck-fill); color: var(--deck-on-fill); font-family: var(--deck-heading); font-weight: 700; font-variant-numeric: tabular-nums; }
|
|
132
|
+
.deck-card .deck-lead { font-size: 1.05em; }
|
|
133
|
+
.deck-card .deck-text { font-size: 0.9em; }
|
|
134
|
+
/* Key figures: a row of numbers, each with what it counts, a rule between them. */
|
|
135
|
+
.deck-metrics { gap: 1em 0; text-align: center; }
|
|
136
|
+
.deck-metric { display: flex; flex-direction: column; align-items: center; gap: 0.35em; padding: 0.3em 1em; }
|
|
137
|
+
.deck-metric + .deck-metric { border-inline-start: 1px solid var(--deck-line); }
|
|
138
|
+
.deck-metric .deck-lead { font-size: 3.4em; font-weight: 800; line-height: 1; letter-spacing: -0.02em; color: var(--deck-primary); font-variant-numeric: tabular-nums; }
|
|
139
|
+
/* A timeline: its milestones along a line, each its lead over its dot and its words under it. */
|
|
140
|
+
.deck-timeline { grid-template-rows: auto auto auto; gap: 0.6em 0; justify-items: center; text-align: center; }
|
|
141
|
+
.deck-timeline-line { grid-row: 2; grid-column: 1 / -1; justify-self: stretch; align-self: center; block-size: 0.2em; margin-inline: calc(50% / var(--deck-count)); background: var(--deck-line); border-radius: 0.1em; }
|
|
142
|
+
.deck-timeline .deck-lead { align-self: end; padding-inline: 0.4em; font-size: 1.2em; color: var(--deck-primary); }
|
|
143
|
+
.deck-dot { inline-size: 1.15em; block-size: 1.15em; border-radius: 50%; background: var(--deck-fill); border: 0.22em solid var(--deck-paper); }
|
|
144
|
+
.deck-milestone { align-self: start; padding-inline: 0.4em; font-size: 0.9em; }
|
|
145
|
+
/* A process: chevrons, each holding a step's name, its words under it; they point the way the language reads. */
|
|
146
|
+
.deck-process { gap: 0.7em 0; }
|
|
147
|
+
.deck-chevron { --deck-tip: 0.9em; display: flex; align-items: center; justify-content: center; min-block-size: 3.2em; padding: 0.5em calc(var(--deck-tip) + 0.3em) 0.5em calc(var(--deck-tip) * 2); margin-inline-end: calc(0.3em - var(--deck-tip)); background: var(--deck-fill); color: var(--deck-on-fill); text-align: center;
|
|
148
|
+
clip-path: polygon(0% 0%, calc(100% - var(--deck-tip)) 0%, 100% 50%, calc(100% - var(--deck-tip)) 100%, 0% 100%, var(--deck-tip) 50%); }
|
|
149
|
+
.deck-chevron[data-first] { padding-inline-start: 0.9em; clip-path: polygon(0% 0%, calc(100% - var(--deck-tip)) 0%, 100% 50%, calc(100% - var(--deck-tip)) 100%, 0% 100%); }
|
|
150
|
+
.deck-chevron[data-last] { margin-inline-end: 0; }
|
|
151
|
+
.deck[dir="rtl"] .deck-chevron { clip-path: polygon(100% 0%, var(--deck-tip) 0%, 0% 50%, var(--deck-tip) 100%, 100% 100%, calc(100% - var(--deck-tip)) 50%); }
|
|
152
|
+
.deck[dir="rtl"] .deck-chevron[data-first] { clip-path: polygon(100% 0%, var(--deck-tip) 0%, 0% 50%, var(--deck-tip) 100%, 100% 100%); }
|
|
153
|
+
.deck-under { align-self: start; padding-inline: 0.6em; font-size: 0.9em; text-align: center; }
|
|
154
|
+
/* A programme, and the deck's contents: rows of a time (or a number) and its words, in two columns from seven. */
|
|
155
|
+
.deck-agenda { gap: 0 3em; }
|
|
156
|
+
.deck-agenda-col { display: grid; grid-template-columns: auto minmax(0, 1fr); column-gap: 1.2em; align-content: start; }
|
|
157
|
+
.deck-row { display: grid; grid-column: 1 / -1; grid-template-columns: subgrid; align-items: baseline; padding-block: 0.55em; border-block-end: 1px solid var(--deck-line); }
|
|
158
|
+
.deck-row > :not(.deck-lead) { grid-column: 2; }
|
|
159
|
+
.deck-row .deck-lead { color: var(--deck-primary); white-space: nowrap; font-variant-numeric: tabular-nums; }
|
|
160
|
+
.deck-contents { font-size: 1.1em; }
|
|
161
|
+
.deck-contents .deck-row { cursor: pointer; }
|
|
162
|
+
.deck-contents .deck-row:focus-visible { outline: 0.12em solid var(--deck-primary); outline-offset: 0.1em; }
|
|
163
|
+
.deck-contents .deck-row .deck-text { font-family: var(--deck-heading); font-weight: 600; }
|
|
164
|
+
.deck-contents .deck-row.is-sub { padding-block: 0.3em; border-block-end-style: dotted; font-size: 0.85em; }
|
|
165
|
+
.deck-contents .deck-row.is-sub .deck-text { font-family: var(--deck-body); font-weight: 400; }
|
|
166
|
+
/* A pyramid: its levels stacked, the first the narrowest, on top; what each says beside it. */
|
|
167
|
+
.deck-pyramid { gap: 0.3em 2em; justify-content: center; }
|
|
168
|
+
.deck-level { display: flex; align-items: center; justify-content: center; min-block-size: 2.6em; padding-block: 0.45em; background: var(--deck-fill); color: var(--deck-on-fill); text-align: center; }
|
|
169
|
+
.deck-level .deck-words { font-family: var(--deck-heading); font-weight: 700; }
|
|
170
|
+
.deck-beside { align-self: center; font-size: 0.9em; }
|
|
171
|
+
/* A bento: the first tile big, the others around it. */
|
|
172
|
+
.deck-bento { align-content: stretch; gap: 0.9em; grid-template-columns: 1.4fr repeat(2, minmax(0, 1fr)); grid-auto-rows: 1fr; }
|
|
173
|
+
.deck-tile { display: flex; flex-direction: column; justify-content: safe flex-end; gap: 0.35em; padding: 1em 1.1em; background: var(--deck-soft); border-radius: var(--deck-radius); }
|
|
174
|
+
.deck-tile.is-big { grid-row: span 2; background: var(--deck-fill); color: var(--deck-on-fill); }
|
|
175
|
+
.deck-tile .deck-lead { font-size: 1.15em; }
|
|
176
|
+
.deck-tile.is-big .deck-lead { font-size: 1.9em; }
|
|
177
|
+
.deck-tile .deck-block { min-block-size: 6cqi; }
|
|
178
|
+
.deck-bento[data-count="1"] { grid-template-columns: 1fr; }
|
|
179
|
+
.deck-bento[data-count="2"] { grid-template-columns: 1.3fr 1fr; }
|
|
180
|
+
.deck-bento[data-count="2"] .deck-tile.is-big { grid-row: auto; }
|
|
181
|
+
.deck-bento[data-count="3"] { grid-template-columns: 1.3fr 1fr; }
|
|
182
|
+
.deck-bento[data-count="4"] .deck-tile:nth-child(2) { grid-column: span 2; }
|
|
183
|
+
/* Parts face to face, a medallion between them, the last on the fill; or four in a matrix, the second, top at the end, on the fill. */
|
|
184
|
+
.deck-versus, .deck-matrix { align-content: stretch; }
|
|
185
|
+
.deck-versus { column-gap: 0; grid-auto-rows: 1fr; }
|
|
186
|
+
.deck-matrix { grid-template-columns: repeat(2, minmax(0, 1fr)); grid-auto-rows: 1fr; gap: 0.8em; }
|
|
187
|
+
.deck-part { display: flex; flex-direction: column; padding: 1.1em 1.4em; background: var(--deck-soft); border-radius: var(--deck-radius); }
|
|
188
|
+
.deck-part.is-highlight { background: var(--deck-fill); color: var(--deck-on-fill); }
|
|
189
|
+
.deck-slide .deck-part :is(h1, h2, h3, h4, .deck-h) { font-size: 1.35em; color: inherit; margin-block-end: 0.45em; }
|
|
190
|
+
.deck-vs { position: relative; z-index: 1; align-self: center; display: grid; place-items: center; text-align: center; inline-size: 3.2em; block-size: 3.2em; margin-inline: -1em; border-radius: 50%; background: var(--deck-ink); color: var(--deck-paper); border: 0.25em solid var(--deck-paper); font-family: var(--deck-heading); font-weight: 800; font-size: 0.9em; letter-spacing: 0.02em; }
|
|
78
191
|
/* Scripts without italics keep their upright letters. */
|
|
79
192
|
.deck :is(blockquote, em):is(:lang(ar), :lang(fa), :lang(ur), :lang(he), :lang(ja), :lang(zh), :lang(ko), :lang(hi), :lang(th)) { font-style: normal; }
|
|
80
193
|
.deck-layout-quote blockquote::before { content: "\201C"; display: block; font-size: 2.4em; line-height: 0.6; color: var(--deck-primary); }
|
|
@@ -86,6 +199,11 @@
|
|
|
86
199
|
:is(.deck-layout-image-left, .deck-layout-image-right).has-image .deck-slot { padding: 6cqi 5cqi; justify-content: center; }
|
|
87
200
|
:is(.deck-layout-image, .deck-layout-cover).has-image .deck-image { position: absolute; inset: 0; }
|
|
88
201
|
:is(.deck-layout-image, .deck-layout-cover).has-image .deck-slot { position: relative; color: #fff; text-shadow: 0 0.05em 0.4em rgb(0 0 0 / 0.6); }
|
|
202
|
+
/* A quote over its picture, the whole slide, behind a veil. */
|
|
203
|
+
.deck-layout-quote.has-image .deck-image { position: absolute; inset: 0; }
|
|
204
|
+
.deck-layout-quote.has-image .deck-image::after { content: ""; position: absolute; inset: 0; background-color: rgb(0 0 0 / 0.55); }
|
|
205
|
+
.deck-layout-quote.has-image .deck-slot { position: relative; color: #fff; text-shadow: 0 0.05em 0.4em rgb(0 0 0 / 0.5); }
|
|
206
|
+
.deck-layout-quote.has-image blockquote::before { color: inherit; }
|
|
89
207
|
.deck-layout-cover.has-image .deck-image::after { content: ""; position: absolute; inset: 0; background: linear-gradient(to top, rgb(0 0 0 / 0.65), rgb(0 0 0 / 0.1)); }
|
|
90
208
|
.deck-class-invert { background: var(--deck-ink); color: var(--deck-paper); }
|
|
91
209
|
.deck-class-text-center .deck-slot { text-align: center; align-items: center; }
|
package/src/deck.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// the deck changes or is saved.
|
|
9
9
|
import { parseDeck } from './parse.js'
|
|
10
10
|
import { renderSlide, h } from './render.js'
|
|
11
|
-
import { tokensOf, schemeOf, VOICES,
|
|
11
|
+
import { tokensOf, schemeOf, VOICES, rgbOf, contrast } from './look.js'
|
|
12
12
|
import { mergeKits } from './kit.js'
|
|
13
13
|
import { stringsFor } from './strings.js'
|
|
14
14
|
import { Lexer } from 'marked'
|
|
@@ -108,7 +108,7 @@ export function createDeck(root, options) {
|
|
|
108
108
|
})) root.style.setProperty(`--deck-${name}`, value)
|
|
109
109
|
root.dataset.scheme = scheme
|
|
110
110
|
// Whether the primary as a fill is dark, for a logo's dark one there.
|
|
111
|
-
root.dataset.fill = ((fill) => (contrast(fill, [255, 255, 255]) > contrast(fill, [0, 0, 0]) ? 'dark' : 'light'))(
|
|
111
|
+
root.dataset.fill = ((fill) => (contrast(fill, [255, 255, 255]) > contrast(fill, [0, 0, 0]) ? 'dark' : 'light'))(rgbOf(tokens.primary) ?? [247, 197, 49])
|
|
112
112
|
root.dispatchEvent(new CustomEvent('deck-scheme', { detail: { scheme, colours, tokens } }))
|
|
113
113
|
}
|
|
114
114
|
const chooseScheme = () => {
|
|
@@ -124,6 +124,7 @@ export function createDeck(root, options) {
|
|
|
124
124
|
|
|
125
125
|
function draw() {
|
|
126
126
|
context.total = deck.slides.length
|
|
127
|
+
context.deck = deck
|
|
127
128
|
drawn = deck.slides.map((slide) => renderSlide(slide, context))
|
|
128
129
|
for (const [i, { element }] of drawn.entries()) {
|
|
129
130
|
element.setAttribute('aria-label', words.where.replace('{n}', number(i + 1)).replace('{count}', number(drawn.length)))
|
|
@@ -210,7 +211,8 @@ export function createDeck(root, options) {
|
|
|
210
211
|
*/
|
|
211
212
|
function name(slide, toward) {
|
|
212
213
|
const key = (/** @type {Element} */ el) => `${el.tagName}:${el.tagName === 'IMG' ? el.getAttribute('src') : (el.textContent ?? '').trim().slice(0, 80)}`
|
|
213
|
-
|
|
214
|
+
// A mind map's words, not its items, which hold no box of their own.
|
|
215
|
+
const parts = (/** @type {HTMLElement} */ s) => /** @type {HTMLElement[]} */ ([...s.querySelectorAll('h1, h2, h3, h4, .deck-h, li, p, img, .deck-block, blockquote, .deck-image, .deck-map-node')].filter((el) => !el.matches('.deck-mindmap li')))
|
|
214
216
|
if (toward) {
|
|
215
217
|
const there = new Map(parts(toward).map((el) => [key(el), el]))
|
|
216
218
|
let n = 0
|
|
@@ -296,6 +298,8 @@ export function createDeck(root, options) {
|
|
|
296
298
|
target.addEventListener('blur', blur)
|
|
297
299
|
})
|
|
298
300
|
|
|
301
|
+
// The deck's contents: a part chosen, its slide shown.
|
|
302
|
+
const chosen = (/** @type {Event} */ event) => /** @type {HTMLElement | null} */ (/** @type {Element} */ (event.target).closest('.deck-slide.is-shown [data-go]'))
|
|
299
303
|
// The keys a clicker sends, and the deck's own.
|
|
300
304
|
root.addEventListener('keydown', (event) => {
|
|
301
305
|
const target = /** @type {HTMLElement} */ (event.target)
|
|
@@ -303,6 +307,8 @@ export function createDeck(root, options) {
|
|
|
303
307
|
if ((event.metaKey || event.ctrlKey) && event.key === 's') { event.preventDefault(); options.onSave?.(source) }
|
|
304
308
|
return
|
|
305
309
|
}
|
|
310
|
+
const part = chosen(event)
|
|
311
|
+
if (part && event.key === 'Enter') { event.preventDefault(); go(Number(part.dataset.go)); return }
|
|
306
312
|
const forward = dir === 'rtl' ? 'ArrowLeft' : 'ArrowRight', backward = dir === 'rtl' ? 'ArrowRight' : 'ArrowLeft'
|
|
307
313
|
if (event.altKey || event.ctrlKey || event.metaKey) {
|
|
308
314
|
if ((event.metaKey || event.ctrlKey) && event.key === 's') { event.preventDefault(); options.onSave?.(source) }
|
|
@@ -317,6 +323,10 @@ export function createDeck(root, options) {
|
|
|
317
323
|
event.preventDefault()
|
|
318
324
|
act()
|
|
319
325
|
})
|
|
326
|
+
stage.addEventListener('click', (event) => {
|
|
327
|
+
const part = chosen(event)
|
|
328
|
+
if (part && !root.classList.contains('is-editing') && !root.classList.contains('is-overview')) { event.stopPropagation(); go(Number(part.dataset.go)) }
|
|
329
|
+
}, true)
|
|
320
330
|
stage.addEventListener('click', (event) => {
|
|
321
331
|
if (document.fullscreenElement && !(/** @type {Element} */ (event.target).closest('a, button, input'))) move(1)
|
|
322
332
|
})
|
package/src/figures.js
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
// The figures a layout draws from what a slide holds, as slides usually show it: a slot's lists as
|
|
2
|
+
// a row of numbered cards (steps), of key figures (metrics), a timeline, chevrons (process), a timed
|
|
3
|
+
// programme (agenda), a pyramid or a bento; its `##` parts face to face (versus) or four in a
|
|
4
|
+
// matrix; the deck's contents (summary). An item's lead, the words in bold it starts with (`**Q1**`,
|
|
5
|
+
// `**56%**`, `**6:00 PM**`), is its label and the rest its words, each changed in place as a list's
|
|
6
|
+
// are. What is drawn as a shape says so (.deck-shape), and what is words (.deck-words), for
|
|
7
|
+
// PowerPoint to write them as its own shapes and boxes of words.
|
|
8
|
+
import { h, inline, step, range, block } from './render.js'
|
|
9
|
+
import { layoutNameOf } from './kit.js'
|
|
10
|
+
|
|
11
|
+
/** @typedef {import('./render.js').Context} Context */
|
|
12
|
+
/** @typedef {{ tokens: any[], start: number, end: number, blocks: { token: any, start: number }[], step: number }} Entry */
|
|
13
|
+
/** @typedef {{ lead: any[] | null, leadAt: [number, number] | null, rest: any[], restAt: [number, number] | null }} Split */
|
|
14
|
+
|
|
15
|
+
// What may come between a lead and its words: a colon, a dash, a bar, a dot.
|
|
16
|
+
const SEP = /^\s*(?:[::—–|·-]\s*)?/
|
|
17
|
+
// A lead not in bold: a time starting a programme's line, a number starting a key figure's.
|
|
18
|
+
const TIME = /^\d{1,2}(?:[:h.]\d{2}|h)?(?:\s?[ap]\.?m\.?)?(?:\s?[–-]\s?\d{1,2}(?:[:h.]\d{2}|h)?(?:\s?[ap]\.?m\.?)?)?(?=\s|$)/i
|
|
19
|
+
const NUMBER = /^[+−-]?[^\p{L}\p{N}\s]?\d[\d., ]*\S*(?=\s|$)/u
|
|
20
|
+
const PATTERNS = /** @type {Record<string, RegExp>} */ ({ agenda: TIME, metrics: NUMBER })
|
|
21
|
+
|
|
22
|
+
/** A paragraph whose every line starts in bold, as key figures are written without a list. @param {any} token */
|
|
23
|
+
export const boldLines = (token) => token.type === 'paragraph' && token.raw.trim().split('\n').every((/** @type {string} */ line) => /^\s*(\*\*|__)\S/.test(line))
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* A list's items, or a paragraph's lines that start in bold, as a figure's entries: each one's words (its first line's
|
|
27
|
+
* inline tokens) and where they are in the source, `end` -1 when they run over lines; what is under an item (a list, a
|
|
28
|
+
* chart) its blocks. @param {any} token @param {number} start where its raw starts @param {(markdown: string) => any[]} lex
|
|
29
|
+
* @returns {Entry[]}
|
|
30
|
+
*/
|
|
31
|
+
export function entriesOf(token, start, lex) {
|
|
32
|
+
if (token.type === 'paragraph') {
|
|
33
|
+
let at = start
|
|
34
|
+
return token.raw.split('\n').flatMap((/** @type {string} */ line) => {
|
|
35
|
+
const from = at
|
|
36
|
+
at += line.length + 1
|
|
37
|
+
const text = line.trim()
|
|
38
|
+
if (!text) return []
|
|
39
|
+
const [first] = lex(text)
|
|
40
|
+
return [{ tokens: first?.tokens ?? [], start: from + line.length - line.trimStart().length, end: from + line.trimEnd().length, blocks: [], step: 0 }]
|
|
41
|
+
})
|
|
42
|
+
}
|
|
43
|
+
let from = 0
|
|
44
|
+
return token.items.map((/** @type {any} */ item) => {
|
|
45
|
+
const offset = token.raw.indexOf(item.raw, from)
|
|
46
|
+
from = offset + item.raw.length
|
|
47
|
+
const itemStart = start + Math.max(0, offset)
|
|
48
|
+
const marker = /^\s*(?:[-*+]|\d+[.)])\s+(?:\[[ xX]\]\s+)?/.exec(item.raw)
|
|
49
|
+
const [first, ...rest] = item.tokens
|
|
50
|
+
const words = first && (first.type === 'text' || first.type === 'paragraph') ? first : null
|
|
51
|
+
const at = itemStart + (marker?.[0].length ?? 0)
|
|
52
|
+
return {
|
|
53
|
+
tokens: words ? words.tokens ?? [{ type: 'text', raw: words.text, text: words.text }] : [],
|
|
54
|
+
start: at, end: words && marker && !words.raw.trim().includes('\n') ? at + words.raw.trim().length : -1,
|
|
55
|
+
blocks: (words ? rest : item.tokens).map((/** @type {any} */ inner) => ({ token: inner, start: itemStart + Math.max(0, item.raw.indexOf(inner.raw)) })), step: 0,
|
|
56
|
+
}
|
|
57
|
+
})
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* An entry's lead and the rest of its words, each with where it is in the source: the bold it starts with, else for a
|
|
62
|
+
* programme a time and for key figures a number; what comes between them (a colon, a dash) left out.
|
|
63
|
+
* @param {Entry} entry @param {string} kind @returns {Split}
|
|
64
|
+
*/
|
|
65
|
+
export function leadOf(entry, kind) {
|
|
66
|
+
const [first, ...others] = entry.tokens
|
|
67
|
+
const editable = entry.end >= 0
|
|
68
|
+
const after = (/** @type {any[]} */ tokens, /** @type {number} */ from) => {
|
|
69
|
+
const next = tokens[0], cut = next?.type === 'text' ? /** @type {RegExpExecArray} */ (SEP.exec(next.raw))[0].length : 0
|
|
70
|
+
const rest = (cut ? [{ ...next, raw: next.raw.slice(cut), text: next.text.slice(/** @type {RegExpExecArray} */ (SEP.exec(next.text))[0].length) }, ...tokens.slice(1)] : tokens).filter((t) => t.raw.trim() || t.type !== 'text')
|
|
71
|
+
return { rest, restAt: /** @type {[number, number] | null} */ (editable && rest.length && from + cut < entry.end ? [from + cut, entry.end] : null) }
|
|
72
|
+
}
|
|
73
|
+
if (first?.type === 'strong') return { lead: first.tokens, leadAt: editable ? [entry.start + 2, entry.start + first.raw.length - 2] : null, ...after(others, entry.start + first.raw.length) }
|
|
74
|
+
const found = first?.type === 'text' && PATTERNS[kind]?.exec(first.raw)
|
|
75
|
+
if (found) {
|
|
76
|
+
const tail = first.raw.slice(found[0].length)
|
|
77
|
+
return { lead: [{ type: 'text', raw: found[0], text: found[0] }], leadAt: editable ? [entry.start, entry.start + found[0].length] : null, ...after([...(tail ? [{ type: 'text', raw: tail, text: tail }] : []), ...others], entry.start + found[0].length) }
|
|
78
|
+
}
|
|
79
|
+
return { lead: null, leadAt: null, rest: entry.tokens, restAt: editable ? [entry.start, entry.end] : null }
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Where a slot's parts start: at each heading of the level its last heading has, but the heading the slot opens with when
|
|
84
|
+
* there are more of them than the parts asked for (the slide's title, written at their level).
|
|
85
|
+
* @param {any[]} tokens @param {number} count how many parts the layout has @returns {Set<number>}
|
|
86
|
+
*/
|
|
87
|
+
export function partHeads(tokens, count) {
|
|
88
|
+
const heads = tokens.flatMap((token, i) => (token.type === 'heading' ? [{ depth: token.depth, i }] : []))
|
|
89
|
+
if (!heads.length) return new Set()
|
|
90
|
+
const depth = /** @type {{ depth: number }} */ (heads.at(-1)).depth
|
|
91
|
+
const at = heads.filter((one) => one.depth === depth).map((one) => one.i)
|
|
92
|
+
return new Set(at.length > count && at[0] === tokens.findIndex((token) => token.type !== 'space') ? at.slice(1) : at)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* The deck's contents, as Slidev's <Toc> reads them: each slide's title and level, but a slide with `hideInToc: true`,
|
|
97
|
+
* the contents themselves, a cover, an introduction, the end and slides with no title. Where the deck has parts (slides
|
|
98
|
+
* on `section` or `subsection`, or with a `level`), the parts alone, a section at level 1 and a subsection at 2; else
|
|
99
|
+
* every slide, at its heading's level. `hideInToc: false` brings a slide back. Levels start at 1, `maxDepth` at most.
|
|
100
|
+
* @param {import('./parse.js').Slide[]} slides @param {import('./kit.js').Kit} kit @param {number} at the contents' own slide
|
|
101
|
+
* @param {number} [maxDepth] @returns {{ index: number, title: string, level: number }[]}
|
|
102
|
+
*/
|
|
103
|
+
export function contentsOf(slides, kit, at, maxDepth = 2) {
|
|
104
|
+
const all = slides.map((slide) => {
|
|
105
|
+
const name = layoutNameOf(slide, kit), given = slide.settings.level, hidden = slide.settings.hideInToc
|
|
106
|
+
const depth = /^(#{1,6})\s/m.exec(slide.slots.map((slot) => slot.text).join('\n').replace(/^(`{3,}|~{3,})[\s\S]*?^\1/gm, ''))?.[1].length ?? 1
|
|
107
|
+
const part = Number.isInteger(given) || name === 'section' || name === 'subsection'
|
|
108
|
+
const level = Number.isInteger(given) ? given : name === 'section' ? 1 : name === 'subsection' ? 2 : depth
|
|
109
|
+
const kept = Boolean(slide.title) && (hidden === false || (hidden !== true && slide.index !== at && !kit.layouts[name]?.contents && !['cover', 'intro', 'end'].includes(name)))
|
|
110
|
+
return { index: slide.index, title: slide.title, level, part, kept, shown: hidden === false }
|
|
111
|
+
}).filter((one) => one.kept)
|
|
112
|
+
const chosen = all.some((one) => one.part) ? all.filter((one) => one.part || one.shown) : all
|
|
113
|
+
const top = Math.min(...chosen.map((one) => one.level))
|
|
114
|
+
return chosen.map(({ index, title, level }) => ({ index, title, level: level - top + 1 })).filter((one) => one.level <= Math.max(1, maxDepth))
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Columns for a row of n, wrapping beyond `most`. @param {HTMLElement} figure @param {number} n @param {number} most */
|
|
118
|
+
const columns = (figure, n, most) => figure.style.setProperty('grid-template-columns', `repeat(${n <= most ? n : Math.ceil(n / 2)}, minmax(0, 1fr))`)
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* A slot's lists as the figure its layout draws. Entries a click each (<v-clicks>) show their card, milestone, chevron,
|
|
122
|
+
* row, level or tile at their step. @param {string} kind @param {Entry[]} entries @param {Context} context @returns {HTMLElement}
|
|
123
|
+
*/
|
|
124
|
+
export function listFigure(kind, entries, context) {
|
|
125
|
+
const items = entries.map((entry) => ({ entry, ...leadOf(entry, kind) }))
|
|
126
|
+
const n = items.length
|
|
127
|
+
const figure = h('div', { class: `deck-figure deck-${kind}`, 'data-count': n })
|
|
128
|
+
figure.style.setProperty('--deck-count', String(n))
|
|
129
|
+
/** @typedef {(typeof items)[number]} Item */
|
|
130
|
+
const at = (/** @type {[number, number] | null} */ where) => (where ? range(...where) : undefined)
|
|
131
|
+
const lead = (/** @type {Item} */ item) => (item.lead ? h('span', { class: 'deck-words deck-lead', dir: 'auto', 'data-edit': at(item.leadAt) }, inline(item.lead, context)) : null)
|
|
132
|
+
const text = (/** @type {Item} */ item) => (item.rest.length ? h('span', { class: 'deck-words deck-text', dir: 'auto', 'data-edit': at(item.restAt) }, inline(item.rest, context)) : null)
|
|
133
|
+
// What is under an item: a list as words, a chart or a diagram as itself.
|
|
134
|
+
const more = (/** @type {Item} */ item) => item.entry.blocks.map(({ token, start }) => {
|
|
135
|
+
const element = block(token, start, context, null)
|
|
136
|
+
if (element && !element.matches('.deck-block, .deck-table, pre')) element.classList.add('deck-words')
|
|
137
|
+
return element
|
|
138
|
+
})
|
|
139
|
+
const stepped = (/** @type {Item} */ item, /** @type {(Element | null)[]} */ elements) => { if (item.entry.step) for (const one of elements) if (one) step(one, item.entry.step) }
|
|
140
|
+
const cell = (/** @type {HTMLElement} */ element, /** @type {number} */ column, /** @type {number} */ row) => { element.style.setProperty('grid-column', String(column)); element.style.setProperty('grid-row', String(row)); return element }
|
|
141
|
+
// The lead, else the words, as what a chevron or a level says; the words beside or under it when there is a lead.
|
|
142
|
+
const label = (/** @type {Item} */ item) => lead(item) ?? text(item) ?? h('span', { class: 'deck-words deck-lead' })
|
|
143
|
+
const under = (/** @type {Item} */ item, /** @type {string} */ name) => h('div', { class: name }, [item.lead ? text(item) : null, ...more(item)])
|
|
144
|
+
switch (kind) {
|
|
145
|
+
case 'steps':
|
|
146
|
+
columns(figure, n, 5)
|
|
147
|
+
for (const [i, item] of items.entries()) {
|
|
148
|
+
const card = h('div', { class: 'deck-card deck-shape' }, [h('span', { class: 'deck-words deck-badge deck-shape' }, [context.number(i + 1)]), lead(item), text(item), ...more(item)])
|
|
149
|
+
stepped(item, [card])
|
|
150
|
+
figure.append(card)
|
|
151
|
+
}
|
|
152
|
+
break
|
|
153
|
+
case 'metrics':
|
|
154
|
+
columns(figure, n, 4)
|
|
155
|
+
for (const item of items) {
|
|
156
|
+
const metric = h('div', { class: 'deck-metric deck-shape' }, [lead(item), text(item), ...more(item)])
|
|
157
|
+
stepped(item, [metric])
|
|
158
|
+
figure.append(metric)
|
|
159
|
+
}
|
|
160
|
+
break
|
|
161
|
+
case 'timeline':
|
|
162
|
+
columns(figure, n, 99)
|
|
163
|
+
// The line first, under its dots, from the first to the last.
|
|
164
|
+
figure.append(h('span', { class: 'deck-timeline-line deck-shape' }))
|
|
165
|
+
for (const [i, item] of items.entries()) {
|
|
166
|
+
const parts = [cell(lead(item) ?? h('span', { class: 'deck-lead' }), i + 1, 1), cell(h('span', { class: 'deck-dot deck-shape' }), i + 1, 2), cell(h('div', { class: 'deck-milestone' }, [text(item), ...more(item)]), i + 1, 3)]
|
|
167
|
+
stepped(item, parts)
|
|
168
|
+
figure.append(...parts)
|
|
169
|
+
}
|
|
170
|
+
break
|
|
171
|
+
case 'process':
|
|
172
|
+
columns(figure, n, 99)
|
|
173
|
+
for (const [i, item] of items.entries()) {
|
|
174
|
+
const parts = [cell(h('div', { class: 'deck-chevron deck-shape', 'data-first': i === 0 || undefined, 'data-last': i === n - 1 || undefined }, [label(item)]), i + 1, 1), cell(under(item, 'deck-under'), i + 1, 2)]
|
|
175
|
+
stepped(item, parts)
|
|
176
|
+
figure.append(...parts)
|
|
177
|
+
}
|
|
178
|
+
break
|
|
179
|
+
case 'agenda': {
|
|
180
|
+
// In two columns from seven lines, the first holding the more.
|
|
181
|
+
const halves = n >= 7 ? [items.slice(0, Math.ceil(n / 2)), items.slice(Math.ceil(n / 2))] : [items]
|
|
182
|
+
figure.style.setProperty('grid-template-columns', `repeat(${halves.length}, minmax(0, 1fr))`)
|
|
183
|
+
for (const half of halves) {
|
|
184
|
+
figure.append(h('div', { class: 'deck-agenda-col' }, half.map((item) => {
|
|
185
|
+
const row = h('div', { class: 'deck-row deck-shape' }, [lead(item) ?? h('span', { class: 'deck-lead' }), text(item), ...more(item)])
|
|
186
|
+
stepped(item, [row])
|
|
187
|
+
return row
|
|
188
|
+
})))
|
|
189
|
+
}
|
|
190
|
+
break
|
|
191
|
+
}
|
|
192
|
+
case 'pyramid': {
|
|
193
|
+
// Narrowest at the top, widest at the bottom, each level a band of one triangle; its words held to its top edge.
|
|
194
|
+
const beside = items.some((item) => item.lead && (item.rest.length || item.entry.blocks.length))
|
|
195
|
+
figure.style.setProperty('grid-template-columns', beside ? 'minmax(0, 1.15fr) minmax(0, 1fr)' : 'minmax(0, 30em)')
|
|
196
|
+
const half = (/** @type {number} */ k) => Math.round(5000 * (0.16 + (0.84 * k) / n)) / 100
|
|
197
|
+
for (const [i, item] of items.entries()) {
|
|
198
|
+
const band = cell(h('div', { class: 'deck-level deck-shape' }, [label(item)]), 1, i + 1)
|
|
199
|
+
band.style.setProperty('clip-path', `polygon(${50 - half(i)}% 0%, ${50 + half(i)}% 0%, ${50 + half(i + 1)}% 100%, ${50 - half(i + 1)}% 100%)`)
|
|
200
|
+
band.style.setProperty('padding-inline', `max(0.4em, ${Math.round((50 - half(i)) * 100) / 100}%)`)
|
|
201
|
+
const parts = [band, beside ? cell(under(item, 'deck-beside'), 2, i + 1) : null]
|
|
202
|
+
stepped(item, parts)
|
|
203
|
+
figure.append(...parts.filter((one) => one !== null))
|
|
204
|
+
}
|
|
205
|
+
break
|
|
206
|
+
}
|
|
207
|
+
case 'bento':
|
|
208
|
+
for (const [i, item] of items.entries()) {
|
|
209
|
+
const tile = h('div', { class: `deck-tile deck-shape${i === 0 ? ' is-big' : ''}` }, [lead(item), text(item), ...more(item)])
|
|
210
|
+
stepped(item, [tile])
|
|
211
|
+
figure.append(tile)
|
|
212
|
+
}
|
|
213
|
+
break
|
|
214
|
+
}
|
|
215
|
+
return figure
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* A slot's parts as its layout sets them: face to face, the last on the fill, a medallion between each two (versus), or
|
|
220
|
+
* in a grid of two columns, the second, top at the end, on the fill (matrix). Their words stay as they were drawn.
|
|
221
|
+
* @param {string} kind @param {Node[][]} parts @returns {HTMLElement}
|
|
222
|
+
*/
|
|
223
|
+
export function partsFigure(kind, parts) {
|
|
224
|
+
const figure = h('div', { class: `deck-figure deck-${kind}`, 'data-count': parts.length })
|
|
225
|
+
if (kind === 'versus') figure.style.setProperty('grid-template-columns', parts.map(() => 'minmax(0, 1fr)').join(' auto '))
|
|
226
|
+
for (const [i, children] of parts.entries()) {
|
|
227
|
+
for (const child of children) if (child instanceof HTMLElement && !child.matches('.deck-block, .deck-table, pre, .deck-picture')) child.classList.add('deck-words')
|
|
228
|
+
if (kind === 'versus' && i > 0) figure.append(h('span', { class: 'deck-vs deck-shape deck-words', 'aria-hidden': 'true' }, ['VS']))
|
|
229
|
+
const highlight = parts.length > 1 && (kind === 'versus' ? i === parts.length - 1 : i === 1)
|
|
230
|
+
figure.append(h('div', { class: `deck-part deck-shape${highlight ? ' is-highlight' : ''}` }, children))
|
|
231
|
+
}
|
|
232
|
+
return figure
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* The deck's contents as rows, a part's number and its title, each going to its slide when chosen; a subsection under
|
|
237
|
+
* its section, without a number; in two columns from seven rows. @param {Context} context @param {number} at its own slide
|
|
238
|
+
* @param {number} [maxDepth] @returns {HTMLElement | null}
|
|
239
|
+
*/
|
|
240
|
+
export function contentsFigure(context, at, maxDepth) {
|
|
241
|
+
const entries = context.deck ? contentsOf(context.deck.slides, context.kit, at, maxDepth) : []
|
|
242
|
+
if (!entries.length) return null
|
|
243
|
+
let n = 0
|
|
244
|
+
const rows = entries.map(({ index, title, level }) => {
|
|
245
|
+
const [heading] = context.lex(`# ${title}`)
|
|
246
|
+
return h('div', { class: `deck-row deck-shape${level > 1 ? ' is-sub' : ''}`, 'data-go': index, role: 'link', tabindex: 0 }, [
|
|
247
|
+
h('span', { class: 'deck-words deck-lead' }, level > 1 ? [] : [context.number(++n)]),
|
|
248
|
+
h('span', { class: 'deck-words deck-text', dir: 'auto' }, inline(heading?.tokens ?? [{ type: 'text', text: title }], context)),
|
|
249
|
+
])
|
|
250
|
+
})
|
|
251
|
+
const halves = rows.length >= 7 ? [rows.slice(0, Math.ceil(rows.length / 2)), rows.slice(Math.ceil(rows.length / 2))] : [rows]
|
|
252
|
+
const figure = h('div', { class: 'deck-figure deck-agenda deck-contents', 'data-count': rows.length }, halves.map((half) => h('div', { class: 'deck-agenda-col' }, half)))
|
|
253
|
+
figure.style.setProperty('grid-template-columns', `repeat(${halves.length}, minmax(0, 1fr))`)
|
|
254
|
+
return figure
|
|
255
|
+
}
|
package/src/index.js
CHANGED
|
@@ -3,7 +3,7 @@ export { createDeck, markdownOf } from './deck.js'
|
|
|
3
3
|
export { parseDeck } from './parse.js'
|
|
4
4
|
export { renderSlide } from './render.js'
|
|
5
5
|
export { STANDARD, CORNERS, mergeKits, layoutOf, logoOf, inferLayout, labelOf, tokensFrom } from './kit.js'
|
|
6
|
-
export { tokensOf, schemeOf, LOOKS, readable, contrast } from './look.js'
|
|
6
|
+
export { tokensOf, schemeOf, LOOKS, readable, contrast, rgbOf } from './look.js'
|
|
7
7
|
export { readDeckFile, writeDeckFile, rendererUrl, escape, unescape } from './file.js'
|
|
8
8
|
export { toPptx } from './pptx.js'
|
|
9
9
|
export { defaultBlocks, libraryAt } from './blocks.js'
|