Malevich

v1.0 architecture

v1.0 master diff-merge

Malevich v1.0 — Master Diff-Merge Document

Status: Architectural decisions finalized, implementation pending. Date: 2026-05-18 Source documents: ai-native-design-system.mdx (architectural spec), v0.1.0 implementation (10 components shipped).

This document is the single source of truth for the architectural transition from v0.1.0 to v1.0. It records every adopted decision, every rejected alternative, and every modification we made to the source spec.


Quick orientation


1. Architectural framing

1.1 What stays from the source spec

Adopted with minimal changes:

1.2 What we changed from the source spec

Source spec Malevich v1.0 Why
Card variants per content type (BlogCard, ProductCard, CartCard) One Card with named slots + data-pattern hints Composability > component multiplication for our open-source audience
React + Tailwind + CVA stack Vanilla CSS + optional JS enhancement Framework-agnostic, broader audience, CSS-first principle
Field-group only as composite Field-group + Label + Input + HelperText + Error as full family Form is its own subsystem
Overlays as presentation mode Overlays as their own layer Developer mental model preserved
Surface options: ghost, island, elevated, primary Surface options: flat, elevated, float "Primary" is a variant axis, not a surface axis. Three elevation tiers cover full range.
Category name "treatment" Category name "effect" More universally understood; treatment ambiguous
Charts as a Block Charts out of scope Data viz is its own domain; we delegate to Recharts/Visx/D3
Form-button as Button with type=submit Form-button as separate component Form-context buttons have distinct states (submitting, validation-error, success-confirmed); separation keeps generic Button aesthetically free
Abstract typography sizes (xl, l, m, s) Semantic typography roles (hero, statement, title, lead, regular, support) Names describe role, not size — better for AI, decoupled from values
Single icon library bundled Pluggable icon packs with canonical name registry Documentation focuses on icon selection principles; default pack = Phosphor
No mention of shader/WebGL @malevich/shaders as separate v2.0 package Progressive enhancement; CSS-first base stays lightweight
Static treatments only CSS effects (core) + interactive shaders (opt-in via @malevich/shaders) Both worlds supported

1.3 What we added beyond the source spec

1.4 What we deferred

1.5 What we explicitly rejected


2. Component layer architecture

2.1 Final v1.0 component inventory

Layer: Foundations (8)

Layer: Elements

Interactive (6):

Form (12):

Display (6):

Layer: Blocks (7)

Layer: Sections

Layout (4):

Ready (4):

Layer: Overlays (6)

Total v1.0: 53 components

2.2 Component layer rules

A component belongs to a layer based on what it does, not how complex it looks:

Layer Test
Foundation No slots, no logic, called from many components, no own semantics
Element Fixed structure, takes props, doesn't accept arbitrary content
Block Slots for elements, structural brick for sections
Section Slots for blocks, organizes a page region
Overlay Same component identity, but rendered in portal/top-layer

2.3 Tabs migration

Was: packages/components/src/blocks/tabs/ Becomes: packages/components/src/elements/interactive/tabs/

Rationale: Tabs has fixed structure (tab list + panels), doesn't accept arbitrary content, fits Element criteria better than Block.

2.4 Alert migration

Was: packages/components/src/blocks/alert/ Becomes: packages/components/src/elements/display/alert/

Rationale: Alert has fixed structure (icon + title + message + up to 2 buttons), doesn't accept arbitrary content. Inline alert is a Display Element. Overlay-style notifications are Toast/Notifications (separate components in Overlays layer).

2.5 Avatar migration

Was: packages/components/src/elements/avatar/ Becomes: packages/components/src/foundations/avatar/

Rationale: Avatar is called from many components (Card, Header, Comment lists), has no inherent semantics beyond display, no slots, no logic. Foundation case.


3. Token system architecture

3.1 Three-tier structure

┌─────────────────────────────────────────────────┐
│ Tier 3 — Component-specific                     │
│ Flat naming: button.background, card.padding    │
│ Sparse, used only when semantic insufficient    │
│ References semantic                             │
├─────────────────────────────────────────────────┤
│ Tier 2 — Semantic                               │
│ Nested in JSON: color.text.primary, etc.        │
│ Flat in CSS: --color-text-primary               │
│ Theme-aware (light/dark vary HERE)              │
│ Primary consumption layer                       │
│ References foundations                          │
├─────────────────────────────────────────────────┤
│ Tier 1 — Foundations (PRIVATE)                  │
│ Nested in JSON: color.neutral.900, space.4      │
│ Flat in CSS: --color-neutral-900                │
│ Raw values, narrow set, never read by component │
│ Theme-invariant                                 │
└─────────────────────────────────────────────────┘

