diff --git a/package-lock.json b/package-lock.json index 82f789204..75ad21d92 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,7 @@ "polylabel": "^2.0.1" }, "devDependencies": { - "@biomejs/biome": "^2.4.13", + "@biomejs/biome": "^2.4.6", "@playwright/test": "^1.57.0", "@types/d3": "^7.4.3", "@types/delaunator": "^5.0.3", @@ -104,9 +104,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT OR Apache-2.0", "optional": true, "os": [ @@ -124,9 +121,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT OR Apache-2.0", "optional": true, "os": [ @@ -144,9 +138,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT OR Apache-2.0", "optional": true, "os": [ @@ -164,9 +155,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT OR Apache-2.0", "optional": true, "os": [ diff --git a/public/index.css b/public/index.css index 0e825c539..d8fe775f6 100644 --- a/public/index.css +++ b/public/index.css @@ -2411,32 +2411,44 @@ svg.button { #tourPromptButton { position: fixed; - bottom: 87px; - right: 26px; - width: 56px; - height: 56px; + bottom: 100px; + right: 36px; + width: 36px; + height: 36px; border-radius: 50%; + background-color: var(--bg-main); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.35); display: flex; align-items: center; justify-content: center; cursor: pointer; user-select: none; animation: fadeIn 1s ease-in; - transform: scale(0.65); - opacity: var(--bg-opacity); z-index: 9999; - overflow: hidden; } -#tourPromptButton img { - width: 100%; - height: 100%; - object-fit: cover; +#tourPromptButton button { + all: unset; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; +} + +#tourPromptButton svg path { + fill: #000000; + stroke: none; } #tourPromptButton:hover { - transform: scale(0.7); - opacity: 1; + box-shadow: 0 4px 14px rgba(0, 0, 0, 0.45); +} + +#tourPromptButton:hover svg path { + fill: none; + stroke: #000000; + stroke-width: 5; + stroke-linejoin: round; } @media print { @@ -2465,60 +2477,35 @@ svg.button { background: #25252a; } } -.driver-popover { - background-color: #1a1a2e; - border: 1px solid #4a4a6a; - color: #c8c8d0; - font-family: Georgia, serif; +.fmg-tour.driver-popover { border-radius: 4px; } -.driver-popover-title { - color: #e8d5a3; +.fmg-tour .driver-popover-title { font-size: 1.1em; - border-bottom: 1px solid #4a4a6a; + border-bottom: 1px solid; padding-bottom: 0.4em; margin-bottom: 0.5em; } -.driver-popover-description { +.fmg-tour .driver-popover-description { font-size: 0.9em; line-height: 1.5; } -.driver-popover-footer button { - background-color: #3a3a5c; - border: 1px solid #5a5a7a; - color: #c8c8d0; +.fmg-tour .driver-popover-footer button { border-radius: 3px; cursor: pointer; } -.driver-popover-footer button:hover { - background-color: #4a4a6c; +.fmg-tour .driver-popover-footer button:hover { + background-color: #e0e0e0; } -.driver-popover-progress-text { - color: #8a8aaa; +.fmg-tour .driver-popover-progress-text { font-size: 0.8em; } -.driver-popover-arrow-side-left.driver-popover-arrow { - border-left-color: #4a4a6a; -} - -.driver-popover-arrow-side-right.driver-popover-arrow { - border-right-color: #4a4a6a; -} - -.driver-popover-arrow-side-top.driver-popover-arrow { - border-top-color: #4a4a6a; -} - -.driver-popover-arrow-side-bottom.driver-popover-arrow { - border-bottom-color: #4a4a6a; -} - /* Tooltip demo step: hide overlay and restore all pointer events so the map is fully hoverable */ body.tour-free-roam .driver-overlay { opacity: 0 !important; diff --git a/src/index.html b/src/index.html index 11dbf51e0..4eaa912b3 100644 --- a/src/index.html +++ b/src/index.html @@ -8636,7 +8636,11 @@ diff --git a/src/modules/ui-tour.ts b/src/modules/ui-tour.ts index dd569bfbb..0db2ce8a2 100644 --- a/src/modules/ui-tour.ts +++ b/src/modules/ui-tour.ts @@ -1,5 +1,6 @@ import { driver } from "driver.js"; import "driver.js/dist/driver.css"; + const byId = (id: string) => document.getElementById(id); function clickTab(tabId: string) { @@ -41,7 +42,32 @@ function start() { const tour = driver({ showProgress: true, allowClose: true, + popoverClass: "fmg-tour", + overlayColor: "rgb(0,0,0)", + overlayOpacity: 0.75, + stagePadding: 4, + stageRadius: 4, + onPopoverRender: (popover) => { + Object.assign(popover.wrapper.style, { + backgroundColor: "#ffffff", + color: "#000000", + border: "1px solid #cccccc", + fontFamily: "Georgia, serif", + }); + popover.title.style.color = "#000000"; + popover.title.style.borderBottomColor = "#cccccc"; + popover.progress.style.color = "#666666"; + popover.closeButton.style.color = "#000000"; + for (const btn of [popover.previousButton, popover.nextButton]) { + Object.assign(btn.style, { + backgroundColor: "#f0f0f0", + border: "1px solid #cccccc", + color: "#000000", + }); + } + }, onDestroyStarted: () => { + document.removeEventListener("keydown", handleKeydown); hideHeightmapCustomizationPanel(); closeDialogs(); tour.destroy(); @@ -72,6 +98,9 @@ function start() { }, { element: "#tooltip", + onHighlightStarted: () => { + document.body.classList.add("tour-free-roam"); + }, popover: { title: "Hover Tooltips", description: @@ -84,6 +113,7 @@ function start() { element: "#optionsTrigger", onHighlightStarted: () => { document.body.classList.remove("tour-free-roam"); + closeOptionsPanel(); }, popover: { title: "Open the Options Menu", @@ -201,6 +231,7 @@ function start() { { element: "#configureWorld", onHighlightStarted: () => { + closeDialogs(); clickTab("optionsTab"); }, popover: { @@ -209,7 +240,6 @@ function start() { "This button opens the World Configurator where you can set the map's position on the globe, adjust equatorial and polar temperatures, and configure precipitation to shape the world's climate.", side: "right", onNextClick: () => { - editWorld(); tour.moveNext(); }, }, @@ -217,6 +247,9 @@ function start() { { element: "#worldConfigurator", disableActiveInteraction: false, + onHighlightStarted: () => { + editWorld(); + }, popover: { title: "World Configurator", description: @@ -254,7 +287,6 @@ function start() { "Open the Heightmap editor to manually sculpt terrain by raising or lowering elevation. Changes here reshape coastlines, rivers, and biomes.", side: "right", onNextClick: () => { - showHeightmapCustomizationPanel(); tour.moveNext(); }, }, @@ -262,6 +294,9 @@ function start() { { element: "#customizationMenu", disableActiveInteraction: false, + onHighlightStarted: () => { + showHeightmapCustomizationPanel(); + }, onDeselected: () => { hideHeightmapCustomizationPanel(); }, @@ -302,13 +337,15 @@ function start() { // ── Export / Save / Load ───────────────────────────────────────────────── { element: "#exportButton", + onHighlightStarted: () => { + closeDialogs(); + }, popover: { title: "Export", description: "Click Export to open the export dialog where you can download the map as an SVG, PNG, or JPEG image, split it into tiles, or export the world data as JSON.", side: "top", onNextClick: () => { - showExportPane(); tour.moveNext(); }, }, @@ -316,6 +353,9 @@ function start() { { element: "#exportMapData", disableActiveInteraction: false, + onHighlightStarted: () => { + showExportPane(); + }, popover: { title: "Export Options", description: @@ -343,6 +383,28 @@ function start() { ], }); + function isEditableTarget(target: EventTarget | null): boolean { + if (!(target instanceof HTMLElement)) return false; + if (target.isContentEditable) return true; + return !!target.closest( + "input, textarea, select, [contenteditable], [contenteditable='plaintext-only']", + ); + } + + function handleKeydown(e: KeyboardEvent): void { + if (!tour.isActive() || isEditableTarget(e.target)) return; + if (e.key === "ArrowDown") { + e.preventDefault(); + e.stopPropagation(); + document.querySelector(".driver-popover-next-btn")?.click(); + } else if (e.key === "ArrowUp") { + e.preventDefault(); + e.stopPropagation(); + document.querySelector(".driver-popover-prev-btn")?.click(); + } + } + + document.addEventListener("keydown", handleKeydown); tour.drive(); } diff --git a/tests/e2e/ui-tour.spec.ts b/tests/e2e/ui-tour.spec.ts index 93417aca1..7a40708da 100644 --- a/tests/e2e/ui-tour.spec.ts +++ b/tests/e2e/ui-tour.spec.ts @@ -47,6 +47,16 @@ async function nextStep(page: Page, expectedTitle: string) { ); } +/** Click Previous and wait for the popover title to become expectedTitle. */ +async function prevStep(page: Page, expectedTitle: string) { + await page.locator(".driver-popover-prev-btn").click(); + await page.waitForFunction( + (title: string) => document.querySelector(".driver-popover-title")?.textContent?.trim() === title, + expectedTitle, + { timeout: 5000 }, + ); +} + /** Click Next N times with a fixed delay — used for bulk advancing where we * don't need to assert intermediate titles. */ async function advanceSteps(page: Page, count: number) { @@ -365,6 +375,125 @@ test.describe("UI Tour", () => { await expect(page.locator("#exportMapData")).toBeHidden(); }); + // ── Back navigation ──────────────────────────────────────────────────────── + + test("back to Hover Tooltips step restores tour-free-roam class", async ({ page }) => { + await page.evaluate(() => (window as any).UITour.start()); + await page.waitForSelector(".driver-popover", { state: "visible" }); + + // Advance past Navigate the Map so tour-free-roam is added, then to Open + // the Options Menu where onHighlightStarted removes it. + await nextStep(page, STEP_TITLES[1]); // → Navigate the Map + await nextStep(page, STEP_TITLES[2]); // → Hover Tooltips (free-roam added by onNextClick) + await nextStep(page, STEP_TITLES[3]); // → Open the Options Menu (free-roam removed) + await expect(page.locator("body")).not.toHaveClass(/tour-free-roam/); + + // Go back: onHighlightStarted on Hover Tooltips must re-add the class. + await prevStep(page, STEP_TITLES[2]); + await expect(page.locator("body")).toHaveClass(/tour-free-roam/); + }); + + test("back to Open the Options Menu step closes the options panel", async ({ page }) => { + await page.evaluate(() => (window as any).UITour.start()); + await page.waitForSelector(".driver-popover", { state: "visible" }); + + // Advance to Layers Tab so the options panel is open. + await advanceSteps(page, 4); + expect(await popoverTitle(page)).toBe(STEP_TITLES[4]); + await expect(page.locator("#options")).toBeVisible(); + + // Go back to Open the Options Menu: onHighlightStarted must close the panel. + await prevStep(page, STEP_TITLES[3]); + await expect(page.locator("#options")).toBeHidden(); + }); + + test("back from World Configurator to Configure World closes the dialog", async ({ page }) => { + await page.evaluate(() => (window as any).UITour.start()); + await page.waitForSelector(".driver-popover", { state: "visible" }); + + // Advance to World Configurator step — dialog is open. + await advanceSteps(page, 13); + expect(await popoverTitle(page)).toBe(STEP_TITLES[13]); + await expect(page.locator("#worldConfigurator")).toBeVisible(); + + // Go back: onHighlightStarted on Configure World must close the dialog. + await prevStep(page, STEP_TITLES[12]); + await expect(page.locator("#worldConfigurator")).toBeHidden(); + await expect(page.locator("#optionsContent")).toBeVisible(); + }); + + test("back from Tools Tab to World Configurator reopens the dialog", async ({ page }) => { + await page.evaluate(() => (window as any).UITour.start()); + await page.waitForSelector(".driver-popover", { state: "visible" }); + + // Advance to Tools Tab — World Configurator dialog is closed. + await advanceSteps(page, 14); + expect(await popoverTitle(page)).toBe(STEP_TITLES[14]); + await expect(page.locator("#worldConfigurator")).toBeHidden(); + + // Go back: onHighlightStarted on World Configurator must reopen the dialog. + await prevStep(page, STEP_TITLES[13]); + await expect(page.locator("#worldConfigurator")).toBeVisible(); + }); + + test("back from About Tab to Heightmap Editor shows the customization panel", async ({ page }) => { + await page.evaluate(() => (window as any).UITour.start()); + await page.waitForSelector(".driver-popover", { state: "visible" }); + + // Advance to About Tab — customization panel was hidden when leaving step 16. + await advanceSteps(page, 17); + expect(await popoverTitle(page)).toBe(STEP_TITLES[17]); + await expect(page.locator("#customizationMenu")).toBeHidden(); + + // Go back: onHighlightStarted on Heightmap Editor must show the panel. + await prevStep(page, STEP_TITLES[16]); + await expect(page.locator("#customizationMenu")).toBeVisible(); + await expect(page.locator("#toolsContent")).toBeHidden(); + }); + + test("back from Heightmap Editor to Edit the Heightmap hides the customization panel", async ({ page }) => { + await page.evaluate(() => (window as any).UITour.start()); + await page.waitForSelector(".driver-popover", { state: "visible" }); + + // Arrive at Heightmap Editor from About Tab backward (panel visible). + await advanceSteps(page, 17); + await prevStep(page, STEP_TITLES[16]); + await expect(page.locator("#customizationMenu")).toBeVisible(); + + // Go back one more: onDeselected on Heightmap Editor must hide the panel. + await prevStep(page, STEP_TITLES[15]); + await expect(page.locator("#customizationMenu")).toBeHidden(); + await expect(page.locator("#toolsContent")).toBeVisible(); + }); + + test("back from Export Options to Export closes the export dialog", async ({ page }) => { + await page.evaluate(() => (window as any).UITour.start()); + await page.waitForSelector(".driver-popover", { state: "visible" }); + + // Advance to Export Options — export dialog is open. + await advanceSteps(page, 20); + expect(await popoverTitle(page)).toBe(STEP_TITLES[20]); + await expect(page.locator("#exportMapData")).toBeVisible(); + + // Go back: onHighlightStarted on Export must close the dialog. + await prevStep(page, STEP_TITLES[19]); + await expect(page.locator("#exportMapData")).toBeHidden(); + }); + + test("back from Save and Load Maps to Export Options reopens the export dialog", async ({ page }) => { + await page.evaluate(() => (window as any).UITour.start()); + await page.waitForSelector(".driver-popover", { state: "visible" }); + + // Advance to the final step — export dialog was closed by step 20's onNextClick. + await advanceSteps(page, 21); + expect(await popoverTitle(page)).toBe(STEP_TITLES[21]); + await expect(page.locator("#exportMapData")).toBeHidden(); + + // Go back: onHighlightStarted on Export Options must reopen the dialog. + await prevStep(page, STEP_TITLES[20]); + await expect(page.locator("#exportMapData")).toBeVisible(); + }); + // ── Cleanup ──────────────────────────────────────────────────────────────── test("dismissing the tour removes driver-active and closes the options panel", async ({