@praxisui/core
Shared contracts, tokens, services, metadata models, widget runtime primitives, and utilities used by the public @praxisui/* Angular packages.
Use this package directly when a host application or library needs Praxis UI types, global actions, metadata registries, schema clients, resource discovery, i18n, theme bridge, dynamic widget contracts, or shared runtime services.
Contextual table defaults
createDefaultTableConfig() leaves pagination.strategy and sorting.strategy
unset. These shared contracts cannot choose a data source: @praxisui/table
infers server processing for remote data and client processing for local data.
Explicit client or server choices remain authored choices. Optional strategy
properties allow sparse metadata and editor resets to preserve contextual
resolution; auto is not a public serialized value.
Governed bulk client: basic synchronous slice
BulkOperationClientService consumes the Metadata published BulkExecutionContract
from an action or CRUD capability. discoverAction and discoverCapability use the
existing resource discovery surface; operation paths and filtered schema URLs come
from that descriptor. evaluationSchema loads the backend's filtered request schema
for a caller to render the operation's form. The client admits only SYNC,
EXPLICIT and PER_ITEM in this first Angular slice. A descriptor that also
advertises query selection is usable only with an explicit target request here.
The Quickstart candidate migrates its existing uniform update operation to an
ASYNC-only profile; that host adoption is still being validated. BASIC omits
an ASYNC-only operation without a SYNC fallback. Angular ASYNC support needs
its own consumer implementation and acceptance; this note does not establish a
Render deployment or an ASYNC UI journey.
Provide the exact wire ID and opaque resource expectedVersion for every selected
target. In particular, int64/Long IDs remain decimal strings; do not convert them
through JavaScript numbers. Evaluate first, review the returned READY or BLOCKED
proposal and its authorized paged results, then confirm a READY proposal using
only { proposalId } and one Idempotency-Key. BulkSyncStateService.createSession
keeps that key fixed across a retry of the same confirmation. A new intention gets
a new session. The key and proposal live only in that session; it makes no claim
of recovery across a page reload.
For a selected collection row that publishes a self link,
readTargetVersion(row._links, wireId, identityPath, options) reads that exact
item, verifies its returned identity and returns the original strong quoted ETag
as expectedVersion. It rejects absent/weak ETags, foreign origins and mismatched
IDs. Callers should bound concurrent reads and cancel them when their resource
context closes. A row value, schema ETag or locally inferred URL is not a resource
version.
const contract = await firstValueFrom(bulkClient.discoverCapability(resourceResponse, operationId));
const session = bulkState.createSession(contract);
const proposal = await session.evaluate(explicitSyncRequest);
const preview = await firstValueFrom(bulkClient.readProposalResults(contract, proposal.proposalId));
// Present proposal and preview to the person; a BLOCKED proposal cannot be confirmed.
// In a separate, explicit confirmation action for a READY proposal:
await session.confirm(idempotencyKeyForThisIntent);
// On an uncertain HTTP failure, retain this session and retry with the same key.
// The host schedules further authorized readbacks for an accepted nonterminal execution.
const reads = new AbortController(); // This view owns GET lifetime, not confirmation custody.
if (!session.isTerminal()) await session.refreshExecution(reads.signal);
if (session.isTerminal()) {
const results = await session.readResults(200, undefined, reads.signal);
}
resourceResponse, operationId, the explicit request and the key come from the
caller's governed selection and form flow; the client resolves operation URLs from
the descriptor. The caller controls when to read status again and presents each
authorized page of results. This example is a service integration sketch, not a
ready visual workflow or a deployment claim.
refreshExecution(signal?) and readResults(size?, after?, signal?) retain their
Promise API and optionally accept a standard AbortSignal. An already aborted
signal dispatches no GET; abort unsubscribes the actual HTTP request, releases the
read slot, and prevents a late Promise continuation from updating the session.
Abort listeners are removed on success, failure and cancellation. A cancelled GET
keeps the original proposal, confirmation key and accepted execution; it does not
mark that execution terminal or prove rollback. A new authorized read may observe
the same execution. Overlapping execution refreshes remain rejected, while an
aborted generation cannot release the slot of its successor.
The caller owns GET cancellation and its observation budget; the Core session does
not invent a global read timeout. Use a fresh read lifetime for a fresh consultation
and abort reads when the owning view closes. Do not race a Promise with a timer:
connect the signal to the read itself so the HTTP subscription is torn down.
evaluate and confirm do not accept this signal: a dispatched POST and its original
key stay in intent custody across view teardown. GET failure/cancellation must never
start a new evaluation or confirmation automatically.
The backend is the authority on proposal expiry. A client clock hint must not block a same-key replay after an uncertain confirmation; the backend may return 409 or 410 and the host must explain that state without inventing a new intent.
HTTP 202, RUNNING, CANCEL_REQUESTED and RECONCILIATION_REQUIRED are not
business success. Read the authorized execution until its reconciled terminal
state and use the paged result reader for each item outcome. Preserve backend
errors (including 409, 410, 429 and 503) for the host's governed UX mapping;
do not turn them into a new submission with a new key. The backend continues to
own authorization, policy, identity, idempotency, limits and the domain transaction.
The table library has a candidate visual path for a published CRUD
bulk-update capability in UNIFORM_UPDATE mode. It uses this client and a
separate review/confirmation surface; it does not add a second backend contract.
The service also supports typed command and per-item requests, but that table
surface does not advertise them. Query selection, ASYNC jobs and ATOMIC mode are
outside this Angular slice. This is not a claim that the reference host or full
backend B7 acceptance is complete.
Published asynchronous execution tracking
PraxisBulkClientService.waitForExecution is separate from the BASIC client
above. Use its published execution operation only when that operation actually
supports the asynchronous profile; an enum member does not enable a backend
profile. The Observable emits one canonical terminal snapshot, preserving
COMPLETED, COMPLETED_WITH_ERRORS, CANCELLED, STOPPED and
RECONCILIATION_REQUIRED. CANCEL_REQUESTED remains pending.
Each subscription starts its own strict overall deadline. At most one GET is in flight: interval ticks do not abort a useful slow read or queue overlapping reads. Pending responses do not renew the deadline, which also bounds a GET that never responds. Terminal completion, transport error, deadline and unsubscribe release the active read and polling timers; a late response cannot revive an expired subscription. Stopping a GET does not reverse server effects or cancel a POST.
A read failure means the outcome is unknown. Explicitly resume reads for the same execution and preserve the original proposal and confirmation key; do not confirm or evaluate again to restore progress. This transport does not provide custody across closing/reopening a dialog, reload recovery or operational ASYNC acceptance.
Governed color palettes
materializeGovernedPaletteEntries(palette, purposes) returns MaterializedGovernedColorToken[], pairing each original entry with its resolved color. It preserves distinct token IDs even when CSS references/colors match, applies purpose filtering, and resolves fallback dependencies against the complete palette. Use this projection for identified swatches; do not zip color arrays to entries or infer identity from color equality. materializeGovernedPaletteColors remains the string-only projection of the same resolver.
GovernedColorPaletteService is the canonical Angular client for color-palette projections published by Praxis Config. Published palettes carry a stable familyKey, an explicit variant identity and token-level displayName/aliases; incomplete legacy projections are rejected before caching. The service supports list/read/capabilities/preview, exact list(familyKey) queries, tenant/environment-isolated caching, conditional ETag revalidation and deterministic version/etag pins. Consumers may use names and aliases for presentation and discovery only: paletteKey and tokenId remain the persisted identities, and local metadata must not become a second source of governance. GovernedColorTokenSelection carries the exact palette/version/ETag/token observation beside the existing CSS-string field value; applications persist it only through an explicit domain field or mapping.
Declarative Page DataSources (page.dataSources)
WidgetPageDefinition.dataSources and WidgetPageDataSourceRuntimeService allow dynamic pages and dashboards to fetch backend KPIs, group-by aggregations, and time-series metrics without imperative host HttpClient orchestration:
- Endpoint resolution (
endpointKey+resourcePath): resolves URLs through theAPI_URLtoken usingendpointKey(falling back to'default') so multi-service enterprise hosts route each DataSource to its target microservice. - Transform presets & JsonLogic: supports
{ kind: 'path', path },{ kind: 'group-by-map', mappings, totalField }(initializing declared keys with0for sparse buckets),{ kind: 'timeseries-last', mappings }, or arbitrary JsonLogic expressions. - Transient state overlay (
targetStatePath): merges resolved payloads into a non-persisted runtime overlay overpage.state.values[targetStatePath]and coalesces concurrent emissions via microtask batching before re-evaluatingpage.state.derivedandcomposition.links. Live runtime payloads never dirtyauthoredPageDefinitionor triggerautoPersistwrites. - Resilience & visibility-aware polling:
errorPolicy('fallback' | 'silent' | 'propagate') appliesfallbackValueonly before the first successful response so transient network errors during polling (refreshIntervalMs) never overwrite live data with static fallbacks. Subsequent poll ticks pause whiledocument.visibilityState === 'hidden'and resume immediately onvisibilitychange.
LLM Recommendation Guide
BUILTIN_WIDGET_SHELL_PRESETSis the canonical catalog for reusable shell treatments.integrated-card,outlined-card,elevated-cardandtonal-cardkeep header actions and body on one continuous surface;sectioned-carddeliberately separates the header;framelessremoves the containing surface while retaining the standard content inset, aligned with the header. For content that owns its own spacing, explicitly setappearance.body.padding: "0"(and hide the header if the content owns its title). Persist the presetidinWidgetShellConfig.presetand use the descriptor for authoring labels and previews. Do not maintain a second preset list in consumers. The builtin Portuguese and English shell labels and descriptions are Core fallbacks inPraxisI18nService; consumers do not need a Page Builder i18n provider. Host/ancestor dictionaries and translators take precedence. Published IDsdark-glass,light-neutral,graphite,data-panel,chart-panel,metric-panel,executive-cardandfilter-barremain resolvable in the same catalog. Their finishes were recalibrated after visual review; ID compatibility does not promise pixel-for-pixel historical appearance.BUILTIN_SHELL_PRESETSprovides the public ID-to-appearance record derived from that catalog. Configure host overrides throughcontext.ui.shell.presets, then widget-specificappearance; use CSS variables there for corporate theming. Thedark-glassandgraphitematerial palettes remain dark by default; adaptive finishes follow the host Material tokens. Do not mutate either exported catalog to configure an application. The deliberateframelessinset improvement differs from 9.0.67: persistappearance.body.padding: "0"when the original edge-to-edge layout is required.WidgetShellConfig.stickyHeader: trueretains the existing composition-derived header within its widget during page scrolling. Omission/false keeps normal flow. This differs frombodyLayout: 'scroll', which governs body scrolling. No second selection state, cloned header or page geometry is persisted.Hosts with fixed chrome may set
--pdx-shell-sticky-header-offset(CSS length, default0px). Measure responsive chrome; do not assume a desktop-only height. The lab uses a lifecycle-owned ResizeObserver for its functional heading.Pinning releases at the widget boundary and is suspended during drag authoring, collapse, hidden/disabled headers, expanded/fullscreen windows and scroll recovery. This is not a global selected-record bar. Continuous-surface presets repeat their opaque card surface on the header so pinned content does not bleed through. Transparent/gradient custom headers are painted over an opaque Material surface. Keyboard focus is revealed below pinned chrome without changing its target.
Dynamic Page publishes its existing
contextinput as aview-contextport. Composition links can deliver runtime context to a nested page through Tabs or Expansion using stablenestedPathsegments. The child page owns its internal bindings; callers should target its context boundary, not invent paths through its widget array. Register the canonical Dynamic Page metadata before preflight. Context is runtime data, not a replacement for backend authorization or a saved business record. The metadata port is also preserved in the generated AI catalog.Nested sticky shells add the measured height of sticky ancestor headers in the same scrollport. This is transient geometry, not a saved offset or another selection context. The stack stops at an independent scrolling container; hidden/inactive headers cease contributing. Ancestor resize observation is disconnected on destruction. Body scrolling/fill and window-layout rules apply to the owning shell's direct body, not to every nested widget body. For dynamic-height containers that should follow page scrolling, use content flow (omit
bodyLayoutor usecontent), notfill. An intentional independent scrollport remains a boundary for the sticky stack.An invisible shell (
kind: 'none') removes padding only from its own body. Nested dashboard shells retain their authored body spacing. Hiding a header withshowHeader: falsedoes not hide child headers or reset child spacing.The Page Builder shell editor exposes the option. Existing saved layouts are preserved; changing the source recipe does not silently migrate saved pages.
Coordinated manual resize: an unambiguous edge collision yields space from the affected neighbors, preserving their opposite edges and respecting
minColSpan,minRowSpan,lockSizeandlockPosition. It does not swap widgets or push a chain of unrelated items. Diagonal/corner collisions with no unambiguous yielding axis fail closed. Drag still uses the authoredblock/swapcollision policy. The actual widgets preview the valid distribution: green means available, amber means minimum reached, red means the requested resize exceeds available space. Icons, localized labels and live announcements accompany colors. Pointer release applies the clamped distribution in one authoring event; Escape/cancel/capture loss discards the entire preview. Feedback is transient, never serialized. Author useful content-specific minimums and device overrides: the grid default of one unit is a geometric limit, not a guarantee of readable enterprise content.autoRows: 'content'measures content-driven tracks and uses independent pixelcontentSizefor vertical resizing;rowSpanremains at least one. Resizing does not imply automatic packing or moving unrelated widgets to a new row.Widget Shell sizing: the shell includes its border inside the height reserved by its host (
border-box). Consumers do not need a global CSS reset to prevent a 2px overflow. In a content-sized canvas, collapsing every tall widget in a shared row lets the next row move upward; an expanded sibling still determines the shared row height. This is CSS Grid reflow, not automatic compaction of authored fixed coordinates.Dynamic Page resize presentation: selecting an unlocked widget in an editable grid exposes eight edge/corner grips with targets up to 24px per axis, themed focus and localized names. The widget wrapper is keyboard focusable: focus selects it, Tab reaches its controls, arrow keys resize, and Escape from a grip returns focus to the wrapper. Selection alone does not change the document. Header drag remains the reorder entrypoint.
Configure widget → Size offers width in grid columns and height in rows (fixed grid) or pixels (content rows). More actions → Size… opens this same settings tab. Widget and Size share the SettingsPanel footer and preserve drafts when switching tabs. Hosts without
SETTINGS_PANEL_BRIDGEretain the Core-owned size overlay with explicit Apply and Cancel actions, so the canvas authoring capability does not depend on an optional host service. An optional fixed-corner choice preserves the opposite edges. Valid changes preview the same constrained distribution as the grips; Apply commits once, Cancel/Escape discard the unapplied draft and restore widget focus; an earlier Apply remains. Out-of-range or blocked sizes disable Apply rather than silently changing the requested numbers. The dialog identifies whether the edit targets the base layout or a device override. Opening it preserves automatic height; changing only width does not create a fixed height.This menu is available with canvas authoring permission, including headerless widgets and hosts that disable removal or shell configuration. Size-locked widgets disable it. There is no dedicated Resize widget icon, global selector or detached directional panel. The size action is transient and never persisted in page JSON. Shell actions can explicitly use
placement: 'menu'to stay in More actions even when header slots are free; the shell editor and capability catalog share that value. Existing header/window placements retain their behavior. All sizing reuses canonical canvas items, constraints and device targeting. The editor offers exact grid-width presets (⅓, ½, ⅔, 100%) when the column count can represent them without rounding. They change the width draft, preserve the fixed corner and require Apply; an unavailable full width is rejected, never silently clamped. After a committed resize, reopen Size… on that widget to undo the last resize in this session, including yielded neighbors. Recovery restores prior device inheritance and keeps unrelated content edits. It refuses changed affected geometry/limits, new collisions, a different device scope or page identity. This is one-operation recovery, not a general history or redo stack. Side and corner double-click shortcuts use the same constrained geometry.Dynamic Page gesture note: drag/resize previews belong to one primary pointer at a time. Secondary buttons, non-primary touches and additional pointers do not replace an active gesture. Cancellation or lost capture discards only transient canvas geometry, never a snapshot of the whole page. External page/context/identity or authoring-mode changes cancel the current gesture; a late pointerup cannot commit it. The same cleanup runs on destruction. Cancellation is announced through
dynamicWidgetPagein pt-BR/en-US, not as a collision. This does not certify physical touch ergonomics or fixed-grid compaction. No additional persisted layout or shell setting is required.Widget Shell header note: identity and actions wrap according to the available card width, with one direct header action below 480px and up to three in wider widgets; secondary actions remain available in the existing overflow menu. This uses the widget surface width, including when it is resized or moved into fullscreen, rather than the viewport. including narrow cards inside wide canvases. Titles/subtitles retain up to two visible lines and expose their complete localized content through Material tooltips. Icon action targets remain 44px; resize affordances are a separate canvas concern. Existing
disabledactions stay disabled in overflow menus and cannot dispatch through the shell handler. This is UI state enforcement, not backend authorization. No additional shell JSON field is required.Widget Shell overlay note: expanded/fullscreen shells place the authored appearance over an opaque backing from the existing Material surface token. Transparent inline shells therefore remain transparent inline without exposing the obscured page through fullscreen. The active overlay is programmatically focusable (
tabindex="-1"), so blank-area clicks retain a shell keyboard target without adding a sequential Tab stop. Escape received inside the shell closes the overlay and restores its opening button when still connected. Closing is deferred until event propagation completes: a child overlay that prevents the default Escape action, including a Material select handled by the CDK body dispatcher, keeps priority. The first Escape closes that select; the next closes the shell. Deferred handling is ignored after shell destruction. These are presentation changes only and do not rewrite saved shell appearance or canvas coordinates. Expanded/fullscreen hosts are lifted into the CDK overlay container, outside parent sticky, tab and transformed stacking contexts. The existing host/view is retained; closing restores that same host to its original position. A placeholder reserves its inline space, a focus trap bounds keyboard navigation, and locally inherited theme variables are bridged from the original ancestry while open. Root theme values are not frozen. This is presentation state, not a second widget instance or a persisted window layout. Connected Material overlays keep Escape priority. Floating shells exposerole="dialog"and an accessible name from the existing localized title/subtitle (exact duplicates are announced once), with a localized content-window fallback. Inline shells do not retain dialog semantics. The active Widget stack isolates background roots and lower registered windows with nativeinert, preserving preexisting or subsequently assigned host restrictions. The stack owns one CDK scroll block until its last window closes, including parent-before-child closure. Connected menus/selects remain outside the isolated branches; the CDK container is not disabled. This private lifecycle does not infer ownership of foreign CDK overlays or coordinate their independent scroll locks. No automaticaria-modalclaim is made. If the opening control becomes disabled, hidden or detached while the shell survives, closing recovers focus to the restored shell with a localized accessible name andtabindex="-1"only until focus leaves it. This does not add a sequential Tab stop or steal focus from an active child. Hosts still own the destination when the entire invoking view is destroyed or the window was opened without a recorded control. Before moving its host, the shell closes owned Material selects and menus through the public CDK detach lifecycle, including the paginator's select. Scoped scroll-strategy providers preserve the upstream host strategy; ordinary inline renders do not close panels and independently floating children are excluded. A descendant overriding these providers can bypass coordination. This is not a generic registry for other CDK overlays, autocomplete or datepicker. Synchronous reopening from a close callback still requires host coordination; the shell deliberately does not run an unbounded closing loop. It does not remove foreign overlay DOM or synthesize Escape. This is not a full modal accessibility or fixed-grid collapse/reflow certificate.Recommend when: use it as the first dependency for shared Praxis UI contracts, metadata models, resource discovery, global actions, i18n, theme bridge, and dynamic widget runtime primitives.
Avoid when: a consuming app wants to redefine contracts owned by core, metadata starter, config starter, or component packages.
Pair with: Every runtime package in the @praxisui/* family.
Official Links
- Documentation: https://praxisui.dev/docs/components
- Live demo: https://praxis-ui-4e602.web.app
- Quickstart app: https://github.com/codexrodrigues/praxis-ui-quickstart
- Metadata starter: https://github.com/codexrodrigues/praxis-metadata-starter
Install
npm i @praxisui/core@rc
Peer dependencies:
@angular/common,@angular/core,@angular/forms,@angular/material,@angular/platform-browser,@angular/router^21.0.0rxjs~7.8.0
Resource HTTP responses and revision custody
GenericCrudService.getByIdHttpResponse(id, options?) and updateHttpResponse(id, entity, options?) expose the original HttpResponse<RestApiResponse<T>>, pairing a representation with its response headers. Existing getById, getByIdResponse, update and updateResponse keep their original DTO/envelope return shapes.
CrudOperationOptions.resourceVersion overrides a write's shared item cache: a strong quoted ETag supplies the exact If-Match, null intentionally omits that precondition, and undefined preserves existing cache → DTO resourceVersion fallback (or the configured header when neither is present). Consumers that edit a captured representation must provide its original revision, rather than a revision obtained by another read. readStrongResourceEtag validates a single strong tag without normalizing its bytes; weak, unquoted and multi-valued headers cannot stand in for a write revision. An explicitly invalid/weak response header clears the previous cached token. When the header is absent, the existing Core cache can capture DTO resourceVersion; this legacy/manual fallback does not authorize an automatic Form snapshot. Explicit overrides never fall back to cached, DTO or configured tokens.
Theme And Icons
Canonical SCSS Theming (Recommended)
Import the canonical theming mixins to bridge host design tokens (OKLCH, Material 3, Tailwind, etc.) to Praxis components:
@use "@angular/cdk/overlay-prebuilt.css";
@use "@praxisui/core/theming" as praxis;
// Option A: Bundle light theme, dark mode overrides, widget shell, and tooltip enhancements
@include praxis.praxis-theme-bundle();
// Option B: Granular configuration with custom design tokens
:root {
@include praxis.define-praxis-theme((
'primary': var(--primary),
'surface': var(--card),
'border': var(--border)
));
}
.dark {
@include praxis.praxis-dark-theme-overrides();
}
@include praxis.praxis-shell-styles();
@include praxis.praxis-tooltip-styles();
Static Theme Bridge (Legacy)
For simple or static setups, you can still import the prebuilt CSS bridge directly:
@import "@angular/cdk/overlay-prebuilt.css";
@import "@praxisui/core/theme-bridge.css";
Use PraxisIconDirective to normalize Material Icons and Material Symbols names.
import { Component } from "@angular/core";
import { MatIconModule } from "@angular/material/icon";
import { PraxisIconDirective } from "@praxisui/core";
@Component({
standalone: true,
selector: "app-icons",
imports: [MatIconModule, PraxisIconDirective],
template: `
<mat-icon [praxisIcon]="'mi:pending'"></mat-icon>
<mat-icon [praxisIcon]="'mso:right_click'"></mat-icon>
`,
})
export class IconsComponent {}
Hosts should load the icon fonts they use. Without the classic Material Icons font, ligature names such as clear can render as visible text.
Use PraxisIconButtonComponent for icon-only actions. It keeps the native
button contract, centers the glyph, and preserves a 44px pointer target even
when the visible surface is compact. It accepts the same mi:, mso:, msr:
and mss: icon-family prefixes as PraxisIconDirective; unprefixed names keep
the legacy Material Icons behavior.
import { Component, signal } from "@angular/core";
import { PraxisIconButtonComponent } from "@praxisui/core";
@Component({
imports: [PraxisIconButtonComponent],
template: ` <button type="button" [praxisIconButton]="'mic'" size="compact" [pressed]="isListening()" aria-label="Ditar pedido"></button> `,
})
export class ComposerActionsComponent {
readonly isListening = signal(false);
}
Public inputs:
| Input | Values | Default | Purpose |
|---|---|---|---|
praxisIconButton |
icon name or canonical family-prefixed icon | required | Glyph materialized inside the button |
size |
dense, compact, standard |
standard |
Governed target, visible surface and glyph density |
appearance |
plain, filled |
plain |
Transparent or emphasized action surface |
presentation |
icon, projected |
icon |
Uses the named glyph or host-projected governed presentation |
pressed |
true, false, null |
null |
Persistent toggle/listening state and native aria-pressed; null omits the attribute |
busy |
boolean | false |
Replaces the glyph with the canonical progress indicator and publishes aria-busy |
The component defaults the native host to type="button"; an explicit host
attribute still documents intent and prevents accidental form submission in
templates that are later refactored. Every icon-only action must have an
accessible name, normally aria-label, supplied by the host or its i18n layer.
The consumer remains responsible for [disabled] while busy when repeated
activation must be blocked.
Use presentation="projected" when an icon-only action is presented by a
governed component such as PraxisRichContent; the canonical button continues
to own interaction geometry while the projected renderer owns content semantics.
The component owns target, surface and glyph alignment. Consumers may set
--praxis-icon-button-filled-background and
--praxis-icon-button-filled-foreground to materialize a governed semantic
tone, but should not override the target, surface or icon size variables.
Hosts must load the font family selected by the icon prefix and provide the
Praxis Material 3 semantic color tokens.
Governed density matrix:
| Size | Pointer target | Visible surface | Glyph | Intended use |
|---|---|---|---|---|
standard |
44px | 40px | 22px | Primary and standalone icon actions |
compact |
44px | 36px | 20px | Composers, headers and frequent application actions |
dense |
36px | 32px | 18px | Secondary actions inside constrained editorial toolbars |
On coarse pointers, dense automatically restores a 44px pointer target while
keeping its 32px visible surface. Do not use dense for a primary action, an
isolated destructive action, pagination, or a control that must remain easy to
acquire under operational pressure. Density changes presentation only; it must
not encode authorization, importance or business state.
Migration from mat-icon-button should remove local MDC/Material internal
variables and local width, height, padding or icon-margin fixes. Keep
domain-specific state, labels and action authorization in the owning component;
the Core primitive owns presentation and native interaction only.
Semantic theme surfaces
PraxisThemeSurfaceTokens is the public semantic bridge for runtime surfaces. It deliberately avoids Material or MDC selectors, so a host can theme Praxis overlays, borders and focus states without patching component internals.
import { buildPraxisThemeSurfaceCss } from "@praxisui/core";
const themeCss = buildPraxisThemeSurfaceCss({
surfaceOverlay: "var(--app-overlay-surface)",
focusOutline: "var(--app-focus-ring)",
});
Apply the generated variables in the host stylesheet or define the --praxis-theme-* variables directly. Runtime libraries consume these roles with Material-token fallbacks while their existing component-specific tokens remain supported.
Component Metadata
ComponentMetadataRegistry is the shared registry used by builders and runtime loaders to resolve component metadata.
import { ComponentDocMeta, ComponentMetadataRegistry } from "@praxisui/core";
const metadata: ComponentDocMeta = {
id: "app-widget",
selector: "app-widget",
component: AppWidgetComponent,
friendlyName: "App widget",
description: "Host-owned widget.",
icon: "widgets",
lib: "app-host",
};
registry.register(metadata);
Component-owned config editors, AI authoring manifests, and runtime profiles are declared on ComponentDocMeta. A runtime profile describes the input constraints and concrete effects of one audited configuration shape; it does not authorize those effects. Hosts must evaluate the profile and grant capabilities independently, bound to the exact document being executed, instead of duplicating component behavior in a host-specific allowlist.
Composition validation against a target registry
CompositionValidatorService is the public canonical validator for executable page composition. validatePage(page, { registry, links }) verifies every page widget against the supplied target registry, rejects metadata without a materializable Angular component type, resolves top-level and nested ports, and then applies the existing direction, state, Json Logic, transform, semantic-compatibility and feedback-cycle checks. It returns canonical DiagnosticRecord[]; consumers must preserve those codes, subjects, paths and blocking flags rather than translating them into a second diagnostics model.
The validator performs no rendering, dispatch, HTTP call or persistence. Pass the same ComponentMetadataRegistry that the target host uses for dynamic materialization. Registry presence is the materializability evidence supported by the current contract; component packages remain responsible for publishing and testing their own metadata providers.
Remote Configuration Persistence
providePraxisGlobalConfigBootstrap is the official host entry point for
ApiConfigStorage. Set scope explicitly whenever ownership is a product
decision instead of a lookup preference:
providePraxisGlobalConfigBootstrap({
remote: { apiPath: "/api" },
scope: "tenant",
headersFactory: () => enterpriseRuntimeContext.headers(),
errorPolicy: "fail",
});
Use scope: "tenant" for a shared governed document such as an application
page composition, and scope: "user" for personal preferences. Omitting the
option deliberately preserves the praxis-config-starter resolution rule:
requests carrying X-User-ID resolve user config first, while tenant config is
the fallback for reads. Scope selects persistence ownership; it never replaces
server-side identity resolution or write authorization.
A tenant document returned by an unscoped fallback read remains read-only for
that storage session. Its response scope and ETag are cache evidence, not an
implicit ownership grant. A later unscoped save or clear still follows the request
identity (USER when X-User-ID is present), and never sends a tenant ETag as
If-Match for that user-owned write. Editing or deleting the tenant document
requires an explicitly tenant-scoped provider.
Dynamic Page Runtime
DynamicWidgetPageComponent renders WidgetPageDefinition documents.
import { Component } from "@angular/core";
import { DynamicWidgetPageComponent, WidgetPageDefinition } from "@praxisui/core";
@Component({
standalone: true,
selector: "app-page",
imports: [DynamicWidgetPageComponent],
template: `<praxis-dynamic-page [page]="page"></praxis-dynamic-page>`,
})
export class PageComponent {
page: WidgetPageDefinition = {
widgets: [
{
key: "summary",
definition: {
id: "app-widget",
inputs: { title: "Summary" },
},
},
],
composition: { links: [] },
};
}
Use page.composition.links for persisted wiring. page.connections is legacy/residual and should not be used for new pages or examples. Nested component ports should use component-port endpoints with ref.nestedPath.
When a host enables dynamic-page customization, it can pass a transient
WidgetPageAuthoringCapabilities grant through authoringCapabilities to expose only the canvas,
page settings, widget shell, component-input, removal and child-customization operations that the
host actually supports. Omitted entries preserve full authoring for backward compatibility. This
grant controls visible authoring operations only: it is not persisted in WidgetPageDefinition and
does not replace component manifests, backend capabilities, authorization or validation.
Hosts that maintain an authored source separately from runtime state should consume
pageAuthoringChange. It emits only explicit canvas, shell, page, widget-input and removal edits.
pageChange remains the compatibility stream for every canonical page update, including persistent
runtime state; transient composition projections remain excluded from both outputs.
With autoPersist enabled, pass a stable pageIdentity to scope the document and
listen to persistenceChange: WidgetPagePersistenceState for loading, saving,
saved, resetting, idle or error. savedAt is emitted only after the
storage confirms a save. An empty { widgets: [] } document is a valid customized
page; absence of a stored document falls back to the host's page. The runtime
serializes save and reset operations for the identity so each storage call uses
the latest provider ETag. A 409/412 conflict blocks later writes until the host
explicitly calls reloadPersistedPage(defaultPage), which discards the current
draft. resetPersistedPage(defaultPage) clears storage and applies the factory
page only after acknowledgement. persistCurrentPage(page?) requests an
explicit save or retries a recoverable save failure; callers can pass the exact
authored snapshot they want saved. Completion without a storage value reports
errorKind: 'unacknowledged' rather than claiming success.
Pointer drag/resize in the canvas snapshots rendered CSS Grid track sizes and gutters at gesture
start. Content-sized rows are not treated as uniformly sized rowUnit rows. Horizontal resize
snaps to columns; vertical resize in autoRows: 'content' authors the item's independent
contentSize: { heightPx, offsetTopPx? }, while fixed rows still resize by row spans.
Captured DOM measurements remain private; only the resulting authored size is persisted. A gesture that leaves
the canonical placement unchanged does not emit an authoring update or persist the page.
It announces that no adjustment was applied instead of claiming a successful resize.
Content-row widgets with contentSize use the existing shell bodyLayout: 'scroll'
as a render projection (child inputs and authored shell are unchanged). North handles
preserve the bottom by changing height and top inset, and stop at the track origin.
South handles release occupied space. Shared rows only shrink when no other occupant
still needs that height. Collapsing temporarily clears size/inset; expanding restores them.
constraints.minHeightPx (floor/default 160) and maxHeightPx govern this mode;
grid minRowSpan/maxRowSpan remain row-occupancy constraints, not pixel limits.
The widget's More actions → Size editor exposes Occupied grid rows when
autoRows: 'content'. This edits the existing canvas.items[key].rowSpan, allowing
a detail widget to span a table and a contact list in adjacent rows. It preserves
automatic content height unless the author also changes the pixel-height field.
The draft previews before Apply, respects grid constraints and locked neighbors,
and uses the same active-device override, authoring output and undo as other size edits.
Content-height gestures additionally protect the measured header, shell padding/borders
and up to 160px of scrollable body. Naturally shorter content may use less. This is
a structural viewport reserve, not business-component UX certification. Measurements
remain transient and do not overwrite authored constraints. An incompatible authored
maximum blocks the gesture. The page-owned settings flow also supplies a transient
validator through the existing Settings Panel input envelope. Apply/Save and reopen
check the effective active-device height against the currently rendered shell; these
observations never enter the page JSON. Changed columns, gap, width or shell styling
require Automatic height first, then applying the geometry and reopening to measure.
Unrendered device variants and editors opened without that runtime validator retain
numeric validation only. Scroll-layout shells now observe late header/content and
width changes. If the measured reserve no longer fits, the header temporarily joins
the body's outer scroll flow, keeping content reachable inside the authored box.
When space is sufficient again, normal body-only scrolling returns and the outer
scroll resets to the header. This recovery neither increases height beyond an
authored maximum nor writes constraints/configuration. It is suspended for collapse,
expanded/fullscreen overlays and non-scroll layouts. Shared private measurement
keeps gesture/editor checks and recovery consistent. This is an access safeguard,
not a guarantee that a small viewport is ergonomically adequate; virtualized/custom
child scroll containers still need their own proof.
Double-clicking a corner fills the largest free grid rectangle while preserving the opposite
corner and all siblings. The Size editor provides the same four commands with preview,
Apply and Cancel. Fixed rows maximize column span × row span, breaking ties by width;
one-axis growth is allowed and repeated filling in an unchanged layout is a no-op.
Content rows require an explicit height and one uncrossed row. They combine free columns
with the independently authored vertical envelope (top inset or an eligible lower peer
boundary); automatic target heights are unsupported. The destination is measured at its
new width before authoring. Undo similarly certifies the inverse at its original width.
These private observations are not persisted, and failed certification leaves the document
unchanged. A corner fill never implicitly reorganizes neighbors.
Keyboard resize moves vertically by 16px (Shift: 32px). Omit contentSize to inherit
the base item; set 'auto' to explicitly restore natural height in a device override.
Fixed-row mode retains this preference but does not materialize it. Page Builder exposes
these settings under Page settings → Widget limits. See the canvas RFC for the
manual-validation boundary and remaining AI-manifest publication work.
Own minimum/maximum and outer-grid clamping retain gesture evidence until feedback
is resolved, so an over-limit request is not shown as green merely because its
candidate was already clamped. That evidence remains transient, not page JSON.
During pointer resize, the selected widget's message follows the pointer in a
viewport-constrained CDK overlay rather than its potentially off-screen footer.
The pane passes through pointer hits, does not take focus, and disappears on
commit/cancel or authoring revocation. Neighbor badges remain attached to their
widgets; the existing live announcement describes the affected distribution.
This does not add multi-widget push/reflow or compact fixed-grid rows when a shell is collapsed.
Canvas resize exposes grips up to 24px per axis on the selected widget. Extremely narrow
or short widgets reduce their hit areas to keep adjacent grips disjoint in both axes; this does not
make them comfortable touch targets. More actions → Size provides larger equivalent
controls for precise sizing and filling. All presentations reuse the canonical geometry
and canvas.items. Keyboard controls and themed feedback remain available.
Physical-touch ergonomics and save/reopen persistence still
require separate proof; successful local interaction is not an enterprise certification.
Page-owned business copy can use page.i18n plus explicit PraxisTextValue
descriptors inside widget shells or nested widget inputs. The runtime resolves
those descriptors only in its render projection, so the authored and persisted
document keeps stable semantic keys. Plain strings remain untouched, and
framework chrome continues to come from each library's i18n catalog.
During generic document traversal, a record containing only { key } remains
structural. A descriptor must also carry text or params to be localized.
This keeps contracts such as analytics mappings ({ key: "status" }) intact,
even if a page catalog happens to use the same key. Typed component metadata
may still resolve key-only PraxisTextValue values directly through
PraxisI18nService; the UI Composition Plan compiler also materializes
fallback text for page-owned key-only copy before persistence.
Composition link delivery and failures
Composition links execute in their persisted array order inside one synchronous
dispatch cycle. policy.delivery may be omitted or set to sync; the former
microtask and batched values were removed during the version 9 beta because
the dynamic-page cycle has no lifecycle-aware asynchronous continuation or
batch boundary. Canonical ingress rejects those removed values instead of
silently treating them as synchronous.
A successful state delivery schedules every canonical link sourced from that
exact state path and layer in the same causal cycle. The runtime executes each
scheduled link at most once per cycle, preserves deterministic document order,
updates matchedLinkIds, link telemetry and trace, and relies on semantic cycle
validation to block unguarded feedback graphs. This makes
component/global-action -> state -> component an executable chain rather than
a bootstrap-only projection.
policy.errorPolicy governs runtime condition, transform and delivery failures:
diagnostic(the default) keeps the structured failure evidence and allows independent links to continue;dropdiscards only the failed delivery, recordsRUNTIME_LINK_ERROR_DROPPED, and does not count the value as delivered;halt-pagerecords a fatal diagnostic, stops subsequent effects, and blocks dispatch until the page receives a new bootstrap.
These policies never downgrade catalog, schema, semantic-validation, or bootstrap failures. They also do not catch arbitrary Angular exceptions or promise failures produced after a host/global-action adapter accepts a delivery.
Resource Discovery
resourcePath and resourceKey serve different purposes:
resourcePath: operational URL/path for CRUD, fetch, schema, read, submit, and filter flowsresourceKey: stable semantic identity from backend discovery catalogs, surfaces, actions, and capabilities
If the problem is URL or HTTP execution, start with resourcePath. If the problem is semantic discovery, surface/action context, or stable runtime identity, start with resourceKey.
ResourceCapabilitySnapshot.canonicalOperations preserves structural support only.
operations carries the current operation contract and availability for CRUD, query,
options, stats, and export. The core keeps unknown operation IDs for forward compatibility;
it must not infer authorization from a canonicalOperations boolean or discard denial metadata.
ResourceRecordOpenService materializes the minimal ResourceRecordOpenRef published by governed analytics. It resolves the target resource catalog, loads the real item, follows its HATEOAS surfaces relation, requires the exact item surface to be available for the current context, and only then delegates to ResourceSurfaceOpenAdapterService. Consumers must not replace this flow with ${item.id}, a copied surface path, a label match, or a prebuilt surface.open payload.
When a discovered item surface publishes relatedResource, ResourceSurfaceOpenAdapterService delegates to RelatedResourceSurfaceResolverService. The resulting table targets the canonical child resource and receives the parent identity through queryContext.filters; it does not prefetch the item projection and rebuild a host-local table before opening. When the child surface publishes childParentField, related create actions seed that field through actions[].form.initialValue, and both create/update command policies project it into groupedCommand.contextFields. The field therefore remains in the schema-backed form and submit payload without rendering a redundant lookup for context the dossier already established. The generated related-resource empty state is informational: CREATE remains the stable, capability-governed toolbar action instead of being duplicated inside the empty state. An explicit host emptyState.actions declaration is preserved when the product journey intentionally needs a different contextual action. The materializer declares openMode: "drawer" but does not redefine drawer geometry: positioning, viewport height, focus, and close behavior remain owned by @praxisui/crud. This keeps the drawer responsive and preserves child operations, empty states, endpoint routing, and resource identity from the backend contract.
The outlet's ComponentDocMeta.configEditor intentionally owns presentation only: title, subtitle, icon, mode, presentation, compact, and the closed copy/layout subset of emptyState. Opening it does not resolve runtime/materialized inputs. Apply and Save preserve every other persisted input opaquely, including Table-owned tableConfig and behavior-bearing empty-state fields. The relationship path, parent field/id, operation, filters, query context, surface and runtime state remain metadata/runtime-owned and cannot be authored by this editor or its AI manifest.
Schema And Metadata
Core exports schema and metadata infrastructure used by form, table, list, chart, CRUD, and page-builder packages:
SchemaMetadataClient— request deadlines, cancellation and host transport policySchemaNormalizerService- ETag/cache helpers
FieldDefinitionandFieldMetadatamodelsx-ui.optionSourcemodels and serializersx-ui.analyticsmodels andAnalyticsSchemaContractServicevaluePresentationmodels and resolverFieldPresentationConfigfor semantic read-only/list/table-cell presentation- form layout item models
- JSON Logic models and runtime service
For governed analytics cross-filtering, bindings.primaryDimension.keyFilterField is the public request field that receives the raw bucket.key. Consumers must preserve its type, verify the target request schema, and must not infer this field from the dimension name, display label, or backend property paths.
valuePresentation is the shared display contract for scalar read-only values such as currency, number, date, datetime, time, percentage, and boolean.
presentation is the shared semantic wrapper contract for presentation-capable consumers. It can request chip, badge, status or iconValue rendering while preserving the raw value used for filtering, sorting and export.
FieldPresentationConfig is the shared semantic presentation contract for read-only surfaces that need a visual primitive around the value, such as chip, badge, status, iconValue, or microVisualization. It keeps business data in the original field and lets consumers map tone, appearance, icon, tooltip, and optional fixed label values to their own renderer and theme tokens.
Global Actions
Global actions let widgets and shells request host-mediated work such as navigation, dialog, toast, analytics, API calls, or surface opening.
import { providePraxisGlobalActions, providePraxisToastGlobalActions } from "@praxisui/core";
export const appConfig = {
providers: [providePraxisGlobalActions(), providePraxisToastGlobalActions()],
};
Use GlobalActionRef and the catalog helpers when declaring or validating action payloads. The host remains responsible for registered executors and policy.
GlobalActionService.has(actionId) proves only that a handler is registered. Before claiming that a materialized action is executable, use getReadiness(actionId, context?); it checks the handler plus the provider, platform API, or ephemeral runtime context declared by the Core-owned handler without executing the side effect. The result distinguishes host-scoped proof from context-scoped proof. Contributed GLOBAL_ACTION_HANDLERS must publish the same non-executing proof through GlobalActionHandlerEntry.readiness; omitting, returning an empty array, or returning malformed evidence fails closed. Only Core built-ins that are genuinely self-contained may publish an explicit empty probe.
This was classified as lacuna-real-de-contrato: an injector token alone could not distinguish an operational host adapter from a fallback or placeholder that always fails. Official provider factories therefore bind their adapters with markGlobalActionProviderOperational(...), and Core validates the exact action through getGlobalActionProviderEvidence(...). Direct adapters supplied by a host must carry the same evidence; unmarked adapters remain unavailable even when their token exists. This marker is non-executing operational evidence for the current host injector, not a security boundary or proof that every future browser/runtime context will succeed. For example, surface.open is not ready without an adapter produced by the official surface factory, and the default dialog.open/API fallbacks remain fail-closed.
Isolated target injectors that use the official analytics adapter should install ...providePraxisTelemetry() before providePraxisAnalyticsGlobalActions(). This bootstraps the same TelemetryService/transport contract used by application hosts, and lets a certification gate observe a deliberately safe trackEvent separately from non-executing readiness.
Resource action discovery also carries an optional execution contract. Consumers must materialize its interaction, idempotency, correlation, resource-version, selection, outcome and refresh policies instead of inferring command behavior from labels or HTTP methods. ResourceDiscoveryService.getActionsByResourceKey(...) resolves the canonical /schemas/actions?resource=... catalog without requiring the consumer to know a resource path. A collection action may declare resourceVersionTargetResourceKey and resourceVersionTargetIdField when its If-Match belongs to another canonical resource. ResourceActionOpenAdapterService then projects that exact pair into Dynamic Form runtime inputs when no version is available at open time; after the request payload is prepared, Dynamic Form reads only the declared scalar id field, resolves the exact target resource catalog, fetches its current representation and requires the response ETag before executing the command. Missing pairs, divergent catalogs, absent ids and absent ETags fail closed. This path does not deduce targets from URLs, labels, lookup display objects or action names. The adapter also uses transient schema-owned command layout and keeps materialized host inputs authoritative over saved preferences.
Collection Export
PraxisCollectionExportService is the shared export contract used by table, list, and future collection components.
import { providePraxisHttpCollectionExportProvider } from "@praxisui/core";
export const appConfig = {
providers: [providePraxisHttpCollectionExportProvider()],
};
Without an HTTP provider, local CSV/JSON export uses loaded items. With the HTTP provider, remote scopes such as filtered and all delegate execution to the backend export endpoint and should be gated by backend capabilities or HATEOAS links.
Runtime Observations And AI
PraxisRuntimeComponentObservationRegistryService registers redacted runtime observations for active components. These snapshots are not a source of truth for capabilities. Backend authoring services must reconcile observations with manifests, schemas, resource capabilities, actions, surfaces, and tenant/environment policy.
Core also exports shared AI authoring types, capability types, dynamic-page context packs, and domain catalog context packs used by component packages.
Component Config Authoring
DynamicWidgetPageComponent treats the editor declared by ComponentDocMeta.configEditor as the canonical authoring surface for the Configurar conteúdo action. When the Settings Panel bridge is available, the host resolves any transient contextResolver evidence and opens that metadata-owned editor even if the live widget also exposes an internal settings shortcut. This keeps the action deterministic across widget loading states.
Component owners publish that editor in exactly one of two forms: component for an eager standalone type, or loadComponent() for an owner-controlled lazy import. Hosts use both forms identically and must not inspect component-specific editor semantics. Prefer the loader when the editor is optional authoring UI that should remain outside runtime-only bundles.
Widget-local settings dispatch remains only a fallback when the canonical Settings Panel/editor is unavailable. ComponentConfigEditorContextRequest.persistedInputs is cloned exclusively from widget.definition.inputs. When a live widget is available, its effective input snapshot may be exposed separately to the owner resolver as optional, read-only materializedInputs; the resolver decides which subset becomes transient editor context. Opening the editor never applies that snapshot to the page. Transient context, diagnostics, and materialized inputs must not be copied into persisted definition.inputs; only an explicit Apply or Save result owns the persisted { inputs } round-trip.
Native editors that publish the existing explicit inputPatch protocol also produce
pageAuthoringChange, after projecting the patch onto declared owner inputs. This
notification is required even when a preceding legacy configChange already applied
the same value at runtime. Ordinary runtime change outputs keep using pageChange;
composition targets still pass through canonical restoration before persistence.
This lets Page Builder save the edited config instead of its previous draft.
Dynamic widgets may implement WidgetShellActionContributor to publish transient, stateful content actions to their owning WidgetShell. The loader observes that signal and the shell merges contributions with authored actions by stable id without writing them into the page document. The widget remains the owner of command handling, localized labels, availability and controlled-region state; the shell owns responsive toolbar materialization and ARIA projection.
Global configuration snapshots
PraxisI18nService resolves individual translation keys against current config
layers without merging whole catalogs on every label lookup. Provider precedence,
namespace fallback, parent scopes and host translators remain unchanged. Global
snapshot replacements and in-place dictionary updates remain visible without
cache invalidation. mergePraxisI18nConfigs remains available for explicit catalog
materialization outside this lookup path.
GlobalConfigService synchronous getters (get, getCrud, getDynamicFields,
getTable, getDialog, getI18n) read the effective snapshot without scheduling
bootstrap work again when the current tenant is already loaded. While the initial
load is pending, repeated reads share one background request. This keeps template
and translation reads from feeding asynchronous change-detection cycles.
ready() remains the explicit awaited loading boundary. Tenant changes, save and
clear continue to refresh the same canonical snapshot; precedence and persistence
contracts are unchanged.
Domain Governance
DomainKnowledgeService and DomainRuleService are shared clients for governed semantic decision flows exposed by praxis-config-starter. browseDefinitionCatalog exposes the bounded, redacted and server-scoped decision catalog; consumers then use getDefinition with the exact returned definition id instead of downloading every rule body or joining versions by key. getDefinitionFacts projects the same definition's versioned fact vocabulary, including type, localized meaning, provider evidence, sensitivity and redaction policy; Angular must not reconstruct that vocabulary from JSON Logic or host-specific fixtures. getDefinitionCapabilities returns the authenticated principal's server-scoped CREATE_NEW_VERSION action per definition; consumers must not treat projection metadata such as editor support as authorization. For change workspaces, getChangeWorkspaceCapabilities returns the authenticated principal's availableActions and stable blockers; consumers must not infer submit, review or promotion authority from lifecycle status. DomainRuleService also exposes the safe RuleSet version catalog, active head/status, redacted execution summary, aggregate host alignment, forward activation and rollback protected by the current head ETag. The same client projects immutable rollout-policy versions, their independent anti-ABA head, maker-checker approval, activation and append-only timeline; policy authority and lifecycle validation remain server-owned. It also exposes the two-phase operational rollout through a recoverable human catalog, redacted readiness, create/cancel commands and candidate activation bound to the rollout identity. Consumers must obey the Config-owned availableActions; they must not recalculate quorum or head validity. Execution summaries contain only aggregate outcome counts, distinct-host count and observation timestamps for an immutable snapshot. Host-status summaries contain only aligned, snapshot-drifted, runtime-incompatible, unavailable and stale counts derived against the active head and its approved compatibility coordinates; facts and host identities remain outside Angular. Runtime surfaces should treat materializations as derived projections of backend-governed decisions, not as frontend-owned business rules.
Public API Areas
Core exports:
- shared services such as
GenericCrudService,GlobalConfigService,GlobalActionService,ResourceDiscoveryService,ComponentMetadataRegistry,PraxisJsonLogicService,LoadingOrchestratorService - governed submit-error normalization through
ErrorMessageService, preserving publiccode,target,category, and safe messages while keeping transport details out of UI fallbacks - tokens and providers for API URLs, global config, global actions, loading, i18n, settings panel bridge, surface drawer bridge, collection export, field selector registry, overlay/layer scale, the shared collection-search visual contract (
PRAXIS_COLLECTION_SEARCH_DEFAULTS,PRAXIS_COLLECTION_SEARCH_VARS,buildPraxisCollectionSearchCss), and the shared action-control geometry contract (PRAXIS_ACTION_CONTROL_DEFAULTS,PRAXIS_ACTION_CONTROL_VARS,buildPraxisActionControlCss) - models for table config, fields, forms, rich content, editorial content, widget pages, global actions, resource discovery, domain knowledge/rules, analytics, query context, loading, and collection export
- helpers for schema ids, ETag fetch, field mapping, config merge, validation, IDs, inline filter controls, global action refs, and form hooks
- dynamic widget/page runtime components and metadata
- UI helpers such as icon picker, empty state card, resource quick connect, schema viewer,
PraxisIconDirective, andPraxisIconButtonComponent
See the package public-api.ts for the full export list.
Collection-search consumers must theme the semantic --praxis-collection-search-* variables instead of targeting Table or Dynamic Fields internals. The public radius token accepts square (0), intermediate (for example 8px) and pill (999px) geometries; surface, text, outline, focus, density, typography and motion remain in the same shared contract.
Action-bearing components must use the semantic --praxis-action-control-* variables for height, density, radius, padding, icon gap, typography, focus and disabled state. Component-specific public overrides remain stronger, but consumers should not target Angular Material internals to align actions across toolbars, empty states or responsive surfaces. The default disabled opacity is intentionally 0.62: unavailable actions must remain recognizable and explainable without appearing enabled; authorization and execution continue to be governed by capabilities, availability and aria-disabled, never by visual opacity.
Notes
@praxisui/corecentralizes shared Angular/runtime contracts, but backend metadata semantics are still defined by the appropriate backend starter.- Do not use consuming apps to redefine contracts owned by core, metadata starter, config starter, or a component package.
- Prefer
composition.linksfor page wiring andvaluePresentationfor scalar display semantics. - Use the official documentation for full recipes on schema flow, option sources, analytics, global actions, dynamic pages, and governed AI flows.
Catálogo no quick connect e isolamento do editor de página
ResourceQuickConnectComponent apresenta recursos publicados em /schemas/catalog usando o seletor pdx-material-select registrado em ComponentMetadataRegistry. O host deve registrar os descritores oficiais de DynamicFields. O controle é inicializado pelo protocolo setInputMetadata e ControlValueAccessor; Core não importa a biblioteca de campos. A conexão avançada por rota continua disponível (com <datalist> de sugestões do catálogo e abertura automática da seção avançada quando o seletor dinâmico não está disponível ou quando o recurso atual é uma rota customizada fora do catálogo).
O componente expõe [resourcePath], (resourcePathChange), [endpointKey], [apiUrlEntry], ControlValueAccessor (ngModel / formControl) e os hooks reset() / acceptAppliedValue() de SettingsValueProvider. A projeção e reconciliação de opções do catálogo são centralizadas em ResourceDiscoveryService.getDiscoveredResources(), loadDiscoveredResources(), projectSchemaCatalogResources() e resolveCatalogResourceOptionValue(), deduplicando chamadas concorrentes a /schemas/catalog por URL e reconciliando caminhos relativos com ou sem prefixo /api. ResourceDiscoveryService reutiliza a instância do injetor ancestral quando um componente standalone cria injetor filho sem override local de API_URL.
O editor de configurações da página recebe uma cópia da definição autoral. Dados carregados e entradas transitórias dos widgets não devem virar configuração ao alterar o layout.
Insertion preset composition shell
ComponentDocMeta.insertionPresets[].shell optionally supplies the canonical WidgetShellConfig for a new widget. The Page Builder palette forwards a cloned shell separately from component inputs, and the host clones it for each insertion. Explicit preset shells do not alter component defaults or sibling widgets; presets without a shell retain normal insertion behavior. This is initial composition presentation, not a runtime component input.
Margem lateral compartilhada da página
WidgetPageDefinition.layout.paddingInline define o respiro lateral compartilhado por
cabeçalho, tabela, formulário e demais widgets. É uma medida CSS (24px, 1.5rem)
ou referência a token (var(--app-page-inset, 24px)). O valor 0px desliga o inset;
a omissão herda o token público --pdx-page-padding-inline do host, cujo default é 0px.
O host pode definir esse token em praxis-dynamic-page ou em um ancestral.
A precedência é deviceLayouts[device].layout.paddingInline → layout.paddingInline
→ token do host → 0px. Defaults do preset participam da resolução do layout base;
um valor autorado na página ou variante tem precedência sobre esse default.
A margem é aplicada no wrapper com box-sizing: border-box,
fora do canvas, preservando a origem dos cálculos de arraste e redimensionamento.
O espaço de segurança dos controles no modo edição permanece separado. Não se trata
do gap entre widgets, do padding interno do formulário ou do padding das células da tabela.
{
"layout": { "paddingInline": "24px", "gap": "16px" },
"deviceLayouts": { "mobile": { "layout": { "paddingInline": "16px" } } }
}
Nos editores de página, Margem lateral da página permite definir e limpar o valor sem alterar os widgets. Vazio na variante herda o layout base; vazio no layout base retorna ao default do preset, quando definido; caso contrário, ao token do host. Referências CSS são preservadas ao aplicar, salvar e reabrir.
Widget settings UX: identity and ready-made styles precede advanced CSS. Size includes a before/after occupancy map of the current container; row heights in this diagram are illustrative, not a pixel-accurate content preview. The map reads canonical canvas items and transient placements and never writes page geometry. Applying a size-only draft clears the draft but keeps Save available; Save obtains the current page rather than replaying the previous resize request. Backend persistence still follows the host configuration.
Widget settings: applied baseline and live inspection
The unified widget editor acknowledges appearance only after the page owner accepts
its commit. The optional provider hook acceptAppliedValue updates the draft baseline;
rejected operations do not acknowledge it. Reset discards the current appearance/size
draft to the last applied state. Explicit resize Undo remains a separate operation,
including affected neighbors. Save after Apply reads current state without replaying
an expansion. The open bridge carries an optional, ephemeral previewTarget callback
for SettingsPanel to reveal the live widget without destroying the editor or draft.
Neither capability changes the persisted page schema or confirms an HTTP save.
Widget settings topics delegate vertical scrolling to their SettingsPanel owner. The shared topic navigation keeps all sections discoverable, and the optional appearance sample is collapsed below the active editor. Topic changes retain the form draft without changing persistence or apply/save/reset behavior.
Selection-driven widget headers
WidgetShellConfig.avatar is optional and uses the existing Core avatar vocabulary:
{ imageSrc?: string, name?: string, initials?: string }. It displays beside the
shell title, taking precedence over icon. Omit it to retain the existing icon.
name supplies the accessible image name and fallback initials; initials overrides
the fallback (at most two characters). Missing/failed images show initials, then a
person icon. Resolved values that are not strings are treated as absent; objects
are never coerced into URLs. Failed images retry after clearing/changing the identity
or URL and returning to the same photo. Equivalent inputs on unrelated page renders
retain the fallback; late failures belong to their original load cycle. URLs use Angular URL sanitization; the shell does not fetch records or
bypass protected-image authentication.
Dynamic Page resolves shell strings recursively against state, transient,
context and the existing widget context. ${transient.employee.nomeCompleto}
reads runtime-only composition state. It does not merge that state into state
or write selected rows into the authored page. ${state...} keeps its existing
persistable-state semantics. Runtime isolation does not replace backend field-access
controls: deliver only fields the current user may see.
A selected-row header can use this shell after the ordered identity projection described below (the field names belong to the source):
{
"showHeader": true,
"title": "${transient.employee.nomeCompleto}",
"subtitle": "${transient.employee.subtitle}",
"avatar": {
"imageSrc": "${transient.employee.avatarUrl}",
"name": "${transient.employee.nomeCompleto}"
}
}
Connect the table's canonical selectionChange event to the state endpoint
{ "kind": "state", "ref": { "path": "employee", "layer": "transient", "writable": true } }.
A pick-path link transform reads payload.selectedRows.0 from the event envelope.
For formatted identity, use the complete ordered recipe below rather than only
copying the row: that projection also initializes and composes employee.subtitle.
Deliver an empty selection too; skipping empty events leaves the previous person
visible. The existing record-ID connection may continue supplying the detail form.
The header consumes the selected row without a second request. If fields are absent
from that row, author a projection from the authorized detail source instead.
Formatting dates and identifiers belongs to the authored projection/source; shell
interpolation does not add date masks, CPF masks or an expression language. Literal
separators remain when both bound fields are empty.
The canonical link pipeline now supports explicit format-value for civil dates,
digit masks and numeric presentation.
The complete identity recipe
resets the transient identity before formatting and composes only available subtitle
parts. The lab and Core integration test consume the same executable page artifact.
The opt-in widget-shell-persistence.playwright.spec.ts reuses the fictional
selection fixture in /remote-config-storage-persistence-lab. It proves a real
Config save with If-Match, document equality after selecting a person, and a
reload with no selected identity. It owns an initially absent UUID configuration
and deletes only that record with its current ETag. Run with
PRAXIS_REAL_BACKEND_E2E=true; this is a persistence proof, not an LLM authoring gate.
Configurações em hosts controlados
Quando o host devolve pageChange por [page], o eco do mesmo documento canônico mantém as configurações do widget abertas após Aplicar. Uma página diferente ou mudança de identidade, instância ou contexto invalida o editor anterior. Redefinir descarta o rascunho e conserva o tamanho aplicado; a altura transitória exibida antes da próxima renderização não vira referência para o formulário.
Authored widget action identities
getWidgetShellActionIdError(id, siblingIds) validates the trimmed ID and returns
required, duplicate, reserved, or null. Exclude the current action from siblings.
Reserved IDs are collapse, expand, fullscreen, component-settings,
widget-assistant, widget-settings, widget-size, and widget-remove: these belong
to platform controls, not custom authored actions. Native window controls remain supported.
Page Builder uses this helper for draft feedback; DynamicWidgetPage also uses it before
applying widget settings, including settings supplied by alternate providers. Rejected
settings leave the page unchanged. Existing custom actions with invalid/reserved IDs need
renaming before applying settings. The helper adds no persisted field and is not a global
page-import validator, business-command verifier or authorization boundary.
Live shell action availability
WidgetShellActionContributor.getWidgetShellActionAvailability?(action) may return
Pick<WidgetShellAction, 'disabled' | 'tooltip'>. It must be side-effect free and read live
owner state; undefined means not assessed, not enabled. Reactive owner signals can be
read directly so an open editor updates when prerequisites change. disabled:false means
currently available in the UI, not backend authorization. Return a localized tooltip
when disabled. The loader delegates to the current contributor; DynamicWidgetPage passes
a transient callback to the shell editor. It must never be serialized as widget inputs,
shell actions, or persisted page configuration. Contributors without this optional method
remain supported and their availability is shown as unverified.
Chart input updates preserve the live instance
Charts uses normal Angular input updates in DynamicWidgetLoader. Changing chartDocument,
query context or other inputs must not recreate the chart solely because its type is
praxis-chart. Recreating it after an editor Apply destroys subscriptions owned by that
instance and can drop the subsequent Save from the still-open panel. Declared
requiresInitialInputs behavior and existing detached Form/Filter initialization paths
remain separate and unchanged. Input updates still occur before detectChanges.
Settings Panel owner lifetime
SettingsPanelOpenOptions.owner?: DestroyRef associates an opening with its calling
component's lifetime. The official Settings Panel bridge forwards this transient option
to its service. Destruction emits the existing closed$ reason cancel, without Save,
and prevents a pending opening from replacing another panel. The option is not editor
input or persisted configuration. Omit it for an independently managed panel; existing
unowned callers retain their behavior. Use the actual target component's DestroyRef,
not a root service or the overlay content's own lifetime.
DynamicWidgetPageComponent creates a transient Angular lifetime for each page,
widget appearance/size, or registered content editor session. It uses the existing
owner option; no session identity is persisted in the page document.
The session is cancelled when its panel closes, the page is destroyed, or an external
page/context/identity/authoring input change invalidates it. An acknowledgement identical
to the current authored page keeps the session alive for subsequent Apply/Save, including
when the host resends an equivalent pageIdentity object. After the first input change,
identity equivalence means strict equality of its five fields: appId, tenantId,
userId, routePath and locale; whitespace, delimiters and missing values are not
normalized. Core retains a private snapshot of those accepted primitive values, so
mutating the host's previous object before resending a clone still changes the scope.
Resending only that equivalent identity also preserves data-source overlays,
composition and persistence scope. Context, instance and authoring capability changes
remain invalidating. A different external document cancels open editors even when a
particular widget happens to be unchanged. This does not confirm an HTTP save.
Confirmed widget removal cancels sessions targeting that key, plus page-wide settings that could restore the removed widget. Asynchronous editor results cannot open a panel once their session has expired, and late Apply/Save events are no longer observed. A removal confirmation is ignored after external identity/context changes or changes to the target itself; when still valid, removal uses the latest document so intervening edits to other widgets are preserved.
When unified widget settings close normally, Core restores the initiating control through its existing focus-return helper instead of forcing focus onto the whole widget. Invalidated sessions do not schedule this restoration. A detached, disabled initiator or destroyed page is never focused by that helper.
Preparação do editor após aceitar a troca
SettingsPanelOpenContent.inputs aceita um objeto ou uma função síncrona que retorna
os inputs. A bridge oficial encaminha a função ao Settings Panel sem executá-la.
O serviço a executa uma única vez ao materializar a abertura aceita; um veto ou um
owner já expirado não executa a função. Hosts que implementam a bridge devem respeitar
essa mediação antes de resolver os inputs. Leitores desse campo devem distinguir
objeto e função antes de acessar propriedades.
O editor de widget usa essa preparação para trocar sua sessão e seu estado de tamanho. Enquanto a troca aguarda decisão, o editor anterior continua dono do rascunho e de suas assinaturas. Recusar a troca libera somente a sessão candidata. A função é código transitório de integração: não pertence ao documento, ao JSON de configuração, aos presets nem aos contratos de authoring por IA.
Responsive form-column numeric resolution
resolveSpan, resolveOffset and resolveOrder resolve the existing XS → SM → MD → LG → XL inheritance without modifying the supplied map. Supported integer domains are span 1..12, offset 0..11 and order -12..12, matching runtime grid classes. Missing/null values inherit, starting at 12/0/0 in XS. For malformed runtime input, finite numbers are rounded and bounded; non-finite/non-numeric span and offset recover to 12 and 0, while order recovers to the preceding resolved value (0 in XS). Recovery emits a warning and does not rewrite the authoring document. Editors must reject invalid values before applying them.
Acesso às dimensões do widget
Quando o Settings Panel e o editor de aparência do widget estão disponíveis, use Configurar widget → Tamanho. O menu de contexto e as ações projetadas não repetem o atalho Tamanho. Em contextos sem editor unificado, o atalho permanece para não retirar o acesso às dimensões. Arraste e comandos de tamanho mantêm sua semântica; esta regra modifica somente a descoberta visual do comando.
Quando constraints.lockSize estiver ativo e a customização de canvas estiver
habilitada, o editor unificado mantém a aba Tamanho com uma explicação do bloqueio,
sem controles editáveis. A restrição continua pertencendo ao documento da página;
o aviso não concede permissão nem gera uma alteração de tamanho.
Composed settings navigation
SettingsValueProvider may expose getSettingsSections() and selectSettingsSection(id) together.
SettingsEditorSection contains an editor-local stable id, a resolved i18n label, and an optional
invalid flag. These are transient presentation metadata, never persisted settings. Reading sections
and selecting one must not modify the draft or emit document changes. Editors must emit their existing
validity/dirty streams when validation changes so the composing host can refresh section diagnostics.
Core's widget settings host namespaces provider section IDs, appends its page-owned Size section and
keeps the shell editor mounted while switching sections. It provides SETTINGS_EDITOR_SECTIONS_HOSTED
so section-aware editors suppress their own navigation. Unsectioned settings providers remain valid:
the host presents their single Widget topic. Apply, Save, Reset and applied-baseline acknowledgement
continue to aggregate the original providers; section navigation never becomes the value provider.
SettingsSectionTabsComponent is the shared presentation primitive for these topics. It exposes all
labels, selected state, per-section errors and keyboard navigation, with unique tab/panel associations.
Page Builder imports it directly from Core; Settings Panel does not reexport this component or the new
topic contracts. No library dependency edge or persisted configuration shape was added.
The widget host keeps topic navigation sticky inside the Settings Panel's existing scroll area. Narrow panels present a compact, labeled native selector with the same section IDs and validation messages; wider panels expose the shared tabs. Switching topics reveals the beginning of the selected section without remounting the provider or moving focus out of the navigation control.
On editable canvases, content-only widgets expose contextual actions above the north resize handle, with reserved space at the top of the canvas. Both controls remain pointer-accessible when the widget is narrow.
The dark-glass preset uses a fixed dark material palette with explicit header/body
foreground pairs, including in light application themes. Projected content inherits
the shell foreground when it does not define its own surface.
Shared page preset resolution
resolveWidgetPageLayoutPreset(page) exposes the same organization resolution used by
DynamicWidgetPageComponent: a recognized, trimmed layoutPreset wins; otherwise the
runtime uses the existing layoutPresetOptions.presetFamily. It reads the existing
WidgetPageDefinition fields and returns the builtin catalog definition without modifying
the page. Page editors use it to explain inherited organization and its suggested theme.
An explicit themePreset still takes precedence over defaultThemePreset. This extraction
adds no page field, domain semantics, dependency on another library, or persistence behavior.
Selection-bound workflow forms
ResourceActionOpenAdapterService projects the existing collection action's
selection.idsField and, for SELECTION_MAP, selection.versionsField into
groupedCommand.contextFields. The form layout retains these schema fields in
runtime metadata and submit data even when hidden, while excluding them from the
interactive layout. Primitive arrays such as selected IDs remain FormControl
values; object collections declared by arrayInput or array use FormArray.
A PER_ITEM action suppresses generic HTTP-success notifications: its opening
consumer must present the per-item outcome rather than claim all items succeeded.
Shell materials and faithful editor preview
WidgetShellConfig.appearance.card.backdropFilter accepts a CSS backdrop effect such as
blur(16px) saturate(125%). Pair translucent backgrounds with an opaque
appearance.card.fallbackBackground. The shell uses that fallback when backdrop filters
are unsupported, the user requests reduced transparency, or the card is expanded/fullscreen.
A sticky glass header uses the same opaque fallback for legibility over scrolling content.
none disables a backdrop effect; kind: 'none' still disables the entire external surface.
The runtime resolves a widget preset, the inherited page/theme preset, custom context presets,
and explicit appearance overrides in that order of responsibility. Widget appearance fields
win over the chosen preset. The editor receives the resolved inherited context; it never
persists the theme-derived preset as an explicit widget or page choice just to render its preview.
The original ID light-neutral now has the honest adaptive-neutral label. Presets style the shell;
charts, tables and rich content retain ownership of their internal appearance and behavior.
Token ownership: host Material tokens → canonical widget-shell-presets.ts → shell appearance
bindings → WidgetShellComponent; Page Builder thumbnails and live previews consume the same
catalog. Fixed glass/graphite palettes are material defaults in that catalog, overridable through
existing context presets or explicit appearance. No host CSS patch is required.
Shell border widths, strokes and layered backgrounds
appearance.card.borderWidth and borderStyle accept CSS border values, including
per-side shorthands such as 1px 1px 1px 6px and solid dashed dotted double.
appearance.header.borderWidth and borderStyle control the bottom divider only
(single values). Omitted values retain the existing 1px solid geometry. none
removes the corresponding stroke; kind: 'none' disables the complete shell.
The same fields participate in preset/context inheritance and explicit override precedence.
Backgrounds already accept solid colors, gradients and CSS background layers. For a rounded gradient border, use a transparent solid border and two background layers:
{
"card": {
"background": "linear-gradient(#102c46,#102c46) padding-box,linear-gradient(125deg,#b9d9ec,#ffd38a) border-box",
"borderColor": "transparent",
"borderWidth": "3px",
"borderStyle": "solid",
"borderRadius": "16px"
},
"header": { "background": "transparent", "titleColor": "#ffffff", "subtitleColor": "#ffffff", "iconColor": "#ffffff" },
"body": { "background": "transparent", "textColor": "#ffffff" }
}
The background layer is continuous: a transparent dashed/dotted/double border does
not cut gaps into that gradient. Use a colored stroke and another fill for visibly
dashed/dotted/double outlines. borderStyle: 'none' hides the gradient border area.
Authoring valid CSS does not certify contrast: consumers must pair foregrounds,
backgrounds and opaque alternatives appropriately. Inner component surfaces keep
ownership of their own colors and interaction states.
Nested shells isolate their material CSS properties: an outer thick/dashed border or
blur does not become an inner widget's default. Intentional inheritance through
context.ui.shell.preset/presets remains supported. This isolation does not remove
the host's shared theme tokens.
App shell: host appearance
PraxisAppShellComponent materializes the host theme for both the sidebar and
header. The existing theme roles --sidebar (falling back to --card),
--surface-gradient (falling back to --card), --command-gradient,
--sidebar-foreground, --sidebar-primary, --sidebar-accent,
--sidebar-accent-foreground, --primary-deep, --primary-foreground,
--accent, --secondary, --overlay
and --shadow-panel apply to the native chrome and brand icon. Navigation,
loading and mobile behavior do not change when these colors change. Without host
theme tokens, the brand uses a dark blue fallback with a white icon; custom host
colors should always supply their corresponding foreground roles.
Semantic badges use tinted surfaces with the host's foreground. Success,
warning and info consume --praxis-success, --praxis-warning and
--praxis-info; the established --ready/--warning variables and bounded
fallbacks remain available. Error consumes --destructive. Hosts should use
these existing roles rather than overriding private shell selectors. No new
shell input, saved layout field or theme-specific component is required.