Guide · @staticflux/ui · @staticflux/kit · @staticflux/shell · @staticflux/messaging · @staticflux/a11y · @staticflux/i18n

UI, shell, messaging, a11y & i18n

Components self-inject what they need, render once per frame and only when what they read changes. The shell frames them; one surface manager owns every overlay; accessibility and localization are built in.

Components

apps/studio/src/ui/app-views.ts (abridged)
const headerSpec = component({
  tag: 'studio-header',
  deps: {
    render: { session: SessionToken, i18n: I18nToken, panel: PanelControllerToken },
    lazy: { meters: MeterRegistryToken },
  },
  size: { block: '48px' },
});

export class StudioHeader extends SfElement<typeof headerSpec> {
  static override readonly spec = headerSpec;
  override render(): TemplateResult {
    const { session, i18n, meters } = this.deps;
    const credits = meters.value?.get(CreditsMeter);
    return html`<strong>${i18n.t(appMessages.title)}</strong> …`;
  }
}
  • SfElement is the only base class (no decorators). Dependencies resolve from the nearest <sf-scope> — one Lit Context key carries the injector handle.
  • Updates go through the runtime frame scheduler: one render pass per frame; MobX tracking re-renders only for values read.
  • A frozen scope (<sf-scope ?frozen>, hidden features) renders nothing and drops effects — hidden features cost zero render work.
  • Hooks per dependency: wireDeps(on) with onReady, onRefresh, onFailed, whileVisible; blocking: true keeps the placeholder until the hook settles.
  • sfSrc(url) adds the right crossorigin under cross-origin isolation; crossOrigin(url, kind) for templates.

The theme contract

TypeScript
import { appearanceUnit, extendTheme, themeContribution } from '@staticflux/ui/theme';
import { defaultTheme } from '@staticflux/ui/default-theme';

export const brandTheme = extendTheme(defaultTheme, {
  id: 'acme/brand',
  values: { color: { accent: '#5B3DF5' }, radius: { control: '9999px' } },   // typed: unknown keys fail to compile
});
const units = [appearanceUnit, themeContribution(brandTheme, { order: 10 })];  // the default theme is order −100

@staticflux/ui/theme defines the token set (themeTokens: color ramp, surfaces, text, accent and agent actions, type, a 4 px space grid, sizes, radii, shadows, motion, z) as typed values, never strings. compileTheme emits CSS that is right before any JavaScript runs (prefers-color-scheme, prefers-contrast, prefers-reduced-motion); after boot the appearance keeps data-sf-* attributes in sync with the user’s choice — scheme, contrast, density (Focus / Standard / Pro) and motion — through appearance.set(…). runThemeContract (testkit) checks every brand theme’s contrast pairs against WCAG 2.2 AA.

The kit

TypeScript
import { defineButton } from '@staticflux/kit/button';
import { defineSlider } from '@staticflux/kit/slider';

defineButton();
defineSlider();
html`<sf-button variant="primary" size="md">Export</sf-button>
      <sf-slider label="Opacity" min="0" max="100" unit="%"></sf-slider>`;

@staticflux/kit is a themed component set on SfElement: buttons, icon buttons, segmented controls, fields, sliders, tags, status, color fields, menus, popovers, tooltips, tabs, toolbars, docks and panels, inspector rows, a virtualized tree and the capability command palette. Styles use theme tokens only, so themes, density, contrast and reduced motion restyle everything; every entry point is its own budgeted chunk; overlays render messaging’s surfaces rather than replacing the manager; every component test runs expectNoA11yIssues.

The shell

TypeScript
export const shellContributionUnits = [
  slotContribution({ id: 'studio/header', slot: 'header', label: 'Studio', render: () => html`<studio-header></studio-header>` }),
  slotContribution({ id: 'studio/ai-suggestions', slot: 'sidebar', label: 'AI suggestions',
    gate: gateResource('ui-slot', 'studio/ai-panel'), render: () => html`<ai-suggestions></ai-suggestions>` }),
  workspaceContribution(defineWorkspace({ id: 'studio/expert', label: 'Expert',
    gate: gateResource('workspace', 'workspace:studio/expert'), add: ['studio/inspector'] })),
];

