Frontend and Theme Builds
FOSSBilling builds shared browser assets from frontend/ and theme assets from each theme workspace.
Build Commands
npm run buildchecks frontend source, builds core assets, and builds all themes.npm run build:productionruns the same build withNODE_ENV=production.npm run checkruns TypeScript checks and validates theme icon manifests.npm run build-admin_defaultandnpm run build-huragabuild a single theme.
Generated assets are written to src/public/assets and src/themes/**/assets/build. These paths are ignored by Git and are copied into release artifacts by the Docker build.
Admin widget regression tests use isolated browser fixtures and require no application database or admin credentials. Build production assets first so the tests cover purged CSS:
npm run build:production
npx playwright test tests/E2E/Playwright/specs/admin/tabler-widgets.spec.ts
Theme Dependencies
Themes own the UI packages they consume. Shared tooling in frontend/tools can help compile assets, generate sprites, and post-process CSS, but it should not make a theme depend on a specific UI package.
Examples (the shipped default theme package bundles both — src/themes/default/admin and src/themes/default/client; their npm workspace package names, admin_default and huraga, are unrelated to their directory paths):
admin_default(src/themes/default/admin) owns Tabler, Coloris, Chart.js, Vanilla Calendar Pro, Tom Select, Flag Icons, and its Tabler icon package.huraga(src/themes/default/client) owns Tabler, intl-tel-input, Tom Select, Flag Icons, and its Tabler icon package.svg-spriteis a root development dependency because the shared sprite helper imports it.
The admin theme uses Tabler's component bundle and data-bs-* APIs. Do not load Bootstrap's JavaScript alongside it. The tabler global exposes the bundle; bootstrap remains a compatibility alias for extensions.
When using admin components:
- Keep
.datepicker,YYYY-MM-DD, anddata-name-from/data-name-tofor API date fields. The datepicker adapter loads Vanilla Calendar Pro and synchronizes range values within each form. - Use the clipboard adapter to retain copy support on HTTP and when Clipboard API access is denied.
- Use
partial_password_strength.html.twigfor translated password feedback; server-side validation remains authoritative. - Use
data-bs-toggle="autosize"for plain-text notes, not CKEditor fields. Growth is capped at 24rem.
Icons
Theme icon sprites are generated from each theme's icon-manifest.json.
- Add static SVG icon usage to the relevant theme manifest.
- Mark icons as
"dynamic": truewhen the icon name is resolved at runtime, such as admin navigation icons from module metadata. - Put theme-specific SVG files in the theme's
custom-iconsdirectory. - Reference generated symbols with inline sprite IDs, for example
<use href="#home" />.
Run npm run check-icons before changing icon usage.
Shared UI
| Resource | Location | Responsibility |
|---|---|---|
| Components | src/themes/default/shared/html |
Common markup; theme partials/macros remain override entrypoints |
| UI behaviour | frontend/ui |
Tabler lifecycle, accessibility and focus handling |
| API/editor runtime | frontend/core, frontend/editor |
Requests, completion, editor integration |
| Theme styles | frontend/styles/_shared-theme.scss |
Rules identical across client and admin |
| Reading styles | frontend/styles/markdown.css |
Theme-aware Markdown typography and media containment |
Use Tabler components and utilities before adding custom CSS or JavaScript. Share implementations that have multiple consumers; keep one-off code with its owner. Retain public import paths, Twig blocks, IDs and API attributes used by extensions. Admin density, client mobile lists, branding settings and workflow callbacks remain local decisions.
Components
| Entrypoint | Contract |
|---|---|
status_badge(status, type) |
Shared semantics; solid admin and subtle client palettes. badge_palette accepts extension overrides. |
partial_pagination.html.twig |
Shared context/items; retain filters, anchors, page parameter names and each area's record controls. |
partial_sortable_th.html.twig |
Preserve query filters and reset the relevant page on sort; expose scope and aria-sort. |
partial_client_empty.html.twig, admin partial_empty.html.twig |
Text and optional recovery link. Keep legacy empty_list() table-cell behaviour. |
partial_search.html.twig, partial_client_search.html.twig |
Shared GET form. search_preserve_fields allowlists filters; explicit search_hidden_fields win. Omit page fields. |
partial_notice.html.twig |
Escaped copy or the notice_content block. Static notices have no live role; use alert/status for submission feedback. |
partial_card_heading.html.twig |
card_heading_content and card_heading_actions preserve caller headings, IDs and native controls. |
partial_page_heading.html.twig |
H1 with optional subtitle/actions; use page_title_class for compact admin titles. |
partial_breadcrumbs.html.twig |
Named landmark; retain client breadcrumbs/breadcrumb and admin breadcrumbs blocks and client visibility settings. |
partial_company_logo.html.twig |
Explicit URL/null settings, light/dark variants, text fallback, caller link and sizing options. |
partial_progress.html.twig |
Named determinate progress with a bounded value and visible percentage. Use status text/spinners for unknown durations. |
partial_help_popover.html.twig |
Short supplementary text in a native button outside field labels. Essential instructions stay inline. |
partial_ui_feedback.html.twig |
One container and translated labels per layout; retain client dataset aliases. |
Keep admin form/summary/detail-card block names and client action-group ordering.
Do not add generic subtitles that merely describe the title or visible controls.
Status mappings live in partial_status_badge_shared.html.twig; unknown statuses
use primary, and unknown types use the generic fallback. Search/filter recovery
belongs outside responsive tables.
Runtime and extension contracts
tabler-modal.tshandles lifecycle, focus restoration and cleanup. Admin keeps its HTML templates and immediate callbacks; client callbacks wait until hidden. Both respect an extension-suppliedModals.create. Static dialogs use Tabler.tabler-toast.tsshares rendering. Theme wrappers retain announcement, auto-dismiss and focus policies.notice-dismissal.tsrestores focus only if removing a notice would leave it lost.form-errors.tshandles inline errors, summaries, enhanced controls and focus in inactive tabs. Keep theme adapters, existing IDs andFOSSBilling.uihooks.form-loading.tspresents progress; the API owns locking and retry cleanup. The optional submitter argument identifies which button spins. Preserve prior disabled/loading states and form-associated controls. Modal status stays outside the busy form. Client unsaved-change tracking forwards this hook.api-completion.tskeeps callback → redirect → reload → inline feedback precedence. Only explicit messages cross navigation throughflash.ts; preserve its legacy storage keys and globalflashMessage. Forms are not reset implicitly.theme-preference.tsshares System/Light/Dark behaviour and pre-paint setup. Respect configured defaults and extension controls.tab-navigation.tsshares URL updates and horizontal tab reveal. Admin retains list namespace pruning, active-tab resets anddata-tab-jump. Keep history state, original field/input nodes and local pane IDs.navigation-disclosure.tsanddropdown-focus.tsrecover keyboard focus without stealing focus moved elsewhere. Tabler owns transitions and keyboard menus.floating-help.tshandles Escape and hidden-container cleanup. Tabler initializes tooltips/popovers; do not add another initialization loop. Legacy extension popovers retain their outside-click policy.email-preview.tsprepares documents inertly: plain messages may follow the theme; authored styles and explicit color schemes remain authoritative. Keep iframe sandbox permissions, existing preview entrypoints and helper overrides.
_createLoader() stays in the API. Preserve its spinner-border class, 250ms
delay, _fadeInTimeout and request-owned cleanup. data-request-loading supplies
translated status text. Configured Orderbutton loader images remain supported.
Styles and assets
Vendor CSS belongs in each theme's assets/css/vendor.css; local layout rules
belong in its Sass entrypoint. Both themes load Tabler, including its Tom Select
integration after the plugin preset. Use theme tokens for color and spacing.
Do not replace branding fonts, invert uploaded images or add light-only surfaces.
Use .icon and .btn.btn-icon with native link/button semantics and a translated
accessible name. Decorative SVGs are hidden from assistive technology; tooltips
cannot be their only label. Shared mobile targets are 2.75rem; Tabler owns glyph
size and interaction styling. Shared spinners respect reduced motion.
Use avatar() with .avatar and an adjacent visible name. Its generated
.db-avatar class is preserved during production cleanup. .fb-image-contain
fits branding, gateway and extension assets; content images use .img-fluid.
Markdown inherits surrounding typography; client reader widths/line spacing and
admin's denser body scale stay local.
PurgeCSS runs in production and failures fail the build. Its inputs include theme,
module and shared templates plus core/UI runtime sources. Preserve runtime-only
selectors such as the calendar's data-vc-* states, CKEditor and generated avatars.
Test production assets when changing dynamic selectors.