3.2 Strict tier separation rule

Components NEVER reference foundations directly. Only semantic or component-specific tokens.

Enforced via @malevich/lint rule no-foundation-direct-reference.

Example:

/* ❌ FORBIDDEN — component references foundation directly */
.card {
  background: var(--color-neutral-50);
}

/* ✅ CORRECT — component references semantic */
.card {
  background: var(--color-surface-raised);
}

/* ✅ CORRECT — component references its own tier */
.card {
  background: var(--card-background);
}

3.3 Foundation tier inventory

color (palette only):
  neutral.0, neutral.50, neutral.100, neutral.200, neutral.400,
    neutral.600, neutral.700, neutral.800, neutral.900, neutral.950
  accent.subtle, accent.muted, accent.base, accent.hover, accent.pressed
  danger.subtle, danger.500
  warning.subtle, warning.500
  success.subtle, success.500
  info.subtle, info.500
  bone.base

space (4px-based):
  0, 1 (4px), 2 (8px), 3 (12px), 4 (16px), 6 (24px), 8 (32px),
  12 (48px), 16 (64px), 24 (96px)

radius:
  none (0), small (4px), field (6px), medium (8px), card (12px), 
  large (16px), full (9999px)

shadow:
  1 (0 1px 2px rgba(0,0,0,0.04))
  2 (0 4px 12px rgba(0,0,0,0.08))
  3 (0 12px 32px rgba(0,0,0,0.12))
  4 (0 24px 64px rgba(0,0,0,0.16))

font:
  family.display    (NAMU 1930)
  family.text       (NAMU Pro)
  family.mono       (JuliaMono)
  weight.regular (400), weight.medium (500), weight.bold (700)
  line-height.tight (1.15), line-height.regular (1.5), 
    line-height.loose (1.7)
  letter-spacing.tight (-0.02em), letter-spacing.regular (0),
    letter-spacing.wide (0.04em), letter-spacing.overline (0.1em)

duration:
  instant (0ms), fast (150ms), standard (250ms), 
  slow (400ms), deliberate (600ms)

easing:
  standard (cubic-bezier(0.2, 0, 0, 1))
  accelerate (cubic-bezier(0.3, 0, 1, 1))
  decelerate (cubic-bezier(0, 0, 0.2, 1))
  emphasized (cubic-bezier(0.2, 0, 0, 1.2))

3.4 Semantic tier inventory

color.text:
  primary, secondary, muted, disabled
  on-accent, on-inverse
  link, link.hover, link.visited
  code

color.surface:
  canvas, raised, float
  inverse, accent, accent.subtle
  bone, scrim

color.border:
  subtle, default, strong
  accent, focus
  on-inverse

color.status:
  danger, danger.subtle, danger.on
  warning, warning.subtle, warning.on
  success, success.subtle, success.on
  info, info.subtle, info.on

color.accent:
  default (= base), hover, pressed, subtle

color.focus-ring:
  default

typography sizes:
  display.hero       (clamp(2.5rem, 1.5rem + 5vw, 5rem))   — landing hero, max impact
  display.statement  (clamp(2rem, 1.5rem + 3vw, 3.5rem))   — section-level statement
  display.title      (clamp(1.75rem, 1.25rem + 2vw, 2.5rem)) — page title emphasis
  
  heading.title       (1.875rem / 30px)  — main heading (h1)
  heading.section     (1.5rem / 24px)    — section heading (h2)
  heading.subsection  (1.25rem / 20px)   — subsection heading (h3)
  heading.group       (1.125rem / 18px)  — group heading (h4)
  
  body.lead           (1.125rem / 18px)  — lead paragraph
  body.regular        (1rem / 16px)      — standard body
  body.support        (0.875rem / 14px)  — supporting text

  caption             (0.75rem / 12px)
  overline            (0.6875rem / 11px, uppercase, wide spacing)
  eyebrow             (0.75rem / 12px, mixed case)
  code                (0.875rem / 14px, monospace)
  
  mono.l              (1rem / 16px, monospace)
  mono.m              (0.875rem / 14px, monospace)
  mono.s              (0.75rem / 12px, monospace)

radius (semantic):
  control  → foundation.radius.field
  card     → foundation.radius.card
  surface  → foundation.radius.large
  pill     → foundation.radius.full

shadow (semantic):
  raised   → foundation.shadow.2
  overlay  → foundation.shadow.3
  popover  → foundation.shadow.4

motion (semantic):
  duration.enter, duration.exit, duration.emphasis, duration.deliberate
  easing.enter, easing.exit, easing.emphasis, easing.standard
  distance.subtle (4px), distance.moderate (12px), distance.expressive (24px)
  preset.none, preset.subtle, preset.standard, preset.expressive