<sf-shell> lays out slots per shell preset (expanded, medium, compact, embedded — chosen per app profile, never by if), wraps each region in an error boundary, hides gated contributions until allowed, applies the theme (the theme contract above; createThemeController remains for the older token set), switches workspaces in a View Transition, and renders skip links and landmarks first. The command palette and command shortcuts sit on the a11y shortcut registry.

Layout specs

TypeScript
import { createSpecLayoutPort, createSpecRuntime, createSpecVocabulary, defineSpecContribution,
  defineSpecSection } from '@staticflux/shell/spec';

const vocabulary = createSpecVocabulary({
  sections: [defineSpecSection({ id: 'tool', pinnable: true }), defineSpecSection({ id: 'panel', placements: ['dock', 'inspector'] })],
  contributions: [
    defineSpecContribution({ section: 'tool', id: 'app/pen', label: 'Pen' }),
    defineSpecContribution({ section: 'tool', id: 'app/heal', label: 'Heal', when: { lens: ['app/pixel'] } }),
    defineSpecContribution({ section: 'panel', id: 'app/layers', label: 'Layers', gate: gateResource('ui-slot', 'app/layers') }),
  ],
});
if (!vocabulary.ok) throw vocabulary.error;           // a malformed vocabulary is a programmer error

const layout = createSpecLayoutPort();                 // reactive: views render entries(group) with phases
const specs = createSpecRuntime({ vocabulary: vocabulary.value, port: layout, decide });
const applied = await specs.apply({
  id: 'app/retouch', context: { lens: 'app/pixel' },
  slots: [{ section: 'tool', id: 'app/pen' }, { section: 'tool', id: 'app/heal' }, { section: 'panel', id: 'app/layers', placement: 'dock' }],
}, { signal });                                        // Result<SpecApplied>: the diff, failures, dropped pins

@staticflux/shell/spec generalizes workspaces: a screen is a spec — data naming registered contributions, their sections and placements. validateSpec reports every issue at once (unknown contribution, context mismatch, gated out, cardinality, placement); apply diffs against what is mounted (diffSpecs: unmount, move, mount, kept, pinned) and drives the mount port in that order within one frame, each step animated for 120–200 ms (0 under reduced motion). Agents apply specs too, through their own subject’s DecidePort.

One surface manager

TypeScript
export const SavedToast = defineSurface<{ readonly message: string }>({
  id: 'studio/saved-toast', kind: 'toast', scope: 'app', durationMs: 2500,
  render: ({ props }) => html`${props.message}`,
});
surfaces.request(SavedToast, { owner: ownerOf(context.injector), props: { message: 'Saved' } });

Dialog, sheet, popover, tooltip, toast, banner, coach mark and inline alert share one stacking model, focus behaviour, theming and motion. Every surface has an owner scope (I-7), attention is arbitrated (toast caps, banner dedup, coach marks suppressed while a modal is up or the user is busy), surfaces are gated (a denied premium dialog resolves to its upsell) and every one reports shown / dismissed / dwell metrics.

Accessibility

@staticflux/a11y provides the focus manager (route-change focus, stacked traps with restore and inert background), a debounced live-region announcer, a typed keyboard-shortcut registry with scopes and conflict detection (checkShortcuts in CI), reactive preference facts (reduced motion, contrast, forced colors, color scheme) and aria-busy helpers. Target: WCAG 2.2 AA. Placeholders expose busy state; announced text never goes to telemetry.

Localization

TypeScript
export const appMessages = defineMessages('studio/app', {
  title: 'Studio',
  credits: '{count, plural, one {# credit} other {# credits}} left',
});
i18n.t(appMessages.credits, { count: 3 });     // params inferred from the ICU literal

Typed catalogs per package (keys are values, never strings), an ICU subset (plural, select, selectordinal, number, date, list), each locale a lazy chunk, negotiation with a fallback chain, runtime locale switch without reload (it flips only after every catalog in use loaded), RTL via dir, Intl formatters, and checkCatalogs for missing, unused and mismatched keys in CI.

AGENTS.md on GitHub: @staticflux/ui · @staticflux/kit · @staticflux/shell · @staticflux/messaging · @staticflux/a11y · @staticflux/i18n