}
diff --git a/src/lib/suggestions/config.mjs b/src/lib/suggestions/config.mjs
index dbc5ba7..0fd33f6 100644
--- a/src/lib/suggestions/config.mjs
+++ b/src/lib/suggestions/config.mjs
@@ -6,3 +6,9 @@ export const siteKey = process.env.VIZARD_TURNSTILE_SITE_KEY || '';
export const enabled = Boolean(api && siteKey && !process.env.VIZARD_DOCS_VERSION);
if (api && !/^https:\/\/[^\s]+$/.test(api) && !/^http:\/\/(localhost|127\.0\.0\.1):\d+$/.test(api))
throw new Error('VIZARD_SUGGESTIONS_API must be an HTTPS URL (or localhost for development).');
+
+// Unconfigured mirrors still offer the canonical, account-free editor.
+export const contributionEnabled = !process.env.VIZARD_DOCS_VERSION;
+export function editorUrl(path, base = '') {
+ return `${enabled ? base.replace(/\/$/, '') : 'https://plmn95.github.io/vizard-docs'}/edit/${path.replace(/\.md$/, '')}/`;
+}
diff --git a/src/styles/vizard.css b/src/styles/vizard.css
index 257de5e..6ef445d 100644
--- a/src/styles/vizard.css
+++ b/src/styles/vizard.css
@@ -537,3 +537,14 @@ mobile-starlight-toc:has(li:only-child > a[href='#_top']) {
transition-duration: 0.01ms !important;
}
}
+
+/* Match wrapper metrics to our heading sizes; Starlight uses them for icon placement. */
+.sl-markdown-content .sl-heading-wrapper.level-h2 { font-size: clamp(1.35rem, 2.4vw, 1.65rem); }
+.sl-markdown-content .sl-heading-wrapper.level-h3 { font-size: 1.15rem; }
+.sl-markdown-content .sl-anchor-link { vertical-align: 0.15em; }
+.sl-markdown-content .sl-anchor-icon > svg {
+ display: inline-block;
+ height: var(--sl-anchor-icon-size);
+ vertical-align: baseline;
+ transform: none;
+}
diff --git a/tests/docs-controls.spec.ts b/tests/docs-controls.spec.ts
new file mode 100644
index 0000000..16abf90
--- /dev/null
+++ b/tests/docs-controls.spec.ts
@@ -0,0 +1,24 @@
+import { test, expect } from '@playwright/test';
+
+test('page editing is discoverable and section links copy without scrolling', async ({ page }) => {
+ const errors: string[] = [];
+ page.on('pageerror', error => errors.push(error.message));
+ await page.addInitScript(() => Object.defineProperty(navigator, 'clipboard', {value: {writeText: async (text: string) => { (window as any).copiedLink = text; }}}));
+ await page.goto('./');
+ await expect(page).toHaveTitle(/Vizard/);
+ await expect(page.locator('.title-row').getByRole('link', {name: 'Edit this page'})).toBeVisible();
+ const link = page.getByRole('link', {name: 'Copy link to Start here', exact: true});
+ await link.scrollIntoViewIfNeeded();
+ const before = await page.evaluate(() => ({y: scrollY, url: location.href}));
+ await link.click();
+ await expect(page.getByRole('status')).toHaveText('Link to Start here copied.');
+ expect(await page.evaluate(() => ({y: scrollY, url: location.href}))).toEqual(before);
+ expect(await page.evaluate(() => (window as any).copiedLink)).toContain('#start-here');
+ await page.screenshot({path: '/tmp/vizard-doc-controls-desktop.png'});
+ await page.setViewportSize({width: 390, height: 844});
+ await page.goto('./');
+ await expect(page.locator('.title-row').getByRole('link', {name: 'Edit this page'})).toBeVisible();
+ expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true);
+ await page.screenshot({path: '/tmp/vizard-doc-controls-mobile.png'});
+ expect(errors).toEqual([]);
+});