@rettangoli/vt 1.0.0-rc1 → 1.0.0-rc13

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 CHANGED
@@ -5,12 +5,18 @@ Visual regression testing for Rettangoli specs using Playwright screenshots.
5
5
  ## Commands
6
6
 
7
7
  - `rtgl vt generate`
8
+ - `rtgl vt screenshot`
8
9
  - `rtgl vt report`
9
10
  - `rtgl vt accept`
10
11
 
11
- ## Public Generate Options
12
+ Behavior split:
13
+
14
+ - `generate` builds candidate HTML only (no Playwright capture)
15
+ - `screenshot` runs generate flow and captures candidate screenshots
16
+ - `report` compares existing artifacts only (does not run generate/screenshot)
17
+
18
+ ## Public Screenshot Options
12
19
 
13
- - `--skip-screenshots`
14
20
  - `--headed`
15
21
  - `--concurrency <number>`
16
22
  - `--timeout <ms>`
@@ -30,10 +36,10 @@ Visual regression testing for Rettangoli specs using Playwright screenshots.
30
36
 
31
37
  ## Scoped Runs
32
38
 
33
- Use selectors to run only part of VT in both `generate` and `report`:
39
+ Use selectors to run only part of VT in both `screenshot` and `report`:
34
40
 
35
41
  - `folder`: matches specs by folder prefix under `vt/specs` (example: `components/forms`)
36
- - `group`: matches section page key from `vt.sections` (`title` for flat sections, `items[].title` for grouped sections)
42
+ - `group`: matches derived section page key from `vt.sections` titles (`kebab-case(title)`)
37
43
  - `item`: matches a single spec path relative to `vt/specs` (with or without extension)
38
44
 
39
45
  Selector rules:
@@ -45,21 +51,21 @@ Examples:
45
51
 
46
52
  ```bash
47
53
  # Only specs under a folder
48
- rtgl vt generate --folder components/forms
54
+ rtgl vt screenshot --folder components/forms
49
55
 
50
56
  # Only one section/group key from vt.sections
51
- rtgl vt generate --group components_basic
57
+ rtgl vt screenshot --group components-basic
52
58
 
53
59
  # Only one spec item (extension optional)
54
- rtgl vt generate --item components/forms/login
55
- rtgl vt generate --item components/forms/login.html
60
+ rtgl vt screenshot --item components/forms/login
61
+ rtgl vt screenshot --item components/forms/login.html
56
62
 
57
63
  # Combine selectors (union)
58
- rtgl vt generate --group components_basic --item pages/home
64
+ rtgl vt screenshot --group components-basic --item pages/home
59
65
 
60
66
  # Same selectors for report
61
67
  rtgl vt report --folder components/forms
62
- rtgl vt report --group components_basic
68
+ rtgl vt report --group components-basic
63
69
  rtgl vt report --item components/forms/login
64
70
  ```
65
71
 
@@ -73,20 +79,31 @@ Everything else in capture is internal and intentionally not user-configurable.
73
79
  vt:
74
80
  path: ./vt
75
81
  port: 3001
76
- skipScreenshots: false
82
+ url: http://127.0.0.1:4173
83
+ service:
84
+ start: bun run preview
77
85
  concurrency: 4
78
86
  timeout: 30000
79
87
  waitEvent: vt:ready
88
+ viewport:
89
+ id: desktop
90
+ width: 1280
91
+ height: 720
80
92
  sections:
81
- - title: components_basic
93
+ - title: Components Basic
82
94
  files: components
