diff --git a/docs/index.html b/docs/index.html
index 9ceca6c2..07f1f5ac 100644
--- a/docs/index.html
+++ b/docs/index.html
@@ -158,20 +158,28 @@
-
diff --git a/docs/style.css b/docs/style.css
index 7106f098..214a896c 100644
--- a/docs/style.css
+++ b/docs/style.css
@@ -1172,6 +1172,18 @@ html, body {
}
.blink-warn { animation: blink-warn 1.2s ease-in-out infinite; }
+/* Quick-action row at the top of the toolbar (#191). Always visible so the
+ * 5 most-used actions don't require expanding a section. */
+#tb-quick {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+ padding-bottom: 4px;
+ border-bottom: 1px solid #3a3636;
+ margin-bottom: 2px;
+}
+#tb-quick button { font-weight: 600; }
+
/* magnifying glass */
#tool-magnifier { font-size: 18px; line-height: 1; padding: 2px 6px; }
#tool-magnifier.active { background: #3a3636; border-color: #ffcc33; color: #ffcc33; }
diff --git a/tests/quick-action-row.spec.js b/tests/quick-action-row.spec.js
new file mode 100644
index 00000000..198c4a76
--- /dev/null
+++ b/tests/quick-action-row.spec.js
@@ -0,0 +1,141 @@
+// @ts-check
+// PR #245 — Quick-action row at the top of the toolbar: 5 most-used buttons
+// are always visible above the collapsible sections, with no duplicate IDs.
+const { test, expect } = require('./_setup');
+
+async function boot(page) {
+ await page.addInitScript(() => {
+ try {
+ if (localStorage.getItem('__test_qar_init') !== '1') {
+ localStorage.clear();
+ sessionStorage.clear();
+ localStorage.setItem('__test_qar_init', '1');
+ }
+ } catch (e) {}
+ });
+ await page.goto('/?lang=en');
+ await page.waitForFunction(() => typeof state !== 'undefined');
+}
+
+test.describe('Quick-action row (#tb-quick)', () => {
+ test('exists with the 5 expected buttons', async ({ page }) => {
+ await boot(page);
+ const quick = page.locator('#tb-quick');
+ await expect(quick).toBeVisible();
+
+ const ids = await quick.locator('button').evaluateAll(btns => btns.map(b => b.id));
+ expect(ids).toEqual(['tool-add', 'tool-note', 'fit', 'plan', 'save']);
+ });
+
+ test('is not a collapsible section (no .tb-section class, no header)', async ({ page }) => {
+ await boot(page);
+ const quick = page.locator('#tb-quick');
+ await expect(quick).not.toHaveClass(/tb-section/);
+ await expect(quick.locator('.tb-section-head')).toHaveCount(0);
+ });
+
+ test('always visible when toolbar is expanded', async ({ page }) => {
+ await boot(page);
+ await expect(page.locator('#tb-quick')).toBeVisible();
+ });
+
+ test.describe('no duplicate IDs — buttons removed from collapsible sections', () => {
+ test('tool-add and tool-note not in Build section', async ({ page }) => {
+ await boot(page);
+ const build = page.locator('.tb-section[data-sec="build"]');
+ await expect(build.locator('#tool-add')).toHaveCount(0);
+ await expect(build.locator('#tool-note')).toHaveCount(0);
+ });
+
+ test('fit not in Build section', async ({ page }) => {
+ await boot(page);
+ const build = page.locator('.tb-section[data-sec="build"]');
+ await expect(build.locator('#fit')).toHaveCount(0);
+ });
+
+ test('plan not in Charts section', async ({ page }) => {
+ await boot(page);
+ const charts = page.locator('.tb-section[data-sec="charts"]');
+ await expect(charts.locator('#plan')).toHaveCount(0);
+ });
+
+ test('save not in Export section', async ({ page }) => {
+ await boot(page);
+ const exportSec = page.locator('.tb-section[data-sec="export"]');
+ await expect(exportSec.locator('#save')).toHaveCount(0);
+ });
+ });
+
+ test.describe('CSS styling', () => {
+ test('buttons have font-weight 600', async ({ page }) => {
+ await boot(page);
+ const btn = page.locator('#tb-quick button').first();
+ const fw = await btn.evaluate(el => getComputedStyle(el).fontWeight);
+ expect(fw).toBe('600');
+ });
+
+ test('row has a bottom border divider', async ({ page }) => {
+ await boot(page);
+ const quick = page.locator('#tb-quick');
+ const bb = await quick.evaluate(el => getComputedStyle(el).borderBottom);
+ expect(bb).toMatch(/1px/);
+ });
+ });
+
+ test.describe('quick-action buttons are functional', () => {
+ test('tool-add enters add-waypoint mode', async ({ page }) => {
+ await boot(page);
+ await page.locator('#tb-quick #tool-add').click();
+ const hasClass = await page.locator('#map').evaluate(el => el.classList.contains('add'));
+ expect(hasClass).toBe(true);
+ // Exit mode
+ await page.keyboard.press('Escape');
+ });
+
+ test('tool-note enters add-note mode', async ({ page }) => {
+ await boot(page);
+ await page.locator('#tb-quick #tool-note').click();
+ const hasAddClass = await page.locator('#map').evaluate(el => el.classList.contains('add'));
+ expect(hasAddClass).toBe(true);
+ const isActive = await page.locator('#tb-quick #tool-note').evaluate(el => el.classList.contains('active'));
+ expect(isActive).toBe(true);
+ await page.keyboard.press('Escape');
+ });
+
+ test('fit triggers fitView', async ({ page }) => {
+ await boot(page);
+ await page.evaluate(() => {
+ state.waypoints.push({ lat: 32.0, lng: 34.8, name: 'A' });
+ state.waypoints.push({ lat: 32.1, lng: 34.9, name: 'B' });
+ });
+ await page.locator('#tb-quick #fit').click();
+ const center = await page.evaluate(() => map.getCenter());
+ expect(center.lat).toBeCloseTo(32.05, 1);
+ });
+
+ test('plan toggles flight plan modal', async ({ page }) => {
+ await boot(page);
+ await page.evaluate(() => {
+ state.waypoints.push({ lat: 32.0, lng: 34.8, name: 'A' });
+ state.waypoints.push({ lat: 32.1, lng: 34.9, name: 'B' });
+ syncLegs();
+ draw();
+ });
+ await page.locator('#tb-quick #plan').click();
+ await expect(page.locator('.modal-back.flight-plan')).toBeVisible();
+ await page.locator('#tb-quick #plan').click();
+ await expect(page.locator('.modal-back.flight-plan')).not.toBeVisible();
+ });
+
+ test('save triggers JSON export', async ({ page }) => {
+ await boot(page);
+ await page.evaluate(() => {
+ state.waypoints.push({ lat: 32.0, lng: 34.8, name: 'A' });
+ state.waypoints.push({ lat: 32.1, lng: 34.9, name: 'B' });
+ });
+ const download = page.waitForEvent('download', { timeout: 3000 });
+ await page.locator('#tb-quick #save').click();
+ await download;
+ });
+ });
+});