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

Display Data with DataViews

Define a filterable, sortable transaction history view that works across web and mobile without duplicating UI code.

1. Define the underlying query

First, create a query operation to fetch the data:

lib/specs/billing/list-transactions.ts
import { defineQuery } from '@lssm-tech/lib.contracts-spec';

export const ListTransactions = defineQuery({
  meta: {
    key: 'billing.listTransactions',
    version: '1.0.0',
    description: 'Fetch customer transaction history',
  },
  io: {
    input: /* pagination + filters */,
    output: /* array of transactions */,
  },
  policy: { auth: 'user' },
});

2. Define the DataView spec

Wrap your query with presentation metadata:

lib/specs/billing/transaction-history.data-view.ts
import { defineDataView } from '@lssm-tech/lib.contracts-spec/data-views';
import { ListTransactions } from './list-transactions';

export const TransactionHistory = defineDataView({
  meta: {
    key: 'billing.transactionHistory',
    version: '1.0.0',
    entity: 'transaction',
    title: 'Transaction History',
    description: 'Customer payment history',
    domain: 'billing',
    owners: ['team-billing'],
    tags: ['payments'],
    stability: 'stable',
  },
  source: {
    primary: {
      key: ListTransactions.meta.key,
      version: ListTransactions.meta.version,
    },
  },
  view: {
    kind: 'table',
    executionMode: 'client',
    selection: 'multiple',
    columnVisibility: true,
    columnResizing: true,
    columnPinning: true,
    rowExpansion: {
      fields: ['notes', 'renewalDate', 'lastActivityAt'],
    },
    initialState: {
      pageSize: 4,
      hiddenColumns: ['notes'],
      pinnedColumns: {
        left: ['account'],
      },
      sorting: [{ field: 'arr', desc: true }],
    },
    fields: [
      { key: 'account', label: 'Account', dataPath: 'account', sortable: true },
      { key: 'owner', label: 'Owner', dataPath: 'owner', sortable: true },
      { key: 'status', label: 'Status', dataPath: 'status', sortable: true },
      {
        key: 'amount',
        label: 'Amount',
        dataPath: 'amount',
        sortable: true,
        format: { type: 'currency', currency: 'EUR', rounded: true },
      },
      {
        key: 'renewalDate',
        label: 'Renewal',
        dataPath: 'renewalDate',
        format: { type: 'date', dateStyle: 'medium' },
      },
      {
        key: 'processingTime',
        label: 'Processing time',
        dataPath: 'processingMinutes',
        format: { type: 'duration', unit: 'minute', display: 'digital' },
      },
      {
        key: 'notes',
        label: 'Notes',
        dataPath: 'notes',
        visibility: { minDataDepth: 'detailed' },
      },
    ],
    collection: {
      viewModes: {
        defaultMode: 'table',
        allowedModes: ['list', 'grid', 'table'],
      },
      toolbar: {
        search: true,
        viewMode: true,
        filters: true,
        density: true,
        dataDepth: true,
      },
      pagination: {
        pageSize: 25,
        pageSizeOptions: [10, 25, 50],
      },
      density: 'comfortable',
      dataDepth: 'standard',
      personalization: {
        enabled: true,
        persist: {
          viewMode: true,
          density: true,
          dataDepth: true,
          pageSize: true,
        },
      },
    },
    filters: [
      { key: 'status', label: 'Status', field: 'status', type: 'enum' },
      {
        key: 'amount',
        label: 'Amount',
        field: 'amount',
        type: 'currency',
        valueMode: 'range',
      },
      {
        key: 'renewalDate',
        label: 'Renewal',
        field: 'renewalDate',
        type: 'date',
        valueMode: 'range',
      },
    ],
  },
});

The live version of this pattern is available in the canonical

Data Grid Showcase

.

3. Render on the frontend

Use the runtime renderer in your React or React Native app:

app/dashboard/transactions/page.tsx
'use client';

import { DataViewRenderer } from '@lssm-tech/lib.design-system';
import { TransactionHistory } from '@/lib/specs/billing/transaction-history.data-view';
import { useQuery } from '@tanstack/react-query';

export function TransactionsPage() {
  const { data, isLoading } = useQuery({
    queryKey: ['transactions'],
    queryFn: () => fetch('/api/ops/billing.listTransactions').then(r => r.json()),
  });

  return (
    <div className="container py-8">
      <h1 className="text-3xl font-bold mb-6">Payment History</h1>
      <DataViewRenderer
        spec={TransactionHistory}
        items={data?.items ?? []}
        loading={isLoading}
        defaultViewMode="table"
        defaultDensity="comfortable"
        defaultDataDepth="standard"
        onFilterChange={(filters) => {
          // refetch with new filters
        }}
      />
    </div>
  );
}

4. Add personalization

When the app has a user profile or behavior insights, resolve DataView preferences before rendering. The renderer receives plain props; personalization stays in the app/runtime boundary.

app/dashboard/transactions/PersonalizedTransactions.tsx
'use client';

import { DataViewRenderer } from '@lssm-tech/lib.design-system';
import { resolveDataViewPreferences } from '@lssm-tech/lib.personalization/data-view-preferences';
import { createBehaviorTracker } from '@lssm-tech/lib.personalization';

const tracker = createBehaviorTracker({
  store,
  context: { tenantId: tenant.id, userId: user.id },
});

const resolved = resolveDataViewPreferences({
  spec: TransactionHistory,
  preferences: profile.canonical,
  insights,
  record: savedTransactionViewPreference,
});

<DataViewRenderer
  spec={TransactionHistory}
  items={transactions}
  defaultViewMode={resolved.viewMode}
  defaultDensity={resolved.density}
  defaultDataDepth={resolved.dataDepth}
  pagination={{ page, pageSize: resolved.pageSize ?? 25, total }}
  onViewModeChange={(viewMode) =>
    tracker.trackDataViewInteraction({
      dataViewKey: TransactionHistory.meta.key,
      action: 'view_mode_changed',
      viewMode,
    })
  }
  onDataDepthChange={(dataDepth) =>
    tracker.trackDataViewInteraction({
      dataViewKey: TransactionHistory.meta.key,
      action: 'data_depth_changed',
      dataDepth,
    })
  }
/>;

Why DataViews?

  • Same spec renders on web (React) and mobile (React Native)

  • Filters, sorting, and pagination handled automatically

  • Column visibility, pinning, resizing, and row expansion stay contract-driven

  • List, grid, and table modes can share one collection config with toolbar and pagination defaults

  • Data depth lets summary screens hide detailed fields without forking the spec

  • Typed format rules for numbers, percent values, currency, dates, times, datetimes, and durations applied consistently

  • Personalization helpers can seed preferred view mode, density, data depth, and page size from user preferences or behavior insights

  • Export to CSV/PDF using the same spec

  • A/B test different layouts without touching the backend

OSS docsstartStart with OSS. Adopt Studio when you want the operating layer.

Why ContractSpec

Keep educational and comparison content reachable without letting it define the primary OSS learning path.