83
95
  ```
84
96
 
85
97
  Notes:
86
98
 
87
99
  - `vt.sections` is required.
88
- - Section page keys (`title` for flat sections and group `items[].title`) allow only letters, numbers, `-`, `_`.
100
+ - `vt.service` is optional. When set, VT starts the command before capture, waits for `vt.url`, then stops it after capture.
101
+ - when `vt.service` is omitted and `vt.url` is set, VT expects that URL to already be running.
102
+ - Section page keys are derived as `kebab-case(title)` for flat sections and group `items[].title`.
103
+ - Derived section page keys must be unique case-insensitively.
104
+ - `vt.viewport` supports object or array; each viewport requires `id`, `width`, `height`.
89
105
  - `vt.capture` is internal and must be omitted.
106
+ - Viewport contract details: `docs/viewport-contract.md`.
90
107
 
91
108
  ## Spec Frontmatter
92
109
 
@@ -99,23 +116,65 @@ Supported frontmatter keys per spec file:
99
116
  - `waitEvent`
100
117
  - `waitSelector`
101
118
  - `waitStrategy` (`networkidle` | `load` | `event` | `selector`)
119
+ - `viewport` (object or array of viewport objects)
102
120
  - `skipScreenshot`
121
+ - `skipInitialScreenshot`
103
122
  - `specs`
104
123
  - `steps`
105
124
 
125
+ Step action reference:
126
+
127
+ - `docs/step-actions.md`
128
+ - canonical format is structured action objects (`- action: ...`); legacy one-line string steps are not supported.
129
+ - `assert` supports `js` deep-equal checks for object/array values.
130
+
106
131
  Screenshot naming:
107
132
 
108
- - First screenshot is `-01`.
133
+ - By default, VT takes an immediate first screenshot before running `steps`.
134
+ - Set `skipInitialScreenshot: true` in frontmatter to skip that immediate first screenshot.
135
+ - First captured screenshot is `-01`.
109
136
  - Then `-02`, `-03`, up to `-99`.
137
+ - When viewport id is configured, filenames include `--<viewportId>` before ordinal (for example `pages/home--mobile-01.webp`).
138
+
139
+ ## Docker
140
+
141
+ A pre-built Docker image with `rtgl` and Playwright browsers is available:
142
+
143
+ ```bash
144
+ docker pull han4wluc/rtgl:playwright-v1.57.0-rtgl-v1.0.0-rc13
145
+ ```
146
+
147
+ Run commands against a local project:
148
+
149
+ ```bash
150
+ docker run --rm -v "$(pwd):/workspace" han4wluc/rtgl:playwright-v1.57.0-rtgl-v1.0.0-rc13 rtgl vt screenshot
151
+ docker run --rm -v "$(pwd):/workspace" han4wluc/rtgl:playwright-v1.57.0-rtgl-v1.0.0-rc13 rtgl vt report
152
+ docker run --rm -v "$(pwd):/workspace" han4wluc/rtgl:playwright-v1.57.0-rtgl-v1.0.0-rc13 rtgl vt accept
153
+ ```
154
+
155
+ Note:
156
+
157
+ - Image default working directory is `/workspace`.
158
+ - Use `-w /workspace/<subdir>` only when running commands from a subfolder within the mounted project.
159
+
160
+ Supports `linux/amd64` and `linux/arm64`.
110
161
 
111
162
  ## Development
112
163
 
113
- Run tests:
164
+ Run unit tests:
114
165
 
115
166
  ```bash
116
167
  bun test
117
168
  ```
118
169
 
170
+ Default unit run behavior:
171
+
172
+ - `bun test` skips the real-browser smoke tests in `spec/e2e-smoke.spec.js` unless `VT_E2E=1`.
173
+ - Skipped smoke tests are:
174
+ - `runs generate, accept, and report with real screenshots`
175
+ - `supports waitEvent readiness with real browser screenshots`
176
+ - `supports managed service lifecycle with vt.service.start and vt.url`
177
+
119
178
  Run real-browser smoke:
120
179
 
121
180
  ```bash
@@ -125,15 +184,13 @@ VT_E2E=1 bun test spec/e2e-smoke.spec.js
125
184
  Run Docker E2E tests (requires Docker daemon running):
126
185
 
127
186
  ```bash
128
- # Full pipeline: build image → verify → run all Docker E2E tests
129
- bun run test:docker:full
187
+ # Full pipeline: build test image → run all E2E scenarios
188
+ bun run test:e2e:full
130
189
 
131
- # Tests only (skip image build, assumes image already exists)
132
- bun run test:docker
190
+ # Scenarios only (skip image build, assumes image already exists)
191
+ bun run test:e2e
133
192
  ```
134
193
 
135
- The Docker E2E suite builds a local `rtgl-local-test:latest` image, then runs generate/report/accept in temp directories inside containers. Tests validate WebP screenshot headers, report.json schema, metrics.json schema, HTML content, directory structure, accept file copies, multi-spec fixtures, multi-screenshot ordinals, and pixelmatch diff detection.
136
-
137
194
  Optional benchmark fixture:
138
195
 
