Skip to content

OS-Legal-Style Migration Guide

This guide documents the patterns, design language, and practical steps for migrating OpenContracts frontend components from Semantic UI to @os-legal/ui (OS-Legal-Style).

Design Philosophy

OS-Legal-Style is built on three principles:

  1. Transparent Infrastructure — Clarity through structure, not decoration. Subtle shadows (0.03–0.06 opacity) define boundaries instead of heavy borders.
  2. Visible Connections — Emphasize relationships between documents, annotations, and data.
  3. Warm Precision — Professional polish with approachability. Serif headlines (Georgia) for gravitas, Inter for UI text.

The overall aesthetic is clean, spacious, and content-forward. Pages use a light gray background (#fafafa), white content cards, and teal (#0f766e) as the primary accent.

Global Setup

Styles from @os-legal/ui are injected once in frontend/src/index.tsx:

import { allStyles } from "@os-legal/ui";

const styleElement = document.createElement("style");
styleElement.id = "opencontracts-ui-styles";
styleElement.textContent = allStyles;
document.head.appendChild(styleElement);

The library exposes CSS custom properties prefixed with --oc- (spacing, radius, colors, shadows, easing). Use these in styled-components wherever possible — they keep the codebase consistent and let the design system evolve in one place.

Canonical Page Layout

Every refactored view follows the same outer structure. Study DiscoveryLanding.tsx and Documents.tsx as the gold-standard references.

const PageContainer = styled.div`
  height: 100%;
  background: ${OS_LEGAL_COLORS.background};
  font-family: ${OS_LEGAL_TYPOGRAPHY.fontFamilySans};
  overflow-y: auto;
  overflow-x: hidden;
`;

const ContentContainer = styled.main`
  max-width: 900px;
  margin: 0 auto;
  padding: 48px 24px 80px;

  @media (max-width: 768px) {
    padding: 32px 16px 60px;
  }
`;

Within ContentContainer, pages are composed of <Section> blocks separated by consistent vertical spacing (margin-bottom: 56px). Each section with a heading uses the SectionHeader pattern:

<SectionHeader>
  <SectionTitle>Featured Collections</SectionTitle>
  <SectionLink href="/corpuses">View all <ChevronIcon /></SectionLink>
</SectionHeader>

Section titles use Georgia serif at 24px in teal. This pattern repeats identically across the discover page, documents view, and corpus list.

Typography

Element Font Size Weight Color
Page title (hero) Georgia, serif 42–48px 400 #1e293b, accent word in #0f766e
Section title Georgia, serif 24px 400 #0f766e
Body / subtitle Inter, sans-serif 17–18px 400 #64748b
UI text Inter, sans-serif 13–14px 500–600 #1e293b / #475569
Metadata / caption Inter, sans-serif 11–12px 500–600 #94a3b8
Uppercase labels Inter, sans-serif 11px 600 var(--oc-fg-tertiary), letter-spacing: 0.05em

Color Palette

Primary accent: #0f766e (teal) — used for highlighted title words, section headers, active states, selected borders.

Neutral scale (most-used in refactored components): - #1e293b — headings, primary text - #475569 — secondary text, metadata - #64748b — subtitles, tertiary text - #94a3b8 — captions, disabled text, icons - #e2e8f0 — borders, dividers - #f1f5f9 — hover backgrounds, subtle fills - #f8fafc — off-white backgrounds - #fafafa — page background

Semantic colors: green #16a34a (success), red #dc2626 (danger), orange #f59e0b (warning), blue #2563eb (info).

Note: The hex values above are for quick visual reference. The canonical source of truth is OS_LEGAL_COLORS in frontend/src/assets/configurations/osLegalStyles.ts — always check that file for current values.

Design Token Constants

frontend/src/assets/configurations/osLegalStyles.ts exports three typed constant objects that centralize the design system:

  • OS_LEGAL_COLORS — accent, text scale, surface/border, selection states, drag-and-drop feedback, folder theming, danger/success. Includes WCAG contrast notes (accent on white = 4.57:1 AA-compliant; textMuted on white = 2.78:1, large text only). Accessibility warning: textMuted (2.78:1) fails WCAG AA for normal text — do not use it for normal-weight text below 24px or bold text below 19px.
  • OS_LEGAL_TYPOGRAPHYfontFamilySerif (Georgia) and fontFamilySans (Inter) stacks.
  • OS_LEGAL_SPACINGborderRadiusCard (12px), borderRadiusButton (8px), shadowCard, shadowCardHover.

Use these tokens in new styled-components for consistency. Example:

import { OS_LEGAL_COLORS, OS_LEGAL_SPACING } from "../../assets/configurations/osLegalStyles";

const Card = styled.div<{ $isSelected?: boolean }>`
  background: ${OS_LEGAL_COLORS.surface};
  border: 1px solid ${(props) => props.$isSelected ? OS_LEGAL_COLORS.selectedBorder : OS_LEGAL_COLORS.border};
  border-radius: ${OS_LEGAL_SPACING.borderRadiusCard};
  box-shadow: ${OS_LEGAL_SPACING.shadowCard};

  &:hover {
    box-shadow: ${OS_LEGAL_SPACING.shadowCardHover};
  }
`;

Project theme tokens (frontend/src/theme/colors.ts): The color.N* neutral scale and color.O*, color.B* etc. are used for themed contexts (leaderboard badges, charts). For new refactored views, prefer OS_LEGAL_COLORS or the --oc-* CSS variables.

Component Replacement Map

Semantic UI OS-Legal-Style Replacement Notes
<Button> <Button> from @os-legal/ui Props: variant="primary"\|"secondary", size="sm", leftIcon, loading
<Modal> + <Modal.Content> + <Modal.Actions> <Modal> + <ModalHeader> + <ModalBody> + <ModalFooter> Flat composition, no compound components
<Input> / <Form.Input> <Input> from @os-legal/ui Props: label, size="lg", fullWidth, helperText
<Form.TextArea> <Textarea> from @os-legal/ui Props: autoResize, maxRows
<Card> / <Card.Content> Custom styled-components Build cards from plain <div> with styled-components
<Dropdown> <FilterTabs> (for tab-style filters) or custom select No 1:1 replacement; depends on use case
<Icon name="..."> Lucide React icons import { FileText, User, Plus } from "lucide-react"
<Label> <Chip> from @os-legal/ui Props: size="sm", variant="soft"\|"filled", color
<Loader> / <Dimmer> <Spinner> from @os-legal/ui or shimmer skeleton Use skeletons for content areas, Spinner for overlay
<Segment> Plain styled <div> Segments are just boxes — use FormSection pattern
<Popup> <Tooltip> or custom popover
<Menu> <NavBar> (for nav) or custom styled menu
<Checkbox> <Checkbox> (Semantic UI still used in Documents.tsx) This is a remaining migration item

The Four Integration Patterns

1. Direct Composition (cleanest)

Use OS-Legal components as-is with no wrapper styling. This is the target for simple, self-contained components.

// CategoryFilter.tsx — zero custom CSS
import { FilterTabs } from "@os-legal/ui";
import type { FilterTabItem } from "@os-legal/ui";

const items: FilterTabItem[] = data.map(({ node }) => ({
  id: node.id,
  label: node.name,
  count: node.corpusCount > 0 ? String(node.corpusCount) : undefined,
}));

<FilterTabs items={items} value={selectedCategory || "all"} onChange={handleChange} variant="pill" size="md" />

When to use: FilterTabs, SearchBox, CollectionCard/CollectionList, StatBlock/StatGrid, ActivityFeed, NavBar.

2. Wrapper Override (for sizing/layout adjustments)

Wrap an OS-Legal component in a styled <div> that targets its CSS classes (.oc-*) to adjust sizing or spacing. Use sparingly.

// StatsSection.tsx — override font size only
const StatsWrapper = styled.div`
  [class*="StatBlock"] > *:first-child,
  [data-testid="stat-value"] {
    font-size: 42px !important;
  }
`;

<StatsWrapper>
  <StatGrid columns={2}>
    <StatBlock value="142" label="Contributors" sublabel="from the community" />
  </StatGrid>
</StatsWrapper>

When to use: When the component's built-in sizing/spacing doesn't match the design, but behavior is correct.

Fragility note: The [class*="StatBlock"] partial-match selector is brittle — if @os-legal/ui changes its class naming scheme (e.g., CSS Modules → hashed names), it silently breaks with no compiler error. Prefer data-testid attributes or exposed CSS custom properties from the library when available. Use partial class selectors only as a fallback.

Use OS-Legal for atomic elements (Avatar, Chip, Button) inside a custom styled-component layout.

// CompactLeaderboard.tsx
import { Avatar, Chip } from "@os-legal/ui";

const LeaderboardRow = styled.div`
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: 0.75rem 0.5rem;
  /* ... */
`;

<LeaderboardRow>
  <RankChip $rank={rank}>{rank}</RankChip>
  <Avatar size="sm" fallback={getInitials(user.name)}
    style={{ backgroundColor: getAvatarColor(user.id), color: "white" }} />
  <Username>{user.name}</Username>
</LeaderboardRow>

When to use: Cards, list items, custom data displays — anywhere OS-Legal doesn't have a matching composite component.

4. Adapter Pattern (modal/complex components bridging old and new)

For complex OS-Legal components (Modal) that need to coexist with not-yet-migrated sub-components (Semantic UI dropdowns), use a wrapper that overrides both OS-Legal and Semantic UI CSS.

// CorpusModal.tsx — bridges OS-Legal Modal with Semantic UI dropdowns
const StyledModalWrapper = styled.div`
  .oc-modal-overlay { padding: var(--oc-spacing-md); }
  .oc-modal { width: 100%; max-width: 640px; }
  .oc-modal-body { background: var(--oc-bg-subtle, #f1f5f9); }

  /* Ensure Semantic UI dropdowns appear above modal content */
  .ui.dropdown .menu { z-index: 1000 !important; }
`;

When to use: When the component being refactored still depends on un-migrated child components that use Semantic UI.

Step-by-Step Migration Checklist

For each file being refactored:

  1. Read the existing component — understand its props, state, and Semantic UI dependencies.
  2. Replace imports:
  3. semantic-ui-react@os-legal/ui for available components (Button, Modal, Input, etc.)
  4. Icon name="..." → named import from lucide-react (e.g., <Icon name="plus" /><Plus size={16} />)
  5. Flatten compound componentsCard.Content, Modal.Actions etc. become plain styled divs.
  6. Remove !important when targeting Semantic UI classes (.ui.*) — if you need !important to fight Semantic UI defaults, you're still using Semantic UI. Acceptable uses: overriding OS-Legal internals (.oc-* classes) in a Wrapper Override or Adapter pattern, and z-index on .ui.dropdown .menu during the transition period (see Common Pitfalls).
  7. Use transient props — styled-components that receive boolean/enum props should use the $ prefix ($isSelected, $variant, $active) to prevent DOM warnings.
  8. Apply the canonical layoutPageContainerContentContainerSection blocks. Copy from DiscoveryLanding.tsx.
  9. Add loading skeletons — replace <Loader> / <Dimmer> with shimmer skeletons that match the shape of real content. Reuse the shimmer keyframe pattern from FeaturedCollections.tsx.
  10. Handle responsive breakpoints — the project defines three canonical breakpoints in frontend/src/assets/configurations/constants.ts: MOBILE_VIEW_BREAKPOINT (600px), TABLET_BREAKPOINT (768px), and DESKTOP_BREAKPOINT (769px). Use MOBILE_VIEW_BREAKPOINT for bottom-sheet/compact layouts (modals, filters) and TABLET_BREAKPOINT for standard page-level responsive adjustments.
  11. Use CSS variables — for anything inside modals or form sections, prefer var(--oc-spacing-*), var(--oc-radius-*), var(--oc-bg-*) over hardcoded values.
  12. Verify — run yarn lint, yarn build, and yarn test:ct --reporter=list before committing.

Common Pitfalls

  • Semantic UI CSS bleed: Global Semantic UI styles can interfere with OS-Legal components. If something looks wrong, check for .ui.* selectors applying unwanted styles. The fix is usually a scoped override in a styled wrapper.
  • Dropdown z-index in modals: Semantic UI dropdowns inside OS-Legal modals need z-index: 1000 !important on .ui.dropdown .menu.
  • iOS zoom on input focus: Inputs in mobile modals must be font-size: 16px minimum to prevent Safari auto-zoom.
  • count type mismatch on FilterTabs: The FilterTabItem.count field expects string | undefined, not number. Cast with String(count).
  • Don't mix icon systems: A component should use either lucide-react or Semantic UI icons, never both. Lucide is the target.

Remaining Migration Surface

To check the current count of files still importing Semantic UI:

grep -rl "semantic-ui-react" frontend/src --include="*.tsx" --include="*.ts" | wc -l

Priority targets:

Area Key Components Difficulty
Dropdown selectors (LabelSet, Embedder, Corpus filters) FilterToLabelSelector, FilterToCorpusSelector, LabelSetSelector Medium — need custom Select or Combobox
Analysis cards & trays AnalysisItem, AnalysesCards, AnalysisTraySelector Medium — Card → styled-components
Modals (edit, confirm, relation) EditLabelModal, RelationModal, ConfirmModal Low — swap to OS-Legal Modal
PDF annotation toolbar ActionBar, annotation controls High — complex interaction patterns
Admin panels GlobalAgentManagement, BadgeConfigurator Low-Medium — forms + cards
Context menus Menu + Menu.Item in Documents.tsx Low — replace with custom styled menu
Checkbox Checkbox from Semantic UI in Documents.tsx Low — swap or keep temporarily

Reference Files

These files represent the best examples of completed migration:

File Pattern Demonstrated
frontend/src/views/DiscoveryLanding.tsx Full page layout, section composition, data orchestration
frontend/src/components/landing/CategoryFilter.tsx Direct composition (zero custom CSS)
frontend/src/components/landing/StatsSection.tsx Wrapper override pattern
frontend/src/components/landing/FeaturedCollections.tsx Hybrid composition (CollectionCard + custom layout)
frontend/src/components/landing/CompactLeaderboard.tsx Hybrid composition (Avatar + Chip in custom list)
frontend/src/components/landing/NewHeroSection.tsx SearchBox + FilterTabs integration, styled hero layout
frontend/src/components/corpuses/CorpusModal.tsx Adapter pattern (Modal with CSS variable theming)
frontend/src/components/layout/NavMenu.tsx NavBar integration with custom login actions
frontend/src/views/Documents.tsx Full view migration with mixed old/new (context menu still Semantic)