Synvro.Modulus.Blazor
1.8.1
dotnet add package Synvro.Modulus.Blazor --version 1.8.1
NuGet\Install-Package Synvro.Modulus.Blazor -Version 1.8.1
<PackageReference Include="Synvro.Modulus.Blazor" Version="1.8.1" />
<PackageVersion Include="Synvro.Modulus.Blazor" Version="1.8.1" />
<PackageReference Include="Synvro.Modulus.Blazor" />
paket add Synvro.Modulus.Blazor --version 1.8.1
#r "nuget: Synvro.Modulus.Blazor, 1.8.1"
#:package Synvro.Modulus.Blazor@1.8.1
#addin nuget:?package=Synvro.Modulus.Blazor&version=1.8.1
#tool nuget:?package=Synvro.Modulus.Blazor&version=1.8.1
Modulus UI
Admin-panel UI for Blazor, ASP.NET Core MVC and Razor Pages, built on Ant Design Blazor 1.6.2 and its official Pro layout, on .NET 10.
One theme, one menu definition and one set of design tokens produce the same Tabler-style admin template in every flavor. Blazor uses the real Ant Design Blazor components. MVC and Razor Pages use tag helpers that emit the same Ant Design DOM, with htmx and Alpine.js for interactivity. The three sample apps render the same 90+ pages, pixel-for-pixel where the controls allow it.
| Blazor — analytics dashboard | MVC — eCommerce dashboard (dark) |
|---|---|
| Razor Pages — kanban | Blazor — data table (dark) |
| MVC — form wizard | Razor Pages — sign-in |
What you get
- Every Ant Design Blazor component, as-is. Modulus adds thin
App*aliases (AppButton : AntDesign.Button), so every upstream parameter works and new ones arrive automatically on upgrade. The coverage manifest lists 90 aliases; CI verifies them against the pinned AntDesign package. - Tabler-style shell: header with page search (Ctrl K), an app switcher, a tenant switcher, notifications and account menu; a horizontal navbar,
a condensed one-row header or a vertical sidebar, all switchable in a settings panel and remembered in cookies. Layout
settings: content width (boxed, fluid, framed), header (fixed, sticky, static, floating; light or dark), sidebar
(start or end side; default, transparent or floating; full, folded with flyouts, or folded until hovered), light or
dark menu, and left-to-right or right-to-left. Fifteen one-click presets cover the usual demo layouts (Boxed,
Condensed, Fluid, Fluid vertical, Horizontal, Navbar dark / floating / sticky, Right vertical, RTL, Vertical,
Vertical transparent / floating, Folded sidebar, Folded hover); defaults come from
ModulusThemeOptions; footer; light/dark mode with no flash on load. One renderer (ShellRenderer) produces the markup for Blazor'sAppShelland MVC's_ModulusLayout. The Ant Design ProModulusLayoutis still available for Blazor. Header content (user, notifications,Apps,Tenants, links) comes from yourIShellContentProvider. A tenant without anHrefis switched by writing themodulus-tenantcookie and reloading (cancel themodulus:tenant-changeevent to handle it yourself); setShellContent.CurrentTenantIdwhen your app resolves the tenant itself. Nested menus fold as a tree to any depth: in a navbar dropdown, a group directly under it is a bold section with its entries indented (as in Odoo's app menus) and deeper groups fold open in place; in the sidebar, groups are folding branches. The branches on the path to the current page start open.MenuScope.Appshows only the current app's menu (Odoo style), with the other apps in the launcher. Nested menus fold as a tree to any depth: in a navbar dropdown, a group directly under it is a bold section with its entries indented (as in Odoo's app menus) and deeper groups fold open in place; in the sidebar, groups are folding branches. The branches on the path to the current page start open.MenuScope.Appshows only the current app's menu (Odoo style), with the other apps in the launcher. - Pro components following ProComponents conventions:
ProTable(server paging, sorting, selection and batch actions, expandable row details viaRowDetail, resizable columns, density, reload, a filter row, Columns and Export CSV, a Clear button while a search or filter is set),QueryFilter(collapsible search form),ModalForm/DrawerForm(validated, async submit),StatisticCard+Trend,FooterToolbar. - Row drag and drop in the data table, the editable grid and every nested grid level: drag the handle, or focus it
and press Up, Down, Home or End. Expanded and nested rows move with their row, and the handles lock while a column
sort is on. Blazor reports the move (
ProTable.OnRowMove,EditableGrid.RowDraggable/OnRowMove,RowMove); in MVC the grid posts its rows in the new order (row-draggable). - One pager everywhere (
Paging): page sizes 10/20/50/100, the "1–10 of 42" range and Ant Design's page window with ••• jumps, used by the data table, the editable grid, nested levels andapp-pagination. - Editable grid (
EditableGrid/app-editable-grid): columns are data (GridColumn: text, number, currency, percent, select, multi-select, tag, date, switch, progress, rating). Cells edit in place with the matching control and are validated; rows expand into a detail template; columns pin left or right, hide, reorder, resize (drag the header edge) and are added or removed at run time. It looks and works like the data table: a search box, a filter row (GridColumn.Filterable), Columns and Export CSV buttons, row checkboxes with a batch bar (set a value, export or delete the selected rows), and cells with an initials avatar (Avatar), a second line (SubtitleKey) or option icons (GridOption.Icon). Blazor and MVC share one schema, formatter, search and validator (GridSchema). - Nested grid (
NestedGrid/app-nested-grid): one component for any depth.NestedGridSettingslists the levels, each with its own columns (or a column set picked by a field of the parent row), search, filters, sort, page size, editing and expand rules; the number of levels is the nesting limit. Every level has the editable grid's toolbar, selection and export, and searches and filters on the server (NestedGridRequest.Filters). A child table loads the first time its parent row expands (BlazorLoad(NestedGridRequest), MVC an htmx GET), and each level saves on its own (NestedGridChanges: the page as edited and the deleted ids). - Large data (
TableQuery<T>inModulus.Ui.Abstractions.Data): turns aProTableRequestinto anIQueryableyour database runs, so a table over millions of rows reads one page. Only registered sorts and column searches are applied; numbered pages useSkip/Takeand the rows after a page use a keyset cursor.CountMode.NoneskipsCOUNT(*). MVC adds<app-load-more>(infinite scroll) and<app-virtual-table>(only the rows in view are in the DOM); Blazor usesVirtualizeover the same query./tables/largeshows all three over 100,000 rows.static readonly TableQuery<Order> Orders = TableQuery<Order>.By(o => o.Id) .Sort("date", o => o.Date).Sort("amount", o => o.Amount) .SearchText("customer", o => o.Customer) .DefaultSort("date", descending: true); var page = await Orders.PageAsync(db.Orders, request, CountMode.Exact, (q, ct) => q.CountAsync(ct), ct); - Pivot grid in the manner of Odoo's pivot view (
AppPivotGrid/app-pivot-grid, onePivotHtmlrenderer):PivotQuery<T>registers the dimensions (text, enum, dates by year, quarter, month, week or day) and the measures (sum, average, min, max; count is built in). As in Odoo only open headers are read: one groupedIQueryableper row depth × column depth that has open headers, filtered to the records under them, so a collapsed header costs one group and every total comes from the database (averages as sum/count), capped byMaxGroups. The whole layout lives in the URL (PivotState): every control is a link — expand a header on the next group-by (a date field gets an interval submenu), collapse, flip axis, expand all, pick the measures, sort by one. Value cells drill down (CellHref/cell-href; the sample opens/tables/largefiltered) andPivotXlsxwrites the xlsx download (merged headers, bold totals) with no library./tables/pivotpivots the 100,000 shipments in all three samples. - Multi-column sort and edit states: Shift+click a header to sort by several columns (
ProTable MultiSort,thenByin the URL, applied byTableQueryas oneORDER BY); the editable and nested grids colour changed rows and cells, added rows and the row being edited until you save. - Group by:
ProTable GroupFieldsadds a Group by menu (?groupBy=in the URL);TableQuery.Group(…)andTotal(…)sort by the group first and read each group's row count and sums, averages, minima or maxima in one SQLGROUP BY, so every group gets a header row (collapsible) and a subtotal row, with totals under the table. MVC renders the same rows withTableGroups.HeaderRow/FooterRow. - Multi-level headers:
HeaderGroupsonProTableandEditableGridputs titled bands ("Job", "Employment") over neighbouring columns, by column key, on as many levels as you need; a hidden column narrows its band. MVC hasheader-groupson<app-editable-grid>, and other tables write the same rows withHeaderGroups.Rows(shownColumns, groups)at the top of the<thead>. - Checkbox filter menu: a column's search menu lists its values with checkboxes, a search box, Select all,
"(Blanks)" and counts, as in Excel. The pick is a column search (
=a|bor!=a|b) thatTableQueryandModulusDataGridrun as SQLIN/NOT IN. Blazor:ColumnHeader Values; MVC:ColumnHeader.Content(values:); the editable grids list Select, Tag and Switch columns by themselves (GridSchema.SearchValues). - Row states:
GridRecord.Statemakes a rowReadOnly(cells fixed) orLocked(no edit, delete or move), with a reason shown as a lock or eye mark;GridError.ForRowflags a whole row (BlazorValidateRow, or therowRuleofGridSchema.Validate); MVC holds a post to the store withGridSchema.Protect. - Saved views and field access (
Modulus.Ui.Abstractions.State):AppSavedViews/app-saved-viewsis a "Views" dropdown. A signed-in user names the current layout (its query string), stars one view as the default and deletes views. Views are kept per user inIUiStateStore; the default is in memory, so register your own store to keep them (the samples useEfUiStateStoreon SQLite). The samples have it on/tables/pivotand/tables/advanced; a BlazorProTablewithUrlStatekeeps its paging, sort and column searches in the URL with the MVC keys, so its views match.FieldAccesshides fields by role:TableQuery.Restrict,PivotQuery.RestrictandGridSchema.Allowedapply it before the query runs. MVC and Razor Pages map the endpoint withapp.MapModulusSavedViews(). - SPA navigation for MVC and Razor Pages (
ModulusThemeOptions.SpaNavigation): links and forms in the shell load through htmxhx-boost. The server answers with only the page body, the sidebar and the header, so scripts and Alpine state stay loaded. It aborts stale navigations, shows a progress bar, prefetches on hover (SpaPrefetch), restores scroll positions and moves focus to the page. Page code runs throughModulus.page(fn)orModulus.onPage(fn). See UPGRADING before turning it on. - Liquid templates (
Modulus.AspNetCore.Fluid, Fluid 2.40.0): pages and documents written as.liquidfiles, with a layout ({{ content }}is the page body) and the same markup as the components. Every printed value is HTML-escaped; only aLiquidHtmlis printed as it is.{% modulus_head %}links the Modulus stylesheets and scripts,{% icon "name" %}draws an Ant Design icon,{% barcode "value" %}a Code 128 symbol,{{ text | status_badge: "success" }}a status pill and{{ amount | money }}the amount with two decimals. A model's properties are readable only once its type is registered (ModulusLiquidOptions.Register<T>()). The samplesamples/Modulus.Samples.Fluid(port 5103) lists and shows the invoices. - Hosting defaults (
Modulus.AspNetCore.Hosting):AddModulusHardening()andUseModulusHardening()add Brotli and gzip for text, the security headers (no MIME sniffing, a referrer policy, same-site framing, no camera, microphone or location), and a Content-Security-Policy with a per-request nonce (CspNonce.Of(Context)). MVC and Razor Pages enforce it; Blazor reports it, because its import map is an inline script with no nonce. TheMapModulusHealth()endpoint answers/health.AddModulusObservability()adds OpenTelemetry traces, metrics and logs, sent over OTLP whenOTEL_EXPORTER_OTLP_ENDPOINTis set. The versioned asset URLs the MVC head writes (?v=) are cached for a year. See UPGRADING. - Charts with no JS dependency:
LineChart(line/area, crosshair tooltip),ColumnChart(grouped/stacked),BarChart,DonutChart,HeatmapChart,Sparkline,MiniBar, plus business charts (treemap, pareto, waterfall, variance, bullet, gauge, funnel, combo and more). The geometry is shared by Blazor and MVC, hover tooltips are CSS-only, and each chart carries a screen-reader data table.LegendToggle/legend-togglelets the legend hide series;Links/linksmakes columns and axis labels drill-down links. - Plugins, each a Blazor component and a tag helper over one shared renderer in
Modulus.Ui.Abstractions.Plugins:AppEditor/app-editor(WYSIWYG on Tiptap; the HTML is cleaned byRichText.Sanitize(images: true)),AppMap/app-map(MapLibre GL JS tile map with markers and popups),AppPdf/app-pdf(pdf.js viewer: one page at a time, zoom and a download link; same-site or http(s) addresses only),AppVectorMap/app-vector-map(jsVectorMap choropleth with hover values and a legend),AppSpreadsheet/app-spreadsheet(Excel-style multi-sheet grid with formulas on Univer; posts its sheets as JSON, follows the light and dark theme; prints from its ribbon, Ctrl+P or anydata-m-print="{id}"button, as does the document editor),AppMediaPlayer/app-media-player(video and audio with custom controls, poster and captions),AppUploader/app-uploader(a drop area on Uppy: each file uploads as it is dropped, with previews, progress and type, size and count limits),AppImageCropper/app-image-cropper(Cropper.js: aspect ratio buttons; the crop goes to a form field as JSON),AppCodeEditor/app-code-editor(CodeMirror 6: highlighting, folding and search for JavaScript, TypeScript, JSON, CSS and HTML; the text posts as a form field) andAppSortable/app-sortable(reorder a list, drag between lists, handles, keyboard moves; posts the order).modulus-plugins.jsloads Tiptap, MapLibre, Univer, jsVectorMap, pdf.js, Uppy, Cropper.js and CodeMirror only when a page uses them.app-uploadalso gainsaction(upload with per-file progress),max-size,max-countandlist-type="picture", andapp-date-pickergainspicker="quarter"/"year";app-range-picker presetsusesDatePeriods. - Ant Design 5 components that AntD Blazor 1.6.2 lacks, in both flavours:
AppQRCode/app-qr-code(a tested QR encoder, drawn as SVG, with icon and status overlays),AppColorPicker/app-color-picker(palette, hue and alpha sliders, hex/RGB input, presets, clear),AppFloatButton/AppFloatButtonGroup/AppFloatBackTopandAppTour/app-tour(spotlight mask, placement with flipping, keyboard). - Select with an "Add item" row:
AppCreatableSelect/app-creatable-select, single or multiple. The dropdown ends with a name, a tag colour and Add; the new option joins the list and is selected. Adding is checked byGridSchema.AddOption(unique, trimmed, 50 at most);OnAdd/add-hrefcan save it first. The editable grid's Select, Multi-select and Tag editors use it, and the data table sample filters and bulk-labels employees with it. For real records,OnAddNew/add-modalswap the inline row for an "Add new" button that opens a create form (aModalForm, or anapp-modalwhose save answersResponse.HxOptionAdded(list, option)); the new record is added to the list and selected. - Business fields for ERP-style forms, in both flavours (
Components/Fields/app-*), all posting plain invariant values:AppMoneyInput(grouped amount, currency picker, the currency's decimals),AppQuantityInput(unit of measure with its precision),AppDurationInput(1:30,1.5,90m→ minutes),AppNumberRange,AppPhoneInput(country picker + national number →+1 202 555-0111),AppMaskedInput(99-9999999),AppOtpInput(one box per character, paste and SMS autofill spread over the boxes),AppLookup(a many-to-one field with type-ahead and a "Search more…" dialog; local items or a server search),AppStatusBar(workflow stages with folded ones),AppSignaturePad(PNG data URL) andAppRichText(a smallcontenteditableeditor whose HTML is cleaned byRichText.Sanitize; its toolbar inserts a table and adds or deletes rows and columns, and Tab moves between cells).AppSchemaForm/app-schema-formdraws a whole form from aFormSchema(read from JSON withFormSchema.Parse, or built in code): text, multiline, number, money, date, choice and switch fields, with the same rules on the server (Validate, thenNormalizefor what is stored). The parsing and checks live inModulus.Ui.Abstractions.Forms(NumberText,DurationText,Currency,UnitOfMeasure,InputMask,PhoneNumber,LookupSearch,RichText,SignatureImage,FormSchema), so the server validates exactly what the browser accepts. A server lookup takesGET {search-href}?q=text&max=nand answers[{ value, label, description, fields }]. - Theming from one options object: primary, success, warning, error and info colours plus border radius.
The 10-step palette is generated with a C# port of
@ant-design/colors(tested against the official palettes). Light/dark mode persists in a cookie, so the server renders the correct theme on the first byte. The brand colour also replaces Ant Design's blue in dark mode (AntDarkBrand,antd-dark-brand.css). - Localization: list languages in
ModulusThemeOptions.Culturesand the settings panel gets a language picker that sets the culture cookie.app.UseModulusLocalization()(MVC / Razor Pages) orUseRequestLocalizationwith the same cultures (Blazor) sets the request culture, which Ant Design'sLocaleProviderand the tag helpers follow. - MVC / Razor Pages parity: the same layout (
_ModulusLayout), menu, header, theme toggle, drawer, confirm dialog and toast messages, driven byResponse.HxMessage(),HxCloseDrawer()andHxTrigger(). - Full Ant Design markup for MVC: 80+ tag helpers render the same DOM as Ant Design Blazor, so one stylesheet styles both flavours. Interactive pieces (tabs, dropdowns, collapse, modal, upload, rate) use small Alpine components with keyboard support. They need no build step.
- Responsive shell: below 992 px the navigation moves behind a menu button that slides the sidebar in as a drawer. Wide tables and segmented controls scroll inside their card instead of widening the page.
- Three complete sample admin apps (Blazor, MVC and Razor Pages) with the same pages and URLs:
- Dashboards: analytics, CRM, eCommerce, ERP.
- Apps: email, chat, calendar, kanban, invoices (list, preview, add, edit), users (list, grid, view), roles, permissions, customers CRUD, eCommerce products and orders.
- Pages: profile, account settings, pricing, FAQ, help center, changelog; sign-in and sign-out (a real cookie
sign-in against the sample users, password
modulus), register, forgot/reset password, verify email, two-step verification; 404, 403, 500, maintenance and coming soon. - Interface, forms and tables: component gallery (every Ant Design category), cards, charts, icons (every Ant Design icon, search, copy the markup), typography, colours, form layouts, a validated multi-step wizard, a validation showcase, a business-fields form, a data table (search, filters, sorting, paging, column picker, density, selection and CSV export), the editable and nested grids, and a 100,000-row large data page.
- Extra: 22 Tabler extra pages (gallery, search results, activity, tasks, logs, license, job listing, uptime, trial ended, cookie banner, widgets and more).
- Plugins: advanced charts, colour picker, date picker, dropzone, sortable, media player, lightbox, map, full-size map, vector map, tour, WYSIWYG editor, code editor, image cropper, PDF viewer, spreadsheet and a dropzone on Uppy.
Quick start — Blazor
// Program.cs
builder.Services.AddRazorComponents().AddInteractiveServerComponents();
builder.Services.AddModulusBlazor(theme =>
{
theme.Title = "Contoso ERP";
theme.PrimaryColor = "#1677ff";
});
builder.Services.AddTransient<IMenuContributor, AppMenuContributor>();
@* App.razor <head> *@
<ModulusHead />
@* before </body> *@
<script src="_content/AntDesign/js/ant-design-blazor.js"></script>
@* Routes.razor — once, after the Router *@
<AntContainer />
@* MainLayout.razor — the Tabler shell (or <ModulusLayout> for the Ant Design Pro BasicLayout) *@
<AppShell>@Body</AppShell>
public sealed class AppMenuContributor : IMenuContributor
{
public Task ConfigureMenuAsync(MenuConfigurationContext context)
{
context.Add(new MenuItemDefinition("dashboard", "Dashboard", "", "dashboard") { Order = 0, ExactMatch = true })
.Add(new MenuItemDefinition("customers", "Customers", "customers", "team") { Order = 10, RequiredPolicy = "Customers.Read" });
return Task.CompletedTask;
}
}
A page:
<PageContainer Title="Customers">
<ProTable TItem="Customer" HeaderTitle="Customer list" RowKey="c => c.Id" Request="LoadAsync">
<Columns>
<PropertyColumn Property="c => c.Name" Sortable />
<PropertyColumn Property="c => c.Company" />
</Columns>
</ProTable>
</PageContainer>
@code {
private Task<ProTableResponse<Customer>> LoadAsync(ProTableRequest request) =>
Task.FromResult(_customers.SortBy(request, "Name", c => c.Name).ToPage(request));
}
See samples/Modulus.AdminTemplate.Blazor/Components/Pages/Apps/Customers/CustomerList.razor for the full pattern:
filter, table, batch delete and drawer form.
Quick start — MVC / Razor Pages
builder.Services.AddControllersWithViews(); // or AddRazorPages()
builder.Services.AddModulusMvc(theme => theme.Title = "Contoso ERP");
builder.Services.AddTransient<IMenuContributor, AppMenuContributor>();
@* _ViewImports.cshtml *@
@addTagHelper *, Modulus.AspNetCore.Mvc
@* _ViewStart.cshtml *@
@{ Layout = "_ModulusLayout"; }
<app-page-container title="Customers">
<app-card title="Customer list">
<app-card-extra>
<app-button type="primary" icon="plus" hx-get="/customers/new" hx-target="#modulus-drawer-body"
x-on:click="$store.drawer.show('New customer')">New customer</app-button>
</app-card-extra>
<div id="table" hx-get="/customers/table" hx-trigger="load, customers:refresh from:body"></div>
</app-card>
</app-page-container>
// Controller, after a successful save:
Response.HxMessage(MessageType.Success, $"Created {customer.Name}");
Response.HxCloseDrawer();
Response.HxTrigger("customers:refresh");
return NoContent();
Tag helpers. /ui/components in the MVC and Razor Pages samples shows each one; the Blazor sample shows its twin at the same URL:
| Category | Tag helpers |
|---|---|
| General | app-button (loading), app-icon, app-title, app-text, app-paragraph, app-link, app-float-button, app-float-button-group (click/hover trigger), app-float-back-top |
| Layout | app-divider, app-space, app-row / app-col (24-column grid, gutters, xs–xxl), app-flex, app-layout / app-layout-header / app-layout-sider / app-layout-content / app-layout-footer, app-splitter / app-splitter-panel (drag and arrow keys) |
| Navigation | app-breadcrumb, app-tabs / app-tab, app-steps / app-step, app-dropdown / app-dropdown-item, app-dropdown-button, app-menu / app-sub-menu / app-menu-item / app-menu-divider (horizontal, vertical, inline), app-pagination (htmx or plain links), app-anchor / app-anchor-link (scroll-spy), app-affix, app-back-top |
| Data entry | app-form-item (asp-for + validation), app-input (affixes, addons, clear, password, datalist AutoComplete), app-input-search, app-textarea (count), app-input-number, app-select, app-date-picker (date/week/month/time), app-range-picker, app-checkbox, app-checkbox-group, app-radio-group, app-switch, app-slider, app-rate, app-segmented, app-upload, app-cascader, app-tree-select, app-transfer, app-auto-complete, app-mentions, app-input-group (compact), app-color-picker (hex/rgb, alpha, presets, clear), app-creatable-select ("Add item" row with tag colours; add-href saves new options), app-schema-form (a dynamic form from a FormSchema) |
| Data display | app-card (app-card-cover, app-card-meta, app-card-action), app-comment / comment-action, app-countdown, app-image-group (shared preview), app-avatar / app-avatar-group, app-badge / app-badge-ribbon, app-tag, app-statistic, app-descriptions, app-table (hidden-columns, density, scroll-x, table-footer), app-table-columns (column picker), app-table-settings (reload, density), app-list, app-timeline, app-collapse / app-panel, app-image, app-tooltip, app-popover, app-empty, app-tree (checkable), app-calendar, app-carousel / carousel-slide, app-qr-code (icon, status), app-tour / tour-step |
| Feedback | app-alert, app-progress (line/circle/dashboard), app-result, app-skeleton, app-spin, app-modal, app-drawer (right/left/top/bottom), app-watermark; plus Response.HxMessage, HxNotify, HxCloseModal and hx-confirm |
| Pro | app-dashboard (tiles on a 12-column grid, moved and resized by buttons, saved per user; DashboardModel, MapModulusDashboard), app-file-manager (folders, breadcrumb, search, create, rename, delete; FileManagerModel), app-page-container, app-page-header, app-statistic-card, app-trend, app-editable-grid (grid-toolbar, grid-title-extra, grid-filters, grid-batch, grid-summary, row-draggable; read posts with EditableGridForm.Read / Apply), app-nested-grid (one level per request; state and saves through NestedGridForm), app-pivot-grid (Odoo-style pivot: expand/flip/measures/sort in the URL, cell-href drill-down, xlsx download) |
| Large data | app-load-more (infinite scroll), app-virtual-table (virtual scrolling, modulus-virtual.js) |
| Charts | app-line-chart, app-column-chart, app-bar-chart, app-donut-chart, app-heatmap, app-sparkline, app-mini-bar and the business charts; legend-toggle, links |
| Plugins | app-barcode (Code 128, EAN-13 as SVG), app-editor (Tiptap), app-map (MapLibre), app-vector-map (jsVectorMap), app-pdf (pdf.js), app-spreadsheet (Univer), app-uploader (Uppy), app-image-cropper (Cropper.js), app-code-editor (CodeMirror), app-media-player, app-sortable / app-sortable-item, app-lightbox |
| Icons | app-icon (Ant Design), app-ti (an Ant Design outline icon by name, drawn inline like the shell's; tb: names for the Tabler fallbacks where Ant Design has no icon) |
Controls bind with asp-for (name, id, value and validation state) or a plain name. Any other attributes you add,
such as placeholder, hx-* or x-*, go to the real <input>. Forms stay plain HTML and post normally.
Theme options
Bind the options from configuration or set them in code:
"Modulus": { "Theme": { "Title": "Modulus Admin", "PrimaryColor": "#066fd1", "BorderRadius": 6,
"DefaultMode": "Light", "Layout": "Horizontal", "MenuScope": "App",
"SpaNavigation": true, "Cultures": [ "en-US", "de-DE", "fr-FR" ] } }
| Option | Default | |
|---|---|---|
Title, LogoUrl |
"Modulus Admin", built-in mark | Header brand and document title suffix |
PrimaryColor, SuccessColor, WarningColor, ErrorColor, InfoColor |
Tabler palette (#066fd1, #2fb344, #f59f00, #d63939; info follows primary) |
10-step palettes generated server-side |
BorderRadius |
6 | Controls; cards and overlays use radius + 2 |
DefaultMode |
Light |
Light, Dark or System; the user's choice is stored in the modulus-theme cookie |
NavTheme, HeaderTheme |
Light, Light |
Navigation and header row colour |
Layout |
Horizontal |
Horizontal, Vertical or Condensed |
ContentWidth, Header |
Boxed, Fixed |
Width Boxed / Fluid / Framed; header Fixed / Sticky / Static / Floating |
SidebarPosition, SidebarStyle, SidebarFold |
Left, Default, Full |
Vertical layout only |
Direction |
Ltr |
Rtl sets dir="rtl" on <html> |
MenuScope |
All |
App shows only the current app's menu (Odoo) |
Density, Font, Base |
Compact, Sans, Gray |
Spacing and type size, font family, neutral palette |
ColorPresets, ShowCustomizer |
eight colours, true |
The theme settings panel |
Cultures |
none | Two or more add a language picker (see Localization above) |
SpaNavigation, SpaPrefetch |
false, true |
MVC / Razor Pages SPA navigation and hover prefetch |
SiderWidth, HeaderHeight |
240, 56 | Comfortable density; compact uses 224 and 48 |
Copyright, Version |
"{year} Modulus", none | Footer |
Users change most of these in the settings panel; their choices are kept in cookies, so the server renders them on the first paint.
Projects
| Project | Purpose |
|---|---|
src/Modulus.Ui.Abstractions |
Flavor-agnostic: theme options and palette generation, menu contributors, the shell renderer, ProTableRequest/Response, TableQuery, Paging, chart geometry, form rules, plugin renderers |
src/Modulus.Ui.Assets |
CSS (modulus.css, modulus-shell.css, modulus-pages.css, antd-dark-brand.css), scripts (modulus-theme.js, modulus-shell.js, modulus-mvc.js, modulus-widgets.js, modulus-plugins.js, modulus-spa.js, modulus-virtual.js), vendored htmx 2.0.11, Alpine.js 3.17.4, MapLibre GL JS 5.24.0, Tiptap 3.31.4 (bundle), Univer 1.0.3 (bundle), jsVectorMap and pdf.js 6.3.289 (ESM) |
src/Modulus.Blazor |
AddModulusBlazor, App* aliases, ProLayout shell, Pro components, charts (no server framework dependency, so it is WASM-compatible) |
src/Modulus.AspNetCore.Mvc |
AddModulusMvc, tag helpers, _ModulusLayout and partials, htmx response helpers, AntIcons |
samples/Modulus.AdminTemplate.* |
The Blazor, MVC and Razor Pages admin templates |
src/Modulus.AspNetCore.Hosting |
Hosting defaults: response compression, security headers, the CSP with a nonce, the /health endpoint, OpenTelemetry traces, metrics and logs |
src/Modulus.AspNetCore.Fluid |
Liquid templates for pages and documents: ModulusLiquid (escaped output, layouts, the file cache), the modulus_head, icon and barcode tags and the status_badge and money filters |
samples/Modulus.Samples.Shared |
Models, in-memory stores, menu and page data shared by all three samples |
samples/Modulus.Samples.Fluid |
A Liquid sample on port 5103: an invoice list and an invoice page, rendered from Templates/ |
samples/Modulus.Samples.Web |
Page bodies (Views/Shared/Ui/**) shared by MVC and Razor Pages: controllers and page models only bind input and pick the view |
tests/* |
bUnit component tests, palette/menu/table/chart unit tests, tag helper and icon tests, the Liquid and hosting tests |
tests/browser |
The quality gates (CI's browser job, run against the samples on 5100 to 5103): smoke.py (pages, console, CSP, compression, health, SPA), parity.py (the pivot markup across the flavours) and budgets.py (gzipped asset sizes, in the build job) |
tools/antd-drift |
Checks the manifest and aliases against the pinned AntDesign package (see UPGRADING) |
Build, test, run
dotnet build Modulus.slnx
dotnet test --solution Modulus.slnx
dotnet run --project tools/antd-drift
dotnet run --project samples/Modulus.AdminTemplate.Blazor # http://localhost:5100
dotnet run --project samples/Modulus.AdminTemplate.Mvc # http://localhost:5101
dotnet run --project samples/Modulus.AdminTemplate.RazorPages # http://localhost:5102
dotnet run --project samples/Modulus.Samples.Fluid # http://localhost:5103
Run the samples in the Development environment (the default launch profile does this) or publish them;
dotnet run in Production does not serve library static assets. Sign in with any sample user's email and the
password modulus; set Samples:RequireSignIn to true in appsettings.json to require it on every page.
NuGet packages
The four libraries in src/ share one version (VersionPrefix in src/Directory.Build.props) and ship with
SourceLink, symbols and this README:
dotnet pack Modulus.slnx -c Release -o artifacts/packages
CI packs -ci.<run> prereleases as the packages artifact; a local dotnet pack without a suffix packs the release version.
Known limitations
- Ant Design Blazor 1.6 ships antd v4 styling. Modulus adds a v5-style refresh layer (6 px radius, softer shadows, v5 menu and table styling) over it. The result looks like modern Ant Design Pro, but it is not pixel-identical to antd v5.
- Brand colour in dark mode is generated. AntDesign's dark stylesheet hard-codes antd blue, so
antd-dark-brand.cssrepeats the declarations that use it with the brand's dark palette. It is generated from the pinned AntDesign version;tools/antd-driftfails when it no longer matches (see UPGRADING). - Icons in MVC reuse Ant Design Blazor's icon store through reflection, because the API is internal. A unit test fails if an AntDesign upgrade moves it.
- Interactive MVC widgets need Alpine. Cascader, TreeSelect, Transfer, Tree, Calendar, Carousel, Anchor, Mentions,
Splitter, ColorPicker, Tour and the rest run on small Alpine components (
modulus-widgets.js); form widgets post through hidden inputs, so forms still submit without script. The manifest lists the MVC equivalent for every component. - Business fields are Modulus components with no AntD Blazor counterpart. The
AppRichTextfield covers formatting, lists, links and tables only (useAppEditorfor images), and the HTML of both must still be sanitized on the server (RichText.Sanitize). Phone numbers are checked for shape (+, calling code, 8–15 digits), not against each country's numbering plan. - ColorPicker, FloatButton, QRCode and Tour are Modulus components, not AntD Blazor ones (1.6.2 has none). They emit
the antd v5 DOM, styled by
modulus.css, and may differ in detail from antd v5. - Native pickers in MVC.
app-date-pickerandapp-range-pickeruse the browser's date/time inputs inside antd chrome, so the calendar popup follows the browser, not antd. - Plugin libraries are third-party. The editor, maps, vector map, PDF viewer, drop area, cropper and code editor wrap Tiptap, MapLibre, jsVectorMap, pdf.js, Uppy, Cropper.js and CodeMirror, loaded from
Modulus.Ui.Assetson first use. Tile maps load their tiles fromtile.openstreetmap.org, whose usage policy does not cover heavy production traffic, so they need network access and, for real use, your own tile server. - Localization covers the framework, not the sample texts: Ant Design's built-in strings, dates and numbers follow the request culture, but the sample pages stay in English.
- Authorization is wired for menus (
RequiredPolicy). The samples sign in with a cookie against in-memory sample users; there is no user store, registration or password reset behind the auth pages.
Documentation
All the framework's documentation (the CLI commands, upgrading, design notes, templates status, the plan and the roadmaps) is in one file, docs/DOCUMENTATION.md. Its contents list links to each part:
- Design notes: design decisions and the analysis of other Ant Design-based libraries they come from
- Upgrading: upgrading AntDesign, htmx, Alpine.js and the plugin libraries safely, and turning on SPA navigation
- DEPENDENCIES.md: every NuGet package, vendored script, font and icon set, with its version and licence
- Plan and progress: the plan, page inventory, verification results and change log
- Roadmap: what is planned next, in Plan and progress §7 (Modulus 2.0) and §7.3 (ERP UI roadmap)
manifest/antd-coverage.json: component-by-component coverage, verified in CI
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- AntDesign (>= 1.6.2)
- AntDesign.ProLayout (>= 1.6.2)
- Synvro.Modulus.Ui.Abstractions (>= 1.8.1)
- Synvro.Modulus.Ui.Assets (>= 1.8.1)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.