@abreen/tada 1.13.2 → 1.13.7
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/build/bundle.ts +17 -2
- package/build/generate-content-assets.ts +1 -0
- package/build/types.d.ts +1 -0
- package/build/utils/derive-theme.ts +10 -0
- package/build/utils/render.ts +1 -0
- package/build/utils/trace-core.ts +78 -29
- package/build/utils/trace.ts +27 -10
- package/build/watch/assets.ts +2 -2
- package/build/watch/compiler-types.d.ts +1 -0
- package/init/content/labs/02/selection_sort.py +1 -1
- package/package.json +1 -1
- package/src/_base.scss +73 -5
- package/src/anchor/README.md +9 -0
- package/src/anchor/index.ts +115 -16
- package/src/anchor/style.scss +70 -10
- package/src/print/style.scss +0 -4
- package/src/slides/index.ts +72 -24
- package/src/style.scss +1 -1
- package/src/trace/index.ts +39 -14
- package/src/trace/style.scss +11 -10
- package/templates/_theme.scss +3 -0
- package/templates/default.html +4 -3
package/build/bundle.ts
CHANGED
|
@@ -9,10 +9,20 @@ import {
|
|
|
9
9
|
getDistDir,
|
|
10
10
|
toPosix,
|
|
11
11
|
} from './utils/paths';
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
deriveTheme,
|
|
14
|
+
deriveLinkHue,
|
|
15
|
+
deriveTraceLineActiveHue,
|
|
16
|
+
} from './utils/derive-theme';
|
|
13
17
|
import type { PluginBuilder } from 'bun';
|
|
14
18
|
import type { SiteVariables } from './types';
|
|
15
19
|
import timezones from '../src/timezone/timezones.json' with { type: 'json' };
|
|
20
|
+
import pkg from '../package.json' with { type: 'json' };
|
|
21
|
+
|
|
22
|
+
export function getBundleNaming(): string {
|
|
23
|
+
const version = pkg.version.replace(/[^a-zA-Z0-9.-]/g, '-');
|
|
24
|
+
return `[name].bundle.tada-${version}.[ext]`;
|
|
25
|
+
}
|
|
16
26
|
|
|
17
27
|
function formatCssNumber(value: number, precision = 4): string {
|
|
18
28
|
if (Number.isInteger(value)) {
|
|
@@ -40,6 +50,9 @@ function renderThemeScss(siteVariables: SiteVariables): string {
|
|
|
40
50
|
const linkColorHover = `hsl(${linkHue}deg 34% 60%)`;
|
|
41
51
|
const linkColorDark = `hsl(${linkHue}deg 50% 72%)`;
|
|
42
52
|
const linkColorHoverDark = `hsl(${linkHue}deg 40% 80%)`;
|
|
53
|
+
const traceLineActiveHue = formatCssNumber(deriveTraceLineActiveHue(tintHue));
|
|
54
|
+
const bgTraceLineActive = `hsl(${traceLineActiveHue}deg 100% 86%)`;
|
|
55
|
+
const bgTraceLineActiveDark = `hsl(${traceLineActiveHue}deg 90% 18%)`;
|
|
43
56
|
|
|
44
57
|
const rendered = _.template(template)({
|
|
45
58
|
...theme,
|
|
@@ -54,6 +67,8 @@ function renderThemeScss(siteVariables: SiteVariables): string {
|
|
|
54
67
|
linkColorHover,
|
|
55
68
|
linkColorDark,
|
|
56
69
|
linkColorHoverDark,
|
|
70
|
+
bgTraceLineActive,
|
|
71
|
+
bgTraceLineActiveDark,
|
|
57
72
|
});
|
|
58
73
|
|
|
59
74
|
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tada-'));
|
|
@@ -109,7 +124,7 @@ export async function bundle(
|
|
|
109
124
|
const result = await Bun.build({
|
|
110
125
|
entrypoints,
|
|
111
126
|
outdir: resolvedDistDir,
|
|
112
|
-
naming:
|
|
127
|
+
naming: getBundleNaming(),
|
|
113
128
|
minify: mode === 'production',
|
|
114
129
|
sourcemap: isDev ? 'inline' : 'none',
|
|
115
130
|
define: createDefine(siteVariables, isDev),
|
package/build/types.d.ts
CHANGED
|
@@ -16,6 +16,8 @@ const DARK_TEXT_L_MAX = 0.82;
|
|
|
16
16
|
|
|
17
17
|
const LINK_ANCHOR_HUE = 212;
|
|
18
18
|
const LINK_TINT_BLEND = 0.05;
|
|
19
|
+
const TRACE_LINE_ACTIVE_ANCHOR_HUE = 52;
|
|
20
|
+
const TRACE_LINE_ACTIVE_TINT_BLEND = 0.1;
|
|
19
21
|
|
|
20
22
|
function clamp(v: number, min: number, max: number): number {
|
|
21
23
|
return Math.min(Math.max(v, min), max);
|
|
@@ -83,6 +85,14 @@ export function deriveLinkHue(tintHue: number): number {
|
|
|
83
85
|
return (LINK_ANCHOR_HUE + diff * LINK_TINT_BLEND + 360) % 360;
|
|
84
86
|
}
|
|
85
87
|
|
|
88
|
+
export function deriveTraceLineActiveHue(tintHue: number): number {
|
|
89
|
+
const diff = ((tintHue - TRACE_LINE_ACTIVE_ANCHOR_HUE + 540) % 360) - 180;
|
|
90
|
+
return (
|
|
91
|
+
(TRACE_LINE_ACTIVE_ANCHOR_HUE + diff * TRACE_LINE_ACTIVE_TINT_BLEND + 360) %
|
|
92
|
+
360
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
86
96
|
export function getTextOnColor(cssColor: string): '#fff' | '#000' {
|
|
87
97
|
const parsed = parse(cssColor);
|
|
88
98
|
if (!parsed) {
|
package/build/utils/render.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
|
+
import { createHash } from 'crypto';
|
|
3
4
|
import { renderCodeSegment } from './code';
|
|
4
5
|
import { splitLines } from './literate-java';
|
|
5
6
|
import { normalizeOutputPath } from './paths';
|
|
@@ -14,18 +15,46 @@ export interface ChunkTraceOutputOptions {
|
|
|
14
15
|
ignoreFields?: Record<string, string[]>;
|
|
15
16
|
}
|
|
16
17
|
|
|
18
|
+
export interface ChunkTraceOutputResult {
|
|
19
|
+
manifest: TraceManifest;
|
|
20
|
+
artifactId: string;
|
|
21
|
+
outputPaths: string[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function hashTraceFiles(files: { name: string; content: string }[]): string {
|
|
25
|
+
const hasher = createHash('sha256');
|
|
26
|
+
for (const file of files) {
|
|
27
|
+
hasher.update(file.name, 'utf8');
|
|
28
|
+
hasher.update('\0', 'utf8');
|
|
29
|
+
hasher.update(file.content, 'utf8');
|
|
30
|
+
hasher.update('\0', 'utf8');
|
|
31
|
+
}
|
|
32
|
+
return `sha256-${hasher.digest('hex').slice(0, 16)}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function buildTraceOutputPath(
|
|
36
|
+
relDir: string,
|
|
37
|
+
traceName: string,
|
|
38
|
+
artifactId: string,
|
|
39
|
+
fileName: string,
|
|
40
|
+
): string {
|
|
41
|
+
return [relDir, '_traces', traceName, artifactId, fileName]
|
|
42
|
+
.filter(Boolean)
|
|
43
|
+
.join('/');
|
|
44
|
+
}
|
|
45
|
+
|
|
17
46
|
export function chunkTraceOutput(
|
|
18
47
|
output: string,
|
|
19
48
|
traceOutputDir: string,
|
|
49
|
+
relDir: string,
|
|
50
|
+
traceName: string,
|
|
20
51
|
sourceFile: string,
|
|
21
52
|
source: string,
|
|
22
53
|
options: ChunkTraceOutputOptions = {},
|
|
23
|
-
):
|
|
54
|
+
): ChunkTraceOutputResult {
|
|
24
55
|
const chunkSize = options.chunkSize ?? DEFAULT_CHUNK_SIZE;
|
|
25
56
|
const ignoreFields = options.ignoreFields ?? {};
|
|
26
57
|
|
|
27
|
-
fs.mkdirSync(traceOutputDir, { recursive: true });
|
|
28
|
-
|
|
29
58
|
const lines = output
|
|
30
59
|
.trim()
|
|
31
60
|
.split('\n')
|
|
@@ -46,26 +75,27 @@ export function chunkTraceOutput(
|
|
|
46
75
|
|
|
47
76
|
let chunkEntries: TraceChunkEntry[] = [];
|
|
48
77
|
let chunkIndex = 0;
|
|
78
|
+
const chunkFiles: { name: string; content: string }[] = [];
|
|
49
79
|
|
|
50
80
|
for (const step of allSteps) {
|
|
51
81
|
const svg = generateStepSvg(step, layout);
|
|
52
82
|
chunkEntries.push({ line: step.line, stdout: step.stdout, svg });
|
|
53
83
|
|
|
54
84
|
if (chunkEntries.length >= chunkSize) {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
JSON.stringify(chunkEntries),
|
|
58
|
-
);
|
|
85
|
+
chunkFiles.push({
|
|
86
|
+
name: `chunk-${chunkIndex}.json`,
|
|
87
|
+
content: JSON.stringify(chunkEntries),
|
|
88
|
+
});
|
|
59
89
|
chunkEntries = [];
|
|
60
90
|
chunkIndex++;
|
|
61
91
|
}
|
|
62
92
|
}
|
|
63
93
|
|
|
64
94
|
if (chunkEntries.length > 0) {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
JSON.stringify(chunkEntries),
|
|
68
|
-
);
|
|
95
|
+
chunkFiles.push({
|
|
96
|
+
name: `chunk-${chunkIndex}.json`,
|
|
97
|
+
content: JSON.stringify(chunkEntries),
|
|
98
|
+
});
|
|
69
99
|
}
|
|
70
100
|
|
|
71
101
|
const manifest: TraceManifest = {
|
|
@@ -76,12 +106,26 @@ export function chunkTraceOutput(
|
|
|
76
106
|
lineToSteps,
|
|
77
107
|
};
|
|
78
108
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
JSON.stringify(manifest),
|
|
82
|
-
|
|
109
|
+
const manifestFile = {
|
|
110
|
+
name: 'manifest.json',
|
|
111
|
+
content: JSON.stringify(manifest),
|
|
112
|
+
};
|
|
113
|
+
const files = [manifestFile, ...chunkFiles];
|
|
114
|
+
const artifactId = hashTraceFiles(files);
|
|
115
|
+
const artifactDir = path.join(traceOutputDir, artifactId);
|
|
83
116
|
|
|
84
|
-
|
|
117
|
+
fs.mkdirSync(artifactDir, { recursive: true });
|
|
118
|
+
for (const file of files) {
|
|
119
|
+
fs.writeFileSync(path.join(artifactDir, file.name), file.content);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
manifest,
|
|
124
|
+
artifactId,
|
|
125
|
+
outputPaths: files.map(file =>
|
|
126
|
+
buildTraceOutputPath(relDir, traceName, artifactId, file.name),
|
|
127
|
+
),
|
|
128
|
+
};
|
|
85
129
|
}
|
|
86
130
|
|
|
87
131
|
export function highlightTraceSource(
|
|
@@ -96,16 +140,25 @@ export function highlightTraceSource(
|
|
|
96
140
|
export function getTraceOutputPaths(
|
|
97
141
|
relDir: string,
|
|
98
142
|
traceName: string,
|
|
143
|
+
artifactId: string,
|
|
99
144
|
totalSteps: number,
|
|
100
145
|
): string[] {
|
|
101
|
-
const
|
|
102
|
-
|
|
146
|
+
const outputPaths = [
|
|
147
|
+
buildTraceOutputPath(relDir, traceName, artifactId, 'manifest.json'),
|
|
148
|
+
];
|
|
103
149
|
for (
|
|
104
150
|
let chunkIndex = 0;
|
|
105
151
|
chunkIndex * DEFAULT_CHUNK_SIZE < totalSteps;
|
|
106
152
|
chunkIndex++
|
|
107
153
|
) {
|
|
108
|
-
outputPaths.push(
|
|
154
|
+
outputPaths.push(
|
|
155
|
+
buildTraceOutputPath(
|
|
156
|
+
relDir,
|
|
157
|
+
traceName,
|
|
158
|
+
artifactId,
|
|
159
|
+
`chunk-${chunkIndex}.json`,
|
|
160
|
+
),
|
|
161
|
+
);
|
|
109
162
|
}
|
|
110
163
|
return outputPaths;
|
|
111
164
|
}
|
|
@@ -148,18 +201,18 @@ export function materializeTraceOutputs({
|
|
|
148
201
|
export function buildManifestUrl({
|
|
149
202
|
relDir,
|
|
150
203
|
traceName,
|
|
204
|
+
artifactId,
|
|
151
205
|
applyBasePath,
|
|
152
206
|
}: {
|
|
153
207
|
relDir: string;
|
|
154
208
|
traceName: string;
|
|
209
|
+
artifactId: string;
|
|
155
210
|
applyBasePath: (subPath: string) => string;
|
|
156
211
|
}): string {
|
|
157
212
|
return applyBasePath(
|
|
158
213
|
normalizeOutputPath(
|
|
159
214
|
'/' +
|
|
160
|
-
|
|
161
|
-
.filter(Boolean)
|
|
162
|
-
.join('/'),
|
|
215
|
+
buildTraceOutputPath(relDir, traceName, artifactId, 'manifest.json'),
|
|
163
216
|
),
|
|
164
217
|
);
|
|
165
218
|
}
|
|
@@ -187,17 +240,13 @@ export function renderTraceWidgetHtml({
|
|
|
187
240
|
`<button class="trace-btn trace-first" disabled tabindex="-1" aria-label="First step" title="First step">${iconFirst}</button>` +
|
|
188
241
|
`<button class="trace-btn trace-prev" disabled tabindex="-1" aria-label="Previous step" title="Previous step">${iconPrev}</button>` +
|
|
189
242
|
`<span class="trace-step-counter"${counterStyle}>${counterContent}</span>` +
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
: `<button class="trace-btn trace-next" aria-label="Next step" title="Next step">${iconNext}</button>`) +
|
|
193
|
-
(disabled
|
|
194
|
-
? `<button class="trace-btn trace-last" disabled tabindex="-1" aria-label="Last step" title="Last step">${iconLast}</button>`
|
|
195
|
-
: `<button class="trace-btn trace-last" tabindex="-1" aria-label="Last step" title="Last step">${iconLast}</button>`);
|
|
243
|
+
`<button class="trace-btn trace-next" disabled tabindex="-1" aria-label="Next step" title="Next step">${iconNext}</button>` +
|
|
244
|
+
`<button class="trace-btn trace-last" disabled tabindex="-1" aria-label="Last step" title="Last step">${iconLast}</button>`;
|
|
196
245
|
const wrapperAttrs = manifestUrl
|
|
197
246
|
? ` data-trace-manifest="${manifestUrl}"`
|
|
198
247
|
: '';
|
|
199
248
|
const wrapperClass = disabled
|
|
200
249
|
? 'trace-widget trace-disabled'
|
|
201
250
|
: 'trace-widget';
|
|
202
|
-
return `<div class="${wrapperClass}"${wrapperAttrs}><noscript><p>This interactive trace requires JavaScript.</p></noscript><div class="trace-body"><div class="trace-toolbar"><div class="trace-controls" role="toolbar" aria-label="Trace navigation">${controls}</div></div><div class="trace-content"><div class="trace-diagram"></div><div class="trace-source-wrapper"><div class="trace-source">${highlightedSource}</div></div
|
|
251
|
+
return `<div class="${wrapperClass}"${wrapperAttrs}><noscript><p>This interactive trace requires JavaScript.</p></noscript><div class="trace-body"><div class="trace-toolbar"><div class="trace-controls" role="toolbar" aria-label="Trace navigation">${controls}</div></div><div class="trace-content"><div class="trace-diagram"></div><div class="trace-source-wrapper"><div class="trace-source">${highlightedSource}</div></div></div></div></div>`;
|
|
203
252
|
}
|
package/build/utils/trace.ts
CHANGED
|
@@ -42,6 +42,7 @@ export interface TraceContext {
|
|
|
42
42
|
|
|
43
43
|
export interface TraceResult {
|
|
44
44
|
manifestUrl: string;
|
|
45
|
+
artifactId: string;
|
|
45
46
|
highlightedSource: string;
|
|
46
47
|
totalSteps: number;
|
|
47
48
|
mtime: number;
|
|
@@ -141,6 +142,7 @@ export function createTraceHelpers(context: TraceContext): {
|
|
|
141
142
|
const outputPaths = getTraceOutputPaths(
|
|
142
143
|
relDir,
|
|
143
144
|
traceName,
|
|
145
|
+
cached.artifactId,
|
|
144
146
|
cached.totalSteps,
|
|
145
147
|
);
|
|
146
148
|
if (
|
|
@@ -155,7 +157,12 @@ export function createTraceHelpers(context: TraceContext): {
|
|
|
155
157
|
}
|
|
156
158
|
return {
|
|
157
159
|
...cached,
|
|
158
|
-
manifestUrl: buildManifestUrl({
|
|
160
|
+
manifestUrl: buildManifestUrl({
|
|
161
|
+
relDir,
|
|
162
|
+
traceName,
|
|
163
|
+
artifactId: cached.artifactId,
|
|
164
|
+
applyBasePath,
|
|
165
|
+
}),
|
|
159
166
|
};
|
|
160
167
|
}
|
|
161
168
|
}
|
|
@@ -176,32 +183,42 @@ export function createTraceHelpers(context: TraceContext): {
|
|
|
176
183
|
);
|
|
177
184
|
const traceOutputDir = path.join(distDir, relDir, '_traces', traceName);
|
|
178
185
|
|
|
179
|
-
const
|
|
186
|
+
const traceOutput = chunkTraceOutput(
|
|
180
187
|
output,
|
|
181
188
|
traceOutputDir,
|
|
189
|
+
relDir,
|
|
190
|
+
traceName,
|
|
182
191
|
sourceFileNameForTrace(sourceFile),
|
|
183
192
|
source,
|
|
184
193
|
{ chunkSize: DEFAULT_CHUNK_SIZE, ignoreFields },
|
|
185
194
|
);
|
|
186
|
-
for (const outputPath of
|
|
187
|
-
relDir,
|
|
188
|
-
traceName,
|
|
189
|
-
manifest.totalSteps,
|
|
190
|
-
)) {
|
|
195
|
+
for (const outputPath of traceOutput.outputPaths) {
|
|
191
196
|
dependencyCollector?.generatedOutputPaths?.add(outputPath);
|
|
192
197
|
}
|
|
193
198
|
|
|
194
|
-
const manifestUrl = buildManifestUrl({
|
|
199
|
+
const manifestUrl = buildManifestUrl({
|
|
200
|
+
relDir,
|
|
201
|
+
traceName,
|
|
202
|
+
artifactId: traceOutput.artifactId,
|
|
203
|
+
applyBasePath,
|
|
204
|
+
});
|
|
195
205
|
|
|
196
|
-
const totalSteps = manifest.totalSteps;
|
|
206
|
+
const totalSteps = traceOutput.manifest.totalSteps;
|
|
197
207
|
const mtime = fs.statSync(sourceFilePath).mtimeMs;
|
|
198
208
|
cache.set(sourceFilePath, {
|
|
199
209
|
manifestUrl,
|
|
210
|
+
artifactId: traceOutput.artifactId,
|
|
200
211
|
highlightedSource,
|
|
201
212
|
totalSteps,
|
|
202
213
|
mtime,
|
|
203
214
|
});
|
|
204
|
-
return {
|
|
215
|
+
return {
|
|
216
|
+
manifestUrl,
|
|
217
|
+
artifactId: traceOutput.artifactId,
|
|
218
|
+
highlightedSource,
|
|
219
|
+
totalSteps,
|
|
220
|
+
mtime,
|
|
221
|
+
};
|
|
205
222
|
}
|
|
206
223
|
|
|
207
224
|
return {
|
package/build/watch/assets.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
|
-
import { bundle } from '../bundle';
|
|
3
|
+
import { bundle, getBundleNaming } from '../bundle';
|
|
4
4
|
import { copyFonts } from '../generate-fonts';
|
|
5
5
|
import { copyKatexAssets } from '../generate-katex-assets';
|
|
6
6
|
import { generateFavicons } from '../generate-favicon';
|
|
@@ -42,7 +42,7 @@ export async function bundleWatchAssets(
|
|
|
42
42
|
const reloadAssets = await Bun.build({
|
|
43
43
|
entrypoints: [RELOAD_CLIENT_PATH],
|
|
44
44
|
outdir: outputDir,
|
|
45
|
-
naming:
|
|
45
|
+
naming: getBundleNaming(),
|
|
46
46
|
sourcemap: 'inline',
|
|
47
47
|
});
|
|
48
48
|
return [
|
package/package.json
CHANGED
package/src/_base.scss
CHANGED
|
@@ -38,12 +38,13 @@ body {
|
|
|
38
38
|
* Enable Inter's features:
|
|
39
39
|
* liga - standard ligatures
|
|
40
40
|
* calt - contextual alternates
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
41
|
+
* ss07 - square punctuation
|
|
42
|
+
* ss08 - square quotes
|
|
43
|
+
* cv01 - alternate 1
|
|
44
|
+
* cv08 - uppercase I with serif
|
|
45
|
+
* cv10 - capital G with spur
|
|
45
46
|
*/
|
|
46
|
-
font-feature-settings: 'liga', 'calt', '
|
|
47
|
+
font-feature-settings: 'liga', 'calt', 'ss07', 'ss08', 'cv01', 'cv08', 'cv10';
|
|
47
48
|
line-height: var(--line-height);
|
|
48
49
|
color: var(--fg-color);
|
|
49
50
|
background-color: var(--bg-color);
|
|
@@ -180,6 +181,73 @@ input[type='search'] {
|
|
|
180
181
|
}
|
|
181
182
|
}
|
|
182
183
|
|
|
184
|
+
label:has(> input[type='checkbox']) {
|
|
185
|
+
display: inline-flex;
|
|
186
|
+
gap: 0.45em;
|
|
187
|
+
align-items: center;
|
|
188
|
+
font-size: var(--font-size-small);
|
|
189
|
+
font-weight: 500;
|
|
190
|
+
|
|
191
|
+
@include user-select(none);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
input[type='checkbox'] {
|
|
195
|
+
display: inline-grid;
|
|
196
|
+
flex: 0 0 auto;
|
|
197
|
+
place-content: center;
|
|
198
|
+
width: 1.5em;
|
|
199
|
+
height: 1.25em;
|
|
200
|
+
padding: 0;
|
|
201
|
+
margin: 0;
|
|
202
|
+
font: inherit;
|
|
203
|
+
color: var(--fg-color);
|
|
204
|
+
appearance: none;
|
|
205
|
+
cursor: pointer;
|
|
206
|
+
background: var(--bg-color);
|
|
207
|
+
border: var(--control-border-width) solid var(--fg2-color);
|
|
208
|
+
border-radius: 0.25em;
|
|
209
|
+
|
|
210
|
+
&::after {
|
|
211
|
+
display: block;
|
|
212
|
+
width: 1.1em;
|
|
213
|
+
height: 1.1em;
|
|
214
|
+
content: '';
|
|
215
|
+
background-color: currentcolor;
|
|
216
|
+
opacity: 0;
|
|
217
|
+
|
|
218
|
+
@include contained-mask(var(--icon-check));
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
&:checked {
|
|
222
|
+
&::after {
|
|
223
|
+
opacity: 1;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
&:disabled {
|
|
228
|
+
cursor: not-allowed;
|
|
229
|
+
opacity: 0.4;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
&:active:not(:disabled) {
|
|
233
|
+
color: var(--bg-color) !important;
|
|
234
|
+
background: var(--link-active-color) !important;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
&:focus {
|
|
238
|
+
outline: 2px solid var(--theme-color);
|
|
239
|
+
outline-offset: 1px;
|
|
240
|
+
background-color: var(--bg2-color);
|
|
241
|
+
border-color: var(--fg-color);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
@media (hover: hover) {
|
|
246
|
+
input[type='checkbox']:hover:not(:disabled) {
|
|
247
|
+
background: var(--bg2-color);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
183
251
|
figure {
|
|
184
252
|
margin: 1rem 0;
|
|
185
253
|
|
package/src/anchor/README.md
CHANGED
|
@@ -4,3 +4,12 @@ Append a link to each heading having an `id` attribute.
|
|
|
4
4
|
|
|
5
5
|
When the link is clicked, `window.location` is updated and the heading is
|
|
6
6
|
highlighted using the same function as the `toc` component.
|
|
7
|
+
|
|
8
|
+
The heading link includes a real inline SVG hash icon, revealed on heading
|
|
9
|
+
hover or link focus.
|
|
10
|
+
|
|
11
|
+
For slide title headings (`div.slide > h2:first-child`), append a
|
|
12
|
+
`Present from this slide` icon button. Clicking the button dispatches a
|
|
13
|
+
bubbling `tada:slides-present` custom event with the slide index so the slides
|
|
14
|
+
component can open presentation mode from that slide using the current
|
|
15
|
+
`Full screen` checkbox state.
|
package/src/anchor/index.ts
CHANGED
|
@@ -2,32 +2,131 @@ function getElements(parent: HTMLElement): HTMLHeadingElement[] {
|
|
|
2
2
|
return Array.from(parent.querySelectorAll('h1, h2, h3, h4, h5, h6'));
|
|
3
3
|
}
|
|
4
4
|
|
|
5
|
+
function createIcon(window: Window, type: 'hash' | 'present'): SVGSVGElement {
|
|
6
|
+
const svg = window.document.createElementNS(
|
|
7
|
+
'http://www.w3.org/2000/svg',
|
|
8
|
+
'svg',
|
|
9
|
+
);
|
|
10
|
+
svg.setAttribute('aria-hidden', 'true');
|
|
11
|
+
svg.setAttribute('viewBox', '0 0 24 24');
|
|
12
|
+
svg.setAttribute('width', '16');
|
|
13
|
+
svg.setAttribute('height', '16');
|
|
14
|
+
svg.setAttribute('fill', 'none');
|
|
15
|
+
svg.setAttribute('stroke', 'currentColor');
|
|
16
|
+
svg.setAttribute('stroke-width', '1.5');
|
|
17
|
+
svg.setAttribute('stroke-linecap', 'round');
|
|
18
|
+
svg.setAttribute('stroke-linejoin', 'round');
|
|
19
|
+
|
|
20
|
+
const paths =
|
|
21
|
+
type === 'hash'
|
|
22
|
+
? ['M10 3 8 21', 'M16 3l-2 18', 'M4 9h17', 'M3 15h17']
|
|
23
|
+
: [
|
|
24
|
+
'M5 5h14a1 1 0 0 1 1 1v9a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1Z',
|
|
25
|
+
'M9 20h6',
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
for (const d of paths) {
|
|
29
|
+
const path = window.document.createElementNS(
|
|
30
|
+
'http://www.w3.org/2000/svg',
|
|
31
|
+
'path',
|
|
32
|
+
);
|
|
33
|
+
path.setAttribute('d', d);
|
|
34
|
+
svg.appendChild(path);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return svg;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function getSlideIndex(slide: HTMLElement): number {
|
|
41
|
+
const rawIndex = slide.getAttribute('data-slide-index');
|
|
42
|
+
if (rawIndex != null) {
|
|
43
|
+
const parsed = Number.parseInt(rawIndex, 10);
|
|
44
|
+
if (Number.isFinite(parsed)) {
|
|
45
|
+
return parsed;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const root = slide.closest('[data-slides-root]');
|
|
50
|
+
if (!root) {
|
|
51
|
+
return 0;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return Array.from(root.querySelectorAll('.slide')).indexOf(slide);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function isSlideTitleHeading(
|
|
58
|
+
heading: HTMLHeadingElement,
|
|
59
|
+
): heading is HTMLHeadingElement & { parentElement: HTMLElement } {
|
|
60
|
+
const slide = heading.parentElement;
|
|
61
|
+
return (
|
|
62
|
+
heading.tagName === 'H2' &&
|
|
63
|
+
slide?.tagName === 'DIV' &&
|
|
64
|
+
slide.classList.contains('slide') &&
|
|
65
|
+
slide.firstElementChild === heading
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
5
69
|
export default (window: Window) => {
|
|
70
|
+
const view = window as Window & typeof globalThis;
|
|
6
71
|
const elements = getElements(window.document.body);
|
|
7
|
-
|
|
8
|
-
return;
|
|
9
|
-
}
|
|
72
|
+
const cleanups: (() => void)[] = [];
|
|
10
73
|
|
|
11
74
|
elements.forEach(el => {
|
|
12
|
-
if (
|
|
75
|
+
if (el.id) {
|
|
76
|
+
const link = window.document.createElement('a');
|
|
77
|
+
link.className = 'heading-anchor';
|
|
78
|
+
link.href = `#${el.id}`;
|
|
79
|
+
link.title = 'Link to this heading';
|
|
80
|
+
|
|
81
|
+
// Move all existing child nodes into the link.
|
|
82
|
+
while (el.firstChild) {
|
|
83
|
+
link.appendChild(el.firstChild);
|
|
84
|
+
}
|
|
85
|
+
link.appendChild(createIcon(window, 'hash'));
|
|
86
|
+
el.appendChild(link);
|
|
87
|
+
|
|
88
|
+
const handleClick = () => {
|
|
89
|
+
el.focus();
|
|
90
|
+
};
|
|
91
|
+
link.addEventListener('click', handleClick);
|
|
92
|
+
cleanups.push(() => {
|
|
93
|
+
link.removeEventListener('click', handleClick);
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (!isSlideTitleHeading(el)) {
|
|
13
98
|
return;
|
|
14
99
|
}
|
|
15
100
|
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
101
|
+
const button = window.document.createElement('button');
|
|
102
|
+
button.type = 'button';
|
|
103
|
+
button.className = 'heading-present-button';
|
|
104
|
+
button.setAttribute('aria-label', 'Present from this slide');
|
|
105
|
+
button.title = 'Present from this slide';
|
|
106
|
+
button.appendChild(createIcon(window, 'present'));
|
|
107
|
+
el.appendChild(button);
|
|
20
108
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
link.appendChild(el.firstChild);
|
|
24
|
-
}
|
|
25
|
-
el.appendChild(link);
|
|
109
|
+
const slide = el.parentElement;
|
|
110
|
+
const slideIndex = getSlideIndex(slide);
|
|
26
111
|
|
|
27
|
-
|
|
28
|
-
|
|
112
|
+
const handlePresentClick = () => {
|
|
113
|
+
button.dispatchEvent(
|
|
114
|
+
new view.CustomEvent('tada:slides-present', {
|
|
115
|
+
bubbles: true,
|
|
116
|
+
detail: { slideIndex },
|
|
117
|
+
}),
|
|
118
|
+
);
|
|
119
|
+
};
|
|
120
|
+
button.addEventListener('click', handlePresentClick);
|
|
121
|
+
|
|
122
|
+
cleanups.push(() => {
|
|
123
|
+
button.removeEventListener('click', handlePresentClick);
|
|
29
124
|
});
|
|
30
125
|
});
|
|
31
126
|
|
|
32
|
-
return () => {
|
|
127
|
+
return () => {
|
|
128
|
+
for (const cleanup of cleanups) {
|
|
129
|
+
cleanup();
|
|
130
|
+
}
|
|
131
|
+
};
|
|
33
132
|
};
|
package/src/anchor/style.scss
CHANGED
|
@@ -7,29 +7,89 @@ a.heading-anchor {
|
|
|
7
7
|
@include subdued-link;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
.heading-anchor svg,
|
|
11
|
+
.heading-present-button svg {
|
|
11
12
|
display: inline-block;
|
|
12
13
|
width: 1em;
|
|
13
14
|
height: 1em;
|
|
15
|
+
vertical-align: middle;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.heading-anchor svg {
|
|
14
19
|
margin-left: 4px;
|
|
15
20
|
vertical-align: middle;
|
|
16
|
-
content: '';
|
|
17
21
|
opacity: 0;
|
|
22
|
+
}
|
|
18
23
|
|
|
19
|
-
|
|
24
|
+
.heading-present-button {
|
|
25
|
+
display: inline-flex;
|
|
26
|
+
align-items: center;
|
|
27
|
+
justify-content: center;
|
|
28
|
+
width: 1em;
|
|
29
|
+
height: 1em;
|
|
30
|
+
padding: 0;
|
|
31
|
+
margin-left: 4px;
|
|
32
|
+
font: inherit;
|
|
33
|
+
line-height: 1;
|
|
34
|
+
vertical-align: middle;
|
|
35
|
+
color: var(--fg-color);
|
|
36
|
+
background: transparent;
|
|
37
|
+
border: none;
|
|
38
|
+
border-radius: 0;
|
|
39
|
+
opacity: 0;
|
|
20
40
|
}
|
|
21
41
|
|
|
22
|
-
a.heading-anchor:focus-visible
|
|
42
|
+
a.heading-anchor:focus-visible svg,
|
|
43
|
+
.heading-present-button:focus {
|
|
23
44
|
opacity: 1;
|
|
24
45
|
}
|
|
25
46
|
|
|
47
|
+
.heading-present-button:focus {
|
|
48
|
+
outline-offset: 2px;
|
|
49
|
+
background-color: transparent;
|
|
50
|
+
border-color: transparent;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
button.heading-present-button:active:not(:disabled) {
|
|
54
|
+
color: var(--link-active-color) !important;
|
|
55
|
+
background: transparent !important;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
body.is-presenting .heading-present-button {
|
|
59
|
+
display: none;
|
|
60
|
+
}
|
|
61
|
+
|
|
26
62
|
@media (hover: hover) {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
63
|
+
button.heading-present-button:hover:not(:disabled) {
|
|
64
|
+
color: var(--fg2-color);
|
|
65
|
+
background: transparent;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
h1:hover a.heading-anchor svg,
|
|
69
|
+
h2:hover a.heading-anchor svg,
|
|
70
|
+
h3:hover a.heading-anchor svg,
|
|
71
|
+
h4:hover a.heading-anchor svg,
|
|
72
|
+
h5:hover a.heading-anchor svg,
|
|
73
|
+
h6:hover a.heading-anchor svg,
|
|
74
|
+
h1:hover .heading-present-button,
|
|
75
|
+
h2:hover .heading-present-button,
|
|
76
|
+
h3:hover .heading-present-button,
|
|
77
|
+
h4:hover .heading-present-button,
|
|
78
|
+
h5:hover .heading-present-button,
|
|
79
|
+
h6:hover .heading-present-button {
|
|
33
80
|
opacity: 1;
|
|
34
81
|
}
|
|
35
82
|
}
|
|
83
|
+
|
|
84
|
+
@media (hover: none) {
|
|
85
|
+
.heading-present-button {
|
|
86
|
+
display: none;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
@media print {
|
|
91
|
+
.heading-anchor svg,
|
|
92
|
+
.heading-present-button {
|
|
93
|
+
display: none;
|
|
94
|
+
}
|
|
95
|
+
}
|
package/src/print/style.scss
CHANGED
package/src/slides/index.ts
CHANGED
|
@@ -5,6 +5,12 @@ interface TraceToolbarState {
|
|
|
5
5
|
|
|
6
6
|
type PresentationMode = 'normal' | 'fullscreen';
|
|
7
7
|
|
|
8
|
+
interface SlidesPresentDetail {
|
|
9
|
+
slideIndex?: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const FULLSCREEN_STORAGE_KEY = 'slidesFullscreen';
|
|
13
|
+
|
|
8
14
|
function isInteractive(
|
|
9
15
|
view: Window & typeof globalThis,
|
|
10
16
|
target: EventTarget | null,
|
|
@@ -45,17 +51,17 @@ export default function mountSlides(window: Window): void | (() => void) {
|
|
|
45
51
|
const presentButton = document.querySelector(
|
|
46
52
|
'[data-slides-present]',
|
|
47
53
|
) as HTMLButtonElement | null;
|
|
48
|
-
const
|
|
49
|
-
'[data-slides-
|
|
50
|
-
) as
|
|
51
|
-
const rootScopeButton = presentButton ??
|
|
54
|
+
const fullscreenCheckbox = document.querySelector(
|
|
55
|
+
'[data-slides-fullscreen]',
|
|
56
|
+
) as HTMLInputElement | null;
|
|
57
|
+
const rootScopeButton = presentButton ?? fullscreenCheckbox;
|
|
52
58
|
const scopedRoot = rootScopeButton
|
|
53
59
|
?.closest('main')
|
|
54
60
|
?.querySelector('[data-slides-root]');
|
|
55
61
|
const root = (scopedRoot ??
|
|
56
62
|
document.querySelector('[data-slides-root]')) as HTMLElement | null;
|
|
57
63
|
|
|
58
|
-
if (!view || !root
|
|
64
|
+
if (!view || !root) {
|
|
59
65
|
return;
|
|
60
66
|
}
|
|
61
67
|
|
|
@@ -78,8 +84,18 @@ export default function mountSlides(window: Window): void | (() => void) {
|
|
|
78
84
|
if (presentButton) {
|
|
79
85
|
presentButton.disabled = false;
|
|
80
86
|
}
|
|
81
|
-
if (
|
|
82
|
-
|
|
87
|
+
if (fullscreenCheckbox) {
|
|
88
|
+
try {
|
|
89
|
+
const stored = window.localStorage.getItem(FULLSCREEN_STORAGE_KEY);
|
|
90
|
+
if (stored === 'false') {
|
|
91
|
+
fullscreenCheckbox.checked = false;
|
|
92
|
+
} else if (stored === 'true') {
|
|
93
|
+
fullscreenCheckbox.checked = true;
|
|
94
|
+
}
|
|
95
|
+
} catch {
|
|
96
|
+
// ignored
|
|
97
|
+
}
|
|
98
|
+
fullscreenCheckbox.disabled = false;
|
|
83
99
|
}
|
|
84
100
|
|
|
85
101
|
const overlay = document.createElement('div');
|
|
@@ -252,7 +268,7 @@ export default function mountSlides(window: Window): void | (() => void) {
|
|
|
252
268
|
}
|
|
253
269
|
}
|
|
254
270
|
|
|
255
|
-
function enterPresentation(mode: PresentationMode): void {
|
|
271
|
+
function enterPresentation(mode: PresentationMode, slideIndex = 0): void {
|
|
256
272
|
isPresenting = true;
|
|
257
273
|
presentationMode = mode;
|
|
258
274
|
hidePresentationControls();
|
|
@@ -260,8 +276,8 @@ export default function mountSlides(window: Window): void | (() => void) {
|
|
|
260
276
|
document.body.classList.add('is-presenting');
|
|
261
277
|
setTraceToolbarVisibility(true);
|
|
262
278
|
resetReadyTraces();
|
|
263
|
-
setActiveSlide(
|
|
264
|
-
focusSlide(slides[
|
|
279
|
+
setActiveSlide(slideIndex);
|
|
280
|
+
focusSlide(slides[activeIndex]);
|
|
265
281
|
}
|
|
266
282
|
|
|
267
283
|
function driveTrace(delta: 1 | -1): boolean {
|
|
@@ -329,12 +345,8 @@ export default function mountSlides(window: Window): void | (() => void) {
|
|
|
329
345
|
focusSlide(slides[activeIndex]);
|
|
330
346
|
}
|
|
331
347
|
|
|
332
|
-
function
|
|
333
|
-
enterPresentation('
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
async function handlePresentFullscreenClick(): Promise<void> {
|
|
337
|
-
enterPresentation('fullscreen');
|
|
348
|
+
async function handlePresentFullscreenClick(slideIndex = 0): Promise<void> {
|
|
349
|
+
enterPresentation('fullscreen', slideIndex);
|
|
338
350
|
|
|
339
351
|
if (typeof fullscreenTarget.requestFullscreen !== 'function') {
|
|
340
352
|
presentationMode = 'normal';
|
|
@@ -348,14 +360,37 @@ export default function mountSlides(window: Window): void | (() => void) {
|
|
|
348
360
|
}
|
|
349
361
|
}
|
|
350
362
|
|
|
351
|
-
function
|
|
352
|
-
|
|
363
|
+
function startPresentation(slideIndex = 0): void {
|
|
364
|
+
if (fullscreenCheckbox?.checked === true) {
|
|
365
|
+
void handlePresentFullscreenClick(slideIndex);
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
enterPresentation('normal', slideIndex);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function handlePresentClick(): void {
|
|
373
|
+
startPresentation(0);
|
|
353
374
|
}
|
|
354
375
|
|
|
355
376
|
function handleCloseClick(): void {
|
|
356
377
|
exitPresentation();
|
|
357
378
|
}
|
|
358
379
|
|
|
380
|
+
function handleFullscreenPreferenceChange(): void {
|
|
381
|
+
if (!fullscreenCheckbox) {
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
try {
|
|
385
|
+
window.localStorage.setItem(
|
|
386
|
+
FULLSCREEN_STORAGE_KEY,
|
|
387
|
+
String(fullscreenCheckbox.checked),
|
|
388
|
+
);
|
|
389
|
+
} catch {
|
|
390
|
+
// Ignore storage errors so the checkbox remains usable.
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
359
394
|
function handleFullscreenChange(): void {
|
|
360
395
|
if (
|
|
361
396
|
isPresenting &&
|
|
@@ -439,29 +474,42 @@ export default function mountSlides(window: Window): void | (() => void) {
|
|
|
439
474
|
}
|
|
440
475
|
}
|
|
441
476
|
|
|
477
|
+
function handleSlidesPresent(event: Event): void {
|
|
478
|
+
const detail = (event as CustomEvent<SlidesPresentDetail>).detail ?? {};
|
|
479
|
+
const slideIndex =
|
|
480
|
+
typeof detail.slideIndex === 'number' &&
|
|
481
|
+
Number.isFinite(detail.slideIndex)
|
|
482
|
+
? detail.slideIndex
|
|
483
|
+
: 0;
|
|
484
|
+
|
|
485
|
+
startPresentation(slideIndex);
|
|
486
|
+
}
|
|
487
|
+
|
|
442
488
|
presentButton?.addEventListener('click', handlePresentClick);
|
|
443
|
-
|
|
444
|
-
'
|
|
445
|
-
|
|
489
|
+
fullscreenCheckbox?.addEventListener(
|
|
490
|
+
'change',
|
|
491
|
+
handleFullscreenPreferenceChange,
|
|
446
492
|
);
|
|
447
493
|
closeButton.addEventListener('click', handleCloseClick);
|
|
448
494
|
document.addEventListener('fullscreenchange', handleFullscreenChange);
|
|
449
495
|
window.addEventListener('keydown', handleKeydown);
|
|
450
496
|
window.addEventListener('mousemove', handleMouseMove);
|
|
451
497
|
slidesRoot.addEventListener('click', handleSlideClick);
|
|
498
|
+
slidesRoot.addEventListener('tada:slides-present', handleSlidesPresent);
|
|
452
499
|
|
|
453
500
|
return () => {
|
|
454
501
|
exitPresentation();
|
|
455
502
|
presentButton?.removeEventListener('click', handlePresentClick);
|
|
456
|
-
|
|
457
|
-
'
|
|
458
|
-
|
|
503
|
+
fullscreenCheckbox?.removeEventListener(
|
|
504
|
+
'change',
|
|
505
|
+
handleFullscreenPreferenceChange,
|
|
459
506
|
);
|
|
460
507
|
closeButton.removeEventListener('click', handleCloseClick);
|
|
461
508
|
document.removeEventListener('fullscreenchange', handleFullscreenChange);
|
|
462
509
|
window.removeEventListener('keydown', handleKeydown);
|
|
463
510
|
window.removeEventListener('mousemove', handleMouseMove);
|
|
464
511
|
slidesRoot.removeEventListener('click', handleSlideClick);
|
|
512
|
+
slidesRoot.removeEventListener('tada:slides-present', handleSlidesPresent);
|
|
465
513
|
overlay.remove();
|
|
466
514
|
};
|
|
467
515
|
}
|
package/src/style.scss
CHANGED
package/src/trace/index.ts
CHANGED
|
@@ -11,7 +11,7 @@ interface WidgetElements {
|
|
|
11
11
|
root: HTMLElement;
|
|
12
12
|
source: HTMLElement;
|
|
13
13
|
controls: HTMLElement;
|
|
14
|
-
|
|
14
|
+
content: HTMLElement;
|
|
15
15
|
diagram: HTMLElement;
|
|
16
16
|
}
|
|
17
17
|
|
|
@@ -58,13 +58,20 @@ async function goToStep(
|
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
function updateSourceHighlight(panel: HTMLElement, currentLine: number): void {
|
|
61
|
-
panel
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
61
|
+
panel
|
|
62
|
+
.querySelectorAll(
|
|
63
|
+
'.code-row.trace-line-active, .line-number.trace-line-active',
|
|
64
|
+
)
|
|
65
|
+
.forEach(el => {
|
|
66
|
+
el.classList.remove('trace-line-active');
|
|
67
|
+
});
|
|
68
|
+
const lineNumber = panel.querySelector(
|
|
69
|
+
`.line-number[data-line="${currentLine}"]`,
|
|
70
|
+
);
|
|
71
|
+
const row = lineNumber?.closest('.code-row') as HTMLElement | null;
|
|
72
|
+
if (row) {
|
|
73
|
+
row.classList.add('trace-line-active');
|
|
74
|
+
const elRect = row.getBoundingClientRect();
|
|
68
75
|
const panelRect = panel.getBoundingClientRect();
|
|
69
76
|
panel.scrollTop += elRect.top - panelRect.top - panel.clientHeight / 2;
|
|
70
77
|
}
|
|
@@ -193,6 +200,27 @@ function scaleDiagramSvg(diagram: HTMLElement, doc: Document): void {
|
|
|
193
200
|
svg.setAttribute('height', String(baseHeight * scale));
|
|
194
201
|
}
|
|
195
202
|
|
|
203
|
+
function updateOutput(
|
|
204
|
+
content: HTMLElement,
|
|
205
|
+
output: string,
|
|
206
|
+
doc: Document,
|
|
207
|
+
): void {
|
|
208
|
+
const existing = content.querySelector(
|
|
209
|
+
':scope > .trace-output',
|
|
210
|
+
) as HTMLPreElement | null;
|
|
211
|
+
if (!output) {
|
|
212
|
+
existing?.remove();
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const outputElement = existing ?? doc.createElement('pre');
|
|
217
|
+
if (!existing) {
|
|
218
|
+
outputElement.className = 'trace-output';
|
|
219
|
+
content.append(outputElement);
|
|
220
|
+
}
|
|
221
|
+
outputElement.textContent = output;
|
|
222
|
+
}
|
|
223
|
+
|
|
196
224
|
function renderWidgetState(
|
|
197
225
|
state: WidgetState,
|
|
198
226
|
elements: WidgetElements,
|
|
@@ -215,7 +243,7 @@ function renderWidgetState(
|
|
|
215
243
|
output += s.stdout;
|
|
216
244
|
}
|
|
217
245
|
}
|
|
218
|
-
elements.output
|
|
246
|
+
updateOutput(elements.content, output, doc);
|
|
219
247
|
|
|
220
248
|
const prevHeight = elements.diagram.scrollHeight;
|
|
221
249
|
elements.diagram.innerHTML = entry.svg;
|
|
@@ -249,12 +277,9 @@ async function initWidget(root: HTMLElement, doc: Document): Promise<void> {
|
|
|
249
277
|
|
|
250
278
|
await loadChunk(state, manifestUrl, 0);
|
|
251
279
|
|
|
252
|
-
const entry = getStep(state);
|
|
253
|
-
|
|
254
280
|
const source = root.querySelector('.trace-source') as HTMLElement;
|
|
281
|
+
const content = root.querySelector('.trace-content') as HTMLElement;
|
|
255
282
|
const diagram = root.querySelector('.trace-diagram') as HTMLElement;
|
|
256
|
-
const output = root.querySelector('.trace-output') as HTMLPreElement;
|
|
257
|
-
output.textContent = entry.stdout || '';
|
|
258
283
|
|
|
259
284
|
const controls = root.querySelector('.trace-controls') as HTMLElement;
|
|
260
285
|
const firstBtn = controls.querySelector('.trace-first') as HTMLButtonElement;
|
|
@@ -296,7 +321,7 @@ async function initWidget(root: HTMLElement, doc: Document): Promise<void> {
|
|
|
296
321
|
next.focus();
|
|
297
322
|
});
|
|
298
323
|
|
|
299
|
-
const elements: WidgetElements = { root, source, controls,
|
|
324
|
+
const elements: WidgetElements = { root, source, controls, content, diagram };
|
|
300
325
|
|
|
301
326
|
renderWidgetState(state, elements, doc);
|
|
302
327
|
}
|
package/src/trace/style.scss
CHANGED
|
@@ -112,16 +112,17 @@
|
|
|
112
112
|
}
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
-
.
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
115
|
+
.code-row.trace-line-active {
|
|
116
|
+
background: var(--bg-trace-line-active);
|
|
117
|
+
|
|
118
|
+
.line-number {
|
|
119
|
+
font-weight: bold;
|
|
120
|
+
color: var(--fg-color);
|
|
121
|
+
background: var(--bg-trace-line-active);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
code {
|
|
125
|
+
background: none;
|
|
125
126
|
}
|
|
126
127
|
}
|
|
127
128
|
}
|
package/templates/_theme.scss
CHANGED
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
--bg2-color-translucent: hsl(
|
|
33
33
|
var(--tint-hue) calc(12% * var(--tint-amount)) 92% / 66%
|
|
34
34
|
);
|
|
35
|
+
--bg-trace-line-active: <%= bgTraceLineActive %>;
|
|
35
36
|
--box-shadow: 0 2px 9px
|
|
36
37
|
hsl(var(--tint-hue) calc(85% * var(--tint-amount)) 10% / 25%);
|
|
37
38
|
--backdrop-filter: blur(10px);
|
|
@@ -58,6 +59,7 @@
|
|
|
58
59
|
--icon-anchor: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 28'><path d='M11 4 L8 20 M17 4 L14 20 M5 9 H21 M3 15 H19' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round' fill='none'/></svg>");
|
|
59
60
|
--icon-warning: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><polygon points='12,2 22,20 2,20' fill='none' stroke='black' stroke-width='1.5' stroke-linejoin='round'/><line x1='12' y1='9' x2='12' y2='13' stroke='black' stroke-width='2' stroke-linecap='round'/><line x1='12' y1='16' x2='12' y2='16' stroke='black' stroke-width='2' stroke-linecap='round'/></svg>");
|
|
60
61
|
--icon-note: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><circle cx='12' cy='12' r='10' fill='none' stroke='black' stroke-width='1.5'/><line x1='12' y1='10' x2='12' y2='16' stroke='black' stroke-width='2' stroke-linecap='round'/><line x1='12' y1='7' x2='12' y2='7' stroke='black' stroke-width='2' stroke-linecap='round'/></svg>");
|
|
62
|
+
--icon-check: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24'><path d='M5 12.5 10 17 19 7' stroke='black' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round' fill='none'/></svg>");
|
|
61
63
|
--icon-search: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24'><circle cx='11' cy='11' r='7' stroke='<%= iconColor %>' stroke-width='2' fill='none'/><line x1='16.5' y1='16.5' x2='22' y2='22' stroke='<%= iconColor %>' stroke-width='2' stroke-linecap='round'/></svg>");
|
|
62
64
|
--icon-chevron: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'><polyline points='9 18 15 12 9 6' stroke='<%= iconColor %>' stroke-width='2' fill='none' stroke-linecap='round' stroke-linejoin='round'/></svg>");
|
|
63
65
|
--icon-breadcrumb: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24'><path d='M3 7 H17 V22' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round' fill='none'/><polyline points='11 16 17 22 23 16' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round' fill='none'/></svg>");
|
|
@@ -86,6 +88,7 @@
|
|
|
86
88
|
--bg2-color-translucent: hsl(
|
|
87
89
|
var(--tint-hue) calc(12% * var(--tint-amount)) 12% / 66%
|
|
88
90
|
);
|
|
91
|
+
--bg-trace-line-active: <%= bgTraceLineActiveDark %>;
|
|
89
92
|
--box-shadow: 0 3px 8px
|
|
90
93
|
hsl(var(--tint-hue) calc(33% * var(--tint-amount)) 8% / 70%);
|
|
91
94
|
--link-active-color: #fff;
|
package/templates/default.html
CHANGED
|
@@ -6,9 +6,10 @@
|
|
|
6
6
|
<% if (page.slides) { %>
|
|
7
7
|
<div class="slides-header" data-pagefind-ignore>
|
|
8
8
|
<button type="button" data-slides-present disabled>Present</button>
|
|
9
|
-
<
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
<label>
|
|
10
|
+
<input id="slides-fullscreen" type="checkbox" data-slides-fullscreen checked disabled>
|
|
11
|
+
Full screen
|
|
12
|
+
</label>
|
|
12
13
|
</div>
|
|
13
14
|
<% } %>
|
|
14
15
|
<%= content %>
|