diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 5f60fe24..cb7763c4 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "picpeak-frontend", - "version": "3.122.5-beta.0", + "version": "3.122.6-beta.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "picpeak-frontend", - "version": "3.122.5-beta.0", + "version": "3.122.6-beta.0", "dependencies": { "@dagrejs/dagre": "^3.0.0", "@fullcalendar/core": "^6.1.20", @@ -59,6 +59,7 @@ }, "devDependencies": { "@eslint/js": "^9.29.0", + "@tailwindcss/typography": "^0.5.20", "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.1.0", "@testing-library/user-event": "^14.6.1", @@ -2843,6 +2844,33 @@ "@swc/counter": "^0.1.3" } }, + "node_modules/@tailwindcss/typography": { + "version": "0.5.20", + "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.20.tgz", + "integrity": "sha512-hwbzQuNUfcPvbegQFatVPl/MY/tcM9KLl963hQ5laJKPh81TEZ1+dNG9PirGvcaDBkp+BCshExAyKVPW91dozw==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "6.0.10" + }, + "peerDependencies": { + "tailwindcss": ">=3.0.0 || >=4.0.0 || insiders" + } + }, + "node_modules/@tailwindcss/typography/node_modules/postcss-selector-parser": { + "version": "6.0.10", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz", + "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/@tanstack/query-core": { "version": "5.90.16", "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.16.tgz", @@ -7592,7 +7620,6 @@ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -8843,6 +8870,7 @@ "integrity": "sha512-6A2rnmW5xZMdw11LYjhcI5846rt9pbLSabY5XPxo+XWdxwZaFEn47Go4NzFiHu9sNNmr/kXivP1vStfvMaK1GQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", diff --git a/frontend/package.json b/frontend/package.json index 6bb121b5..6f73ecdc 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -66,6 +66,7 @@ }, "devDependencies": { "@eslint/js": "^9.29.0", + "@tailwindcss/typography": "^0.5.20", "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.1.0", "@testing-library/user-event": "^14.6.1", diff --git a/frontend/src/__tests__/tailwindTypography.test.ts b/frontend/src/__tests__/tailwindTypography.test.ts new file mode 100644 index 00000000..962df44e --- /dev/null +++ b/frontend/src/__tests__/tailwindTypography.test.ts @@ -0,0 +1,72 @@ +/** + * The `prose` classes must actually resolve to CSS (#1288). + * + * Tailwind's Preflight resets h1-h6 to inherit size and weight, and strips + * list-style and padding from ul/ol. Ten places in this app render rich text + * in a `prose` container and depend on @tailwindcss/typography to put that + * back. With the plugin missing, every `prose*` class is a no-op — so applying + * a heading or a list in the CMS editor changed the document and changed + * nothing on screen. The toolbar button lit up (the editor state was right) + * while the text stayed visually a paragraph, which is why it read as "the + * formatting buttons do nothing". + * + * A missing plugin produces no error and no warning — the class simply does + * not exist — so a config guard is the only cheap place to notice. + */ +import { describe, it, expect } from 'vitest'; +import { readFileSync } from 'fs'; +import { resolve } from 'path'; + +const root = resolve(__dirname, '../..'); + +describe('tailwind typography plugin (#1288)', () => { + const config = readFileSync(resolve(root, 'tailwind.config.js'), 'utf8'); + + it('is registered in the tailwind config', () => { + expect(config).toMatch(/require\(['"]@tailwindcss\/typography['"]\)/); + }); + + it('is a declared dependency', () => { + const pkg = JSON.parse(readFileSync(resolve(root, 'package.json'), 'utf8')); + const deps = { ...pkg.dependencies, ...pkg.devDependencies }; + expect(deps['@tailwindcss/typography']).toBeDefined(); + }); + + it('actually resolves, rather than just being named in the config', async () => { + const plugin = await import('@tailwindcss/typography'); + expect(plugin.default).toBeDefined(); + }); + + describe('theme-owned prose colours', () => { + const overrides = readFileSync(resolve(root, 'src/styles/prose-overrides.css'), 'utf8'); + + // The plugin ships a fixed near-black palette and sets `color` on the + // heading ELEMENT, which beats the container's inherited colour. A dark + // gallery preset writes a near-white --color-text on :root and does NOT + // add a `.dark` class, so `dark:prose-invert` never engages — without + // this mapping the promo block, info banner and public CMS pages render + // near-black headings on a dark background. + it('remaps the prose palette wherever the theme owns the text colour', () => { + expect(overrides).toMatch(/\.prose\.text-theme\s*\{/); + expect(overrides).toMatch(/--tw-prose-headings:\s*var\(--color-text\)/); + expect(overrides).toMatch(/--tw-prose-body:\s*var\(--color-text\)/); + expect(overrides).toMatch(/--tw-prose-bold:\s*var\(--color-text\)/); + }); + + it('keeps the gallery prose surfaces carrying the text-theme marker', () => { + // The mapping is keyed on that class; a consumer that drops it silently + // falls back to the near-black palette. + for (const file of [ + 'src/components/gallery/GalleryLayout.tsx', + 'src/components/common/CMSContentBlock.tsx', + ]) { + const src = readFileSync(resolve(root, file), 'utf8'); + const proseLines = src.split('\n').filter((l) => /className|class=/.test(l) && /\bprose\b/.test(l)); + expect(proseLines.length).toBeGreaterThan(0); + for (const line of proseLines) { + expect(line).toMatch(/text-theme/); + } + } + }); + }); +}); diff --git a/frontend/src/components/common/CMSContentBlock.tsx b/frontend/src/components/common/CMSContentBlock.tsx index 56ade091..c2077920 100644 --- a/frontend/src/components/common/CMSContentBlock.tsx +++ b/frontend/src/components/common/CMSContentBlock.tsx @@ -107,7 +107,7 @@ export const CMSContentBlock: React.FC = ({ slug, fallback {page.title}
{ will see (#482). */} { classes — so the admin sees what guests will see. */}
)} diff --git a/frontend/src/pages/admin/event-details/EventInformationCard.tsx b/frontend/src/pages/admin/event-details/EventInformationCard.tsx index 5f2c2b9d..38aa686e 100644 --- a/frontend/src/pages/admin/event-details/EventInformationCard.tsx +++ b/frontend/src/pages/admin/event-details/EventInformationCard.tsx @@ -527,7 +527,7 @@ export const EventInformationCard: React.FC = ({

{t('events.promoBanner.preview', 'Preview')}

- + )} @@ -575,7 +575,7 @@ export const EventInformationCard: React.FC = ({

{t('events.infoBanner.preview', 'Preview')}

- + )} diff --git a/frontend/src/styles/prose-overrides.css b/frontend/src/styles/prose-overrides.css index 11ce5ba1..3347b830 100644 --- a/frontend/src/styles/prose-overrides.css +++ b/frontend/src/styles/prose-overrides.css @@ -34,6 +34,16 @@ */ .prose pre { background-color: var(--color-elevated, #f5f5f5); + /* + * The typography plugin sets an explicit `color: var(--tw-prose-pre-code)` + * on
 — #e5e7eb, chosen for ITS dark default background. This rule
+   * replaces that background with the light `--color-elevated`, which left
+   * near-white text on a near-white block everywhere a code sample appears.
+   * Inheriting instead restores the pre-plugin behaviour and stays correct in
+   * every context: themed prose inherits --color-text, admin prose inherits
+   * the (possibly inverted) prose body colour.
+   */
+  color: inherit;
   border: 1px solid var(--color-surface-border, #e5e5e5);
   border-radius: 0.375rem;
   padding: 1rem;
@@ -44,7 +54,14 @@
 .prose pre code {
   background-color: transparent;
   padding: 0;
-  font-size: 0.875em;
+  /*
+   * `inherit`, not a second 0.875em. This rule predates the typography
+   * plugin, which now sizes the 
 itself to 0.875em (0.857em under
+   * prose-sm) and deliberately leaves its `pre code` at `inherit`. Keeping a
+   * multiplier here scaled block code TWICE — 12.25px in base prose, 10.5px
+   * in prose-sm, against 14px body.
+   */
+  font-size: inherit;
   color: inherit;
 }
 
@@ -107,4 +124,78 @@
 .prose p:empty::before {
   content: "\200B"; /* Zero-width space */
   display: inline;
-}
\ No newline at end of file
+}
+/* ---------------------------------------------------------------------------
+ * Theme-owned prose colours (#1288 review follow-up)
+ *
+ * Enabling @tailwindcss/typography gave `.prose` its own fixed palette:
+ * `--tw-prose-headings` and `--tw-prose-bold` default to near-black, and the
+ * generated `.prose :where(h1)…` rules set `color` on the ELEMENT, which beats
+ * the container's inherited colour.
+ *
+ * That is wrong on every gallery surface. A dark gallery preset (darkModern
+ * and friends) writes a near-white `--color-text` on :root but deliberately
+ * does NOT add a `.dark` class, so `dark:prose-invert` never engages — and the
+ * promo block, the info banner and public CMS pages would have rendered
+ * near-black headings on a dark background. Readable before this plugin
+ * existed, unreadable after: exactly the regression the plugin was meant to
+ * avoid.
+ *
+ * Keyed on `.text-theme`, which is the app's existing marker for "the active
+ * theme owns the text colour here". Admin surfaces (the CMS and email
+ * editors, the update modal) do not carry it and keep the stock palette,
+ * which is what they want — they sit on fixed neutral chrome and already
+ * handle dark mode through the real `.dark` class.
+ * ------------------------------------------------------------------------ */
+.prose.text-theme {
+  --tw-prose-body: var(--color-text);
+  --tw-prose-headings: var(--color-text);
+  --tw-prose-lead: var(--color-text);
+  --tw-prose-bold: var(--color-text);
+  --tw-prose-quotes: var(--color-text);
+  --tw-prose-captions: var(--color-muted-text, var(--color-text));
+  --tw-prose-code: var(--color-text);
+  /* Markers and rules are decoration, not text — muted so a bullet list does
+     not read as heavier than the copy beside it. */
+  --tw-prose-bullets: var(--color-muted-text, currentColor);
+  --tw-prose-counters: var(--color-muted-text, currentColor);
+  --tw-prose-hr: var(--color-surface-border, currentColor);
+  --tw-prose-quote-borders: var(--color-surface-border, currentColor);
+  --tw-prose-th-borders: var(--color-surface-border, currentColor);
+  --tw-prose-td-borders: var(--color-surface-border, currentColor);
+  /* The gallery surfaces pass `prose-a:text-accent` explicitly; this keeps a
+     consumer that forgets it from falling back to Tailwind's blue. */
+  --tw-prose-links: var(--color-accent, var(--color-text));
+}
+
+/* ---------------------------------------------------------------------------
+ * H5 and H6 (#1288 review round 2)
+ *
+ * @tailwindcss/typography defines heading typography for h1-h4 only. Preflight
+ * still resets h5 and h6 to inherit size and weight, so those two toolbar
+ * buttons stayed exactly as inert as the four this PR fixed — the same bug,
+ * two levels down.
+ *
+ * Sized to continue the plugin's own scale (h4 is 1em/700) rather than
+ * inventing a new one: h5 slightly under body, h6 smaller and uppercase so it
+ * still reads as a heading at that size.
+ * ------------------------------------------------------------------------ */
+.prose :where(h5):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
+  color: var(--tw-prose-headings);
+  font-size: 0.9em;
+  font-weight: 600;
+  margin-top: 1.6em;
+  margin-bottom: 0.5em;
+  line-height: 1.5;
+}
+
+.prose :where(h6):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
+  color: var(--tw-prose-headings);
+  font-size: 0.85em;
+  font-weight: 600;
+  letter-spacing: 0.05em;
+  text-transform: uppercase;
+  margin-top: 1.6em;
+  margin-bottom: 0.5em;
+  line-height: 1.5;
+}
diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js
index 75dae6a1..d0213228 100644
--- a/frontend/tailwind.config.js
+++ b/frontend/tailwind.config.js
@@ -104,6 +104,18 @@ export default {
     ...Array.from({ length: 12 }, (_, i) => `lg:grid-cols-${i + 1}`),
     ...Array.from({ length: 12 }, (_, i) => `xl:grid-cols-${i + 1}`),
   ],
-  plugins: [],
+  plugins: [
+    // Tailwind's Preflight resets h1-h6 to inherit their size and weight, and
+    // strips list-style and padding from ul/ol. Ten places in this app render
+    // rich text inside a `prose` container and rely on this plugin to put that
+    // typography back — the CMS editor and its preview, the public CMS page,
+    // release notes, and the gallery welcome message among them.
+    //
+    // Without it every `prose*` class is a no-op, so applying a heading or a
+    // list in the CMS editor changed the document and changed NOTHING on
+    // screen: the toolbar button lit up (the editor state was correct) while
+    // the text stayed visually a paragraph. That is issue #1288.
+    require('@tailwindcss/typography'),
+  ],
 }