From e52d6e66eb12001109e9498d76c326b69c77f75b Mon Sep 17 00:00:00 2001 From: Shinuwa Date: Mon, 14 Sep 2026 13:58:40 +0200 Subject: [PATCH] Cover remaining Sass families and fix autocomplete hover --- .beads/interactions.jsonl | 1 + DESIGN_SYSTEM.md | 1 + docs/FEATURE_CHECKLIST.md | 8 + e2e/card-surfaces.spec.js | 14 +- e2e/fixtures/compiled-styles.js | 32 ++ e2e/nds-controls.spec.js | 146 +++++++++ e2e/nds-patterns.spec.js | 151 +++++++++ e2e/style-layouts.spec.js | 95 ++++++ e2e/tool-style-states.spec.js | 135 ++++++++ eslint.config.js | 2 + tests/react-lint.test.mjs | 10 +- tests/static-styles.test.mjs | 306 ++---------------- .../design-system/attributes/_fields.scss | 2 +- 13 files changed, 617 insertions(+), 286 deletions(-) create mode 100644 e2e/fixtures/compiled-styles.js create mode 100644 e2e/nds-controls.spec.js create mode 100644 e2e/nds-patterns.spec.js create mode 100644 e2e/style-layouts.spec.js create mode 100644 e2e/tool-style-states.spec.js diff --git a/.beads/interactions.jsonl b/.beads/interactions.jsonl index 2e9278b..a33c391 100644 --- a/.beads/interactions.jsonl +++ b/.beads/interactions.jsonl @@ -48,3 +48,4 @@ {"id":"int-6f399629fa8cc8212d36ca21853eaa52","kind":"field_change","created_at":"2026-09-13T17:46:00.430152831Z","actor":"Shinuwa","issue_id":"sokko-g-zsd","extra":{"field":"status","new_value":"open","old_value":"in_progress"}} {"id":"int-3823a391b02ae7a3d58956b24c10e998","kind":"field_change","created_at":"2026-09-13T18:43:29.457174095Z","actor":"Shinuwa","issue_id":"sokko-g-zsd","extra":{"field":"status","new_value":"open","old_value":"in_progress"}} {"id":"int-e3a2bea901894d43a31eb628b588648a","kind":"field_change","created_at":"2026-09-14T07:08:33.027716428Z","actor":"Shinuwa","issue_id":"sokko-g-zsd","extra":{"field":"status","new_value":"open","old_value":"in_progress"}} +{"id":"int-20d1e0d862e6155eadf2c36eced314db","kind":"field_change","created_at":"2026-09-14T11:56:36.962825878Z","actor":"Shinuwa","issue_id":"sokko-g-zsd","extra":{"field":"status","new_value":"open","old_value":"in_progress"}} diff --git a/DESIGN_SYSTEM.md b/DESIGN_SYSTEM.md index 6508bf5..f23e2eb 100644 --- a/DESIGN_SYSTEM.md +++ b/DESIGN_SYSTEM.md @@ -671,6 +671,7 @@ Select triggers : Menus et palettes : - utiliser `floating-control-menu` pour les menus flottants compacts ; +- les suggestions `.nds-autocomplete-option` utilisent le même fond doré au survol et en état `.is-active`, sans être écrasées par le hover générique des boutons ; - utiliser `compact-menu-button` pour les options d'icones ou d'actions compactes ; - utiliser `color-swatch`, `color-swatch-rainbow` et `color-swatch-active` pour les palettes couleur ; - utiliser `large-icon-picker-trigger` pour les gros triggers de choix d'icone de toolbox ; diff --git a/docs/FEATURE_CHECKLIST.md b/docs/FEATURE_CHECKLIST.md index a11d06b..191cd3b 100644 --- a/docs/FEATURE_CHECKLIST.md +++ b/docs/FEATURE_CHECKLIST.md @@ -71,6 +71,14 @@ Checklist à suivre lors de l'ajout ou de la modification d'un outil, d'une page bordures, séparateurs dorés, hover sans redimensionnement, focus clavier NDS et faces MHW. Ils ne créent pas de snapshots visuels et ne remplacent pas les parcours des pages réelles. +- Les suites `nds-controls`, `nds-patterns`, `style-layouts` et `tool-style-states` + complètent cette couverture : tailles et états NDS, panneaux, feedbacks, + modales/drawers/toasts, scrollbars, mouvement réduit, états des outils et + masques d'icônes décodables. Les layouts home/À propos/librairie sont aussi + vérifiés sur les pages réelles en desktop, tablette et mobile. +- `tests/static-styles.test.mjs` conserve les frontières d'architecture, les + conventions publiques et les limites de taille des fichiers, pas des preuves + de rendu basées sur la présence d'un nom de classe ou de mixin dans le Sass. ## Validation avant push diff --git a/e2e/card-surfaces.spec.js b/e2e/card-surfaces.spec.js index 97f9adf..b12403f 100644 --- a/e2e/card-surfaces.spec.js +++ b/e2e/card-surfaces.spec.js @@ -1,14 +1,9 @@ // Rôle : vérifie les surfaces de cartes à partir du Sass compilé et des styles calculés. import { test, expect } from "@playwright/test"; -import { compile } from "sass"; - -let css; -test.beforeAll(() => { - css = compile("website/src/styles/main.scss").css; -}); +import { mountStyles } from "./fixtures/compiled-styles.js"; async function mountCards(page) { - await page.setContent(` + await mountStyles(page, `
Cover
@@ -20,6 +15,7 @@ async function mountCards(page) {
Art
Endemic
+ Flip
Front
Back
@@ -27,8 +23,6 @@ async function mountCards(page) {
`); - await page.addStyleTag({ content: css }); - await page.mouse.move(0, 0); } async function expectSurface(card, { hovered = false, face = false } = {}) { @@ -78,6 +72,8 @@ for (const width of [1280, 390]) { await global.getByRole("button").focus(); await expectSurface(global, { hovered: true }); const monster = page.locator("#monster"); + await expect(monster.locator(".mhwilds-flip-indicator")).toHaveCSS("pointer-events", "none"); + await expect(monster.locator(".mhwilds-flip-indicator")).toHaveCSS("width", "34px"); for (const face of await monster.locator(".mhwilds-card-face").all()) { await expectSurface(face, { face: true }); await expect(face).toHaveCSS("backface-visibility", "hidden"); diff --git a/e2e/fixtures/compiled-styles.js b/e2e/fixtures/compiled-styles.js new file mode 100644 index 0000000..448a2bb --- /dev/null +++ b/e2e/fixtures/compiled-styles.js @@ -0,0 +1,32 @@ +// Rôle : monte le CSS complet compilé et lit les styles calculés des fixtures visuelles. +import { compile } from "sass"; +import { expect } from "@playwright/test"; + +let css; +export async function mountStyles(page, html) { + css ??= compile("website/src/styles/main.scss").css; + await page.setContent(`${html}`); + await page.addStyleTag({ content: css }); + await page.mouse.move(0, 0); +} + +export function computed(locator, property, pseudo = null) { + return locator.evaluate((element, { property, pseudo }) => + element.ownerDocument.defaultView.getComputedStyle(element, pseudo).getPropertyValue(property), + { property, pseudo }); +} + +export async function expectPseudo(locator, pseudo, property, value) { + await expect.poll(() => computed(locator, property, pseudo)).toEqual(value); +} + +export async function expectFits(locator) { + await expect(locator).toBeVisible(); + const viewport = locator.page().viewportSize(); + await expect.poll(async () => (await locator.boundingBox()).x).toBeGreaterThanOrEqual(-1); + await expect.poll(async () => { + const bounds = await locator.boundingBox(); + return bounds.x + bounds.width; + }).toBeLessThanOrEqual(viewport.width + 1); + expect(await locator.evaluate((element) => element.scrollWidth - element.clientWidth)).toBeLessThanOrEqual(1); +} diff --git a/e2e/nds-controls.spec.js b/e2e/nds-controls.spec.js new file mode 100644 index 0000000..088fb5a --- /dev/null +++ b/e2e/nds-controls.spec.js @@ -0,0 +1,146 @@ +// Rôle : vérifie les tailles et états rendus des familles de contrôles NDS. +import { test, expect } from "@playwright/test"; +import { mountStyles, computed, expectPseudo } from "./fixtures/compiled-styles.js"; + +test("fields, selects and date triggers keep public sizes and keyboard focus", async ({ page }) => { + for (const [size, height, textareaHeight] of [["md", 44, 108], ["sm", 36, 88], ["xs", 32, 72]]) { + await mountStyles(page, `
+ + + + + + +
`); + for (const field of await page.locator("input, select, textarea, button").all()) { + const isTextarea = await field.evaluate((element) => element.tagName === "TEXTAREA"); + await expect(field).toHaveCSS("min-height", `${isTextarea ? textareaHeight : height}px`); + await field.focus(); + await expect(field).toHaveCSS("border-top-color", "rgb(139, 92, 246)"); + await expect(field).toHaveCSS("box-shadow", "rgba(139, 92, 246, 0.16) 0px 0px 0px 3px"); + await expect(field).toHaveCSS("outline-style", "solid"); + await field.evaluate((element) => { element.disabled = true; }); + await expect(field).toHaveCSS("cursor", "not-allowed"); + } + await expect(page.locator("textarea")).toHaveCSS("resize", "vertical"); + await expect(page.locator("select")).toHaveCSS("appearance", "none"); + await expect(page.locator("select")).toHaveCSS("background-image", /data:image\/svg/); + expect(await computed(page.locator(".nds-date-trigger"), "mask-image", "::after")).toContain("data:image/svg"); + await expectPseudo(page.locator(".nds-date-trigger"), "::after", "pointer-events", "none"); + } +}); + +test("text and icon buttons retain dimensions, variants, active and joined edges", async ({ page }) => { + await mountStyles(page, `
+ ${[["md", 40], ["sm", 34], ["xs", 30], ["xxs", 26], ["micro", 22]].map(([size]) => + ``).join("")} + ${["md", "sm", "xs"].map((size) => ``).join("")} + ${["primary", "secondary", "ghost", "dashed", "danger"].map((variant) => ``).join("")} + +
`); + for (const [size, button, icon] of [["md", 40, 21], ["sm", 34, 19], ["xs", 30, 17], ["xxs", 26, 15], ["micro", 22, 12]]) { + const target = page.locator(`.nds-icon-button[data-size="${size}"]`); + await expect(target).toHaveCSS("width", `${button}px`); + await expect(target).toHaveCSS("height", `${button}px`); + await expect(target.locator("i")).toHaveCSS("width", `${icon}px`); + await expect(target.locator("i")).toHaveCSS("height", `${icon}px`); + await target.hover(); + await expect(target).toHaveCSS("background-image", /139, 92, 246/); + await expect(target).toHaveCSS("width", `${button}px`); + } + await page.mouse.move(0, 0); + for (const [size, height] of [["md", 42], ["sm", 34], ["xs", 30]]) { + await expect(page.locator(`.nds-button[data-size="${size}"]`)).toHaveCSS("min-height", `${height}px`); + } + await expect(page.locator('[data-variant="primary"]')).toHaveCSS("background-image", /246, 196, 83/); + await expect(page.locator('[data-variant="danger"]')).toHaveCSS("color", "rgb(251, 113, 133)"); + await expect(page.locator('[data-variant="ghost"]')).toHaveCSS("background-color", "rgba(0, 0, 0, 0)"); + await expect(page.locator('[data-variant="dashed"]')).toHaveCSS("border-top-style", "dashed"); + const toggle = page.locator('[data-variant="secondary"]'); + await toggle.evaluate((element) => element.setAttribute("aria-pressed", "true")); + await expect(toggle).toHaveCSS("background-image", /246, 196, 83/); + await toggle.evaluate((element) => { element.disabled = true; }); + await expect(toggle).toHaveCSS("cursor", "not-allowed"); + await expect(page.locator("#edge")).toHaveCSS("border-top-right-radius", "0px"); + await expect(page.locator("#edge")).toHaveCSS("border-right-color", "rgba(0, 0, 0, 0)"); +}); + +test("switches and native checkboxes expose checked and disabled states at each size", async ({ page }) => { + for (const [size, height, width, trackHeight, off, on] of [["md", 34, 34, 18, 3, 17], ["lg", 40, 30, 16, 3, 15], ["sm", 28, 24, 14, 2, 12], ["xs", 26, 18, 12, 2, 7]]) { + await mountStyles(page, ``); + const control = page.locator("button"); + const track = control.locator("i"); + await expect(control).toHaveCSS("min-height", `${height}px`); + await expect(track).toHaveCSS("width", `${width}px`); + await expect(track).toHaveCSS("height", `${trackHeight}px`); + await expectPseudo(track, "::before", "left", `${off}px`); + await control.evaluate((element) => element.setAttribute("aria-pressed", "true")); + await expectPseudo(track, "::before", "left", `${on}px`); + await expectPseudo(track, "::before", "background-color", "rgb(246, 196, 83)"); + } + await mountStyles(page, `
+ ${["md", "sm", "xs"].map((size) => ``).join("")} + +
`); + for (const [size, dimension] of [["md", 20], ["sm", 18], ["xs", 14]]) { + const checkbox = page.getByRole("checkbox", { name: size, exact: true }); + await expect(checkbox).toHaveCSS("width", `${dimension}px`); + await expect(checkbox).toHaveCSS("height", `${dimension}px`); + await expectPseudo(checkbox, "::before", "transform", "matrix(0, 0, 0, 0, 0, 0)"); + await checkbox.check(); + await expectPseudo(checkbox, "::before", "transform", "matrix(1, 0, 0, 1, 0, 0)"); + await checkbox.evaluate((element) => { element.disabled = true; }); + await expect(checkbox).toHaveCSS("cursor", "not-allowed"); + } + await page.getByText("Native", { exact: true }).click(); + const native = page.locator(".nds-native-switch"); + await expect(native.locator("input")).toBeChecked(); + await expectPseudo(native.locator("span"), "::before", "left", "19px"); + await native.locator("input").evaluate((element) => { element.disabled = true; }); + await expect(native).toHaveCSS("cursor", "not-allowed"); +}); + +test("steppers, radio groups and compound fields retain compact geometry", async ({ page }) => { + for (const [size, buttonSize, optionHeight] of [["md", 28, 34], ["sm", 18, 28], ["xs", 16, 26]]) { + await mountStyles(page, `
+
+
+
+
`); + for (const button of await page.locator(".nds-quantity-stepper button").all()) { + await expect(button).toHaveCSS("width", `${buttonSize}px`); + await expect(button).toHaveCSS("height", `${buttonSize}px`); + } + await expect(page.locator(".nds-radio-button-group > button").first()).toHaveCSS("min-height", `${optionHeight}px`); + await expect(page.locator(".nds-radio-button-group > .active")).toHaveCSS("background-image", /246, 196, 83/); + await expect(page.locator(".nds-compound-toggle-field-input")).toHaveCSS("min-height", "26px"); + await expect(page.locator(".nds-compound-toggle-field-input")).toHaveCSS("width", "34px"); + await expect(page.locator(".nds-compound-toggle-field > button")).toHaveCSS("background-image", /246, 196, 83/); + } +}); + +test("palettes, inline fields and autocomplete menus render their specialised states", async ({ page }) => { + await mountStyles(page, `
+ + + + + +
+
`); + await expectPseudo(page.locator(".nds-swatch").first(), "::before", "background-color", "rgb(34, 211, 238)"); + expect(await computed(page.locator('[data-variant="rainbow"]'), "background-image", "::before")).toContain("conic-gradient"); + await expect(page.locator(".nds-large-icon-picker-trigger")).toHaveCSS("width", "100px"); + await expect(page.locator(".nds-large-icon-picker-trigger i")).toHaveCSS("width", "72px"); + await expect(page.locator(".nds-inline-input")).toHaveCSS("background-color", "rgba(0, 0, 0, 0)"); + await page.locator(".nds-inline-input").focus(); + await expect(page.locator(".nds-inline-input")).toHaveCSS("border-top-color", "rgb(139, 92, 246)"); + await expect(page.locator("textarea")).toHaveCSS("background-color", "rgba(0, 0, 0, 0)"); + await expect(page.locator(".nds-autocomplete-menu")).toHaveCSS("position", "absolute"); + await expect(page.locator(".nds-autocomplete-menu")).toHaveCSS("overflow-y", "auto"); + await page.locator(".nds-autocomplete-option").hover(); + await expect(page.locator(".nds-autocomplete-option")).toHaveCSS("background-color", "rgba(246, 196, 83, 0.1)"); + await page.mouse.move(0, 0); + await page.locator(".nds-autocomplete-option").evaluate((element) => element.classList.add("is-active")); + await expect(page.locator(".nds-autocomplete-option")).toHaveCSS("background-color", "rgba(246, 196, 83, 0.1)"); +}); diff --git a/e2e/nds-patterns.spec.js b/e2e/nds-patterns.spec.js new file mode 100644 index 0000000..79f2b9e --- /dev/null +++ b/e2e/nds-patterns.spec.js @@ -0,0 +1,151 @@ +// Rôle : vérifie les surfaces, feedbacks, overlays et préférences visuelles compilés. +import { test, expect } from "@playwright/test"; +import { mountStyles, computed, expectPseudo, expectFits } from "./fixtures/compiled-styles.js"; + +test("panels, tool surfaces and heroes keep layered non-intercepting decoration", async ({ page }) => { + await mountStyles(page, `
+
+
+
+

Hero

+
+

Heading

+
Empty
+
`); + for (const selector of [".nds-panel", ".nds-tool-panel", ".nds-filter-panel", ".nds-page-hero"]) { + const surface = page.locator(selector); + await expect(surface).toHaveCSS("border-top-width", "1px"); + await expect(surface).toHaveCSS("overflow", "hidden"); + await expectPseudo(surface, "::before", "pointer-events", "none"); + await expectPseudo(surface, "::after", "pointer-events", "none"); + expect(await computed(surface, "background-image", "::after")).toContain("gradient"); + await expect(surface.locator(":scope > *")).toHaveCSS("z-index", "1"); + } + await expect(page.locator(".nds-tool-panel")).toHaveCSS("contain", "paint"); + await page.locator(".nds-tool-panel").hover(); + await expect(page.locator(".nds-tool-panel")).toHaveCSS("box-shadow", /139, 92, 246/); + await expect(page.locator(".nds-filter-panel")).toHaveCSS("backdrop-filter", "blur(10px)"); + await expect(page.locator(".nds-page-hero")).toHaveCSS("min-height", "180px"); + await expect(page.locator(".nds-toolbar")).toHaveCSS("flex-wrap", "wrap"); + await expect(page.locator(".nds-section-heading")).toHaveCSS("justify-content", "space-between"); + await expect(page.locator(".nds-empty-state")).toHaveCSS("min-height", "132px"); + await expect(page.locator(".nds-empty-state")).toHaveCSS("align-content", "center"); +}); + +test("badges, notices, progress and chips expose meaningful visual states", async ({ page }) => { + const tones = { success: "rgb(52, 211, 153)", warning: "rgb(251, 191, 36)", danger: "rgb(251, 113, 133)" }; + await mountStyles(page, `
+ ${Object.keys(tones).map((tone) => `${tone}
${tone}
`).join("")} + +
`); + for (const [tone, color] of Object.entries(tones)) { + await expect(page.locator(`.nds-badge[data-variant="${tone}"]`)).toHaveCSS("color", color); + await expect(page.locator(`.nds-inline-notice[data-variant="${tone}"]`)).toHaveCSS("color", color); + const progress = page.locator(`.nds-progress[data-variant="${tone}"]`); + await expect(progress).toHaveCSS("height", "8px"); + await expect(progress).toHaveCSS("overflow", "hidden"); + expect(await computed(progress.locator("span"), "background-image")).toContain(color); + await expect(progress.locator("span")).toHaveCSS("width", "99px"); + await progress.locator("span").evaluate((element) => { element.style.width = "100%"; }); + await expect(progress.locator("span")).toHaveCSS("width", "198px"); + } + const chip = page.locator(".nds-chip"); + await expect(chip).toHaveCSS("background-color", "rgba(5, 7, 17, 0.32)"); + await chip.locator("input").check(); + await page.mouse.move(0, 0); + await expect(chip).toHaveCSS("background-color", "rgba(139, 92, 246, 0.14)"); +}); + +for (const width of [1280, 390]) { + test(`overlays fit, scroll internally and preserve stacking at ${width}px`, async ({ page }) => { + await page.setViewportSize({ width, height: 800 }); + await mountStyles(page, `
+
+

Confirm

Long content
+
`); + const modal = page.locator(".nds-modal"); + await expectFits(modal); + await expect(modal).toHaveCSS("overflow-y", "auto"); + expect((await modal.boundingBox()).height).toBeLessThanOrEqual(752); + await modal.evaluate((element) => { element.scrollTop = 300; }); + expect(await modal.evaluate((element) => element.scrollTop)).toBeGreaterThan(0); + await expect(page.locator(".confirm-modal-root")).toHaveCSS("overscroll-behavior", "contain"); + await expect(page.locator(".nds-modal-backdrop")).toHaveCSS("position", "fixed"); + await expect(page.locator(".nds-modal-backdrop")).toHaveCSS("backdrop-filter", "blur(6px)"); + await page.locator("body").evaluate((element) => element.classList.add("is-modal-open")); + await expect(page.locator("body")).toHaveCSS("overflow", "hidden"); + + await mountStyles(page, `
+
Drawer
+
`); + const drawer = page.locator(".drawer-panel"); + await expectFits(drawer); + await expect(drawer).toHaveCSS("top", width <= 760 ? "110px" : "68px"); + await expect(drawer).toHaveCSS("position", "fixed"); + await expect(drawer).toHaveCSS("overflow-y", "auto"); + await expect(drawer).toHaveCSS("backdrop-filter", "blur(10px)"); + await expect(page.locator(".drawer-resize-handle")).toHaveCSS("display", width <= 760 ? "none" : "block"); + await page.locator(".drawer").evaluate((element) => element.setAttribute("aria-hidden", "true")); + await expect(drawer).toBeHidden(); + + await mountStyles(page, `
Tools
`); + await expectFits(page.locator(".tool-add-modal")); + await expect(page.locator(".tool-add-modal-root")).toHaveCSS("overscroll-behavior", "contain"); + await expect(page.locator(".tool-add-modal-root")).toHaveCSS("z-index", "130"); + }); + + test(`toast positions, popovers and reduced motion render correctly at ${width}px`, async ({ page }) => { + await page.setViewportSize({ width, height: 800 }); + await mountStyles(page, `
Popover
+
Notification
`); + const stack = page.locator(".nds-toast-stack"); + const toast = page.locator(".nds-toast"); + await expect(stack).toHaveCSS("position", "fixed"); + await expect(stack).toHaveCSS("pointer-events", "none"); + await expect(toast).toHaveCSS("pointer-events", "auto"); + await expect(toast).toHaveCSS("background-image", /conic-gradient/); + await expect.poll(async () => (await toast.boundingBox()).x + (await toast.boundingBox()).width).toBeLessThanOrEqual(width); + await expect(page.locator(".nds-popover")).toHaveCSS("overflow", "hidden"); + await expect(page.locator(".nds-popover")).toHaveCSS("background-color", "rgba(7, 10, 24, 0.96)"); + await stack.evaluate((element) => { element.dataset.position = "left"; element.classList.add("is-left"); }); + await expect(stack).toHaveCSS("left", "20px"); + await expect(toast).toHaveCSS("animation-name", /notification-toast-enter-left/); + await page.emulateMedia({ reducedMotion: "reduce" }); + await expect(toast).toHaveCSS("animation-duration", "1e-05s"); + await expect(toast).toHaveCSS("animation-iteration-count", "1"); + await expectFits(toast); + }); +} + +test("event sizes preserve metadata density and light/dark contrast", async ({ page }) => { + await mountStyles(page, `
${["md", "sm", "xs"].map((size) => `
`).join("")}
`); + for (const [size, height] of [["md", 44], ["sm", 22], ["xs", 10]]) { + const event = page.locator(`[data-size="${size}"]`); + await expect(event).toHaveCSS("min-height", `${height}px`); + await expect(event.locator("span")).toHaveCSS("display", size === "md" ? "block" : "none"); + await expect(event.locator("strong")).toHaveCSS("text-overflow", "ellipsis"); + const light = await computed(event.locator("button"), "color"); + await event.evaluate((element) => { element.dataset.variant = "dark"; element.style.setProperty("--nds-event-color", "#f6c453"); }); + await expect.poll(() => computed(event.locator("button"), "color")).not.toBe(light); + await event.locator("button").hover(); + await expect(event.locator("button")).toHaveCSS("background-color", size === "xs" ? "rgba(0, 0, 0, 0.18)" : "rgba(0, 0, 0, 0.14)"); + } +}); + +test("scrollbars and nebula layers compile without blocking content", async ({ page }) => { + const classes = ["themed-scrollbar", "legacy-scrollbar", "nds-scrollbox", "module-content", "table-grid-wrap", "notepad-surface", "annotation-marker-list", "goal-tree-view-wrap", "filter-options"]; + await mountStyles(page, classes.map((name) => `
Scroll
`).join("")); + for (const name of classes) { + const box = page.locator(`.${name}`); + await expect(box).toHaveCSS("scrollbar-width", "thin"); + await expect(box).toHaveCSS("scrollbar-color", "rgb(88, 55, 174) rgba(0, 0, 0, 0)"); + await expectPseudo(box, "::-webkit-scrollbar", "width", "6px"); + await box.evaluate((element) => { element.scrollTop = 100; }); + expect(await box.evaluate((element) => element.scrollTop)).toBeGreaterThan(0); + } + for (const pseudo of ["::before", "::after"]) { + await expectPseudo(page.locator("body"), pseudo, "position", "fixed"); + await expectPseudo(page.locator("body"), pseudo, "pointer-events", "none"); + expect(await computed(page.locator("body"), "background-image", pseudo)).toContain("radial-gradient"); + } +}); diff --git a/e2e/style-layouts.spec.js b/e2e/style-layouts.spec.js new file mode 100644 index 0000000..3129ff1 --- /dev/null +++ b/e2e/style-layouts.spec.js @@ -0,0 +1,95 @@ +// Rôle : vérifie les layouts responsive réels et les contraintes de placement compilées. +import { test, expect } from "@playwright/test"; +import { mountStyles, computed, expectFits } from "./fixtures/compiled-styles.js"; + +async function columns(locator, count) { + await expect(locator).toHaveCSS("display", "grid"); + await expect.poll(async () => (await computed(locator, "grid-template-columns")).split(" ").length).toBe(count); +} + +for (const width of [1280, 900, 390]) { + test(`home, about and library retain their responsive layout at ${width}px`, async ({ page }) => { + await page.setViewportSize({ width, height: 900 }); + await page.goto("/"); + const onboarding = page.locator(".home-onboarding"); + await expectFits(onboarding); + await columns(page.locator(".home-onboarding-layout"), width <= 760 ? 1 : 2); + await expect(page.locator(".home-onboarding-slider figcaption")).toHaveCSS("position", "absolute"); + await expect(page.locator(".home-onboarding-dots")).toHaveCSS("display", "flex"); + await expect(page.locator(".home-onboarding-slider img")).toHaveCSS("object-fit", "cover"); + const side = page.locator(".sidebar"); + await expect(side).toHaveCSS("display", width <= 760 ? "none" : "flex"); + await expect(page.locator(".sidebar-mascot")).toHaveCSS("display", width <= 1024 ? "none" : "block"); + for (const link of await page.locator(".sidebar-about-link").all()) { + await expect(link).toHaveCSS("align-items", "center"); + if (width <= 1024) await expect(link).toHaveCSS("justify-content", "center"); + } + await page.goto("/#/about"); + await expectFits(page.locator(".about-page")); + await expect(page.locator(".about-story-section")).toHaveCSS("display", "grid"); + await columns(page.locator(".about-tools-list"), width <= 760 ? 1 : 2); + await expect(page.locator(".about-tool-item .module-icon").first()).toHaveCSS("width", "34px"); + await page.goto("/#/library"); + await expectFits(page.locator(".library-page")); + await columns(page.locator(".library-doc-layout"), width <= 760 ? 1 : 2); + expect(await page.locator("body").evaluate((element) => element.scrollWidth)).toBeLessThanOrEqual(width); + }); +} + +for (const width of [1280, 390]) { + test(`game grids and toolbox columns adapt without horizontal overflow at ${width}px`, async ({ page }) => { + await page.setViewportSize({ width, height: 900 }); + await mountStyles(page, `
+
Game
Game
+
A
B
+
Formula
12
+
+
Equipment
Materials
+
Actions
Preferences
+
`); + await columns(page.locator(".game-layout"), width <= 760 ? 1 : 2); + await expect(page.locator(".game-grid")).toHaveCSS("display", "grid"); + await columns(page.locator(".modules"), width <= 760 ? 1 : 2); + await expect(page.locator(".module-column").first()).toHaveCSS("display", width <= 760 ? "contents" : "grid"); + await columns(page.locator(".calculator-module"), width <= 760 ? 1 : 2); + await expect(page.locator(".calculator-result")).toHaveCSS("min-height", "72px"); + await expect(page.locator(".task-planner-module")).toHaveCSS("display", "grid"); + await columns(page.locator(".task-planner-settings"), width <= 760 ? 1 : 2); + await expect(page.locator(".equipment-planner-summary")).toHaveCSS("padding", "12px"); + await columns(page.locator(".equipment-planner-summary-source-grid"), width <= 760 ? 1 : 2); + await page.locator(".equipment-planner-summary-source-grid").evaluate((element) => element.classList.add("columns-1")); + await columns(page.locator(".equipment-planner-summary-source-grid"), 1); + await expect(page.locator(".toolbox-actions-row")).toHaveCSS("flex-direction", width <= 760 ? "column" : "row"); + await page.locator(".modules").evaluate((element) => { element.dataset.layoutCols = "1"; }); + await columns(page.locator(".modules"), 1); + await expectFits(page.locator("main")); + }); + + test(`cover crop and icon picker respect image geometry at ${width}px`, async ({ page }) => { + await page.setViewportSize({ width, height: 900 }); + await mountStyles(page, `
+
+
+
+
Game
Game page +

Aside

+
`); + const frame = page.locator(".toolbox-cover-crop-frame"); + await expectFits(frame); + const bounds = await frame.boundingBox(); + expect(bounds.width / bounds.height).toBeCloseTo(620 / 248, 2); + await expect(frame).toHaveCSS("touch-action", "none"); + await expect(frame).toHaveCSS("overflow", "hidden"); + const icon = page.locator(".toolbox-icon-picker-button"); + await expect(icon.locator("img")).toHaveCSS("object-fit", "contain"); + await expect(icon.locator("img")).toHaveCSS("width", "72px"); + expect(await computed(icon.locator("img"), "border-radius")).toBe(await computed(icon, "border-radius")); + await icon.evaluate((element) => element.classList.add("has-custom-cover")); + await expect(icon.locator("img")).toHaveCSS("object-fit", "cover"); + await expect(page.locator(".toolbox-game-icon img")).toHaveCSS("object-fit", "cover"); + await expect(page.locator(".toolbox-cover-menu-action")).toHaveCSS("grid-column", "1 / -1"); + await expect(page.locator(".toolbox-icon-picker-menu")).toHaveCSS("position", "absolute"); + await expect(page.locator(".toolbox-hero-link")).toHaveCSS("display", "flex"); + await expect(page.locator(".dialogue-line i")).toHaveCSS("font-style", "italic"); + }); +} diff --git a/e2e/tool-style-states.spec.js b/e2e/tool-style-states.spec.js new file mode 100644 index 0000000..576060c --- /dev/null +++ b/e2e/tool-style-states.spec.js @@ -0,0 +1,135 @@ +// Rôle : vérifie les états visuels des outils, leurs pickers et les masques d'icônes compilés. +import { test, expect } from "@playwright/test"; +import { mountStyles, computed, expectPseudo, expectFits } from "./fixtures/compiled-styles.js"; + +test("table cells distinguish selection, move targets, editing and errors", async ({ page }) => { + await mountStyles(page, `
`); + await expect(page.locator(".table-module")).toHaveCSS("display", "grid"); + const cell = page.locator("input"); + await expect(cell).toHaveCSS("caret-color", "rgba(0, 0, 0, 0)"); + await cell.evaluate((element) => element.classList.add("is-selected")); + await expect(cell).toHaveCSS("box-shadow", /34, 211, 238/); + await cell.evaluate((element) => { element.className = "table-cell-input is-move-target"; }); + await expect(cell).toHaveCSS("box-shadow", /246, 196, 83/); + await expect(cell).toHaveCSS("cursor", "move"); + await cell.evaluate((element) => { element.className = "table-cell-input is-editing"; }); + await cell.focus(); + await expect(cell).not.toHaveCSS("caret-color", "rgba(0, 0, 0, 0)"); + await cell.evaluate((element) => { element.blur(); element.className = "table-cell-input has-error"; }); + await expect(cell).toHaveCSS("color", "rgb(254, 202, 202)"); +}); + +test("goal nodes and NDS reorder indicators follow orientation and placement", async ({ page }) => { + await mountStyles(page, `
+
1Goal
+
+
`); + const node = page.locator(".goal-tree-node"); + const card = page.locator(".goal-tree-node-card"); + const indicator = page.locator(".nds-reorder-indicator"); + await expect(card).toHaveCSS("border-top-color", "rgba(246, 196, 83, 0.48)"); + await expect(page.locator(".goal-tree-node-content-row")).toHaveCSS("display", "grid"); + for (const axis of ["vertical", "horizontal"]) { + for (const placement of ["before", "after"]) { + await node.evaluate((element, { axis, placement }) => { element.dataset.reorderOrientation = axis; element.classList.toggle("drop-after", placement === "after"); }, { axis, placement }); + await indicator.evaluate((element, { axis, placement }) => { element.dataset.axis = axis; element.dataset.placement = placement; }, { axis, placement }); + const side = axis === "vertical" ? (placement === "before" ? "top" : "bottom") : (placement === "before" ? "left" : "right"); + const thickness = axis === "vertical" ? "height" : "width"; + await expectPseudo(card, "::after", side, "-8px"); + await expectPseudo(card, "::after", thickness, "3px"); + await expectPseudo(indicator, "::before", side, "0px"); + await expectPseudo(indicator, "::before", thickness, "3px"); + await expectPseudo(card, "::after", "pointer-events", "none"); + } + } +}); + +test("calendar picker opens on focus and alert controls retain blocked states", async ({ page }) => { + await mountStyles(page, `
+
+
+ +
`); + await expect(page.locator("form")).toHaveCSS("display", "grid"); + await expect(page.locator("form")).toHaveCSS("background-color", "rgba(0, 0, 0, 0)"); + const picker = page.locator(".calendar-month-date-picker"); + await expect(picker).toHaveCSS("opacity", "0"); + await expect(picker).toHaveCSS("pointer-events", "none"); + await page.locator(".calendar-month-date-trigger").focus(); + await expect(picker).toHaveCSS("opacity", "1"); + await expect(picker).toHaveCSS("pointer-events", "auto"); + expect((await computed(picker, "grid-template-columns")).split(" ")).toHaveLength(7); + const alert = page.locator(".timer-alert-mode-switch"); + await expect(alert).toHaveCSS("align-self", "start"); + await alert.evaluate((element) => element.classList.add("is-blocked")); + await expect(alert.locator("button")).toHaveCSS("opacity", "0.2"); + await expect(alert.locator("button")).toHaveCSS("cursor", "not-allowed"); + const scroll = page.locator(".tool-split-scroll-toggle"); + await scroll.focus(); + await expect(picker).toHaveCSS("opacity", "0"); + await expectPseudo(scroll.locator("i"), "::before", "left", "3px"); + await scroll.evaluate((element) => element.classList.add("active")); + await expectPseudo(scroll.locator("i"), "::before", "left", "17px"); +}); + +test("combo tokens distinguish analog, directional and held inputs", async ({ page }) => { + await mountStyles(page, `
+
+ Up + Up + +
`); + await expect(page.locator(".combo-hold-controls")).toHaveCSS("width", "92px"); + await expect(page.locator(".combo-hold-controls button")).toHaveCSS("background-image", /246, 196, 83/); + await expect(page.locator("#analog")).toHaveCSS("color", "rgb(165, 243, 252)"); + await expect(page.locator("#direction")).toHaveCSS("color", "rgb(248, 217, 130)"); + await expect(page.locator("#direction")).toHaveCSS("border-radius", "10px"); + await expect(page.locator(".combo-value-grab .ui-icon")).toHaveCSS("width", "24px"); + await expect(page.locator(".has-hold")).toHaveCSS("box-shadow", /7px/); +}); + +for (const width of [1280, 390]) { + test(`text exchange and image annotation keep usable bounds at ${width}px`, async ({ page }) => { + await page.setViewportSize({ width, height: 900 }); + await mountStyles(page, `
+
+
+
`); + await expectFits(page.locator(".text-import-modal-dialog")); + await expect(page.locator(".text-import-actions")).toHaveCSS("flex-wrap", "wrap"); + await expect(page.locator("textarea")).toHaveCSS("border-top-color", "rgba(248, 113, 113, 0.75)"); + const marker = page.locator(".annotation-marker"); + for (const position of ["0%", "50%", "100%"]) { + await marker.evaluate((element, position) => { element.style.setProperty("--marker-x", position); element.style.setProperty("--marker-y", position); }, position); + const box = await marker.boundingBox(); + const frame = await page.locator("#image").boundingBox(); + expect(box.width).toBe(30); + expect(box.height).toBe(30); + expect(box.x).toBeGreaterThanOrEqual(frame.x); + expect(box.y).toBeGreaterThanOrEqual(frame.y); + expect(box.x + box.width).toBeLessThanOrEqual(frame.x + frame.width); + expect(box.y + box.height).toBeLessThanOrEqual(frame.y + frame.height); + } + }); +} + +test("public icon masks resolve to decodable assets instead of empty shapes", async ({ page }) => { + const icons = ["drag", "dropdown", "enter", "calculator", "scrollable", "hide", "eye-open", "eye-closed", "chevron-down", "chevron-up", "flip", "add", "add-row", "add-column", "remove-row", "remove-column", "table", "map", "grab", "shield", "gun"]; + const modules = ["abacus", "calculator", "columns", "table", "chest-armor"]; + await page.route("**/__style-icons", (route) => route.fulfill({ contentType: "text/html", body: "" })); + await page.goto("/__style-icons"); + await mountStyles(page, `
${icons.map((name) => ``).join("")}${modules.map((name) => ``).join("")}
`); + for (const icon of await page.locator("i").all()) { + await expect(icon).toHaveCSS("mask-size", "contain"); + const mask = await computed(icon, "mask-image"); + expect(mask).toMatch(/^url\(".+"\)$/); + const url = JSON.parse(mask.slice(4, -1)); + const decoded = await icon.evaluate(async (element, url) => { + const image = new element.ownerDocument.defaultView.Image(); + image.src = url; + await image.decode(); + return image.naturalWidth > 0 && image.naturalHeight > 0; + }, url); + expect(decoded, url).toBe(true); + } +}); diff --git a/eslint.config.js b/eslint.config.js index 34297b6..81c1e27 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -9,6 +9,8 @@ export default [ ignores: [ "node_modules/**", "website/dist/**", + "playwright-report/**", + "test-results/**", ".codex-home/**" ] }, diff --git a/tests/react-lint.test.mjs b/tests/react-lint.test.mjs index 830bad2..67890c2 100644 --- a/tests/react-lint.test.mjs +++ b/tests/react-lint.test.mjs @@ -1,4 +1,4 @@ -// Rôle : vérifie que les contrôles React critiques restent actifs dans la configuration ESLint effective. +// Rôle : vérifie les règles React actives et l'exclusion des artefacts générés du lint. import { test } from "node:test"; import assert from "node:assert/strict"; import { ESLint } from "eslint"; @@ -10,3 +10,11 @@ test("React effect and purity checks are enabled for application sources", async assert.ok(config.rules[rule][0] > 0, `${rule} must remain enabled`); } }); + +test("browser reports are ignored without excluding authored E2E tests", async () => { + const eslint = new ESLint(); + for (const path of ["playwright-report/trace/assets/viewer.js", "test-results/production/trace.js"]) { + assert.equal(await eslint.isPathIgnored(path), true, path); + } + assert.equal(await eslint.isPathIgnored("e2e/nds-controls.spec.js"), false); +}); diff --git a/tests/static-styles.test.mjs b/tests/static-styles.test.mjs index 46eb1db..eabebb6 100644 --- a/tests/static-styles.test.mjs +++ b/tests/static-styles.test.mjs @@ -1,32 +1,33 @@ -// Rôle : vérifie les conventions statiques des styles Sass. -import { readFile } from "node:fs/promises"; +// Rôle : protège les frontières Sass, les conventions publiques et le découpage des fichiers. +import { readFile, readdir } from "node:fs/promises"; import { test } from "node:test"; import assert from "node:assert/strict"; -async function readJoinedStyles(paths) { - return (await Promise.all(paths.map((path) => readFile(path, "utf8")))).join("\n"); -} - function countLines(source) { return source.trimEnd().split("\n").length; } -test("style entrypoint and theme layers are wired", async () => { - const styles = await readFile("website/src/styles/main.scss", "utf8"); - const styleNebula = await readFile("website/src/styles/_nebula.scss", "utf8"); - const styleTokens = await readFile("website/src/styles/_tokens.scss", "utf8"); - const styleBase = await readFile("website/src/styles/_base.scss", "utf8"); - const styleMixins = await readFile("website/src/styles/_mixins.scss", "utf8"); - const styleShell = await readFile("website/src/styles/_shell.scss", "utf8"); - const styleCards = await readFile("website/src/styles/_cards.scss", "utf8"); - const styleControls = await readFile("website/src/styles/design-system/_controls.scss", "utf8"); - const styleAttributes = await readFile("website/src/styles/design-system/_attributes.scss", "utf8"); - const styleControlsPatterns = await readFile("website/src/styles/design-system/controls/_patterns.scss", "utf8"); - const styleAttributesPatterns = await readFile("website/src/styles/design-system/attributes/_patterns.scss", "utf8"); - const styleControlsButtons = await readFile("website/src/styles/design-system/controls/_buttons.scss", "utf8"); - const styleAttributesButtons = await readFile("website/src/styles/design-system/attributes/_buttons.scss", "utf8"); - const goalTreeBranch = await readFile("website/src/features/toolboxes/modules/GoalTreeBranch.jsx", "utf8"); - const ndsPatternConsumers = await readJoinedStyles([ +test("Sass layers retain ownership boundaries and exclude removed APIs", async () => { + const root = "website/src/styles"; + const paths = (await readdir(root, { recursive: true })).filter((path) => path.endsWith(".scss")); + const sources = new Map(await Promise.all(paths.map(async (path) => [path, await readFile(`${root}/${path}`, "utf8")]))); + assert.doesNotMatch(sources.get("_base.scss"), /\.(?:card-icon-button|drawer-action-button|import-icon-button)/); + assert.doesNotMatch(sources.get("_cards.scss"), /@use "design-system\/surfaces"|controls\.card-pattern|\.compact-data-card\s*\{/); + assert.doesNotMatch(sources.get("_mhwilds.scss"), /\.game-(home-card|layout|grid)/); + const home = [...sources].filter(([path]) => path === "_home.scss" || path.startsWith("home/")).map(([, source]) => source).join("\n"); + assert.doesNotMatch(home, /\.nebula-panel\s*\{/); + assert.doesNotMatch(sources.get("toolboxes/_notepad.scss"), /lecture seule/); + assert.match(sources.get("design-system/_controls.scss"), /@forward "controls\/patterns"/); + assert.match(sources.get("design-system/_attributes.scss"), /@use "attributes\/patterns"/); + const branch = await readFile("website/src/features/toolboxes/modules/GoalTreeBranch.jsx", "utf8"); + assert.doesNotMatch( + [...sources.values(), branch].join("\n"), + /(?:data-size="tiny"|icon-button(?:-icon)?\(tiny\)|\$size == tiny)/ + ); +}); + +test("product components keep the public NDS pattern vocabulary", async () => { + const paths = [ "website/src/pages/HomePage.jsx", "website/src/pages/LibraryPage.jsx", "website/src/pages/AboutPage.jsx", @@ -40,259 +41,14 @@ test("style entrypoint and theme layers are wired", async () => { "website/src/components/ToolboxModals.jsx", "website/src/features/toolboxes/modules/AddToolControls.jsx", "website/src/features/toolboxes/modules/TextImportModal.jsx" - ]); - const styleHome = await readJoinedStyles([ - "website/src/styles/_home.scss", - "website/src/styles/home/_shared.scss", - "website/src/styles/home/_onboarding.scss", - "website/src/styles/home/_library.scss", - "website/src/styles/home/library/_page.scss", - "website/src/styles/home/library/_summary.scss", - "website/src/styles/home/library/_toc.scss", - "website/src/styles/home/library/_tool-cards.scss", - "website/src/styles/home/library/_examples.scss", - "website/src/styles/home/library/_previews.scss", - "website/src/styles/home/_story.scss", - "website/src/styles/home/_about.scss", - "website/src/styles/home/about/_page.scss", - "website/src/styles/home/about/_story.scss", - "website/src/styles/home/about/_contribute.scss", - "website/src/styles/home/about/_tools.scss", - "website/src/styles/home/about/_reminders.scss", - "website/src/styles/home/_stats.scss" - ]); - const styleToolboxesEntry = await readFile("website/src/styles/_toolboxes.scss", "utf8"); - const styleToolboxesCore = await readFile("website/src/styles/toolboxes/_core.scss", "utf8"); - const styleToolboxLayout = await readJoinedStyles([ - "website/src/styles/toolboxes/_layout.scss", - "website/src/styles/toolboxes/layout/_header.scss", - "website/src/styles/toolboxes/layout/_icon-picker.scss", - "website/src/styles/toolboxes/layout/_cover-editor.scss", - "website/src/styles/toolboxes/layout/_tutorial.scss", - "website/src/styles/toolboxes/layout/_add-tools.scss", - "website/src/styles/toolboxes/layout/_preferences.scss", - "website/src/styles/toolboxes/layout/_modules.scss" - ]); - const styleToolboxShell = await readFile("website/src/styles/toolboxes/_module-shell.scss", "utf8"); - const styleToolboxTextExchange = await readFile("website/src/styles/toolboxes/_text-exchange.scss", "utf8"); - const styleToolboxNotepad = await readFile("website/src/styles/toolboxes/_notepad.scss", "utf8"); - const styleToolboxChecklist = await readFile("website/src/styles/toolboxes/_checklist.scss", "utf8"); - const styleToolboxCalculator = await readFile("website/src/styles/toolboxes/_calculator.scss", "utf8"); - const styleToolboxCalculatorResults = await readFile("website/src/styles/toolboxes/_calculator-results.scss", "utf8"); - const styleToolboxTable = await readFile("website/src/styles/toolboxes/_table.scss", "utf8"); - const styleToolboxCombos = await readJoinedStyles([ - "website/src/styles/toolboxes/_combos.scss", - "website/src/styles/toolboxes/combos/_editor.scss", - "website/src/styles/toolboxes/combos/_palette.scss", - "website/src/styles/toolboxes/combos/_list.scss", - "website/src/styles/toolboxes/combos/_sequence.scss", - "website/src/styles/toolboxes/combos/_tokens.scss", - "website/src/styles/toolboxes/combos/_hold.scss" - ]); - const styleToolboxImages = await readFile("website/src/styles/toolboxes/_images.scss", "utf8"); - const styleToolboxGoalTree = await readJoinedStyles([ - "website/src/styles/toolboxes/_goal-tree.scss", - "website/src/styles/toolboxes/goal-tree/_editor.scss", - "website/src/styles/toolboxes/goal-tree/_view.scss", - "website/src/styles/toolboxes/goal-tree/_nodes.scss", - "website/src/styles/toolboxes/goal-tree/_node-card.scss", - "website/src/styles/toolboxes/goal-tree/_node-content.scss", - "website/src/styles/toolboxes/goal-tree/_node-actions.scss", - "website/src/styles/toolboxes/goal-tree/_node-drag.scss", - "website/src/styles/toolboxes/goal-tree/_node-responsive.scss" - ]); - const styleToolboxCalendar = await readJoinedStyles([ - "website/src/styles/toolboxes/_calendar.scss", - "website/src/styles/toolboxes/calendar/_base.scss", - "website/src/styles/toolboxes/calendar/_form.scss", - "website/src/styles/toolboxes/calendar/_form-layout.scss", - "website/src/styles/toolboxes/calendar/_date-picker.scss", - "website/src/styles/toolboxes/calendar/_event-controls.scss", - "website/src/styles/toolboxes/calendar/_form-actions.scss", - "website/src/styles/toolboxes/calendar/_week.scss", - "website/src/styles/toolboxes/calendar/_month.scss", - "website/src/styles/toolboxes/calendar/_responsive.scss" - ]); - const styleToolboxSharedControls = await readJoinedStyles([ - "website/src/styles/toolboxes/_shared-controls.scss", - "website/src/styles/toolboxes/shared-controls/_selects.scss", - "website/src/styles/toolboxes/shared-controls/_split-controls.scss", - "website/src/styles/toolboxes/shared-controls/_split-tree.scss", - "website/src/styles/toolboxes/shared-controls/_mix-tags.scss", - "website/src/styles/toolboxes/shared-controls/_entries.scss" - ]); - const styleOverlays = await readJoinedStyles([ - "website/src/styles/_overlays.scss", - "website/src/styles/overlays/_image-viewer.scss", - "website/src/styles/overlays/_drawers.scss", - "website/src/styles/overlays/_modals.scss", - "website/src/styles/overlays/_notifications.scss", - "website/src/styles/overlays/_tool-add.scss" - ]); - const styleIcons = await readJoinedStyles([ - "website/src/styles/_icons.scss", - "website/src/styles/icons/_base.scss", - "website/src/styles/icons/_navigation.scss", - "website/src/styles/icons/_toolbox.scss", - "website/src/styles/icons/_actions.scss", - "website/src/styles/icons/_equipment.scss" - ]); - const styleGames = await readJoinedStyles([ - "website/src/styles/_games.scss", - "website/src/styles/games/_home-cards.scss", - "website/src/styles/games/_heading.scss", - "website/src/styles/games/_lists.scss", - "website/src/styles/games/_filters.scss", - "website/src/styles/games/_cards.scss" - ]); - const styleMhwilds = await readFile("website/src/styles/_mhwilds.scss", "utf8"); - const styleResponsive = await readFile("website/src/styles/_responsive.scss", "utf8"); - - assert.match(styles, /@use "tokens"/); - assert.match(styles, /@use "nebula"/); - assert.match(styles, /@use "toolboxes"/); - assert.match(styleToolboxesEntry, /@use "toolboxes\/core"/); - assert.match(styleToolboxesCore, /@use "layout"/); - assert.match(styleToolboxesCore, /@use "module-shell"/); - assert.match(styleToolboxesCore, /@use "task-planner"/); - assert.match(styleToolboxesCore, /@use "equipment-planner"/); - assert.match(styleToolboxesCore, /@use "shared-controls"/); - assert.match(styles, /@use "mhwilds"/); - assert.match(styles, /@use "diablo4"/); - assert.match(styleTokens, /--gradient-brand/); - assert.match(styleBase, /:is\(/); - assert.doesNotMatch(styleBase, /\.card-icon-button/); - assert.doesNotMatch(styleBase, /\.drawer-action-button/); - assert.doesNotMatch(styleBase, /\.import-icon-button/); - assert.match(styleBase, /themed-scrollbar/); - assert.match(styleBase, /body\.is-modal-open/); - assert.match(styleNebula, /@function star-field/); - assert.match(styleNebula, /body::after/); - assert.match(styleHome, /\.dialogue-line i/); - assert.match(styleHome, /font-style: italic/); - assert.match(styleHome, /\.about-page/); - assert.doesNotMatch(styleHome, /\.nebula-panel\s*\{/); - assert.match(styleHome, /\.home-onboarding-layout/); - assert.match(styleHome, /\.home-onboarding-slider figcaption/); - assert.match(styleHome, /\.home-onboarding-dots/); - assert.match(styleHome, /\.about-story-section/); - assert.match(styleHome, /\.about-tools-list/); - assert.match(styleHome, /\.about-tool-item \.module-icon/); - assert.match(styleMixins, /@mixin gold-panel-frame/); - assert.match(styleShell, /\.sidebar-about-link/); - assert.match(styleShell, /\.sidebar-mascot/); - assert.match(styleToolboxShell, /\.module-icon-abacus/); - assert.match(styleToolboxShell, /\.module-icon-calculator/); - assert.match(styleToolboxShell, /\.module-icon-columns/); - assert.match(styleToolboxShell, /\.module-icon-table/); - assert.match(styleToolboxCalculator, /\.calculator-module/); - assert.match(styleToolboxTable, /\.table-module/); - assert.match(styleToolboxTable, /\.table-cell-input\.is-selected/); - assert.match(styleToolboxTable, /\.table-cell-input\.is-move-target/); - assert.match(styleToolboxGoalTree, /\.goal-tree-node-card/); - assert.match(styleToolboxGoalTree, /\.goal-tree-node-content-row/); - assert.match(styleToolboxGoalTree, /\.goal-tree-node.is-drop-target/); - assert.match(styleToolboxCalendar, /\.calendar-event-form/); - assert.match(styleToolboxCalendar, /\.calendar-month-date-picker/); - assert.match(styleToolboxCalendar, /\.calendar-alert-mode-switch/); - assert.match(styles, /@use "design-system\/attributes"/); - assert.match(styleToolboxLayout, /\.toolbox-icon-picker/); - assert.match(styleToolboxLayout, /border-radius: inherit/); - assert.match(styleToolboxLayout, /\.toolbox-icon-picker-button\.has-custom-cover img/); - assert.match(styleToolboxLayout, /grid-column: 1 \/ -1/); - assert.match(styleToolboxLayout, /@use "layout\/cover-editor"/); - assert.match(styleToolboxLayout, /\.toolbox-cover-editor/); - assert.match(styleToolboxLayout, /\.toolbox-cover-crop-frame/); - assert.match(styleToolboxLayout, /aspect-ratio: 620 \/ 248/); - assert.match(styleToolboxLayout, /\.toolbox-game-icon/); - assert.match(styleToolboxLayout, /\.toolbox-hero-link/); - assert.match(styleToolboxLayout, /\.toolbox-actions-row/); - assert.match(styleToolboxTextExchange, /\.text-import-actions/); - assert.match(styleToolboxTextExchange, /\.text-import-modal-dialog/); - assert.doesNotMatch(styleToolboxNotepad, /lecture seule/); - assert.match(styleIcons, /ui-icon-drag/); - assert.match(styleIcons, /ui-icon-dropdown/); - assert.match(styleIcons, /ui-icon-enter/); - assert.match(styleIcons, /ui-icon-calculator/); - assert.match(styleIcons, /ui-icon-scrollable/); - assert.match(styleIcons, /ui-icon-hide/); - assert.match(styleIcons, /ui-icon-eye-open/); - assert.match(styleIcons, /ui-icon-eye-closed/); - assert.match(styleIcons, /ui-icon-chevron-down/); - assert.match(styleIcons, /ui-icon-chevron-up/); - assert.match(styleIcons, /ui-icon-flip/); - assert.match(styleIcons, /ui-icon-add/); - assert.match(styleIcons, /ui-icon-add-row/); - assert.match(styleIcons, /ui-icon-add-column/); - assert.match(styleIcons, /ui-icon-remove-row/); - assert.match(styleIcons, /ui-icon-remove-column/); - assert.match(styleIcons, /ui-icon-table/); - assert.match(styleIcons, /ui-icon-map/); - assert.match(styleIcons, /ui-icon-grab/); - assert.match(styleIcons, /ui-icon-shield/); - assert.match(styleIcons, /ui-icon-gun/); - assert.match(styleToolboxShell, /module-icon-chest-armor/); - assert.match(styleToolboxCombos, /\.combo-hold-controls/); - assert.match(styleToolboxCombos, /\.combo-input-direction/); - assert.match(styleToolboxCombos, /\.combo-device-n64\.combo-value-stick-up/); - assert.match(styleToolboxCombos, /\.combo-device-n64\.combo-value-up/); - assert.match(styleToolboxCombos, /\.combo-value-grab \.ui-icon/); - assert.match(styleToolboxChecklist, /\.timer-alert-mode-button/); - assert.match(styleToolboxSharedControls, /\.timer-alert-mode-switch\.is-blocked/); - assert.match(styleToolboxCalculatorResults, /\.calculator-result/); - assert.match(styleToolboxSharedControls, /\.tool-split-scroll-toggle/); - assert.match(styleToolboxImages, /\.annotation-marker/); - assert.match(styleGames, /\.game-home-card/); - assert.match(styleGames, /\.game-layout/); - assert.match(styleGames, /\.game-grid/); - assert.match(styleGames, /\.filter-panel/); - assert.match(styleControls, /@forward "controls\/patterns"/); - assert.match(styleAttributes, /@use "attributes\/patterns"/); - assert.match(styleControlsPatterns, /@mixin panel-surface/); - assert.match(styleControlsPatterns, /@mixin toast-pattern/); - assert.match(styleAttributesPatterns, /\.nds-panel/); - assert.match(styleAttributesPatterns, /\.nds-card/); - assert.match(styleAttributesPatterns, /\.nds-tool-panel/); - assert.match(styleAttributesPatterns, /\.nds-badge/); - assert.match(styleAttributesPatterns, /\.nds-chip/); - assert.match(styleAttributesPatterns, /\.nds-empty-state/); - assert.match(styleAttributesPatterns, /\.nds-inline-notice/); - assert.match(styleAttributesPatterns, /\.nds-progress/); - assert.match(styleAttributesPatterns, /\.nds-filter-panel/); - assert.match(styleAttributesPatterns, /\.nds-modal/); - assert.match(styleAttributesPatterns, /\.nds-drawer-panel/); - assert.match(styleAttributesPatterns, /\.nds-toast/); - assert.match(ndsPatternConsumers, /nds-page-hero/); - assert.match(ndsPatternConsumers, /nds-panel/); - assert.match(ndsPatternConsumers, /nds-card/); - assert.match(ndsPatternConsumers, /nds-card-cover/); - assert.match(ndsPatternConsumers, /nds-tool-panel/); - assert.match(ndsPatternConsumers, /nds-filter-panel/); - assert.match(ndsPatternConsumers, /nds-chip/); - assert.match(ndsPatternConsumers, /nds-badge/); - assert.match(ndsPatternConsumers, /nds-empty-state/); - assert.match(ndsPatternConsumers, /nds-inline-notice/); - assert.match(ndsPatternConsumers, /nds-progress/); - assert.match(ndsPatternConsumers, /nds-modal/); - assert.match(ndsPatternConsumers, /nds-drawer-panel/); - assert.match(ndsPatternConsumers, /nds-toast/); - assert.doesNotMatch(styleCards, /@use "design-system\/surfaces"/); - assert.doesNotMatch(styleCards, /controls\.card-pattern/); - assert.match(styleMhwilds, /\.mhwilds-card/); - assert.match(styleMhwilds, /\.mhwilds-flip-indicator/); - assert.doesNotMatch(styleMhwilds, /\.game-(home-card|layout|grid)/); - assert.doesNotMatch(styleCards, /\.compact-data-card\s*\{/); - assert.doesNotMatch( - [ - goalTreeBranch, - styleToolboxGoalTree, - styleControlsButtons, - styleAttributesButtons - ].join("\n"), - /(?:data-size="tiny"|icon-button(?:-icon)?\(tiny\)|\$size == tiny)/ - ); - assert.match(styleOverlays, /overscroll-behavior: contain/); - assert.match(styleResponsive, /@media \(max-width: 760px\)/); + ]; + const consumers = (await Promise.all(paths.map((path) => readFile(path, "utf8")))).join("\n"); + for (const family of [ + "page-hero", "panel", "card", "card-cover", "tool-panel", "filter-panel", + "chip", "badge", "empty-state", "inline-notice", "progress", "modal", "drawer-panel", "toast" + ]) { + assert.ok(consumers.includes(`nds-${family}`), `public nds-${family} convention must remain in use`); + } }); test("large sass files stay split by stable responsibility", async () => { diff --git a/website/src/styles/design-system/attributes/_fields.scss b/website/src/styles/design-system/attributes/_fields.scss index 3845ab7..31b24e5 100644 --- a/website/src/styles/design-system/attributes/_fields.scss +++ b/website/src/styles/design-system/attributes/_fields.scss @@ -153,7 +153,7 @@ white-space: nowrap; } -.nds-autocomplete-option:hover, +.nds-autocomplete-option:hover:not(:disabled), .nds-autocomplete-option.is-active { border-color: rgba(246, 196, 83, 0.36); background: rgba(246, 196, 83, 0.1);