size-control (semantic, for interactive elements):
  size-control.s, size-control.m, size-control.l, size-control.xl

border-width (semantic):
  border-width.hairline (0.5px), border-width.regular (1px),
  border-width.focus (2px), border-width.emphasis (3px)

3.5 Component-tier convention

Component tokens are flat in both JSON and CSS:

{
  "button": {
    "background": "{color.accent}",
    "text": "{color.text.on-accent}",
    "border": "transparent",
    "padding-block": "{space.3}",
    "padding-inline": "{space.4}",
    "radius": "{radius.control}"
  }
}
--button-background: var(--color-accent);
--button-text: var(--color-text-on-accent);
--button-padding-block: var(--space-3);
--button-padding-inline: var(--space-4);
--button-radius: var(--radius-control);

Variants override via cascade, not via multiplied tokens:

.button {
  background: var(--button-background);
  color: var(--button-text);
}

.button.-secondary {
  --button-background: var(--color-surface-raised);
  --button-text: var(--color-text-primary);
}

.button.-danger {
  --button-background: var(--color-status-danger);
  --button-text: var(--color-status-danger-on);
}

3.6 Token-to-component mapping (per-component documentation)

Each component's AGENTS.md documents which tokens it consumes. Example for Card:

## Tokens consumed

### From semantic tier
- color.surface.raised — default background
- color.surface.float — for surface=float variant
- color.border.subtle — for default border
- radius.card — corner radius
- shadow.raised — for surface=elevated
- shadow.overlay — for surface=float

### From component tier (Card-specific)
- card.padding-block — vertical inner padding
- card.padding-inline — horizontal inner padding
- card.gap — gap between slots
- card.background — final background (resolved from surface)
- card.border — final border (resolved from surface)
- card.shadow — final shadow (resolved from surface)

### Typography classes used in slots
- card__header text typically uses: eyebrow, body.support
- card__body text typically uses: heading.subsection, body.regular
- card__footer text typically uses: body.support, caption

This mapping is mandatory in every component AGENTS.md for v1.0.


4. Modifier system architecture

4.1 Seven modifier categories

Modifiers system
├── background  — what's behind the surface (CSS static)
├── surface     — how the surface sits (flat / elevated / float)
├── effect      — decorative overlays (grain / glow / noise / ring / blur)
├── shader      — interactive WebGL backgrounds (opt-in via @malevich/shaders)
├── rhythm      — internal breathing (compact / regular / spacious)
├── motion      — animation character (none / subtle / standard / expressive)
└── behavior    — runtime behaviors (sticky / dismissible / collapsible / copyable)

4.2 Modifier syntax

Syntactic split signals architectural axis to readers and AI agents.

4.3 Namespace separation rule

Each modifier owns a variable namespace:

Components compose final styles from multiple namespaces:

.card {
  box-shadow: 
    var(--card-surface-shadow, none),
    var(--card-effect-shadow, none);
}

This makes modifiers stack without conflict.

4.4 Selective inheritance

4.5 Applicability matrix

Single source of truth: modifiers/applicability.json

{
  "components": {
    "card": {
      "surface": ["flat", "elevated", "float"],
      "effect": ["grain", "glow", "noise", "ring", "blur"],
      "background": ["solid", "gradient", "image"],
      "shader": "all",
      "rhythm": ["compact", "regular", "spacious"],
      "motion": "all",
      "behavior": []
    },
    "button": {
      "surface": ["flat"],
      "effect": ["glow", "ring"],
      "background": [],
      "shader": [],
      "rhythm": ["compact", "regular"],
      "motion": "all",
      "behavior": []
    }
  }
}

Build pipeline generates from this:

4.6 Custom modifier extension

Public extension API documented in /playbook/extending-modifiers. No plugin system — just data-attribute + CSS convention:

<article class="card" data-corporate="enterprise">
[data-corporate="enterprise"] {
  --card-border: 2px solid var(--color-corporate-accent);
  --card-radius: 0;
}

4.7 Behavior modifier auto-init

init() from @malevich/components runtime discovers and wires up behavior modifiers automatically:

import { init } from '@malevich/components';
init();
// Now [data-sticky="true"] elements have sticky behavior
// [data-dismissible="true"] have dismiss handlers, etc.

4.8 Built-in behaviors per component

Some components have specific behavior modifiers enabled by default, because the behavior is core to the component's identity:

For all other components, behavior modifiers are opt-in via explicit data-{behavior}="true" declaration. This pattern lets common cases work automatically while keeping the API explicit for edge cases.

The copy behavior implementation:


5. Presentation modes architecture

5.1 Two modes

5.2 Why overlays are their own layer (not a mode)

