Living plan. Update the Status table as work lands so a cold start can pick up exactly where we left off: what's done, what's in flight, what's next. Every component ships with a passing test (Rule 5) and, once it looks right, a slot in the demo showroom for TJ to confirm.
A polished, professional Blazor UI component library that renders through SpawnDomRenderer (no Blazor JS
runtime), shadow-root-first, themeable via CSS custom properties, and swappable live. Target quality bar:
Android-grade polish (memory user_spawnwear_android_quality_ui). Inspiration: Radzen Blazor (TJ's
favorite - 145+ components: DataGrid, Charts, ProgressBar/Circular, Tree, Notification, etc.). This library is
the UI foundation for most future SpawnJS apps.
- Custom video & audio players - his recurring build; needs sliders, buttons/icons, time labels, progress.
- File browser / explorer - has prior ones in
Projects\SpawnDev(spawndev.com) and others; needs Tree, list/grid, virtualization, context menu, breadcrumb, icons. - Virtualized DataGrid - Radzen-style: async range loading (
LoadDataover a window) so thousands+ of rows don't all load/process at once. Virtualization is a first-class requirement, not an add-on. - Full apps - dialogs, forms, navigation, notifications, layout.
- Shadow-root-first. Components styled by
wwwroot/razor-ui/base.cssloaded (by code) into each root; theme tokens on:host,:root.:rootalone does NOT match inside a shadow root - always:host,:root. - Theme tokens with fallbacks. Every
var(--ui-x, fallback)has a sane fallback so a component is usable with no theme loaded. New tokens are added to BOTHtheme.light.cssandtheme.dark.css. - Zero raw JS / zero document queries. DOM via SpawnJS typed wrappers; element access via
@refthrough the renderer (shadow-safe). SVG is fine (renderer tracks the SVG namespace). - Attribute splat + class/style merge via
UiComponentBase(CssClass/Style/PassThroughAttributes). - Accessible. Correct roles +
aria-*(e.g.role="progressbar",aria-valuenow/min/max), keyboard where due. - Tested before shown. Each component gets a shadow-root render test in
TestsShared/Tests(12/12 harness is live;dotnet run --project TestRunner). Demo wiring comes after green.
SpawnDev.SpawnJS.RazorUI/<Ui*>.razor@inherits UiComponentBase, rendersclass="@CssClass("ui-x ...")" style="@Style(...)" @attributes="PassThroughAttributes".- Styles: append rules to
wwwroot/razor-ui/base.cssusingvar(--ui-*). - Tokens:
wwwroot/razor-ui/theme.light.css+theme.dark.css(:host,:root { --ui-*: ... }). - Theme service
RazorUITheme(DI singleton,IBackgroundService) registers base+theme sheets on the renderer;SetTheme(name)live-swaps. Fingerprinting disabled on the RCL so the generated bundle filename is stable.
| Area | Component | State | Test | Demo |
|---|---|---|---|---|
| Core | SpawnDomRenderer, RazorUITheme, base/theme CSS | ✅ done | 12/12 harness | ✅ |
| Buttons | UiButton (accent/secondary) |
✅ exists | via demo | ✅ |
| Layout | UiCard, UiStack (row/col + gap) |
✅ exists | - | ✅ |
| Inputs | UiInput (@bind-Value) |
✅ exists | - | ✅ |
| Overlays | UiDialog (backdrop + z-index) |
✅ exists | - | ✅ |
| Feedback | UiProgressBar (linear: determinate/indeterminate/striped/label) |
✅ done | ✅ 2 tests | ✅ (awaiting TJ visual confirm) |
| Feedback | UiProgressCircle (donut/circular SVG: value/size/stroke/indeterminate) |
✅ done | ✅ 2 tests (incl. real dashoffset geometry) | ✅ confirmed |
| Media/Inputs | UiSlider (themed range: @bind-Value, Min/Max/Step, filled track) |
✅ done | ✅ 2 tests (render + two-way drag) | ✅ (drives the Progress demo) |
| Media/Icons | UiIcon (inline-SVG set: play/pause/stop/skip/volume/fullscreen) |
✅ done | ✅ shared with UiIconButton | ✅ media preview |
| Media/Icons | UiIconButton (circular ghost button, Icon/ChildContent, OnClick, aria-label) |
✅ done | ✅ 3 tests (icon shape, label, click) | ✅ media preview |
| Media | UiTime (mm:ss / hh:mm:ss position/duration label) |
✅ done | ✅ 2 tests (mm:ss, hh:mm:ss) | ⬜ |
| Data | UiVirtualList<T> (windowed virtualization + async LoadRange + @key rows) |
✅ done | ✅ 2 tests (virtualizes; scroll loads new window) | ⬜ |
| Data | UiTree<T> / UiTreeNode<T> (recursive, lazy async children, selection, activate) |
✅ done | ✅ 3 tests (lazy roots; expand loads; select) | ⬜ |
Legend: ✅ done · 🔜 in progress/next · ⬜ backlog
UiProgressBar- linear. Params:Value,Min=0,Max=100,Indeterminate,Striped,ShowValue,Label?. Determinate = fill width %; indeterminate = animated sweep; accessible.UiProgressCircle- circular/donut via inline SVG ring (two<circle>: track + fill viastroke-dasharray/stroke-dashoffset). Params:Value,Min,Max,Size,StrokeWidth,Indeterminate(spin),ShowValue. Center label.- Tests: DOM structure +
aria-valuenow+ fill width / dashoffset + indeterminate class. Demo showroom row.
UiSlider/UiRange(seek + volume;@bind-Value, drag, keyboard, buffered ranges overlay).UiIconButton+ an icon story (inline SVG icon set: play/pause/stop/mute/volume/fullscreen/skip).UiTimelabel (mm:ss / hh:mm:ss). Then aUiMediaControlsbar composing the above +UiProgressBarbuffer.
UiBadge,UiChip/UiChipList,UiTooltip,UiSkeleton(loading placeholder),UiAlert,UiNotification/toast host,UiSpinner.
UiTabs,UiBreadcrumb,UiMenu/UiContextMenu,UiAccordion,UiSplitter,UiAppBar/UiSidebar.
UiTree(lazy children) - file browser backbone.UiVirtualList- windowed rendering + async range load callback (LoadRange(start,count)), the core RadzenLoadDatapattern, so thousands+ items stream in by window.@keyed rows (our renderer honors@key1:1).UiDataGridon top ofUiVirtualList: columns, sort, filter, paging OR virtualization, async data source.UiPager.
UiFileBrowser- Tree + virtualized list/grid + breadcrumb + context menu + icons. Reference TJ's existing ones inProjects\SpawnDevand related. OPFS/file-source-agnostic data provider.
- Charts (Line/Bar/Column/Area/Pie/Donut/Scatter/Gauge) - Radzen has a huge set; pick what apps need.
UiDropZone(drag-drop),UiColorPicker,UiDatePicker,UiDropDown/UiSelect,UiSwitch,UiCheckbox,UiRadioGroup,UiNumeric,UiRating,UiUpload,UiScheduler,UiTimeline,UiQRCode.
Ui<Name>.razor@inherits UiComponentBase; variants via computed base class string intoCssClass(...).- Styles →
base.css(var(--ui-*, fallback)); new tokens → both theme files. - Accessibility: role +
aria-*. - Test in
TestsShared/Tests/Ui<Name>Tests.cs; register the class inTestSuiteRunner.TestTypes; rundotnet run --project TestRunnerto green. - Add to the demo (
RazorRendererDemo/App.razor) once it looks right, for TJ to confirm. - Update the Status table + Roadmap here. Commit (bugfix/feature increment = push-worthy, Rule 10).