139
196
  ```bash
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rettangoli/vt",
3
- "version": "1.0.0-rc1",
3
+ "version": "1.0.0-rc13",
4
4
  "description": "Rettangoli Visual Testing",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
@@ -3,11 +3,7 @@ import { dirname, join } from "node:path";
3
3
  import sharp from "sharp";
4
4
  import { createSteps } from "../createSteps.js";
5
5
  import { formatScreenshotOrdinal } from "./screenshot-naming.js";
6
-
7
- const DEFAULT_VIEWPORT = Object.freeze({
8
- width: 1280,
9
- height: 720,
10
- });
6
+ import { DEFAULT_VIEWPORT } from "../viewport.js";
11
7
 
12
8
  function nowMs() {
13
9
  return performance.now();
@@ -128,8 +124,11 @@ export class PlaywrightRunner {
128
124
  return context;
129
125
  }
130
126
 
131
- async configurePage(page) {
132
- await page.setViewportSize(DEFAULT_VIEWPORT);
127
+ async configurePage(page, viewport = DEFAULT_VIEWPORT) {
128
+ await page.setViewportSize({
129
+ width: viewport.width,
130
+ height: viewport.height,
131
+ });
133
132
  await page.setDefaultNavigationTimeout(this.navigationTimeout);
134
133
  await page.setDefaultTimeout(this.readyTimeout);
135
134
  await page.emulateMedia({
@@ -348,6 +347,7 @@ export class PlaywrightRunner {
348
347
 
349
348
  try {
350
349
  resetMs = await resetSession();
350
+ await this.configurePage(page, task.viewport ?? DEFAULT_VIEWPORT);
351
351
  const readyState = await this.navigateToReadyState(page, task, registeredReadyEvents);
352
352
  strategy = readyState.strategy;
353
353
  navigationMs = readyState.navigationMs;
@@ -359,10 +359,12 @@ export class PlaywrightRunner {
359
359
  }
360
360
  settleMs = nowMs() - settleStart;
361
361
 
362
- const firstScreenshotStart = nowMs();
363
- const firstScreenshotPath = await wrappedScreenshot(page, task.baseName);
364
- initialScreenshotMs = nowMs() - firstScreenshotStart;
365
- console.log(`Screenshot saved: ${firstScreenshotPath}`);
362
+ if (!task.frontMatter?.skipInitialScreenshot) {
363
+ const firstScreenshotStart = nowMs();
364
+ const firstScreenshotPath = await wrappedScreenshot(page, task.baseName);
365
+ initialScreenshotMs = nowMs() - firstScreenshotStart;
366
+ console.log(`Screenshot saved: ${firstScreenshotPath}`);
367
+ }
366
368
 
367
369
  const stepsStart = nowMs();
368
370
  const stepsExecutor = createSteps(page, {
@@ -1,3 +1,5 @@
1
+ import { appendViewportToBaseName, resolveViewports } from "../viewport.js";
2
+
1
3
  const removeExtension = (filePath) => filePath.replace(/\.[^/.]+$/, "");
2
4
 
3
5
  const toHtmlPath = (filePath) => {
@@ -58,14 +60,20 @@ export function createCaptureTasks(generatedFiles, options) {
58
60
  waitEvent,
59
61
  waitSelector,
60
62
  waitStrategy,
63
+ viewport,
61
64
  } = options;
62
65
 
63
- return generatedFiles.map((file, index) => {
66
+ const tasks = [];
67
+ let taskIndex = 0;
68
+
69
+ generatedFiles.forEach((file, fileIndex) => {
64
70
  const frontMatter = file.frontMatter || {};
65
71
  const normalizedPath = normalizePathForUrl(file.path);
66
72
  const constructedUrl = toHtmlPath(`${serverUrl}/candidate/${normalizedPath}`);
67
- const rawUrl = frontMatter.url ?? configUrl ?? constructedUrl;
68
- const url = toAbsoluteUrl(rawUrl, serverUrl);
73
+ const resolvedConfigUrl = configUrl ? toAbsoluteUrl(configUrl, serverUrl) : null;
74
+ const urlResolutionBase = resolvedConfigUrl || serverUrl;
75
+ const rawUrl = frontMatter.url ?? resolvedConfigUrl ?? constructedUrl;
76
+ const url = toAbsoluteUrl(rawUrl, urlResolutionBase);
69
77
  const resolvedWaitEvent = frontMatter.waitEvent ?? waitEvent;
70
78
  const resolvedWaitSelector = frontMatter.waitSelector ?? waitSelector;
71
79
 
@@ -75,25 +83,37 @@ export function createCaptureTasks(generatedFiles, options) {
75
83
  waitStrategy,
76
84
  });
77
85
 
78
- const task = {
79
- id: `${index}:${file.path}`,
80
- index,
81
- path: file.path,
82
- url,
83
- baseName: removeExtension(file.path),
84
- frontMatter,
85
- steps: frontMatter.steps || [],
86
- waitStrategy: resolvedWaitStrategy,
87
- estimatedCost: estimateTaskCost(frontMatter.steps || [], resolvedWaitStrategy),
88
- };
86
+ const resolvedViewports = resolveViewports(frontMatter.viewport, viewport);
87
+ resolvedViewports.forEach((resolvedViewport) => {
88
+ const viewportId = resolvedViewport.id;
89
+ const task = {
90
+ id: `${fileIndex}:${file.path}:${viewportId ?? "default"}`,
91
+ index: taskIndex,
92
+ path: file.path,
93
+ url,
94
+ baseName: appendViewportToBaseName(removeExtension(file.path), viewportId),
95
+ frontMatter,
96
+ steps: frontMatter.steps || [],
97
+ waitStrategy: resolvedWaitStrategy,
98
+ estimatedCost: estimateTaskCost(frontMatter.steps || [], resolvedWaitStrategy),
99
+ viewport: {
100
+ id: viewportId,
101
+ width: resolvedViewport.width,
102
+ height: resolvedViewport.height,
103
+ },
104
+ };
89
105
 
90
- if (resolvedWaitEvent !== undefined && resolvedWaitEvent !== null) {
91
- task.waitEvent = resolvedWaitEvent;
92
- }
93
- if (resolvedWaitSelector !== undefined && resolvedWaitSelector !== null) {
94
- task.waitSelector = resolvedWaitSelector;
95
- }
106
+ if (resolvedWaitEvent !== undefined && resolvedWaitEvent !== null) {
107
+ task.waitEvent = resolvedWaitEvent;
108
+ }
109
+ if (resolvedWaitSelector !== undefined && resolvedWaitSelector !== null) {
110
+ task.waitSelector = resolvedWaitSelector;
111
+ }
96
112
 
97
- return task;
113
+ tasks.push(task);
114
+ taskIndex += 1;
115
+ });
98
116
  });
117
+
118
+ return tasks;
99
119
  }
@@ -5,6 +5,7 @@ import { normalizeSelectors } from "../selector-filter.js";
5
5
  export function resolveGenerateOptions(options = {}, configData = {}) {
6
6
  const {
7
7
  skipScreenshots: cliSkipScreenshots,
8
+ captureScreenshots,
8
9
  vtPath: cliVtPath,
9
10
  port: cliPort,
10
11
  concurrency: cliConcurrency,
@@ -25,13 +26,26 @@ export function resolveGenerateOptions(options = {}, configData = {}) {
25
26
  item: cliItem,
26
27
  });
27
28
 
29
+ let resolvedSkipScreenshots;
30
+ if (captureScreenshots === true) {
31
+ resolvedSkipScreenshots = false;
32
+ } else if (captureScreenshots === false) {
33
+ resolvedSkipScreenshots = true;
34
+ } else if (cliSkipScreenshots === true) {
35
+ resolvedSkipScreenshots = true;
36
+ } else {
37
+ resolvedSkipScreenshots = configData.skipScreenshots ?? false;
38
+ }
39
+
28
40
  const resolvedOptions = {
29
41
  vtPath: cliVtPath ?? configData.path ?? "./vt",
30
- skipScreenshots: cliSkipScreenshots ? true : (configData.skipScreenshots ?? false),
42
+ skipScreenshots: resolvedSkipScreenshots,
31
43
  port: cliPort ?? configData.port ?? 3001,
32
44
  waitEvent,
33
45
  headless: cliHeadless ?? true,
34
46
  configUrl: cliUrl ?? configData.url,
47
+ ...(configData.viewport !== undefined ? { viewport: configData.viewport } : {}),
48
+ ...(configData.service?.start ? { serviceStart: configData.service.start } : {}),
35
49
  selectors,
36
50
 
37
51
  // Internal capture defaults (not user-configurable).
@@ -14,6 +14,11 @@ import {
14
14
  filterGeneratedFilesBySelectors,
15
15
  hasSelectors,
16
16
  } from "../selector-filter.js";
17
+ import {
18
+ startManagedService,
19
+ stopManagedService,
20
+ waitForServiceReady,
21
+ } from "./service-runtime.js";
17
22
 
18
23
  const libraryTemplatesPath = new URL("./templates", import.meta.url).pathname;
19
24
  const libraryStaticPath = new URL("./static", import.meta.url).pathname;
@@ -39,6 +44,7 @@ export function buildCaptureOptions({
39
44
  isolationMode,
40
45
  metricsPath,
41
46
  headless,
47
+ viewport,
42
48
  } = resolvedOptions;
43
49
 
44
50
  return {
@@ -59,6 +65,7 @@ export function buildCaptureOptions({
59
65
  isolationMode,
60
66
  metricsPath,
61
67
  headless,
68
+ viewport,
62
69
  };
63
70
  }
64
71
 
@@ -88,6 +95,7 @@ async function main(options = {}) {
88
95
  skipScreenshots,
89
96
  port,
90
97
  configUrl,
98
+ serviceStart,
91
99
  } = resolvedOptions;
92
100
 
93
101
  const specsPath = join(vtPath, "specs");
@@ -162,8 +170,22 @@ async function main(options = {}) {
162
170
  return;
163
171
  }
164
172
 
165
- const server = configUrl ? null : await startWebServer(siteOutputPath, vtPath, port);
173
+ let server = null;
174
+ let managedService = null;
175
+
166
176
  try {
177
+ if (serviceStart) {
178
+ if (!configUrl) {
179
+ throw new Error(
180
+ "vt.service.start requires vt.url (or --url) so VT can wait for readiness and capture against that URL.",
181
+ );
182
+ }
183
+ managedService = startManagedService({ command: serviceStart });
184
+ await waitForServiceReady({ url: configUrl, handle: managedService });
185
+ } else if (!configUrl) {
186
+ server = await startWebServer(siteOutputPath, vtPath, port);
187
+ }
188
+
167
189
  await takeScreenshots(
168
190
  buildCaptureOptions({
169
191
  filesToScreenshot,
@@ -177,6 +199,9 @@ async function main(options = {}) {
177
199
  server.close();
178
200
  console.log("Server stopped");
179
201
  }
202
+ if (managedService) {
203
+ await stopManagedService(managedService);
204
+ }
180
205
  }
181
206
  }
182
207
  }
package/src/cli/index.js CHANGED
@@ -1,9 +1,11 @@
1
1
  import generate from './generate.js';
2
+ import screenshot from './screenshot.js';
2
3
  import report from './report.js';
3
4
  import accept from './accept.js';
4
5
 
5
6
  export {
6
7
  generate,
8
+ screenshot,
7
9
  report,
8
10
  accept,
9
- }
11
+ }
package/src/cli/report.js CHANGED
@@ -159,7 +159,7 @@ async function main(options = {}) {
159
159
 
160
160
  try {
161
161
  if (!fs.existsSync(candidateDir)) {
162
- throw new Error(`Candidate screenshots directory not found: "${candidateDir}". Run "rtgl vt generate" first.`);
162
+ throw new Error(`Candidate screenshots directory not found: "${candidateDir}". Run "rtgl vt screenshot" first.`);
163
163
  }
164
164
 
165
165
  const candidateFiles = getAllFiles(candidateDir).filter((file) => file.endsWith(".webp"));
@@ -0,0 +1,8 @@
1
+ import generate from "./generate.js";
2
+
3
+ export default async function screenshot(options = {}) {
4
+ await generate({
5
+ ...options,
6
+ captureScreenshots: true,
7
+ });
8
+ }
@@ -0,0 +1,116 @@
1
+ import { spawn } from "node:child_process";
2
+
3
+ const READY_TIMEOUT_MS = 120000;
4
+ const READY_INTERVAL_MS = 500;
5
+ const STOP_TIMEOUT_MS = 10000;
6
+
7
+ function sleep(ms) {
8
+ return new Promise((resolve) => setTimeout(resolve, ms));
9
+ }
10
+
11
+ function formatExitMessage(result) {
12
+ if (!result) return "unknown exit state";
13
+ if (result.code !== null && result.code !== undefined) {
14
+ return `code ${result.code}`;
15
+ }
16
+ if (result.signal) {
17
+ return `signal ${result.signal}`;
18
+ }
19
+ return "unknown exit state";
20
+ }
21
+
22
+ function sendSignal(handle, signal) {
23
+ const { child, useProcessGroup } = handle;
24
+ const pid = child.pid;
25
+ if (!pid) {
26
+ return;
27
+ }
28
+
29
+ try {
30
+ if (useProcessGroup) {
31
+ process.kill(-pid, signal);
32
+ return;
33
+ }
34
+ child.kill(signal);
35
+ } catch (error) {
36
+ if (error?.code !== "ESRCH") {
37
+ throw error;
38
+ }
39
+ }
40
+ }
41
+
42
+ export function startManagedService({ command }) {
43
+ const useProcessGroup = process.platform !== "win32";
44
+ const child = spawn(command, {
45
+ cwd: process.cwd(),
46
+ stdio: "inherit",
47
+ shell: true,
48
+ env: process.env,
49
+ detached: useProcessGroup,
50
+ });
51
+
52
+ const exitPromise = new Promise((resolve, reject) => {
53
+ child.once("error", reject);
54
+ child.once("exit", (code, signal) => {
55
+ resolve({ code, signal });
56
+ });
57
+ });
58
+
59
+ console.log(`Starting managed service: ${command}`);
60
+ return { child, exitPromise, useProcessGroup };
61
+ }
62
+
63
+ export async function waitForServiceReady({ url, handle }) {
64
+ const startMs = Date.now();
65
+
66
+ while (Date.now() - startMs < READY_TIMEOUT_MS) {
67
+ if (handle.child.exitCode !== null) {
68
+ const result = await handle.exitPromise.catch(() => null);
69
+ throw new Error(
70
+ `Managed service exited before becoming ready (${formatExitMessage(result)}).`,
71
+ );
72
+ }
73
+
74
+ try {
75
+ const response = await fetch(url);
76
+ console.log(`Managed service ready: ${url} (status ${response.status})`);
77
+ return;
78
+ } catch {
79
+ // Keep polling until timeout.
80
+ }
81
+
82
+ await sleep(READY_INTERVAL_MS);
83
+ }
84
+
85
+ throw new Error(
86
+ `Timed out waiting for managed service at ${url} after ${READY_TIMEOUT_MS}ms.`,
87
+ );
88
+ }
89
+
90
+ export async function stopManagedService(handle) {
91
+ if (!handle) return;
92
+
93
+ const { child, exitPromise } = handle;
94
+ if (child.exitCode !== null) {
95
+ return;
96
+ }
97
+
98
+ console.log("Stopping managed service (SIGTERM)...");
99
+ sendSignal(handle, "SIGTERM");
100
+
101
+ const exitedGracefully = await Promise.race([
102
+ exitPromise.then(() => true).catch(() => true),
103
+ sleep(STOP_TIMEOUT_MS).then(() => false),
104
+ ]);
105
+
106
+ if (exitedGracefully) {
107
+ return;
108
+ }
109
+
110
+ if (child.exitCode === null) {
111
+ console.warn("Managed service did not exit after SIGTERM, sending SIGKILL...");
112
+ sendSignal(handle, "SIGKILL");
113
+ }
114
+
115
+ await exitPromise.catch(() => null);
116
+ }
@@ -3,7 +3,8 @@
3
3
  <head>
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <link rel="stylesheet" href="/public/theme.css">
6
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@rettangoli/ui@1.0.0-rc6/dist/themes/base.css">
7
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@rettangoli/ui@1.0.0-rc6/dist/themes/theme-rtgl-slate.css">
7
8
  <script>
8
9
  window.rtglIcons = {
9
10
  text: `<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M4 12H20M4 8H20M4 16H12" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>`,