Conceptually, overlays are a presentation mode applied to existing components. Practically, developers reach for "Dialog" or "Sheet" as a category, not as a modifier. We organize file structure and documentation as a discoverable layer; we preserve the mode concept in our architectural discussions and AGENTS.md.

5.3 Overlay technical implementation

5.4 Focus management defaults

Overlay type Focus behavior
Modal (Dialog, Sheet) Focus trapped, initial focus configurable, restored on close
Popover Focus moves into overlay, tab cycles within, restored on close
Tooltip Focus stays on trigger, tooltip non-focusable
Toast/Notifications Focus untouched, content announced via aria-live

Overrides available via data-attributes (data-focus-trap, data-focus-restore).


6. Responsive architecture

6.1 Three-tier responsive ownership

Layer Responsive tool
Layout sections Viewport queries (@media) — own breakpoints
Blocks, Elements Container queries (@container) — adapt to slot
Foundations Intrinsic (clamp(), min(), max(), percentages) — natural scaling

This makes responsive logic predictable per layer. An agent writing Card CSS never writes a media query. An agent writing Layout never writes a container query.

6.2 Display typography fluid scaling

Display sizes (hero, statement, title) use clamp():

.display-hero {
  font-size: clamp(2.5rem, 1.5rem + 5vw, 5rem);
}

Heading, body, caption, code — fixed sizes. Hierarchy preserved across viewports.


7. Theming architecture

7.1 Theme variants in v1.0

Detection and persistence: localStorage + matchMedia + pre-paint inline script (no FOUC).

7.2 Theme tier rule

Themes change semantic tier only. Foundations stay theme-invariant. Brand red is brand red whether on light or dark canvas.

:root {
  /* foundations — same in all themes */
  --color-neutral-0: #ffffff;
  --color-neutral-900: #171717;
  
  /* semantic — light theme defaults */
  --color-surface-canvas: var(--color-neutral-0);
  --color-text-primary: var(--color-neutral-900);
}

[data-theme="dark"] {
  /* semantic only — foundations untouched */
  --color-surface-canvas: var(--color-neutral-900);
  --color-text-primary: var(--color-neutral-0);
}

7.3 Theme roadmap


8. Iconography architecture

8.1 Pluggable icon packs

Malevich does not ship its own icon library. It ships:

8.2 Implementation

CSS mask-image for icon rendering. currentColor inheritance via mask background.

<span class="icon" data-icon="check"></span>
.icon {
  display: inline-block;
  inline-size: var(--icon-size, 1em);
  block-size: var(--icon-size, 1em);
  background: currentColor;
  mask-repeat: no-repeat;
  mask-position: center;
  mask-size: contain;
}

[data-icon="check"] { mask-image: url("/icons/check.svg"); }

One SVG file per icon, served over HTTP/2 + CDN for parallel loading.

8.3 Pack switching

Global override via root data-attribute:

<html data-icon-pack="lucide">

Pack adapters map canonical names → pack-specific names internally.

8.4 Documentation focus

The Icon documentation page focuses on principles of icon selection (when to use delete vs trash vs x), not on an icon catalog. AGENTS.md per category guides AI agents on icon-to-intent mapping.


9. Effect (formerly "treatment") category

Renamed from "treatment" to "effect" for universal clarity.

9.1 v1.0 effect values

All implemented as CSS-only overlays. No JS, no WebGL.

9.2 Shader category (separate)

Interactive WebGL backgrounds (mesh-gradient, fluid, particle-flow) ship in @malevich/shaders (v2.0). Without the package, data-shader is ignored. Explicit fallback via data-background="gradient" is recommended pattern.


10. Brand vs architecture separation

10.1 What Malevich brand carries

10.2 What the architecture is

10.3 Why separation matters

This split lets the brand evolve (new palette, refreshed voice, expanded cultural references) without rewriting the system. A future "Malevich Industrial" or "Malevich Editorial" theme pack changes only semantic tokens and typography classes — the architecture stays untouched.


11. Implementation enforcement

11.1 Lint rules (@malevich/lint)

Rule What it enforces
no-raw-values (existing) No hardcoded colors, sizes, durations in CSS
bem-short-modifier (existing) Variants use .-modifier syntax
custom-element-prefix (existing) Reserved m- prefix
no-foundation-direct-reference (new) Components can't read foundation tokens directly
modifier-namespace-check (new) Modifiers write to their own variable namespace only
tier-respect (v1.1) Token reference order: component → semantic → foundation

11.2 Type generation

@malevich/core build pipeline generates:

11.3 Documentation discipline

Every component MUST ship with:


12. Migration path from v0.1.0

See 04-migration-plan.md for step-by-step execution.

Summary: