Architecture
The admin is metadata-driven: it asks the platform for a resolved view or form spec and renders it generically. Two feature areas and their backing services do most of the work.
Core services (src/app/core)
| Service | Role |
|---|---|
SYNTEC_CLIENT (token) |
The @syntec/one-sdk instance, configured with the API base URL. Inject this to call the platform. |
ViewService |
Loads a view's resolved spec + data; owns paging/filter/sort/variant state (one instance per grid). |
FormService |
Loads a form's spec + record; owns edit state, validation, dirty-diff, and save — all as signals. |
| auth / locale | Login/refresh + token storage; current language and value formatting. |
State is held in signals; components are OnPush and react to signal reads. (A consequence:
state a template reads must be a signal — a plain object mutated in place will not trigger a
re-render.)
The view feature (src/app/features/view)
ViewGrid (<syntec-view>) renders any view from its resolved columns: frozen headers, column
filters, global search, sort, pagination, row selection, and per-user variants (saved column
layouts). The same component runs embedded ([embedded]="true") as a child grid inside a
form, scoped to a parent record's guid.
- Column values are read through a small helper that supports dotted paths, so a column can
surface a nested value (e.g. a translation companion
syntec_field_lng.label). - A subview always renders the developer-configured columns — embedded grids ignore per-user variants (variants are a top-level-grid feature).
The form feature (src/app/features/form)
The form renderer walks the resolved component tree (tabs → sections → fields) and maps each field
to a widget via field.ts (text, textarea, select, boolean, timestamp, relation
pickers, …). FormService tracks edits and, on Save, sends a dirty-diff to the SDK
(records(entity).update).
Notable pieces:
- Validation — required markers, server
422field errors mapped back onto fields, optimistic stale-write detection via themodifiedstamp. - Translatable fields — a field bound to a companion column (
<table>_lng.<col>) shows the current language inline plus a globe button (Font Awesomefa-globe) opening a modal to edit every language; edits are staged intoFormServiceand persisted with the normal Save (one PATCH). - Modals — CDK-overlay, promise-based (relation picker, child-form overlay, translation modal).
Theming (src/app/theming)
Every colour and key metric is a CSS custom property (a design token) defined in
theming/tokens.css under two selectors: :root (light) and [data-theme="dark"] (dark).
Components never hardcode colours — they read var(--surface), var(--btn-primary), var(--field-bg),
… — so the whole UI restyles by redefining tokens, nothing else.
- Light / dark —
ThemeServiceholds the active theme as a signal, seeds it at startup fromlocalStorage(falling back to the OSprefers-color-scheme), and applies it by setting thedata-themeattribute on<html>. The toggle just flips that attribute; the cascade selects the matching token block.color-schemeis set per theme so native controls (checkboxes, scrollbars, selects) render in the right mode. There is no second stylesheet — both palettes live in one file. - Customer skins — a final install can ship its own light/dark skin without forking or
rebuilding the admin. The skin is a tokens-only CSS file in the customer module that redefines a
subset of the same tokens under
:root/[data-theme="dark"]. The platform serves the active skin (named by theSYNTEC_THEME_CSSenv var) at the public, ETag-cachedGET /api/v1/theme.css— empty when none is set.skin-loader.ts(run via an app initializer) injects a single<link>to that endpoint aftertokens.css, so the skin's token values win by cascade. The default themes stand whenever no skin is configured. See Customer theme skins for the token contract and how to author one.
Talking to the platform
All I/O goes through @syntec/one-sdk via the SYNTEC_CLIENT token — components and services
never call fetch directly. The SDK is generated from the platform's OpenAPI document; when the
API changes, rebuild and re-sync the SDK (see Getting started).
Conventions
- Standalone components,
ChangeDetectionStrategy.OnPush, signals for state. - Keep components focused; cross-cutting logic lives in
coreservices. - TDD with Vitest; for template changes, also confirm the real Angular build (the
webcontainer) —tsc --noEmitdoes not type-check templates.