Repository navigation
Add the theme toggle, reconnect display and /ui showcase - #2468
Merged
Merged
Conversation
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.
They arrived with the user management screen, and the showcase renders every component in Components/Ui.
4 tasks done
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Refs #2434
What
lightordarkwhen someone chose one and no class otherwise, so the stylesheet follows the system preference. Thedarkvariant and the dark tokens apply with thedarkclass or, with no stored choice, underprefers-color-scheme: dark; token values are unchanged, andcolor-schemenow 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 byadmin.js, which sets the class and keeps the choice in afluentcms-themecookie. The host reads the cookie throughIThemePreference(defined in Admin, implemented in Host, likeICurrentInitiator) and renders the class into<html>, so there is no inline script and no flash of the wrong theme.?theme=is gone;?dir=rtlstays until the UI culture sets the direction.ReconnectModal.razorrenders 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.cssshows the matching part (rejoining, retrying with the countdown, failed with Retry, paused with Resume, resume failed with Reload) andadmin.jsruns 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-permanentkeeps enhanced navigation off it. The CSP is unchanged./uireplaces/ui-spike. It renders every component inComponents/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.AdminOptionsgoes with the Development-only gate. Keyboard hints use shadcn's Kbd classes on a plain<kbd>; no component was added for the page.admin.jsfixes found on/ui: opening a DropdownMenu with the arrow keys from its trigger calledshowPopover()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 usedscrollIntoView, 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:
FluentCMS.Admin:App,Routesand the layouts; interactive server pages (/ui) next to static ones (setup, sign-in, home, audit); composed by Host withMapRazorComponents<App>().AddInteractiveServerRenderMode();ProjectReferenceRulesTestskeeps Admin on Core only.dirand the theme class. This pull request moves the class from the query string to the stored choice.Components/Ui, self-contained, no base classes, no project or package.IStringLocalizer(there are no resource files yet, so the English text is the key), only logical utilities, anddirflips 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/uiacceptance, which includes the WCAG checks.Why
<html>before the first paint, the CSP allows no inline script to read storage that early, andadmin.jsis 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.DialogContentandCommandInputalready do. Generating icon components from the set needs a tool or a package, so that box stays open.No new dependency: no NuGet package, icon set or test tool.
Tests
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);/uirenders every component type inComponents/Uiand fails when one is missing, lists keyboard hints in every section, reports a click on the status line and switches direction.prefers-color-scheme: dark; the reconnect element is hidden until a state class shows one part.dark,light) and is absent for no or an unknown value, on admin and account pages;/uiredirects to sign-in when anonymous and renders for a signed-in user outside Development;/ui-spikeanswers 404; the CSP hasstyle-src 'self'and nounsafe-inlineorunsafe-eval; sign-in, home and/uicarry no inline script, style or event handler. The sign-in tests now use/uias 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;
/uiin 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
/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._contentassets as empty 200 responses: the integration test host, anddotnet run --project src/FluentCMS.Hostas CONTRIBUTING describes it, run without the stylesheet andadmin.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
v2(ordevfor a v1 security fix)