@@ -38,7 +39,8 @@
38
39
  `,
39
40
  }
40
41
  </script>
41
- <script src="https://cdn.jsdelivr.net/npm/rettangoli-ui@0.1.0-rc2/dist/rettangoli-iife-ui.min.js"></script>
42
+ <script src="https://cdn.jsdelivr.net/npm/construct-style-sheets-polyfill@3.1.0/dist/adoptedStyleSheets.min.js"></script>
43
+ <script src="https://cdn.jsdelivr.net/npm/@rettangoli/ui@1.0.0-rc6/dist/rettangoli-iife-ui.min.js"></script>
42
44
  <script src="/public/main.js"></script>
43
45
  </head>
44
46
  <body class="dark">
@@ -46,4 +48,4 @@
46
48
  {{ content }}
47
49
  </div>
48
50
  </body>
49
- </html>
51
+ </html>
@@ -4,7 +4,8 @@
4
4
  <head>
5
5
  <meta charset="UTF-8">
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
- <link rel="stylesheet" href="/public/theme.css">
7
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@rettangoli/ui@1.0.0-rc6/dist/themes/base.css">
8
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@rettangoli/ui@1.0.0-rc6/dist/themes/theme-rtgl-slate.css">
8
9
  <script>
9
10
  window.addEventListener('DOMContentLoaded', () => {
10
11
  if (location.hash) {
@@ -15,7 +16,8 @@
15
16
  }
16
17
  });
17
18
  </script>
18
- <script src="https://cdn.jsdelivr.net/npm/rettangoli-ui@0.1.0-rc2/dist/rettangoli-iife-ui.min.js"></script>
19
+ <script src="https://cdn.jsdelivr.net/npm/construct-style-sheets-polyfill@3.1.0/dist/adoptedStyleSheets.min.js"></script>
20
+ <script src="https://cdn.jsdelivr.net/npm/@rettangoli/ui@1.0.0-rc6/dist/rettangoli-iife-ui.min.js"></script>
19
21
 
20
22
  <style>
21
23
  pre {
@@ -45,14 +47,14 @@
45
47
  </head>
46
48
 
47
49
  <body class="dark">
48
- <rtgl-view d="h" w="100vw" h="100vh">
50
+ <rtgl-view d="h" w="f" h="100vh">
49
51
 
50
52
  <rtgl-view h="f" sm-hidden>
51
53
  <rtgl-sidebar items="{{ sidebarItems }}">
52
54
  </rtgl-sidebar>
53
55
  </rtgl-view>
54
56
 
55
- <rtgl-view id="content" h="100vh" w="1fg" p="lg" g="lg" style="flex-wrap: nowrap;" sv ah="c">
57
+ <rtgl-view id="content" h="100vh" w="1fg" p="lg" g="lg" style="flex-wrap: nowrap; min-width: 0;" sv ah="c">
56
58
  <rtgl-view w="f" g="xl">
57
59
  <rtgl-text s="h2">{{ currentSection.title }} </rtgl-text>
58
60
  {% for file in files %}
@@ -100,8 +102,8 @@
100
102
  <rtgl-view h="33vh"></rtgl-view>
101
103
  </rtgl-view>
102
104
  </rtgl-view>
103
- <rtgl-view lg-hidden>
104
- <rtgl-page-outline id="page-outline" target-id="content"></rtgl-page-outline>
105
+ <rtgl-view lg-hidden style="flex: 0 0 auto;">
106
+ <rtgl-page-outline id="page-outline" target-id="content" scroll-container-id="content"></rtgl-page-outline>
105
107
  </rtgl-view>
106
108
  </rtgl-view>
107
109
  </body>
@@ -4,8 +4,10 @@
4
4
  <head>
5
5
  <meta charset="UTF-8">
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
- <link rel="stylesheet" href="/public/theme.css">
8
- <script src="https://cdn.jsdelivr.net/npm/rettangoli-ui@0.1.0-rc2/dist/rettangoli-iife-ui.min.js"></script>
7
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@rettangoli/ui@1.0.0-rc6/dist/themes/base.css">
8
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@rettangoli/ui@1.0.0-rc6/dist/themes/theme-rtgl-slate.css">
9
+ <script src="https://cdn.jsdelivr.net/npm/construct-style-sheets-polyfill@3.1.0/dist/adoptedStyleSheets.min.js"></script>
10
+ <script src="https://cdn.jsdelivr.net/npm/@rettangoli/ui@1.0.0-rc6/dist/rettangoli-iife-ui.min.js"></script>
9
11
  <style>
10
12
  code {
11
13
  white-space: pre-wrap;