Multi-axis Token Bundle
What merchants see as a “color scheme” is internally a Bundle — a multi-axis token package composed of 5 independent axes:
axes = { color, icon, typography, shape, motion }Each axis is independently optional and independently consumed — when a merchant picks a bundle, it might affect only color, or color + icon together.
v1 launch boundary
Section titled “v1 launch boundary”v1 ships with only color + icon axes implemented. typography / shape / motion are spec-reserved slots with no runtime projection.
This boundary matters: the more complete the spec looks, the easier it is to assume all 5 axes are live. The architecture is open-ended — adding a new axis is purely additive (add
apply-axis/<name>.jsprojection + bundle field, zero existing code changes), but adding any axis requires ≥3 real merchant use cases.
| Axis | v1 status | Trigger to ship |
|---|---|---|
color | ✅ Implemented | — |
icon | ✅ Implemented | — |
typography | 🔴 Reserved | Merchant brand-font import demand |
shape | 🔴 Reserved | Theme shape derivation (radius / shadow) maturity |
motion | 🔴 Reserved | Branded animation requests |
3-layer architecture
Section titled “3-layer architecture”Layer 1: Bundle (editing-time) Sources: 5 internal preset JSONs / theme-derived Storage: packages/widget-presets/src/styles/*.json + widgets.editor_state_json.style.bundle (per-widget snapshot) Evolution: free to evolve (merchant can't see, breaking changes only affect internal code) ↓Layer 2: Widget Config (persistence) Form: resolved flat values (hex strings, Fill objects, iconStyle enum) Storage: D1 widgets.config_json + Shopify metafield + __TTB_BLOCKS__ Evolution: strict additive-only (see 7 contracts below) ↓Layer 3: Renderer (runtime) Code: packages/widget-ui storefront engine Rules: reads only Layer 2; unknown-safe (every switch needs a default)Bundle ↔ Widget Config is a one-way projection — never reverse-derive a Bundle from Widget Config. Layer 1’s freedom of evolution depends on Layer 2’s stability; the projection pipeline is the firewall.
7 forward-compat contracts
Section titled “7 forward-compat contracts”Layer 2 + Layer 3 hard rules — every new feature PR review checks against these:
| # | Contract | Meaning |
|---|---|---|
| 1 | schemaVersion strictly additive | New version: new fields must be optional with sensible defaults; never remove fields, change types, or promote optional → required |
| 2 | Polymorphic values use kind | Fill (solid/gradient/tricolor/pattern), icon source (phosphor/brand/custom), animation kind, font kind — new kind values are additive |
| 3 | New fields optional + default fallback | All new fields must have sensible defaults; renderer never reads undefined and crashes |
| 4 | Renderer unknown-safe | Unknown component type → console.warn + skip; unknown kind → switch default → fallback to v1 known; unknown field → ignore |
| 5 | extensions: {} experimental bag | Every component reserves extensions: {} field; v1 renderer ignores, v2/v3 use to test new features |
| 6 | Bundle → Config one-way | Never reverse-derive bundle from persisted config |
| 7 | Schema evolution audit | widget-schema-evolution-audit.mjs snapshots v1 field tree; any PR change must be additive, else CI fails |
Two bundle sources
Section titled “Two bundle sources”| Source | Count | Behavior |
|---|---|---|
| Internal presets | 5 JSON files (packages/widget-presets/src/styles/) | Merchant picks from a dropdown in wizard / Design tab |
| Theme-derived | 2-4 per shop | See Theme-derived Bundle |
Editor relationship
Section titled “Editor relationship”Switching “color scheme” in the Design tab is actually switching bundles:
- Pick bundle →
applyStyle(widget, bundle)projection - Projection writes to widget config’s flat fields (fill / iconStyle / etc.)
- Merchant-overridden fields are snapshot-locked and not overwritten
Related concepts
Section titled “Related concepts”- Bundle is the Layer 1 editing-time form of the Data shape; projected into Layer 2
- Theme-derived bundles are described in Theme-derived Bundle
- Bundle projection timing and editor interaction in Design tab