Skip to content

Add the theme toggle, reconnect display and /ui showcase - #2468

Merged
pournasserian merged 3 commits into
v2from
2434-admin-shell-theme
Oct 18, 2026
Merged

pournasserian merged 3 commits into
v2from
2434-admin-shell-theme

Conversation

@pournasserian

@pournasserian pournasserian commented Oct 18, 2026 •

Copy link
Copy Markdown
Member

Refs #2434

What

  • Theme: the root element carries light or dark when someone chose one and no class otherwise, so the stylesheet follows the system preference. The dark variant and the dark tokens apply with the dark class or, with no stored choice, under prefers-color-scheme: dark; token values are unchanged, and color-scheme now follows the theme so native controls and scrollbars match. A toggle in the main layout header (shadcn's mode toggle: Light, Dark, System on the existing DropdownMenu) is handled by admin.js, which sets the class and keeps the choice in a fluentcms-theme cookie. The host reads the cookie through IThemePreference (defined in Admin, implemented in Host, like ICurrentInitiator) and renders the class into <html>, so there is no inline script and no flash of the wrong theme. ?theme= is gone; ?dir=rtl stays until the UI culture sets the direction.
  • Reconnect: ReconnectModal.razor renders the element Blazor looks for (#components-reconnect-modal), so Blazor uses it instead of its default display, whose injected <style> the CSP blocked. Blazor reports the state as a class; app.css shows the matching part (rejoining, retrying with the countdown, failed with Retry, paused with Resume, resume failed with Reload) and admin.js runs the buttons, retries when the tab becomes visible after a failure and reloads after a rejected circuit, as the default display does. It is a status card at the bottom end, not a modal. data-permanent keeps enhanced navigation off it. The CSP is unchanged.
  • /ui replaces /ui-spike. It renders every component in Components/Ui (Button, Badge, Input, FormField, Card, Table, Dialog, DropdownMenu, Command) with its keyboard hints, for any signed-in user in every environment, as Admin shell and component set #2434 asks once sign-in exists. Server actions report to a status line, so a round trip and a reconnect are visible. AdminOptions goes with the Development-only gate. Keyboard hints use shadcn's Kbd classes on a plain <kbd>; no component was added for the page.
  • Two admin.js fixes found on /ui: opening a DropdownMenu with the arrow keys from its trigger called showPopover() without a source, so the menu lost its anchor (in RTL it landed in the opposite corner); it now passes the trigger. Command's active option used scrollIntoView, which scrolled the page down to the palette on every load; it now scrolls only the list.
  • CONTRIBUTING.md: a sentence on /ui, and that a new component goes on it in the same pull request.

Boxes on #2434

Ticked now, already true on v2 before this pull request:

  • Blazor Web App in FluentCMS.Admin: App, Routes and the layouts; interactive server pages (/ui) next to static ones (setup, sign-in, home, audit); composed by Host with MapRazorComponents<App>().AddInteractiveServerRenderMode(); ProjectReferenceRulesTests keeps Admin on Core only.
  • Root element carries dir and the theme class. This pull request moves the class from the query string to the stored choice.
  • Components in Components/Ui, self-contained, no base classes, no project or package.
  • Localization and RTL: every string goes through IStringLocalizer (there are no resource files yet, so the English text is the key), only logical utilities, and dir flips the layout.

To tick when this merges: default reconnect handling; shadcn tokens with light and dark switched by a root class and defaulting to the system preference.

Left open: the interop module (focus trapping and roving tab index are there, global keyboard shortcuts are not; they come with the desk, the first screen that needs one), the tailwind-merge port, Lucide icons, the rest of the initial set, bUnit per component (Button has none, and keyboard behaviour lives in admin.js, which bUnit does not run) and the /ui acceptance, which includes the WCAG checks.

Why

  • A cookie the server reads, not local storage. The class has to be on <html> before the first paint, the CSP allows no inline script to read storage that early, and admin.js is a deferred module. A cookie lets the host render the class. The toggle itself stays in the browser, so it works on static pages and while the circuit is down.
  • Light, Dark and System rather than a switch. With two states, choosing once loses the system default for good. It is shadcn's own mode toggle.
  • Sun and Moon as inline SVG. The paths are Lucide's, written inline the way DialogContent and CommandInput already do. Generating icon components from the set needs a tool or a package, so that box stays open.
  • Not modal. ADR-0011 chose native primitives partly because they keep working through a lost circuit; a modal overlay would make the page inert for exactly that gap.

No new dependency: no NuGet package, icon set or test tool.

Tests

  • bUnit: the toggle (named trigger, three choices left to admin.js, no inline script, style or handler attributes); the reconnect markup (the element Blazor looks for, one part per state, the countdown inside the translated sentence, a button per failure, no inline style and no display utility that would override the stylesheet); /ui renders every component type in Components/Ui and fails when one is missing, lists keyboard hints in every section, reports a click on the status line and switches direction.
  • Stylesheet, from the compiled CSS: dark applies with the class or, with no stored choice, under prefers-color-scheme: dark; the reconnect element is hidden until a state class shows one part.
  • Host: the shell has the toggle and the reconnect element; the root class follows the cookie (dark, light) and is absent for no or an unknown value, on admin and account pages; /ui redirects to sign-in when anonymous and renders for a signed-in user outside Development; /ui-spike answers 404; the CSP has style-src 'self' and no unsafe-inline or unsafe-eval; sign-in, home and /ui carry no inline script, style or event handler. The sign-in tests now use /ui as their protected route.

Checked by hand in the browser: the system theme with no choice; Light, Dark and System from the header by mouse and keyboard (the choice survives a reload, System clears it); the menu end-aligned in LTR, RTL and at phone width; /ui in RTL; the reconnect card with the server stopped (the dialog still opens meanwhile), pause and Resume, the failed and resume-failed parts, and the reload after a rejected circuit. No CSP violations in the console.

Proposed, not done here

  • Automated WCAG 2.1 AA checks for /ui (UX-04) need a new test tool, for example axe-core run through Playwright. Playwright arrives with the desk flows (F-DSK); the checks fit there.
  • Unpublished runs outside Development serve the admin's _content assets as empty 200 responses: the integration test host, and dotnet run --project src/FluentCMS.Host as CONTRIBUTING describes it, run without the stylesheet and admin.js. The existing asset test checks only the status, so it passes. That is why the stylesheet test above reads the compiled file. Worth its own issue.

Rebased on v2

Merged origin/v2 (cd2e26c) after #2460, #2466 and #2467. No conflicts: the header keeps the audit and user nav entries next to the theme toggle. The Table components from #2466 are now a section on /ui, as the showcase test requires.

Checklist

  • Linked to an issue a maintainer has confirmed
  • Targets v2 (or dev for a v1 security fix)
  • Tests or docs updated where behaviour changed
  • No tool-generated co-author trailers or footers

The root carries the stored theme (light, dark) or none to follow the
system; the header toggle keeps the choice in a cookie the host reads.
Blazor's reconnect state is shown by the shell's own markup and the
stylesheet, so the strict CSP holds. /ui replaces /ui-spike for any
signed-in user and renders every component with its keyboard hints.
@pournasserian pournasserian added this to the M1 Governance core milestone Oct 18, 2026
@pournasserian pournasserian added v2 FluentCMS v2 (v2 branch) type:chore Repository, tooling or housekeeping area:infra Persistence, host, CI and build labels Oct 18, 2026
They arrived with the user management screen, and the showcase renders
every component in Components/Ui.
@pournasserian
pournasserian merged commit e9e42d0 into v2 Oct 18, 2026
2 checks passed
@pournasserian
pournasserian deleted the 2434-admin-shell-theme branch October 18, 2026 18:44
pournasserian added a commit that referenced this pull request Oct 18, 2026
Closes #2434

## What

Lucide icons as inline SVG components generated from the icon set, the
last open item of #2434.

- **`Icons.targets`** adds a `GenerateIcons` target to the Admin
project:
- `src/FluentCMS.Admin/Icons/icons.txt` lists the icons the admin uses,
one Lucide name per line.
- `dotnet build src/FluentCMS.Admin -t:GenerateIcons` downloads the
pinned Lucide release once per machine into a cache
(`%LocalAppData%\FluentCMS\lucide` or `~/.cache/fluentcms/lucide`, like
the Tailwind binary) and verifies it.
- It then writes one component per listed icon into `Components/Icons`
and copies the release's licence next to the list.
- The generated components are committed. A normal build does not
download or generate anything, so CI and offline builds are unchanged.
- **The components** are named as in lucide-react (`XIcon`,
`ChevronDownIcon`), in `FluentCMS.Admin.Components.Icons`, which
`_Imports.razor` now imports.
- Each renders Lucide's own `<svg>` attributes and shapes, with
`aria-hidden="true"` and the classes `lucide lucide-<name>` merged with
a `Class` parameter.
- Other attributes pass through and override the defaults, for example
`stroke-width="3"`, or `aria-hidden="false"` with a label for an icon
that carries meaning.
- Size comes from the surrounding classes, as in shadcn
(`[&_svg:not([class*='size-'])]:size-4`).
- **The hand-copied SVGs are gone.** Dialog, Sheet and Toaster close
buttons use `XIcon`, Checkbox `CheckIcon`, Select and DiffView
`ChevronDownIcon`, RadioGroupItem `CircleIcon`, CommandInput
`SearchIcon`, the theme toggle `SunIcon` and `MoonIcon`, and `/ui`
`PlusIcon`. The markup they render is unchanged apart from the `lucide`
classes and the `width` and `height` attributes Lucide carries.
- **`/ui`** has an Icons section listing every generated icon with its
component name.
- **`CONTRIBUTING.md`**: how to add an icon, how the set is pinned, and
its licence.

## How the set is pinned and licensed

- **Pinned:** `Icons.targets` sets `LucideVersion` to 1.59.0, the
current release, and pins two SHA-256 checksums:
- the release archive `lucide-icons-1.59.0.zip` from the GitHub release;
  - the release's `LICENSE` at the `1.59.0` tag.

Lucide publishes no checksums, so these were computed from the downloads
and are recorded here. A download that does not match is deleted and the
build fails. Every generated file names the version it came from, and a
test fails if that version differs from the pin. Updating means changing
the version and both checksums, then running the target.
- **Licensed:** Lucide is ISC. The icons it took from Feather are MIT,
and the licence file lists them; seven of the eight icons used here are
among them (all but `sun`). Both licences allow use and modification
with the notice kept. `src/FluentCMS.Admin/Icons/LICENSE` is that file
verbatim, and every generated component points to it.

## Boxes on #2434

Ticked with this pull request:

- "Lucide icons as inline SVG components generated from the icon set."
- "The `/ui` showcase page rendering every component with its keyboard
hints, in light and dark themes …". With #2489 and #2493 every component
of the initial set is on `/ui` with its hints, in both themes and for
signed-in users only (#2468).

That leaves no open box, so this closes #2434. One item in the issue's
Acceptance list has no box and is not done: automated WCAG 2.1 AA checks
on `/ui` (UX-04). They need a browser test tool, and #2468 proposed
running axe-core through Playwright when Playwright arrives with the
desk flows (F-DSK, #2422).

## Why

- **Generated and committed, not generated on every build.** A
build-time generator would download the archive on every clean machine
and CI run for a few kilobytes of markup. Committing the output keeps
the build as it is. The pin, the list and the tests keep the output
honest.
- **Only the icons in use.** The full set has 1,891 icons. A component
for each would add them all to the assembly and to Tailwind's scan for
icons nobody renders, so the list grows with the screens.
- **An MSBuild target, not a tool.** It needs only the .NET SDK, uses
the same download, verify and cache pattern as `Tailwind.targets`, and
its one inline task turns an SVG file into a component.
- **`Icon` suffix.** Several Lucide names collide with components
(`badge`, `command`, `table`). lucide-react exports the suffixed names
as well, so shadcn examples that use `XIcon` read the same.

No new package; the icons are data from the pinned release.

## Tests

- `IconsTests`:
- Every name in `icons.txt` has a generated component, and every
component is listed.
- Every component names the pinned version, and the targets file pins
both checksums.
  - The licence is the ISC one with the Feather MIT section.
- Each icon renders Lucide's `svg` (view box, stroke, stroke width),
hidden from assistive technology, with its classes merged.
  - Attributes override the defaults.
- `ShowcaseTests`: 23 sections, and the Icons section renders every
generated icon.
- The existing tests for Checkbox, Select, DiffView, the theme toggle
and the overlays pass unchanged against the generated icons.

Checked on `/ui`: the icons render at the size their surroundings give
them (16 px in buttons and inputs, 20 px in the Icons section); in dark
mode the theme toggle shows the moon and hides the sun; the checkbox
tick shows only when checked, with its heavier stroke; the radio dot
shows only on the checked option.

`dotnet build -warnaserror`, the full suite and markdownlint pass
locally, and `-t:GenerateIcons` run from an empty cache downloads,
verifies and regenerates byte-identical files.

## Checklist

- [x] Linked to an issue a maintainer has confirmed
- [x] Targets `v2`
- [x] Tests or docs updated where behaviour changed
- [x] No tool-generated co-author trailers or footers
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:infra Persistence, host, CI and build type:chore Repository, tooling or housekeeping v2 FluentCMS v2 (v2 branch)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant