OSS-first docs

These docs teach the open system first: contracts, generated surfaces, runtimes, governance, and incremental adoption. Studio shows up as the operating layer on top, not as the source of truth.

AI index

ThemeSpec guide

Set up ContractSpec Theme

Use ContractSpecEditorialTheme as the versioned source of truth for OSS consumers who want the same warm light palette and landing-aligned dark mode used across ContractSpec web surfaces.

Setup checklist

  • Install @lssm-tech/lib.contracts-spec and @lssm-tech/lib.design-system.

  • Import ContractSpecEditorialTheme instead of copying ad-hoc token maps.

  • Apply the light variables to :root and the dark overlay to .dark.

  • Let next-themes or your app shell own the html class switch.

  • Validate the ThemeSpec in CI before publishing tenant overrides.

1) Import the contract theme

The theme lives in packages/libs/contracts-spec/src/themes.ts. Consumers should reference the exported spec, registry, and CSS variable helpers rather than duplicating tokens in app code.

import {
  ContractSpecEditorialTheme,
  ContractSpecEditorialThemeBodyBackground,
  ContractSpecEditorialThemeCssVariables,
  ThemeRegistry,
  validateThemeSpec,
} from '@lssm-tech/lib.contracts-spec/themes';

const registry = new ThemeRegistry([ContractSpecEditorialTheme]);
const validation = validateThemeSpec(ContractSpecEditorialTheme);

if (!validation.valid) {
  throw new Error('ContractSpec theme is not publishable');
}

export const contractSpecThemeRef = {
  key: 'contractspec.editorial',
  version: '1.0.0',
} as const;

export const pageBackground = ContractSpecEditorialThemeBodyBackground;
export const cssVariables = ContractSpecEditorialThemeCssVariables;
export const theme = registry.get(
  contractSpecThemeRef.key,
  contractSpecThemeRef.version
);

2) Bridge it into Tailwind and UI

Use the design-system theme bridge when you need Tailwind presets, CSS text, runtime variables, or mode-aware component defaults.

import {
  DesignSystemThemeProvider,
  resolveThemeModeTokens,
  themeSpecToCssVariables,
  themeSpecToTailwindCss,
  themeSpecToTailwindPreset,
} from '@lssm-tech/lib.design-system/theme';
import { ContractSpecEditorialTheme } from '@lssm-tech/lib.contracts-spec/themes';

export const lightTokens = resolveThemeModeTokens(
  ContractSpecEditorialTheme,
  'light'
);

export const darkTokens = resolveThemeModeTokens(
  ContractSpecEditorialTheme,
  'dark'
);

export default themeSpecToTailwindPreset(lightTokens);

export const cssText = themeSpecToTailwindCss(
  themeSpecToCssVariables(ContractSpecEditorialTheme),
  { includeCustomVariant: true }
);

export function ContractSpecThemeProvider({
  children,
  mode = 'light',
}: {
  readonly children: React.ReactNode;
  readonly mode?: 'light' | 'dark';
}) {
  return (
    <DesignSystemThemeProvider
      theme={ContractSpecEditorialTheme}
      mode={mode}
      applyCssVariables
    >
      {children}
    </DesignSystemThemeProvider>
  );
}

3) Publish light and dark CSS

If your app owns global CSS directly, mirror the same semantic variables. The landing app uses these light variables plus the dark overlay from the contract theme.

/* Source of truth:
   ContractSpecEditorialTheme in packages/libs/contracts-spec/src/themes.ts */

:root {
  --paper: #f7f0e6;
  --paper-muted: #efe4d3;
  --paper-panel: #fbf7f0;
  --ink: #1d1b18;
  --ink-soft: #5e564d;
  --line: #d2c3ae;
  --rust: #a24f2a;
  --rust-strong: #854120;
  --amber: #d8a24a;
  --blue: #255678;
  --success: #2d6a4f;
  --background: var(--paper);
  --foreground: var(--ink);
  --card: var(--paper-panel);
  --card-foreground: var(--ink);
  --primary: var(--rust);
  --primary-foreground: #fff8f0;
  --secondary: var(--paper-muted);
  --secondary-foreground: var(--ink);
  --muted: color-mix(in srgb, var(--paper-muted) 78%, #ffffff 22%);
  --muted-foreground: var(--ink-soft);
  --accent: var(--blue);
  --accent-foreground: #eef7fb;
  --border: var(--line);
  --ring: color-mix(in srgb, var(--rust) 60%, #ffffff 40%);
  --radius: 1.25rem;
}

.dark {
  --background: #161412;
  --foreground: #f5ede2;
  --card: #211d19;
  --card-foreground: #f5ede2;
  --primary: #c46c43;
  --primary-foreground: #fff8f0;
  --secondary: #2c2723;
  --secondary-foreground: #f5ede2;
  --muted: #322c27;
  --muted-foreground: #cfbeb0;
  --accent: #79a8c6;
  --accent-foreground: #101418;
  --border: #433a34;
  --ring: #c46c43;
}

4) Wire the class switch

ContractSpec keeps the token contract separate from the app-level theme switch. In Next.js, use next-themes to toggle the light / dark class and let CSS variables do the rendering work.

import { ThemeProvider } from 'next-themes';

export function Providers({ children }: { readonly children: React.ReactNode }) {
  return (
    <ThemeProvider
      attribute="class"
      defaultTheme="light"
      enableSystem
      disableTransitionOnChange
    >
      {children}
    </ThemeProvider>
  );
}

Parity with this monorepo

web-landing

Light: :root uses the editorial paper, ink, rust, amber, and blue tokens.

Dark: .dark matches ContractSpecEditorialTheme.modes.dark.

web-application-monolith

Light: :root is locked by tests to ContractSpecEditorialThemeCssVariables and the editorial body gradient.

Dark: .dark is a managed cockpit overlay today; use the contract dark overlay when strict landing parity is required.

Verify adoption

Add a small test like the monolith theme contract test: read your global CSS, assert each exported ContractSpecEditorialThemeCssVariables entry is present, and assert the layout metadata publishes #f7f0e6 as the light browser theme color.