Skip to content

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 422 field errors mapped back onto fields, optimistic stale-write detection via the modified stamp.
  • Translatable fields — a field bound to a companion column (<table>_lng.<col>) shows the current language inline plus a globe button (Font Awesome fa-globe) opening a modal to edit every language; edits are staged into FormService and 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 / darkThemeService holds the active theme as a signal, seeds it at startup from localStorage (falling back to the OS prefers-color-scheme), and applies it by setting the data-theme attribute on <html>. The toggle just flips that attribute; the cascade selects the matching token block. color-scheme is 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 the SYNTEC_THEME_CSS env var) at the public, ETag-cached GET /api/v1/theme.css — empty when none is set. skin-loader.ts (run via an app initializer) injects a single <link> to that endpoint after tokens.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 core services.
  • TDD with Vitest; for template changes, also confirm the real Angular build (the web container) — tsc --noEmit does not type-check templates.