From f1fda23131100babb5db545dabf840758ae9d0fd Mon Sep 17 00:00:00 2001 From: Julien Bourdin Date: Wed, 6 May 2026 08:50:00 +0200 Subject: [PATCH 1/4] docs: remove milestone-based planning guidance Phase milestones exist in the repo but are not used as a planning structure. Drop the milestone instructions from CLAUDE.md (issue creation + board ordering) and remove the Milestones (Phases) section from docs/roadmap.md. The kanban project board remains the source of truth for prioritization. --- CLAUDE.md | 4 ++-- docs/roadmap.md | 57 ++----------------------------------------------- 2 files changed, 4 insertions(+), 57 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index b955f55..d4e97a3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -229,9 +229,9 @@ The project uses a [GitHub Project board](https://github.com/users/jbourdin/proj 5. **Move the issue** to "Ready for Release" when the user confirms the feature works 6. **Move the issue** to "Done" only when the release containing it is published -When creating new features or backlog items, create a GitHub issue with the feature ID, assign it to the correct milestone, and add it to the project board. **Always set the status to "Backlog"** when adding an issue to the board — items must never be left without a kanban status. +When creating new features or backlog items, create a GitHub issue with the feature ID and add it to the project board. **Always set the status to "Backlog"** when adding an issue to the board — items must never be left without a kanban status. Do **not** assign milestones — the phase milestones in the repo are not used as a planning structure. -**Board ordering:** items within each column are manually sorted by **milestone** (Phase A first, then B, C, … J, then no milestone) and then by **priority** (high → medium → low) within a milestone. When adding or moving an issue, place it at the correct position using the `updateProjectV2ItemPosition` GraphQL mutation (`afterId: null` for first position, or `afterId: ""` to insert after a specific item). +**Board ordering:** items within each column are sorted manually by user direction. When adding or moving an issue, place it at the correct position using the `updateProjectV2ItemPosition` GraphQL mutation (`afterId: null` for first position, or `afterId: ""` to insert after a specific item). ## Make Commands diff --git a/docs/roadmap.md b/docs/roadmap.md index e7f4263..cdfe00f 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -4,7 +4,7 @@ ← Back to [Main Documentation](docs.md) | [Feature List](features.md) | [Changelog](changelog.md) | [README](../README.md) -The [GitHub Project board](https://github.com/users/jbourdin/projects/1) is the roadmap for the Expanded Decks project. All issue prioritization, status, and phase assignment live there — this document provides an overview and links to the relevant views. +The [GitHub Project board](https://github.com/users/jbourdin/projects/1) is the roadmap for the Expanded Decks project. All issue prioritization and status live there — this document provides an overview and links to the relevant views. ## Project Board @@ -23,60 +23,7 @@ The Kanban board organizes work into seven columns: **Views:** - [Full board](https://github.com/users/jbourdin/projects/1) — Kanban with all columns -- [All open issues](https://github.com/jbourdin/expandedDecks/issues) — filterable by label and milestone - -## Milestones (Phases) - -Each milestone corresponds to a thematic phase of work. Issues are assigned to milestones on creation. Phases are grouped into priority tiers reflecting the current product direction: **content and gameplay experience first**, borrowing and operational refinements later. - -### Tier 1 — Content, Discovery & Core Scanning - -Priority: content attracts users, SEO makes it findable, search helps them navigate, scanning enables event-day workflows. - -| Phase | Name | Milestone link | -|-------|-------------------------------------|----------------| -| 01 | PDF Labels & Camera Scanning | [Phase 01](https://github.com/jbourdin/expandedDecks/milestone/3) | -| 02 | Homepage | [Phase 02](https://github.com/jbourdin/expandedDecks/milestone/14) | -| 03 | SEO & Indexability | [Phase 03](https://github.com/jbourdin/expandedDecks/milestone/16) | -| 04 | Search | [Phase 04](https://github.com/jbourdin/expandedDecks/milestone/15) | - -### Tier 2 — Gameplay & Event Experience - -Priority: enrich events, physical label management, and freeplay matchmaking. - -| Phase | Name | Milestone link | -|-------|-------------------------------------|----------------| -| 05 | Zebra Labels & HID Scanning | [Phase 05](https://github.com/jbourdin/expandedDecks/milestone/4) | -| 06 | Event Enrichment | [Phase 06](https://github.com/jbourdin/expandedDecks/milestone/2) | -| 07 | Freeplay Matchmaking | [Phase 07](https://github.com/jbourdin/expandedDecks/milestone/10) | - -### Tier 3 — Ecosystem & Integration - -Priority: REST API, MCP server, and LLM-friendly endpoints for third-party tools and AI agents. - -| Phase | Name | Milestone link | -|-------|-------------------------------------|----------------| -| 08 | API Access | [Phase 08](https://github.com/jbourdin/expandedDecks/milestone/11) | - -### Tier 4 — Operational (defer until pain is felt) - -Priority: these address edge cases and operational polish. The borrowing process already works well — refine as pain points surface. - -| Phase | Name | Milestone link | -|-------|-------------------------------------|----------------| -| 09 | UX Polish & Overdue Tracking | [Phase 09](https://github.com/jbourdin/expandedDecks/milestone/1) | -| 10 | Operational Excellence | [Phase 10](https://github.com/jbourdin/expandedDecks/milestone/7) | -| 11 | Auth Hardening & Delegation | [Phase 11](https://github.com/jbourdin/expandedDecks/milestone/5) | -| 12 | Multi-Organizer Events | [Phase 12](https://github.com/jbourdin/expandedDecks/milestone/9) | -| 13 | Lost & Found | [Phase 13](https://github.com/jbourdin/expandedDecks/milestone/8) | -| 14 | Play Pokemon QR Integration | [Phase 14](https://github.com/jbourdin/expandedDecks/milestone/6) | - -### Completed - -| Phase | Name | Milestone link | -|-------|-------------------------------------|----------------| -| 0 | Soft Deletion | [Phase 0](https://github.com/jbourdin/expandedDecks/milestone/12) | -| — | Content Editing Experience | [Phase L](https://github.com/jbourdin/expandedDecks/milestone/13) | +- [All open issues](https://github.com/jbourdin/expandedDecks/issues) — filterable by label ## Feature Catalogue From 12a6da46e8eafa2755fc8f92bed1c2f611fc428a Mon Sep 17 00:00:00 2001 From: Julien Bourdin Date: Wed, 6 May 2026 09:05:32 +0200 Subject: [PATCH 2/4] =?UTF-8?q?feat(theme):=20F20.1=20=E2=80=94=20dark=20t?= =?UTF-8?q?heme=20following=20OS=20preference?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a dark theme variant that auto-follows the OS prefers-color-scheme setting on both the default and expandedtalks channel themes. Strictly auto: no manual override, no user toggle, no persistence. - Inline bridge script in base.html.twig mirrors prefers-color-scheme onto data-bs-theme and data-mantine-color-scheme before first paint and reacts to live OS toggles. Bootstrap 5.3 and Mantine 8 read these attributes natively. - Custom palette ($ed-navy/$ed-blue/$ed-gold/$ed-red/$ed-bg) mirrored as --ed-* CSS custom properties with [data-bs-theme="dark"] overrides so selectors that should react to the scheme can use var(--ed-*). - Dark-mode rules for the few custom surfaces whose light recipe (rgba navy on white) doesn't translate: hero-pokemon, card-header-themed, table-themed thead, cms-content blockquote, rich-text tables, badges. - Same dark-mode override block in themes/expandedtalks/_overrides.scss for the channel theme. - Centralized Mantine provider via assets/components/AppMantineProvider.tsx with defaultColorScheme="auto" so all 17 React island entry points share one config and switch with the OS. - Document F20 — Theming family and F20.1 in docs/features.md. --- assets/archetype-form.tsx | 14 ++-- assets/archetype-variants.tsx | 6 +- assets/banned-card-form.tsx | 6 +- assets/catalog-filters.tsx | 14 ++-- assets/components/AppMantineProvider.tsx | 27 ++++++ assets/deck-card-list.tsx | 6 +- assets/deck-form.tsx | 14 ++-- assets/deck-found.tsx | 6 +- assets/deck-version-compare.tsx | 6 +- assets/event-form.tsx | 6 +- assets/homepage-editor.tsx | 6 +- assets/navbar-search.tsx | 6 +- assets/notification-bell.tsx | 6 +- assets/page-form.tsx | 10 +-- assets/staff-autocomplete.tsx | 6 +- assets/styles/app.scss | 84 +++++++++++++++++++ .../themes/expandedtalks/_overrides.scss | 33 ++++++++ assets/variant-compare-modal.tsx | 6 +- assets/variant-compare.tsx | 6 +- assets/walk-up-autocomplete.tsx | 10 +-- docs/features.md | 10 +++ templates/base.html.twig | 13 +++ 22 files changed, 234 insertions(+), 67 deletions(-) create mode 100644 assets/components/AppMantineProvider.tsx diff --git a/assets/archetype-form.tsx b/assets/archetype-form.tsx index 2844bb0..9e43d32 100644 --- a/assets/archetype-form.tsx +++ b/assets/archetype-form.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { createRoot } from 'react-dom/client'; -import { MantineProvider } from '@mantine/core'; +import AppMantineProvider from './components/AppMantineProvider'; import PlaystyleTagSelect from './components/PlaystyleTagSelect'; import PokemonSpriteSelect from './components/PokemonSpriteSelect'; import MarkdownEditor from './components/MarkdownEditor'; @@ -39,12 +39,12 @@ if (spriteRoot) { const hiddenInputName = spriteRoot.dataset.hiddenInputName ?? 'archetype_form[pokemonSlugs]'; createRoot(spriteRoot).render( - + - , + , ); } @@ -74,14 +74,14 @@ if (playstyleRoot) { const placeholder = playstyleRoot.dataset.placeholder ?? undefined; createRoot(playstyleRoot).render( - + - , + , ); } @@ -98,12 +98,12 @@ editorRoots.forEach((root) => { } createRoot(root).render( - + - , + , ); }); diff --git a/assets/archetype-variants.tsx b/assets/archetype-variants.tsx index 1d37287..c8e7dfd 100644 --- a/assets/archetype-variants.tsx +++ b/assets/archetype-variants.tsx @@ -13,7 +13,7 @@ import React from 'react'; import { createRoot } from 'react-dom/client'; -import { MantineProvider } from '@mantine/core'; +import AppMantineProvider from './components/AppMantineProvider'; import '@mantine/core/styles.css'; import ArchetypeVariantSelector from './components/ArchetypeVariantSelector'; @@ -47,8 +47,8 @@ if (root) { }; createRoot(root).render( - + - , + , ); } diff --git a/assets/banned-card-form.tsx b/assets/banned-card-form.tsx index de152b5..fefb499 100644 --- a/assets/banned-card-form.tsx +++ b/assets/banned-card-form.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { createRoot } from 'react-dom/client'; -import { MantineProvider } from '@mantine/core'; +import AppMantineProvider from './components/AppMantineProvider'; import MarkdownEditor from './components/MarkdownEditor'; import '@mantine/core/styles.css'; @@ -33,12 +33,12 @@ editorRoots.forEach((root) => { } createRoot(root).render( - + - , + , ); }); diff --git a/assets/catalog-filters.tsx b/assets/catalog-filters.tsx index d71efed..5543aee 100644 --- a/assets/catalog-filters.tsx +++ b/assets/catalog-filters.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { createRoot } from 'react-dom/client'; -import { MantineProvider } from '@mantine/core'; +import AppMantineProvider from './components/AppMantineProvider'; import ArchetypeFilterSelect from './components/ArchetypeFilterSelect'; import AsyncAutocomplete from './components/AsyncAutocomplete'; @@ -34,7 +34,7 @@ document.querySelectorAll('[data-catalog-archetype]').forEach((root const initialLabel = root.dataset.initialLabel ?? ''; createRoot(root).render( - + ('[data-catalog-archetype]').forEach((root initialValue={initialValue} initialLabel={initialLabel} /> - , + , ); }); @@ -54,7 +54,7 @@ document.querySelectorAll('[data-catalog-event]').forEach((root) => const initialId = root.dataset.initialId ?? ''; createRoot(root).render( - + ('[data-catalog-event]').forEach((root) => secondary: `${item.date as string} · ${item.location as string}`, })} /> - , + , ); }); @@ -79,7 +79,7 @@ document.querySelectorAll('[data-catalog-owner]').forEach((root) => const initialId = root.dataset.initialId ?? ''; createRoot(root).render( - + ('[data-catalog-owner]').forEach((root) => label: item.screenName as string, })} /> - , + , ); }); diff --git a/assets/components/AppMantineProvider.tsx b/assets/components/AppMantineProvider.tsx new file mode 100644 index 0000000..117159d --- /dev/null +++ b/assets/components/AppMantineProvider.tsx @@ -0,0 +1,27 @@ +/* + * This file is part of the Expanded Decks project. + * + * (c) Expanded Decks contributors + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import React from 'react'; +import { MantineProvider } from '@mantine/core'; + +/** + * Shared Mantine provider for all React islands in the app. Configures + * Mantine to follow the operating-system color scheme (`auto`) so it stays + * in sync with the `data-bs-theme` attribute set on `` by the inline + * bridge script in base.html.twig. + * + * @see docs/features.md F20.1 — Dark theme following OS preference + */ +export default function AppMantineProvider({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ); +} diff --git a/assets/deck-card-list.tsx b/assets/deck-card-list.tsx index 3132be7..814d929 100644 --- a/assets/deck-card-list.tsx +++ b/assets/deck-card-list.tsx @@ -16,7 +16,7 @@ import React from 'react'; import { createRoot } from 'react-dom/client'; -import { MantineProvider } from '@mantine/core'; +import AppMantineProvider from './components/AppMantineProvider'; import { DeckCardList, type DeckCardListProps } from './components/DeckCardList'; import '@mantine/core/styles.css'; @@ -55,7 +55,7 @@ if (root) { }; createRoot(root).render( - + - , + , ); } diff --git a/assets/deck-form.tsx b/assets/deck-form.tsx index 7d901c2..d32f0c8 100644 --- a/assets/deck-form.tsx +++ b/assets/deck-form.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { createRoot } from 'react-dom/client'; -import { MantineProvider } from '@mantine/core'; +import AppMantineProvider from './components/AppMantineProvider'; import ArchetypeSelect from './components/ArchetypeSelect'; import LanguageSelect from './components/LanguageSelect'; import PokemonSpriteSelect from './components/PokemonSpriteSelect'; @@ -32,7 +32,7 @@ if (archetypeRoot) { const initialName = archetypeRoot.dataset.archetypeName ?? undefined; createRoot(archetypeRoot).render( - + - , + , ); } @@ -57,12 +57,12 @@ if (languageRoot) { } createRoot(languageRoot).render( - + - , + , ); } @@ -79,11 +79,11 @@ if (spriteRoot) { } createRoot(spriteRoot).render( - + - , + , ); } diff --git a/assets/deck-found.tsx b/assets/deck-found.tsx index 681c2ad..7bb36c7 100644 --- a/assets/deck-found.tsx +++ b/assets/deck-found.tsx @@ -12,7 +12,7 @@ */ import { createRoot } from 'react-dom/client'; -import { MantineProvider } from '@mantine/core'; +import AppMantineProvider from './components/AppMantineProvider'; import '@mantine/core/styles.css'; import DeckFoundModal from './components/DeckFoundModal'; @@ -26,7 +26,7 @@ if (root) { const labels = JSON.parse(root.dataset.labels ?? '{}'); createRoot(root).render( - + - , + , ); } diff --git a/assets/deck-version-compare.tsx b/assets/deck-version-compare.tsx index f65346d..1539961 100644 --- a/assets/deck-version-compare.tsx +++ b/assets/deck-version-compare.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { createRoot } from 'react-dom/client'; -import { MantineProvider } from '@mantine/core'; +import AppMantineProvider from './components/AppMantineProvider'; import DeckVersionCompare from './components/DeckVersionCompare'; import '@mantine/core/styles.css'; @@ -45,8 +45,8 @@ document.querySelectorAll('[data-version-compare]').forEach((root) }; createRoot(root).render( - + - , + , ); }); diff --git a/assets/event-form.tsx b/assets/event-form.tsx index 3b87427..2f2e372 100644 --- a/assets/event-form.tsx +++ b/assets/event-form.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { createRoot } from 'react-dom/client'; -import { MantineProvider } from '@mantine/core'; +import AppMantineProvider from './components/AppMantineProvider'; import EventTagSelect from './components/EventTagSelect'; import '@mantine/core/styles.css'; @@ -46,13 +46,13 @@ if (tagRoot) { const placeholder = tagRoot.dataset.placeholder ?? ''; createRoot(tagRoot).render( - + - , + , ); } diff --git a/assets/homepage-editor.tsx b/assets/homepage-editor.tsx index 2abe34c..9c75486 100644 --- a/assets/homepage-editor.tsx +++ b/assets/homepage-editor.tsx @@ -12,7 +12,7 @@ */ import { createRoot } from 'react-dom/client'; -import { MantineProvider } from '@mantine/core'; +import AppMantineProvider from './components/AppMantineProvider'; import HomepageEditor from './components/HomepageEditor'; import '@mantine/core/styles.css'; @@ -61,7 +61,7 @@ if (root) { } catch { /* use default */ } createRoot(root).render( - + - , + , ); } diff --git a/assets/navbar-search.tsx b/assets/navbar-search.tsx index fd63338..026bcaa 100644 --- a/assets/navbar-search.tsx +++ b/assets/navbar-search.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { createRoot } from 'react-dom/client'; -import { MantineProvider } from '@mantine/core'; +import AppMantineProvider from './components/AppMantineProvider'; import NavbarSearch from './components/NavbarSearch'; import '@mantine/core/styles.css'; @@ -27,7 +27,7 @@ if (root) { const labelNoResults = root.dataset.labelNoResults ?? 'No results'; createRoot(root).render( - + - , + , ); } diff --git a/assets/notification-bell.tsx b/assets/notification-bell.tsx index e9e1be5..6cdd0f7 100644 --- a/assets/notification-bell.tsx +++ b/assets/notification-bell.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { createRoot } from 'react-dom/client'; -import { MantineProvider } from '@mantine/core'; +import AppMantineProvider from './components/AppMantineProvider'; import NotificationBell from './components/NotificationBell'; import '@mantine/core/styles.css'; @@ -30,7 +30,7 @@ if (root) { const labelEmpty = root.dataset.labelEmpty ?? 'No notifications yet'; createRoot(root).render( - + - , + , ); } diff --git a/assets/page-form.tsx b/assets/page-form.tsx index ab98209..5ea8f9a 100644 --- a/assets/page-form.tsx +++ b/assets/page-form.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { createRoot } from 'react-dom/client'; -import { MantineProvider } from '@mantine/core'; +import AppMantineProvider from './components/AppMantineProvider'; import MarkdownEditor from './components/MarkdownEditor'; import ImageUrlField from './components/ImageUrlField'; @@ -33,13 +33,13 @@ editorRoots.forEach((root) => { } createRoot(root).render( - + - , + , ); }); @@ -83,8 +83,8 @@ imageUrlRoots.forEach((root) => { }; createRoot(root).render( - + - , + , ); }); diff --git a/assets/staff-autocomplete.tsx b/assets/staff-autocomplete.tsx index 7552b25..fa0d9b9 100644 --- a/assets/staff-autocomplete.tsx +++ b/assets/staff-autocomplete.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { createRoot } from 'react-dom/client'; -import { MantineProvider } from '@mantine/core'; +import AppMantineProvider from './components/AppMantineProvider'; import AsyncAutocomplete from './components/AsyncAutocomplete'; import '@mantine/core/styles.css'; @@ -29,7 +29,7 @@ document.querySelectorAll('[data-staff-autocomplete]').forEach((roo const placeholder = root.dataset.placeholder ?? 'Screen name, email, or Pokemon ID'; createRoot(root).render( - + ('[data-staff-autocomplete]').forEach((roo secondary: [item.email, item.playerId].filter(Boolean).join(' · '), })} /> - , + , ); }); diff --git a/assets/styles/app.scss b/assets/styles/app.scss index 93db184..430a0a0 100644 --- a/assets/styles/app.scss +++ b/assets/styles/app.scss @@ -60,6 +60,28 @@ $grid-row-columns: 9; @import "~bootstrap/scss/bootstrap"; @import "~bootstrap-icons/font/bootstrap-icons.css"; +// --- Runtime theme tokens --- +// SCSS variables above feed Bootstrap's compile-time generation for the LIGHT +// theme. These CSS custom properties mirror them so selectors that should +// react to the OS color scheme can use var(--ed-*) instead. +// @see docs/features.md F20.1 — Dark theme following OS preference + +:root { + --ed-navy: #{$ed-navy}; + --ed-blue: #{$ed-blue}; + --ed-gold: #{$ed-gold}; + --ed-red: #{$ed-red}; + --ed-bg: #{$ed-bg}; +} + +[data-bs-theme="dark"] { + --ed-navy: #6478b8; + --ed-blue: #6a9bff; + --ed-gold: #ffd95e; + --ed-red: #ff7080; + --ed-bg: #121826; +} + // --- Layout --- html, body { @@ -721,3 +743,65 @@ footer { letter-spacing: .05em; } } + +// --- Dark theme overrides --- +// Selectors below paint navy-tinted surfaces over a white body in light mode. +// On a dark body that recipe inverts (the surface disappears or sits darker +// than the body), so re-tint them against the dark background. +// @see docs/features.md F20.1 — Dark theme following OS preference + +[data-bs-theme="dark"] { + body { + background-color: var(--ed-bg); + background-image: none; + } + + .hero-pokemon { + background: linear-gradient(135deg, #1c2538 0%, #2a3552 100%); + + .hero-text { + color: rgba(#fff, .8); + } + } + + .card-header-themed { + background-color: rgba(#fff, .06); + border-bottom-color: var(--ed-gold); + + .nav-tabs .nav-link.active { + color: var(--ed-text, #e6e8ee); + background-color: var(--bs-body-bg); + border-color: var(--bs-border-color) var(--bs-border-color) transparent; + } + } + + .table-themed thead { + background-color: rgba(#fff, .06); + } + + .badge-lent { + color: #1a1a1a; + } + + .badge-retired { + background-color: #555; + } + + .cms-content blockquote { + border-left-color: rgba(#fff, .3); + background-color: rgba(#fff, .05); + } + + /* stylelint-disable-next-line selector-class-pattern */ + .mantine-RichTextEditor-content, + .cms-content { + th { + background-color: rgba(#fff, .08); + } + } + + /* stylelint-disable-next-line selector-class-pattern */ + .mantine-RichTextEditor-content .selectedCell { + background-color: rgba(#fff, .12); + } +} diff --git a/assets/styles/themes/expandedtalks/_overrides.scss b/assets/styles/themes/expandedtalks/_overrides.scss index ee24daf..6d64f00 100644 --- a/assets/styles/themes/expandedtalks/_overrides.scss +++ b/assets/styles/themes/expandedtalks/_overrides.scss @@ -132,3 +132,36 @@ a { color: rgba(#fff, .85); } } + +// --- Dark theme overrides --- +// Channel-scoped tweaks for the OS-preference dark mode. The slate and sand +// hues already work on dark; only the body background and a few accent +// surfaces need adjustment. +// @see docs/features.md F20.1 — Dark theme following OS preference + +[data-bs-theme="dark"] { + body { + background-color: #14171c; + background-image: none; + } + + .hero-pokemon { + background: linear-gradient(135deg, #2a2e36 0%, #3d4350 100%); + } + + .card-header-themed { + background: rgba(#fff, .06); + border-bottom: 2px solid $cs-sand; + } + + .btn-outline-primary { + color: #a8c98a; + border-color: #a8c98a; + + &:hover, &:focus { + background-color: #a8c98a; + border-color: #a8c98a; + color: #14171c; + } + } +} diff --git a/assets/variant-compare-modal.tsx b/assets/variant-compare-modal.tsx index 369e009..df3c189 100644 --- a/assets/variant-compare-modal.tsx +++ b/assets/variant-compare-modal.tsx @@ -14,7 +14,7 @@ import React, { useCallback, useEffect, useState } from 'react'; import { createRoot } from 'react-dom/client'; -import { MantineProvider } from '@mantine/core'; +import AppMantineProvider from './components/AppMantineProvider'; import '@mantine/core/styles.css'; import CardImageModal, { type FlatCard } from './components/CardImageModal'; @@ -53,9 +53,9 @@ if (root) { const cards = JSON.parse(root.dataset.cards ?? '[]') as FlatCard[]; createRoot(root).render( - + - , + , ); // Wire up click handlers on card names to dispatch custom event diff --git a/assets/variant-compare.tsx b/assets/variant-compare.tsx index 3f3daa0..4e73e98 100644 --- a/assets/variant-compare.tsx +++ b/assets/variant-compare.tsx @@ -13,7 +13,7 @@ import React from 'react'; import { createRoot } from 'react-dom/client'; -import { MantineProvider } from '@mantine/core'; +import AppMantineProvider from './components/AppMantineProvider'; import '@mantine/core/styles.css'; import VariantComparePicker from './components/VariantComparePicker'; @@ -22,7 +22,7 @@ if (root) { const variants = JSON.parse(root.dataset.variants ?? '[]'); createRoot(root).render( - + - , + , ); } diff --git a/assets/walk-up-autocomplete.tsx b/assets/walk-up-autocomplete.tsx index d7f9968..af2db82 100644 --- a/assets/walk-up-autocomplete.tsx +++ b/assets/walk-up-autocomplete.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { createRoot } from 'react-dom/client'; -import { MantineProvider } from '@mantine/core'; +import AppMantineProvider from './components/AppMantineProvider'; import AsyncAutocomplete from './components/AsyncAutocomplete'; import '@mantine/core/styles.css'; @@ -30,7 +30,7 @@ document.querySelectorAll('[data-walk-up-deck-autocomplete]').forEa const placeholder = root.dataset.placeholder ?? 'Search by deck name or tag...'; createRoot(root).render( - + ('[data-walk-up-deck-autocomplete]').forEa secondary: `${item.ownerName as string} · ${item.shortTag as string}`, })} /> - , + , ); }); @@ -52,7 +52,7 @@ document.querySelectorAll('[data-walk-up-user-autocomplete]').forEa const placeholder = root.dataset.placeholder ?? 'Search by screen name, email, or Pokemon ID...'; createRoot(root).render( - + ('[data-walk-up-user-autocomplete]').forEa secondary: [item.email, item.playerId].filter(Boolean).join(' · '), })} /> - , + , ); }); diff --git a/docs/features.md b/docs/features.md index bcc7d0d..36b904d 100644 --- a/docs/features.md +++ b/docs/features.md @@ -318,3 +318,13 @@ Multi-domain channel system and SEO features. Each channel serves a distinct dom | F18.27 | JSON-LD structured data | Medium | Done | `StructuredDataBuilder` service generating schema.org JSON-LD blocks rendered in `{% block structured_data %}`. **Homepage:** `WebSite` with channel brand name. **CMS pages:** `WebPage` with title, date, publisher. **Archetypes:** `Article` with localized name/description, `hasPart` entries for deck variants (anchored by shortTag). **Events:** `Event` with dates, location, organizer, `EventCancelled` status. **Decks:** `CreativeWork` with owner, dates. Organization name reads from the channel's `brand_name` parameter. Twig functions: `structured_data()` (returns builder), `json_ld(data)` (encodes as safe HTML). Depends on F18.2. | | F18.28 | Open Graph and Twitter Card meta tags | Medium | Done | Full Open Graph and Twitter Card meta tags on all public pages via a reusable `_partials/opengraph.html.twig` included from `{% block opengraph %}`. Tags: `og:title`, `og:description`, `og:image`, `og:url`, `og:type`, `og:locale` (+ alternate), `og:site_name`, `twitter:card` (`summary_large_image` when image available), `twitter:title`, `twitter:description`, `twitter:image`. Page-specific images: CMS pages use `Page.ogImage`, decks use `mosaicImageUrl`, archetypes and events use site default. Consolidates existing partial OG tags from page and archetype templates. Depends on F18.25. | | F18.29 | Locale-prefixed URL routing | High | Done | Add `/{_locale}/` prefix to all public editorial routes (archetypes, CMS pages) so that each language version has a distinct URL (e.g. `/en/archetypes/iron-thorns` vs `/fr/archetypes/iron-thorns`). The **homepage** (`/`) remains session-based for UX but its canonical points to the default locale (`/en/`); localized homepage routes (`/en/`, `/fr/`) also exist for SEO. Non-editorial routes (decks, events, auth, admin) retain the current session-based locale without URL prefix. A navbar locale switcher (`EN | FR`) lets users toggle between languages — on editorial routes it swaps the `_locale` in the URL directly; on session-based routes it goes through `LocaleSwitchController`. `LocaleListener` updated: route-level `_locale` now takes precedence over user/session preference. 301 redirects from all legacy unprefixed paths to `/en/` equivalents for SEO continuity. Sitemap generates entries for each locale for editorial content. robots.txt updated with locale-prefixed allow/disallow rules. Unblocks F18.26 (hreflang). | + +--- + +## F20 — Theming + +OS- and brand-driven visual variants of the application UI. + +| ID | Feature | Priority | Status | Description | +|---------|----------------------------------------------|----------|--------|-------------| +| F20.1 | Dark theme following OS preference | Medium | Done | Auto dark mode that follows `prefers-color-scheme`. An inline `` script in `base.html.twig` mirrors the OS preference onto `` so popovers and inputs match the surrounding chrome. Applied to both the default theme and the `expandedtalks` channel theme. No user toggle, no persistence — strictly follows OS. | diff --git a/templates/base.html.twig b/templates/base.html.twig index 96835a2..71702a1 100644 --- a/templates/base.html.twig +++ b/templates/base.html.twig @@ -11,6 +11,19 @@ + {# F20.1 — Mirror the OS color scheme onto the document so Bootstrap and Mantine see it before first paint. #} + {% block title %}{{ channel_param('brand_name', 'Expanded Decks') }}{% endblock %} {% block canonical %}{% endblock %} From bf9c528c6ba67ff40803bc68a806811486699ab2 Mon Sep 17 00:00:00 2001 From: Julien Bourdin Date: Wed, 6 May 2026 09:14:41 +0200 Subject: [PATCH 3/4] fix(theme): restore Bootstrap dark mode for cards and other surfaces MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The compile-time SCSS overrides at the top of app.scss ($card-bg: #fff, $card-border-color: #ddd, $card-cap-bg: #f7f7f7, $table-striped-bg) get burned into Bootstrap's component selectors as `.card { --bs-card-bg: #fff }`, which has higher specificity than `:root[data-bs-theme="dark"]`. Bootstrap's native dark mode for those surfaces never fires, so cards stay white in dark mode while body text adapts to light gray — light text on a white card is invisible. Re-declare the affected CSS custom properties on the component selectors (.card, .dropdown-menu, .modal-content, .popover, .list-group, .table, .alert, .form-control, .form-select) under [data-bs-theme="dark"] so the dark variant takes priority. Also point the themed card header active tab to var(--bs-card-bg) instead of var(--bs-body-bg) so the active tab sits flush with the (now-dark) card body. --- assets/styles/app.scss | 73 ++++++++++++++++++++++++++++++++++++------ 1 file changed, 64 insertions(+), 9 deletions(-) diff --git a/assets/styles/app.scss b/assets/styles/app.scss index 430a0a0..0a95551 100644 --- a/assets/styles/app.scss +++ b/assets/styles/app.scss @@ -756,27 +756,82 @@ footer { background-image: none; } + // The SCSS overrides at the top of this file ($card-bg, $card-border-color, + // $card-cap-bg, $table-striped-bg, $secondary) hardcode light values into + // Bootstrap's component selectors (e.g. `.card { --bs-card-bg: #fff }`), + // which has higher specificity than `:root[data-bs-theme="dark"]`. To + // restore Bootstrap's native dark mode for those surfaces we re-declare + // the CSS custom properties on the component selectors under dark mode. + .card { + --bs-card-bg: #1a2238; + --bs-card-border-color: rgb(255 255 255 / 12%); + --bs-card-cap-bg: rgb(255 255 255 / 4%); + } + + .dropdown-menu { + --bs-dropdown-bg: #1a2238; + --bs-dropdown-border-color: rgb(255 255 255 / 12%); + --bs-dropdown-link-hover-bg: rgb(255 255 255 / 8%); + --bs-dropdown-divider-bg: rgb(255 255 255 / 10%); + } + + .modal-content { + --bs-modal-bg: #1a2238; + --bs-modal-border-color: rgb(255 255 255 / 12%); + } + + .popover { + --bs-popover-bg: #1a2238; + --bs-popover-border-color: rgb(255 255 255 / 12%); + } + + .list-group { + --bs-list-group-bg: #1a2238; + --bs-list-group-border-color: rgb(255 255 255 / 12%); + --bs-list-group-action-hover-bg: rgb(255 255 255 / 6%); + } + + .table { + --bs-table-striped-bg: rgb(255 255 255 / 4%); + --bs-table-border-color: rgb(255 255 255 / 12%); + } + + .alert { + --bs-alert-border-color: rgb(255 255 255 / 12%); + } + + .form-control, + .form-select { + background-color: rgb(255 255 255 / 4%); + border-color: rgb(255 255 255 / 15%); + color: var(--bs-body-color); + + &:focus { + background-color: rgb(255 255 255 / 6%); + } + } + .hero-pokemon { background: linear-gradient(135deg, #1c2538 0%, #2a3552 100%); .hero-text { - color: rgba(#fff, .8); + color: rgb(255 255 255 / 80%); } } .card-header-themed { - background-color: rgba(#fff, .06); + background-color: rgb(255 255 255 / 6%); border-bottom-color: var(--ed-gold); .nav-tabs .nav-link.active { - color: var(--ed-text, #e6e8ee); - background-color: var(--bs-body-bg); + color: var(--bs-body-color); + background-color: var(--bs-card-bg); border-color: var(--bs-border-color) var(--bs-border-color) transparent; } } .table-themed thead { - background-color: rgba(#fff, .06); + background-color: rgb(255 255 255 / 6%); } .badge-lent { @@ -788,20 +843,20 @@ footer { } .cms-content blockquote { - border-left-color: rgba(#fff, .3); - background-color: rgba(#fff, .05); + border-left-color: rgb(255 255 255 / 30%); + background-color: rgb(255 255 255 / 5%); } /* stylelint-disable-next-line selector-class-pattern */ .mantine-RichTextEditor-content, .cms-content { th { - background-color: rgba(#fff, .08); + background-color: rgb(255 255 255 / 8%); } } /* stylelint-disable-next-line selector-class-pattern */ .mantine-RichTextEditor-content .selectedCell { - background-color: rgba(#fff, .12); + background-color: rgb(255 255 255 / 12%); } } From a11980285d36ff84dfa2b6b2cc976adf117511e8 Mon Sep 17 00:00:00 2001 From: Julien Bourdin Date: Wed, 6 May 2026 09:28:30 +0200 Subject: [PATCH 4/4] docs(changelog): add 1.10.0 release notes --- docs/changelog.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/docs/changelog.md b/docs/changelog.md index ab8b505..581eec2 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -16,6 +16,24 @@ Items marked *(partial)* have scaffolding or basic functionality but are not yet --- +## [1.10.0] — 2026-05-06 + +Minor release: OS-preference dark theme. + +### Features + +- **F20.1 — Dark theme following OS preference** — auto dark mode that mirrors `prefers-color-scheme` onto `` and `` via an inline `` bridge script in `templates/base.html.twig`. The script runs synchronously before first paint (no flash of light theme) and listens for live OS toggles. Bootstrap 5.3's native dark variables drive framework components; new `--ed-navy/--ed-blue/--ed-gold/--ed-red/--ed-bg` CSS custom properties carry the custom palette and switch under `[data-bs-theme="dark"]`. Custom dark-tinted surfaces (`.hero-pokemon`, `.card-header-themed`, `.table-themed thead`, `.cms-content blockquote`, rich-text tables, status badges) get explicit dark overrides since their light recipes use semi-opaque navy on white. Mantine islands wrap a shared `` so popovers and inputs match the surrounding chrome. Applied to both the default theme and the `expandedtalks` channel theme. Strictly auto: no user toggle, no `User.preferredTheme` field, no localStorage. ([#524](https://github.com/jbourdin/expandedDecks/pull/524)) + +### Bug Fixes + +- **Restore Bootstrap dark mode for cards and other surfaces** — the compile-time SCSS overrides at the top of `app.scss` (`$card-bg: #fff`, `$card-border-color: #ddd`, `$card-cap-bg`, `$table-striped-bg`) get burned into Bootstrap's component selectors as `.card { --bs-card-bg: #fff }`, which has higher specificity than `:root[data-bs-theme="dark"]`. In dark mode body text adapted (light gray) but cards stayed white → light-on-white = invisible. Re-declared the affected CSS custom properties on the component selectors (`.card`, `.dropdown-menu`, `.modal-content`, `.popover`, `.list-group`, `.table`, `.alert`, `.form-control`, `.form-select`) under `[data-bs-theme="dark"]`. ([#524](https://github.com/jbourdin/expandedDecks/pull/524)) + +### Documentation + +- **Drop milestone-based planning guidance** — remove milestone instructions from `CLAUDE.md` (issue creation + board ordering) and the Milestones (Phases) section from `docs/roadmap.md`. The phase milestones in the repo are no longer used as a planning structure; the kanban project board is the planning surface. ([#523](https://github.com/jbourdin/expandedDecks/pull/523)) + +--- + ## [1.9.4] — 2026-05-03 Patch release: a deck-form field-order fix and a project-wide test coverage push from 85.87 % to ~92.4 %.