Tokens Architecture & Multi-Platform Pipeline
Arsitektur tokenisasi berjenjang 3-Tier yang menjamin satu sumber kebenaran (Single Source of Truth) untuk Web (Nuxt 4 / Tailwind CSS v4) dan Mobile (React Native / StyleSheet).
1. Arsitektur 3-Tier
Tier 1: Global Tokens
Nilai mentah primitif tanpa konteks semantik spesifik (misal: blue-500: #2F6FEB, space-4: 16px, radius-md: 10px).
export const globalTokens = {
colors: { blue500: '#2F6FEB', blue400: '#4C86F0', blue600: '#2557C9' },
radii: { xs: 6, sm: 8, md: 10, lg: 14, xl: 18 },
spacing: { 1: 4, 2: 8, 3: 12, 4: 16, 6: 24, 8: 32 }
}Tier 2: Semantic / Alias Tokens
Token yang diberi makna fungsional untuk memudahkan konsistensi tema gelap/terang dan aksesibilitas.
export const semanticTokens = {
color: {
primary: globalTokens.colors.blue500,
surface: '#FFFFFF',
textMain: '#16161A',
textMuted: '#6B7280'
}
}Tier 3: Component Tokens
Token spesifik yang terikat pada varian komponen tertentu (misal: button-primary-bg, card-tray-radius).
export const buttonTokens = {
primary: {
bg: semanticTokens.color.primary,
radius: globalTokens.radii.xs, // 6px
heightMd: 44
}
}2. Tabel Referensi Token
Cari nilai satu token untuk Web (CSS Variable/Tailwind) atau Mobile (React Native).
| Nama Token | CSS Variable / Tailwind | Nilai |
|---|---|---|
| surface | --color-surface | #ffffff |
| surface-alt | --color-surface-alt | #f7f8fa |
| surface-sunken | --color-surface-sunken | #f4f5f8 |
| skeleton | --color-skeleton | #e8eaef |
| ink | --color-ink | #16161a |
| muted | --color-muted | #6b7280 |
| faint | --color-faint | #9aa2b1 |
| placeholder | --color-placeholder | #98a2b3 |
| line | --color-line | #e5e7eb |
| hairline | --color-hairline | rgba(148, 163, 184, 0.18) |
| hairline-strong | --color-hairline-strong | rgba(148, 163, 184, 0.28) |
| primary | --color-primary | #2f6feb |
| primary-light | --color-primary-light | #4c86f0 |
| primary-dark | --color-primary-dark | #2557c9 |
| primary-soft | --color-primary-soft | #eaf1ff |
| star | --color-star | #f59e0b |
| success | --color-success | #22c55e |
| success-ink | --color-success-ink | #15803d |
| success-soft | --color-success-soft | #e8f8ee |
| danger | --color-danger | #c42132 |
| danger-ink | --color-danger-ink | #7f151f |
| danger-soft | --color-danger-soft | #faebec |
| warning | --color-warning | #d8730e |
| warning-ink | --color-warning-ink | #a3570a |
| warning-soft | --color-warning-soft | #faf2ea |
| radius-xs | --radius-xs | 6px |
| radius-sm | --radius-sm | 8px |
| radius-md | --radius-md | 10px |
| radius-lg | --radius-lg | 14px |
| radius-xl | --radius-xl | 18px |
| radius-pill | --radius-pill | 999px |
3. Ekspor Token Multi-Platform
Ketiga file di bawah ini bukan salinan manual — semuanya digenerate langsung dari tokens/tokens.json lewat npm run tokens:build, jadi tidak bisa lagi berbeda dari kode sungguhan.
React Native TypeScript Tokens
// AUTO-GENERATED by scripts/build-tokens.mjs — do not edit by hand.
// Source of truth: tokens/tokens.json. Run `npm run tokens:build` after editing it.
// container.* is web-layout-only and intentionally omitted here.
export const tokens = {
"color": {
"surface": "#ffffff",
"surfaceAlt": "#f7f8fa",
"surfaceSunken": "#f4f5f8",
"skeleton": "#e8eaef",
"ink": "#16161a",
"muted": "#6b7280",
"faint": "#9aa2b1",
"placeholder": "#98a2b3",
"line": "#e5e7eb",
"hairline": "rgba(148, 163, 184, 0.18)",
"hairlineStrong": "rgba(148, 163, 184, 0.28)",
"primary": "#2f6feb",
"primaryLight": "#4c86f0",
"primaryDark": "#2557c9",
"primarySoft": "#eaf1ff",
"star": "#f59e0b",
"success": "#22c55e",
"successInk": "#15803d",
"successSoft": "#e8f8ee",
"danger": "#c42132",
"dangerInk": "#7f151f",
"dangerSoft": "#faebec",
"warning": "#d8730e",
"warningInk": "#a3570a",
"warningSoft": "#faf2ea"
},
"font": {
"sans": "Plus Jakarta Sans",
"serif": "Playfair Display",
"mono": "JetBrains Mono"
},
"radius": {
"xs": 6,
"sm": 8,
"md": 10,
"lg": 14,
"xl": 18,
"pill": 999
},
"shadow": {
"xs": {
"shadowColor": "#0f172a",
"shadowOffset": {
"width": 0,
"height": 1
},
"shadowOpacity": 0.04,
"shadowRadius": 2,
"elevation": 1
},
"lift": {
"shadowColor": "#0f172a",
"shadowOffset": {
"width": 0,
"height": 12
},
"shadowOpacity": 0.07,
"shadowRadius": 26,
"elevation": 9
},
"float": {
"shadowColor": "#0f172a",
"shadowOffset": {
"width": 0,
"height": 14
},
"shadowOpacity": 0.07,
"shadowRadius": 34,
"elevation": 11
},
"brand": {
"shadowColor": "#2f6feb",
"shadowOffset": {
"width": 0,
"height": 26
},
"shadowOpacity": 0.22,
"shadowRadius": 54,
"elevation": 18
}
},
"ease": {
"smooth": [
0.22,
1,
0.36,
1
]
}
} as const;Tailwind CSS v4 Theme Block (`@theme`)
/* AUTO-GENERATED by scripts/build-tokens.mjs — do not edit by hand.
Source of truth: tokens/tokens.json. Run `npm run tokens:build` after editing it. */
@theme {
--color-surface: #ffffff;
--color-surface-alt: #f7f8fa;
--color-surface-sunken: #f4f5f8;
--color-skeleton: #e8eaef;
--color-ink: #16161a;
--color-muted: #6b7280;
--color-faint: #9aa2b1;
--color-placeholder: #98a2b3;
--color-line: #e5e7eb;
--color-hairline: rgba(148, 163, 184, 0.18);
--color-hairline-strong: rgba(148, 163, 184, 0.28);
--color-primary: #2f6feb;
--color-primary-light: #4c86f0;
--color-primary-dark: #2557c9;
--color-primary-soft: #eaf1ff;
--color-star: #f59e0b;
--color-success: #22c55e;
--color-success-ink: #15803d;
--color-success-soft: #e8f8ee;
--color-danger: #c42132;
--color-danger-ink: #7f151f;
--color-danger-soft: #faebec;
--color-warning: #d8730e;
--color-warning-ink: #a3570a;
--color-warning-soft: #faf2ea;
--font-sans: 'Plus Jakarta Sans', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
--font-serif: 'Playfair Display', Georgia, serif;
--font-mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
--radius-xs: 6px;
--radius-sm: 8px;
--radius-md: 10px;
--radius-lg: 14px;
--radius-xl: 18px;
--radius-pill: 999px;
--shadow-xs: 0 1px 2px rgba(15, 23, 42, 0.04);
--shadow-lift: 0 12px 26px rgba(15, 23, 42, 0.07);
--shadow-float: 0 14px 34px rgba(15, 23, 42, 0.07);
--shadow-brand: 0 26px 54px rgba(47, 111, 235, 0.22);
--ease-smooth: cubic-bezier(0.22, 1, 0.36, 1);
--container-page: 1720px;
}Universal JSON Tokens (Figma / Design Tokens Community Group format)
{
"color": {
"surface": {
"$type": "color",
"$value": "#ffffff"
},
"surface-alt": {
"$type": "color",
"$value": "#f7f8fa"
},
"surface-sunken": {
"$type": "color",
"$value": "#f4f5f8"
},
"skeleton": {
"$type": "color",
"$value": "#e8eaef"
},
"ink": {
"$type": "color",
"$value": "#16161a"
},
"muted": {
"$type": "color",
"$value": "#6b7280"
},
"faint": {
"$type": "color",
"$value": "#9aa2b1",
"$description": "2.57:1 on white — fails WCAG AA for text. Decorative/large-UI/non-text use only (e.g. large icons, chart strokes); never body/label/caption text — use color.muted for any real readable text instead."
},
"placeholder": {
"$type": "color",
"$value": "#98a2b3",
"$description": "2.58:1 on white — fails WCAG AA for text. Restricted to the native ::placeholder pseudo-element on editable inputs; never for standalone visible UI copy — use color.muted there instead."
},
"line": {
"$type": "color",
"$value": "#e5e7eb"
},
"hairline": {
"$type": "color",
"$value": "rgba(148, 163, 184, 0.18)"
},
"hairline-strong": {
"$type": "color",
"$value": "rgba(148, 163, 184, 0.28)"
},
"primary": {
"$type": "color",
"$value": "#2f6feb"
},
"primary-light": {
"$type": "color",
"$value": "#4c86f0"
},
"primary-dark": {
"$type": "color",
"$value": "#2557c9"
},
"primary-soft": {
"$type": "color",
"$value": "#eaf1ff"
},
"star": {
"$type": "color",
"$value": "#f59e0b"
},
"success": {
"$type": "color",
"$value": "#22c55e"
},
"success-ink": {
"$type": "color",
"$value": "#15803d"
},
"success-soft": {
"$type": "color",
"$value": "#e8f8ee"
},
"danger": {
"$type": "color",
"$value": "#c42132",
"$description": "Icon/border/dot/solid-button-background use only, paired with white text. Never as body text on white — use color.danger-ink for that."
},
"danger-ink": {
"$type": "color",
"$value": "#7f151f",
"$description": "The only Danger tone approved for text-on-light-background use."
},
"danger-soft": {
"$type": "color",
"$value": "#faebec"
},
"warning": {
"$type": "color",
"$value": "#d8730e",
"$description": "Icon/border/dot/solid-button-background use only. Never as body text on white — use color.warning-ink for that."
},
"warning-ink": {
"$type": "color",
"$value": "#a3570a",
"$description": "The only Warning tone approved for text-on-light-background use."
},
"warning-soft": {
"$type": "color",
"$value": "#faf2ea"
}
},
"font": {
"sans": {
"$type": "fontFamily",
"$value": [
"Plus Jakarta Sans",
"system-ui",
"-apple-system",
"BlinkMacSystemFont",
"Segoe UI",
"Roboto",
"sans-serif"
]
},
"serif": {
"$type": "fontFamily",
"$value": [
"Playfair Display",
"Georgia",
"serif"
]
},
"mono": {
"$type": "fontFamily",
"$value": [
"JetBrains Mono",
"ui-monospace",
"SFMono-Regular",
"Menlo",
"Monaco",
"Consolas",
"monospace"
]
}
},
"radius": {
"xs": {
"$type": "dimension",
"$value": "6px"
},
"sm": {
"$type": "dimension",
"$value": "8px"
},
"md": {
"$type": "dimension",
"$value": "10px"
},
"lg": {
"$type": "dimension",
"$value": "14px"
},
"xl": {
"$type": "dimension",
"$value": "18px"
},
"pill": {
"$type": "dimension",
"$value": "999px"
}
},
"shadow": {
"xs": {
"$type": "shadow",
"$value": {
"offsetX": "0px",
"offsetY": "1px",
"blur": "2px",
"spread": "0px",
"color": "rgba(15, 23, 42, 0.04)"
}
},
"lift": {
"$type": "shadow",
"$value": {
"offsetX": "0px",
"offsetY": "12px",
"blur": "26px",
"spread": "0px",
"color": "rgba(15, 23, 42, 0.07)"
}
},
"float": {
"$type": "shadow",
"$value": {
"offsetX": "0px",
"offsetY": "14px",
"blur": "34px",
"spread": "0px",
"color": "rgba(15, 23, 42, 0.07)"
}
},
"brand": {
"$type": "shadow",
"$value": {
"offsetX": "0px",
"offsetY": "26px",
"blur": "54px",
"spread": "0px",
"color": "rgba(47, 111, 235, 0.22)"
}
}
},
"ease": {
"smooth": {
"$type": "cubicBezier",
"$value": [
0.22,
1,
0.36,
1
]
}
},
"container": {
"page": {
"$type": "dimension",
"$value": "1720px",
"$description": "Web layout only — omitted from the React Native export."
}
}
}