@utopia-studio-design/design-system-cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 The Utopia Studio
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,46 @@
1
+ # @utopia-studio-design/design-system-cli
2
+
3
+ CLI for installing, inspecting, and validating the Utopia Studio design system. The CLI doubles as an AI-agent entrypoint: it creates agent docs, prints token data, installs component templates, and checks whether component manifests include the props, anatomy, states, slots, examples, and agent contract fields required for safe UI generation.
4
+
5
+ ```sh
6
+ npx @utopia-studio-design/design-system-cli init
7
+ npx @utopia-studio-design/design-system-cli add button
8
+ npx @utopia-studio-design/design-system-cli tokens
9
+ npx @utopia-studio-design/design-system-cli agent-docs
10
+ npx @utopia-studio-design/design-system-cli doctor
11
+ ```
12
+
13
+ The binary name is `utopia-ds`.
14
+
15
+ ## Doctor Checks
16
+
17
+ `utopia-ds doctor` verifies dependencies, CSS tokens, square-radius compliance, component manifest completeness, component `agentContract` completeness, theme manifest completeness, source-example docs, and the AI entrypoint:
18
+
19
+ AI docs checks:
20
+
21
+ - `docs/design-system/examples.md`
22
+ - `public/llms.txt` references `docs/design-system/examples.md`
23
+
24
+ Component checks:
25
+
26
+ - `props`
27
+ - `anatomy`
28
+ - `states`
29
+ - `slots`
30
+ - `examples`
31
+ - `aiRules`
32
+
33
+ Agent contract checks:
34
+
35
+ - `useWhen`
36
+ - `avoidWhen`
37
+ - `fallbackToShadcn`
38
+ - `requiredTokens`
39
+ - `neverInvent`
40
+ - `validationChecklist`
41
+
42
+ Theme checks:
43
+
44
+ - `defaultTheme`
45
+ - `semanticRoles`
46
+ - default theme values for every semantic role
@@ -0,0 +1,353 @@
1
+ #!/usr/bin/env node
2
+ import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs'
3
+ import { dirname, join, resolve } from 'node:path'
4
+ import { fileURLToPath } from 'node:url'
5
+
6
+ const cliRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..')
7
+ const templateRoot = join(cliRoot, 'templates')
8
+ const args = process.argv.slice(2)
9
+
10
+ function argValue(name, fallback) {
11
+ const index = args.indexOf(name)
12
+ return index >= 0 && args[index + 1] ? args[index + 1] : fallback
13
+ }
14
+
15
+ const cwd = resolve(argValue('--cwd', process.cwd()))
16
+ const command = args.find((arg) => !arg.startsWith('--')) ?? '--help'
17
+
18
+ function ensureDir(path) {
19
+ mkdirSync(path, { recursive: true })
20
+ }
21
+
22
+ function copyTemplate(from, to) {
23
+ ensureDir(dirname(to))
24
+ copyFileSync(join(templateRoot, from), to)
25
+ console.log(`created ${to}`)
26
+ }
27
+
28
+ function readTemplate(path) {
29
+ return readFileSync(join(templateRoot, path), 'utf8')
30
+ }
31
+
32
+ function help() {
33
+ console.log(`Utopia Design System CLI
34
+
35
+ Usage:
36
+ utopia-ds init [--cwd path]
37
+ utopia-ds add <component> [--cwd path]
38
+ utopia-ds tokens
39
+ utopia-ds agent-docs [--cwd path]
40
+ utopia-ds doctor [--cwd path]
41
+
42
+ Components:
43
+ button, icon-button, button-group, card, badge, tooltip, page-hero,
44
+ cta-section, brand-action, topbar
45
+
46
+ Agent contract:
47
+ Components should define useWhen, avoidWhen, fallbackToShadcn,
48
+ requiredTokens, neverInvent, and validationChecklist.
49
+
50
+ Themes:
51
+ Utopia is the default/reference theme. Themeable apps/templates
52
+ should map all visual decisions through semantic roles.
53
+ `)
54
+ }
55
+
56
+ function init() {
57
+ copyTemplate('styles/tokens.css', join(cwd, 'src/styles/utopia-tokens.css'))
58
+ copyTemplate('styles/styles.css', join(cwd, 'src/styles/utopia-styles.css'))
59
+ copyTemplate('lib/utils.ts', join(cwd, 'src/lib/utils.ts'))
60
+ console.log('Add `import "./styles/utopia-styles.css"` to your app entrypoint.')
61
+ }
62
+
63
+ function add() {
64
+ const component = args[1]
65
+ const componentMap = {
66
+ button: 'button.tsx',
67
+ 'icon-button': 'icon-button.tsx',
68
+ 'button-group': 'button-group.tsx',
69
+ card: 'card.tsx',
70
+ badge: 'badge.tsx',
71
+ tooltip: 'tooltip.tsx',
72
+ 'page-hero': 'page-hero.tsx',
73
+ 'cta-section': 'cta-section.tsx',
74
+ 'brand-action': 'brand-action.tsx',
75
+ topbar: 'topbar.tsx',
76
+ }
77
+ const componentDependencies = {
78
+ 'icon-button': ['button', 'tooltip'],
79
+ 'button-group': ['button'],
80
+ 'page-hero': ['brand-action'],
81
+ 'cta-section': ['brand-action'],
82
+ topbar: ['button'],
83
+ }
84
+
85
+ if (!component || !componentMap[component]) {
86
+ throw new Error(`Unknown component "${component ?? ''}". Run utopia-ds --help for available components.`)
87
+ }
88
+
89
+ const componentsToCopy = new Set()
90
+ const queue = [component]
91
+
92
+ while (queue.length) {
93
+ const next = queue.shift()
94
+
95
+ for (const dependency of componentDependencies[next] ?? []) {
96
+ if (!componentsToCopy.has(dependency)) queue.push(dependency)
97
+ }
98
+
99
+ componentsToCopy.add(next)
100
+ }
101
+
102
+ for (const componentName of componentsToCopy) {
103
+ copyTemplate(`components/${componentMap[componentName]}`, join(cwd, `src/components/utopia/${componentMap[componentName]}`))
104
+ }
105
+
106
+ copyTemplate('lib/utils.ts', join(cwd, 'src/lib/utils.ts'))
107
+ }
108
+
109
+ function tokens() {
110
+ process.stdout.write(readTemplate('tokens.json'))
111
+ }
112
+
113
+ function agentDocs() {
114
+ copyTemplate('agent-docs/index.md', join(cwd, 'docs/design-system/agent-docs/index.md'))
115
+ copyTemplate('agent-docs/examples.md', join(cwd, 'docs/design-system/examples.md'))
116
+ copyTemplate('agent-docs/llms.txt', join(cwd, 'public/llms.txt'))
117
+ }
118
+
119
+ function doctor() {
120
+ const packageJsonPath = join(cwd, 'package.json')
121
+ const packageJson = existsSync(packageJsonPath) ? JSON.parse(readFileSync(packageJsonPath, 'utf8')) : {}
122
+ const deps = { ...(packageJson.dependencies ?? {}), ...(packageJson.devDependencies ?? {}) }
123
+ const warnings = []
124
+
125
+ for (const dep of ['react', 'class-variance-authority', 'radix-ui']) {
126
+ if (!deps[dep]) warnings.push(`Missing dependency: ${dep}`)
127
+ }
128
+
129
+ const candidateCssFiles = [
130
+ join(cwd, 'src/index.css'),
131
+ join(cwd, 'src/app/globals.css'),
132
+ join(cwd, 'src/styles/utopia-tokens.css'),
133
+ ]
134
+ const css = candidateCssFiles.filter(existsSync).map((file) => readFileSync(file, 'utf8')).join('\n')
135
+ for (const token of ['--background', '--primary', '--font-sans', '--radius']) {
136
+ if (!css.includes(token)) warnings.push(`Missing CSS token: ${token}`)
137
+ }
138
+
139
+ if (css.includes('border-radius: 16px') || css.includes('--radius: 16px')) {
140
+ warnings.push('Found 16px radius. Utopia DS expects square geometry.')
141
+ }
142
+
143
+ const manifestCandidates = [
144
+ join(cwd, 'packages/design-system/src/manifests/components.json'),
145
+ join(cwd, 'src/manifests/components.json'),
146
+ join(cwd, 'node_modules/@utopia-studio-design/design-system/components.json'),
147
+ ]
148
+ const manifestPath = manifestCandidates.find(existsSync)
149
+ const requiredAgentContractFields = [
150
+ 'useWhen',
151
+ 'avoidWhen',
152
+ 'fallbackToShadcn',
153
+ 'requiredTokens',
154
+ 'neverInvent',
155
+ 'validationChecklist',
156
+ ]
157
+
158
+ if (!manifestPath) {
159
+ warnings.push('Missing components manifest with agent contracts.')
160
+ } else {
161
+ const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'))
162
+ const components = Array.isArray(manifest.components) ? manifest.components : []
163
+
164
+ if (!components.length) {
165
+ warnings.push('Components manifest has no components.')
166
+ }
167
+
168
+ for (const component of components) {
169
+ for (const field of ['props', 'anatomy', 'states', 'slots', 'examples', 'aiRules']) {
170
+ if (!Array.isArray(component[field]) || component[field].length === 0) {
171
+ warnings.push(`Incomplete component ${field}: ${component.name ?? 'unknown component'}`)
172
+ }
173
+ }
174
+
175
+ const contract = component.agentContract
176
+ if (!contract) {
177
+ warnings.push(`Missing agentContract: ${component.name ?? 'unknown component'}`)
178
+ continue
179
+ }
180
+
181
+ for (const field of requiredAgentContractFields) {
182
+ if (!Array.isArray(contract[field]) || contract[field].length === 0) {
183
+ warnings.push(`Incomplete agentContract.${field}: ${component.name ?? 'unknown component'}`)
184
+ }
185
+ }
186
+ }
187
+ }
188
+
189
+ const themeManifestCandidates = [
190
+ join(cwd, 'packages/design-system/src/manifests/themes.json'),
191
+ join(cwd, 'src/manifests/themes.json'),
192
+ join(cwd, 'node_modules/@utopia-studio-design/design-system/themes.json'),
193
+ ]
194
+ const themeManifestPath = themeManifestCandidates.find(existsSync)
195
+ const requiredThemeRoles = [
196
+ 'background',
197
+ 'foreground',
198
+ 'card',
199
+ 'cardForeground',
200
+ 'primary',
201
+ 'primaryForeground',
202
+ 'secondary',
203
+ 'secondaryForeground',
204
+ 'muted',
205
+ 'mutedForeground',
206
+ 'border',
207
+ 'input',
208
+ 'ring',
209
+ 'radius',
210
+ ]
211
+
212
+ if (!themeManifestPath) {
213
+ warnings.push('Missing themes manifest.')
214
+ } else {
215
+ const themeManifest = JSON.parse(readFileSync(themeManifestPath, 'utf8'))
216
+ const defaultTheme = themeManifest.defaultTheme
217
+ const themes = Array.isArray(themeManifest.themes) ? themeManifest.themes : []
218
+ const selectedTheme = themes.find((theme) => theme.id === defaultTheme)
219
+ const semanticRoles = Array.isArray(themeManifest.semanticRoles) ? themeManifest.semanticRoles : []
220
+ const roleNames = new Set(semanticRoles.map((role) => role.name))
221
+
222
+ if (!defaultTheme) warnings.push('themes.json missing defaultTheme.')
223
+ if (!selectedTheme) warnings.push(`themes.json missing default theme entry: ${defaultTheme ?? 'unknown'}`)
224
+
225
+ for (const role of requiredThemeRoles) {
226
+ if (!roleNames.has(role)) warnings.push(`themes.json missing semantic role: ${role}`)
227
+ if (selectedTheme && !(role in (selectedTheme.values ?? {}))) {
228
+ warnings.push(`Default theme missing value for role: ${role}`)
229
+ }
230
+ }
231
+
232
+ const plannedThemeSlots = Array.isArray(themeManifest.plannedThemeSlots) ? themeManifest.plannedThemeSlots : []
233
+ if (!plannedThemeSlots.length) warnings.push('themes.json missing plannedThemeSlots.')
234
+
235
+ for (const slot of plannedThemeSlots) {
236
+ for (const field of ['sourceTemplates', 'recommendedComponents', 'roleGuidance', 'validationChecklist']) {
237
+ if (!Array.isArray(slot[field]) || slot[field].length === 0) {
238
+ warnings.push(`Theme slot ${slot.id ?? 'unknown'} missing ${field}.`)
239
+ }
240
+ }
241
+ }
242
+
243
+ const themeAuthoring = themeManifest.themeAuthoring ?? {}
244
+ for (const field of ['readOrder', 'newThemeChecklist', 'doNot']) {
245
+ if (!Array.isArray(themeAuthoring[field]) || themeAuthoring[field].length === 0) {
246
+ warnings.push(`themes.json missing themeAuthoring.${field}.`)
247
+ }
248
+ }
249
+
250
+ if (!themeAuthoring.requiredDecision) warnings.push('themes.json missing themeAuthoring.requiredDecision.')
251
+ }
252
+
253
+ const siteExamplesCandidates = [
254
+ join(cwd, 'packages/design-system/src/manifests/site-examples.json'),
255
+ join(cwd, 'src/manifests/site-examples.json'),
256
+ join(cwd, 'node_modules/@utopia-studio-design/design-system/site-examples.json'),
257
+ ]
258
+ const siteExamplesPath = siteExamplesCandidates.find(existsSync)
259
+
260
+ if (!siteExamplesPath) {
261
+ warnings.push('Missing site examples manifest.')
262
+ } else {
263
+ const siteExamples = JSON.parse(readFileSync(siteExamplesPath, 'utf8'))
264
+ for (const collection of ['sourceRoutes', 'badges', 'cards', 'heroes', 'ctas']) {
265
+ if (!Array.isArray(siteExamples[collection]) || siteExamples[collection].length === 0) {
266
+ warnings.push(`site-examples.json missing collection: ${collection}`)
267
+ }
268
+ }
269
+ }
270
+
271
+ const examplesDocPath = join(cwd, 'docs/design-system/examples.md')
272
+ const llmsPath = join(cwd, 'public/llms.txt')
273
+
274
+ if (!existsSync(examplesDocPath)) {
275
+ warnings.push('Missing docs/design-system/examples.md source-example summary.')
276
+ }
277
+
278
+ if (!existsSync(llmsPath)) {
279
+ warnings.push('Missing public/llms.txt AI entrypoint.')
280
+ } else {
281
+ const llms = readFileSync(llmsPath, 'utf8')
282
+ if (!llms.includes('docs/design-system/examples.md')) {
283
+ warnings.push('public/llms.txt does not reference docs/design-system/examples.md.')
284
+ }
285
+ }
286
+
287
+ const templateManifestCandidates = [
288
+ join(cwd, 'packages/design-system/src/manifests/templates.json'),
289
+ join(cwd, 'src/manifests/templates.json'),
290
+ join(cwd, 'node_modules/@utopia-studio-design/design-system/templates.json'),
291
+ ]
292
+ const templateManifestPath = templateManifestCandidates.find(existsSync)
293
+
294
+ if (!templateManifestPath) {
295
+ warnings.push('Missing templates manifest.')
296
+ } else {
297
+ const templateManifest = JSON.parse(readFileSync(templateManifestPath, 'utf8'))
298
+ const templates = Array.isArray(templateManifest.templates) ? templateManifest.templates : []
299
+
300
+ if (!templates.length) warnings.push('templates.json has no templates.')
301
+
302
+ for (const template of templates) {
303
+ for (const field of ['id', 'title', 'purpose', 'aiPrompt']) {
304
+ if (!template[field]) warnings.push(`Template missing ${field}: ${template.id ?? 'unknown template'}`)
305
+ }
306
+
307
+ for (const field of ['requiredSections', 'componentStack', 'bestPractices', 'validationChecklist']) {
308
+ if (!Array.isArray(template[field]) || template[field].length === 0) {
309
+ warnings.push(`Template ${template.id ?? 'unknown template'} missing ${field}.`)
310
+ }
311
+ }
312
+ }
313
+ }
314
+
315
+ if (warnings.length) {
316
+ console.log('Utopia DS doctor warnings:')
317
+ for (const warning of warnings) console.log(`- ${warning}`)
318
+ process.exitCode = 1
319
+ return
320
+ }
321
+
322
+ console.log('Utopia DS doctor passed.')
323
+ }
324
+
325
+ try {
326
+ switch (command) {
327
+ case '--help':
328
+ case 'help':
329
+ help()
330
+ break
331
+ case 'init':
332
+ init()
333
+ break
334
+ case 'add':
335
+ add()
336
+ break
337
+ case 'tokens':
338
+ tokens()
339
+ break
340
+ case 'agent-docs':
341
+ agentDocs()
342
+ break
343
+ case 'doctor':
344
+ doctor()
345
+ break
346
+ default:
347
+ help()
348
+ process.exitCode = 1
349
+ }
350
+ } catch (error) {
351
+ console.error(error instanceof Error ? error.message : error)
352
+ process.exitCode = 1
353
+ }
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "@utopia-studio-design/design-system-cli",
3
+ "version": "0.1.0",
4
+ "description": "CLI for installing, inspecting, and validating the Utopia Studio design system.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "bin": {
8
+ "utopia-ds": "bin/utopia-ds.mjs"
9
+ },
10
+ "files": [
11
+ "bin",
12
+ "templates",
13
+ "README.md",
14
+ "LICENSE"
15
+ ],
16
+ "publishConfig": {
17
+ "access": "public"
18
+ },
19
+ "scripts": {
20
+ "build": "node bin/utopia-ds.mjs --help",
21
+ "check": "node bin/utopia-ds.mjs tokens > /tmp/utopia-ds-tokens.json && node bin/utopia-ds.mjs doctor --cwd ../..",
22
+ "prepublishOnly": "npm run check"
23
+ },
24
+ "dependencies": {
25
+ "@utopia-studio-design/design-system": "0.1.0"
26
+ },
27
+ "engines": {
28
+ "node": ">=18.18"
29
+ }
30
+ }
@@ -0,0 +1,33 @@
1
+ # Site Examples
2
+
3
+ Use this page as the human-readable companion to `site-examples.json`.
4
+
5
+ ## Source
6
+
7
+ - Prefer `packages/design-system/src/manifests/site-examples.json` when it exists in the app.
8
+ - If the manifest is installed through the package, read `node_modules/@utopia-studio-design/design-system/site-examples.json`.
9
+ - Use this document as an index for humans and AI agents; keep detailed examples source-driven.
10
+
11
+ ## Rules
12
+
13
+ - Use real Utopia sample copy before inventing placeholder content.
14
+ - Preserve `sourceRoute` and source file references when generating docs or UI examples.
15
+ - Use examples for content shape, tone, and component fit only.
16
+ - Use `tokens.json`, `themes.json`, `components.json`, and `patterns.json` for design rules.
17
+
18
+ ## Expected Collections
19
+
20
+ - `sourceRoutes`: route labels, hrefs, and source files.
21
+ - `badges`: status and taxonomy labels with variants.
22
+ - `cards`: Card-ready examples with eyebrow, title, body, href, source route, and component fit.
23
+ - `heroes`: PageHero-ready examples with actions.
24
+ - `ctas`: CtaSection-ready examples with actions.
25
+ - `agentRules`: source-example rules for AI agents.
26
+
27
+ ## Workflow
28
+
29
+ 1. Read `site-examples.json`.
30
+ 2. Choose the collection that matches the component or template.
31
+ 3. Copy the content shape, not just the words.
32
+ 4. Keep Utopia-specific language and routes.
33
+ 5. Do not create generic SaaS metrics, fake companies, decorative cards, or placeholder copy.
@@ -0,0 +1,78 @@
1
+ # Utopia Studio Design System for AI Agents
2
+
3
+ Use this as the canonical design guidance when generating Utopia Studio UI.
4
+
5
+ ## Intent
6
+
7
+ The interface is architectural, editorial, precise, calm, and commercially credible.
8
+
9
+ ## Rules
10
+
11
+ - Utopia is the default/reference theme, not the whole design system.
12
+ - For the Utopia brand site, use TWK Lausanne, Special Black, White, Brick Red, Light Grey, and black/red tints.
13
+ - For themeable apps/templates, map every visual decision through semantic roles in `themes.json`.
14
+ - Use `site-examples.json` for real Utopia examples before writing placeholder content.
15
+ - Use `docs/design-system/examples.md` when a human-readable source-example summary is enough.
16
+ - Use `templates.json` for page structure before inventing layouts.
17
+ - Keep all primary surfaces square.
18
+ - Use hairline borders instead of shadows.
19
+ - In the Utopia theme, Brick Red is action, focus, active states, and emphasis.
20
+ - Do not create gradients, glass panels, decorative blobs, or rounded SaaS cards.
21
+ - Use semantic headings and visible focus states.
22
+
23
+ ## Theme Decision Gate
24
+
25
+ Before generating UI, decide:
26
+
27
+ 1. Is this for the Utopia Studio brand site?
28
+ 2. Is this for a themeable app/template?
29
+ 3. Is this a new theme?
30
+
31
+ Use the locked `utopia` theme for the brand site. For themeable contexts, use semantic roles and make sure every role in `themes.json` is defined before using a new theme.
32
+
33
+ ## Core Components
34
+
35
+ Before inventing UI, inspect each component's `agentContract` in `packages/design-system/src/manifests/components.json`.
36
+ Read component `props`, `anatomy`, `states`, and `slots` before composing examples.
37
+ Before writing docs examples, inspect `packages/design-system/src/manifests/site-examples.json`.
38
+ When reading quickly, use `docs/design-system/examples.md` for the generated source-example summary.
39
+ Before scaffolding a page, inspect `packages/design-system/src/manifests/templates.json`.
40
+
41
+ - Button
42
+ - IconButton
43
+ - ButtonGroup
44
+ - Card
45
+ - Badge
46
+ - PageHero
47
+ - CtaSection
48
+ - Topbar
49
+
50
+ ## Agent Contract
51
+
52
+ Each manifest component should define:
53
+
54
+ - `useWhen`
55
+ - `avoidWhen`
56
+ - `fallbackToShadcn`
57
+ - `requiredTokens`
58
+ - `neverInvent`
59
+ - `validationChecklist`
60
+
61
+ Component entries can also define:
62
+
63
+ - `props`
64
+ - `anatomy`
65
+ - `states`
66
+ - `slots`
67
+
68
+ If a needed UI primitive is missing, first try `npx shadcn@latest add <primitive> --yes`, then layer Utopia tokens and rules on top.
69
+
70
+ ## CLI
71
+
72
+ ```sh
73
+ utopia-ds init
74
+ utopia-ds add button
75
+ utopia-ds add card
76
+ utopia-ds tokens
77
+ utopia-ds doctor
78
+ ```
@@ -0,0 +1,33 @@
1
+ # Utopia Studio Design System
2
+
3
+ Canonical package: @utopia-studio-design/design-system
4
+ CLI package: @utopia-studio-design/design-system-cli
5
+
6
+ Read first:
7
+ - docs/design-system/agent-docs/index.md
8
+ - docs/design-system/examples.md
9
+ - packages/design-system/src/tokens.json
10
+ - packages/design-system/src/manifests/components.json
11
+ - packages/design-system/src/manifests/patterns.json
12
+ - packages/design-system/src/manifests/themes.json
13
+ - packages/design-system/src/manifests/site-examples.json
14
+ - packages/design-system/src/manifests/templates.json
15
+
16
+ Agent contract:
17
+ - Read each component's agentContract before creating UI.
18
+ - Read component props, anatomy, states, and slots before composing examples.
19
+ - Prefer fallbackToShadcn via `npx shadcn@latest add <primitive> --yes`.
20
+ - Never invent tokens, colors, shapes, or component APIs outside the manifest.
21
+ - Utopia is the default/reference theme, not the whole design system.
22
+ - For the Utopia brand site use the locked Utopia theme; for themeable apps/templates map decisions through semantic roles.
23
+ - Use site-examples.json for real Utopia sample copy before inventing examples.
24
+ - Use docs/design-system/examples.md when a human-readable source-example summary is enough.
25
+ - Use templates.json for page structure before inventing layouts.
26
+
27
+ Design rules:
28
+ - Square geometry.
29
+ - TWK Lausanne only.
30
+ - Utopia theme palette: Brick Red, Special Black, White, Light Grey, and approved tints.
31
+ - Themeable architecture: use semantic roles before raw values.
32
+ - Borders over shadows.
33
+ - No gradients, glassmorphism, blobs, or rounded SaaS cards.
@@ -0,0 +1,12 @@
1
+ import * as React from 'react'
2
+
3
+ import { cn } from '../../lib/utils'
4
+
5
+ export function Badge({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
6
+ return (
7
+ <div
8
+ className={cn('inline-flex w-fit items-center border px-2.5 py-1 text-xs font-medium uppercase', className)}
9
+ {...props}
10
+ />
11
+ )
12
+ }
@@ -0,0 +1,32 @@
1
+ import { cva, type VariantProps } from 'class-variance-authority'
2
+ import type * as React from 'react'
3
+
4
+ import { cn } from '../../lib/utils'
5
+
6
+ const brandActionVariants = cva('brand-action', {
7
+ variants: {
8
+ variant: {
9
+ primary: 'brand-action--primary',
10
+ secondary: 'brand-action--secondary',
11
+ ghost: 'brand-action--ghost',
12
+ },
13
+ },
14
+ defaultVariants: {
15
+ variant: 'primary',
16
+ },
17
+ })
18
+
19
+ export interface BrandActionProps
20
+ extends React.AnchorHTMLAttributes<HTMLAnchorElement>,
21
+ VariantProps<typeof brandActionVariants> {
22
+ label: string
23
+ }
24
+
25
+ export function BrandAction({ className, label, variant, ...props }: BrandActionProps) {
26
+ return (
27
+ <a className={cn(brandActionVariants({ className, variant }))} {...props}>
28
+ <span>{label}</span>
29
+ {variant === 'primary' || !variant ? <span aria-hidden="true">-&gt;</span> : null}
30
+ </a>
31
+ )
32
+ }
@@ -0,0 +1,30 @@
1
+ import * as React from 'react'
2
+
3
+ import { cn } from '../../lib/utils'
4
+ import { Button, type ButtonProps } from './button'
5
+
6
+ export function ButtonGroup({
7
+ className,
8
+ ...props
9
+ }: React.HTMLAttributes<HTMLDivElement>) {
10
+ return (
11
+ <div
12
+ className={cn('utopia-button-group', className)}
13
+ role="group"
14
+ {...props}
15
+ />
16
+ )
17
+ }
18
+
19
+ export const ButtonGroupButton = React.forwardRef<HTMLButtonElement, ButtonProps>(
20
+ ({ className, variant = 'secondary', ...props }, ref) => (
21
+ <Button
22
+ className={cn('utopia-button-group__button', className)}
23
+ ref={ref}
24
+ variant={variant}
25
+ {...props}
26
+ />
27
+ ),
28
+ )
29
+
30
+ ButtonGroupButton.displayName = 'ButtonGroupButton'
@@ -0,0 +1,44 @@
1
+ import * as React from 'react'
2
+ import { Slot } from '@radix-ui/react-slot'
3
+ import { cva, type VariantProps } from 'class-variance-authority'
4
+
5
+ import { cn } from '../../lib/utils'
6
+
7
+ const buttonVariants = cva(
8
+ 'utopia-button inline-flex shrink-0 items-center justify-center disabled:pointer-events-none disabled:opacity-50',
9
+ {
10
+ variants: {
11
+ variant: {
12
+ primary: '',
13
+ default: '',
14
+ destructive: 'bg-[hsl(var(--destructive))] text-[hsl(var(--destructive-foreground))]',
15
+ secondary: 'bg-[hsl(var(--secondary))] text-[hsl(var(--secondary-foreground))]',
16
+ outline: 'border-[hsl(var(--border))] bg-transparent text-[hsl(var(--foreground))]',
17
+ ghost: 'border-transparent bg-transparent text-[hsl(var(--foreground))]',
18
+ link: 'h-auto min-h-0 border-0 bg-transparent px-0 underline underline-offset-4 hover:text-[hsl(var(--primary))]',
19
+ },
20
+ size: {
21
+ default: '',
22
+ sm: 'h-[var(--button-height)] px-[var(--button-padding-x)]',
23
+ lg: 'h-12',
24
+ icon: 'size-11 p-0',
25
+ },
26
+ },
27
+ defaultVariants: { variant: 'primary', size: 'default' },
28
+ },
29
+ )
30
+
31
+ export interface ButtonProps
32
+ extends React.ButtonHTMLAttributes<HTMLButtonElement>,
33
+ VariantProps<typeof buttonVariants> {
34
+ asChild?: boolean
35
+ }
36
+
37
+ export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
38
+ ({ asChild = false, className, size, variant, ...props }, ref) => {
39
+ const Comp = asChild ? Slot : 'button'
40
+ return <Comp className={cn(buttonVariants({ className, size, variant }))} ref={ref} {...props} />
41
+ },
42
+ )
43
+
44
+ Button.displayName = 'Button'
@@ -0,0 +1,23 @@
1
+ import * as React from 'react'
2
+
3
+ import { cn } from '../../lib/utils'
4
+
5
+ export function Card({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
6
+ return <div className={cn('utopia-card', className)} {...props} />
7
+ }
8
+
9
+ export function CardHeader({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
10
+ return <div className={cn('grid gap-1.5 p-5', className)} {...props} />
11
+ }
12
+
13
+ export function CardTitle({ className, ...props }: React.HTMLAttributes<HTMLHeadingElement>) {
14
+ return <h3 className={cn('m-0 text-2xl font-medium leading-tight', className)} {...props} />
15
+ }
16
+
17
+ export function CardDescription({ className, ...props }: React.HTMLAttributes<HTMLParagraphElement>) {
18
+ return <p className={cn('m-0 text-sm leading-6 opacity-75', className)} {...props} />
19
+ }
20
+
21
+ export function CardContent({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
22
+ return <div className={cn('p-5 pt-0', className)} {...props} />
23
+ }
@@ -0,0 +1,34 @@
1
+ import { cn } from '../../lib/utils'
2
+ import { BrandAction } from './brand-action'
3
+
4
+ type CtaAction = {
5
+ href: string
6
+ label: string
7
+ variant?: 'primary' | 'secondary' | 'ghost'
8
+ }
9
+
10
+ export function CtaSection({
11
+ actions,
12
+ className,
13
+ eyebrow,
14
+ title,
15
+ }: {
16
+ actions: CtaAction[]
17
+ className?: string
18
+ eyebrow: string
19
+ title: string
20
+ }) {
21
+ return (
22
+ <section className={cn('brand-cta-section', className)}>
23
+ <div className="brand-cta-section__inner">
24
+ <p className="brand-cta-section__eyebrow">{eyebrow}</p>
25
+ <h2 className="m-0 text-[length:var(--type-hero-title)] font-bold leading-none">{title}</h2>
26
+ <div className="brand-cta-section__actions">
27
+ {actions.map((action) => (
28
+ <BrandAction key={action.label} {...action} />
29
+ ))}
30
+ </div>
31
+ </div>
32
+ </section>
33
+ )
34
+ }
@@ -0,0 +1,44 @@
1
+ import * as React from 'react'
2
+
3
+ import { cn } from '../../lib/utils'
4
+ import { Button, type ButtonProps } from './button'
5
+ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from './tooltip'
6
+
7
+ export interface IconButtonProps
8
+ extends Omit<ButtonProps, 'children' | 'size'> {
9
+ children: React.ReactNode
10
+ label: string
11
+ tooltip?: string
12
+ }
13
+
14
+ export const IconButton = React.forwardRef<HTMLButtonElement, IconButtonProps>(
15
+ ({ children, className, label, tooltip, variant = 'ghost', ...props }, ref) => {
16
+ const control = (
17
+ <Button
18
+ aria-label={label}
19
+ className={cn('utopia-icon-button', className)}
20
+ ref={ref}
21
+ size="icon"
22
+ variant={variant}
23
+ {...props}
24
+ >
25
+ {children}
26
+ </Button>
27
+ )
28
+
29
+ if (!tooltip) {
30
+ return control
31
+ }
32
+
33
+ return (
34
+ <TooltipProvider>
35
+ <Tooltip>
36
+ <TooltipTrigger asChild>{control}</TooltipTrigger>
37
+ <TooltipContent>{tooltip}</TooltipContent>
38
+ </Tooltip>
39
+ </TooltipProvider>
40
+ )
41
+ },
42
+ )
43
+
44
+ IconButton.displayName = 'IconButton'
@@ -0,0 +1,47 @@
1
+ import type { ReactNode } from 'react'
2
+
3
+ import { cn } from '../../lib/utils'
4
+ import { BrandAction } from './brand-action'
5
+
6
+ type PageHeroAction = {
7
+ href: string
8
+ label: string
9
+ variant?: 'primary' | 'secondary' | 'ghost'
10
+ }
11
+
12
+ export function PageHero({
13
+ actions = [],
14
+ body,
15
+ className,
16
+ eyebrow,
17
+ panelClassName,
18
+ title,
19
+ variant = 'split',
20
+ }: {
21
+ actions?: PageHeroAction[]
22
+ body?: ReactNode
23
+ className?: string
24
+ eyebrow: string
25
+ panelClassName?: string
26
+ title: string
27
+ variant?: 'split' | 'stacked'
28
+ }) {
29
+ return (
30
+ <section className={cn('brand-page-hero', `brand-page-hero--${variant}`, className)}>
31
+ <div className="brand-page-hero__copy">
32
+ <p className="brand-page-hero__eyebrow">{eyebrow}</p>
33
+ <h1 className="m-0 text-[length:var(--type-hero-title)] font-bold leading-none tracking-0">{title}</h1>
34
+ </div>
35
+ <div className={cn('brand-page-hero__panel', panelClassName)}>
36
+ {body ? <p className="text-lg leading-7">{body}</p> : null}
37
+ {actions.length ? (
38
+ <div className="brand-page-hero__actions">
39
+ {actions.map((action) => (
40
+ <BrandAction key={action.label} {...action} />
41
+ ))}
42
+ </div>
43
+ ) : null}
44
+ </div>
45
+ </section>
46
+ )
47
+ }
@@ -0,0 +1,48 @@
1
+ import * as React from 'react'
2
+ import { Tooltip as TooltipPrimitive } from 'radix-ui'
3
+
4
+ import { cn } from '../../lib/utils'
5
+
6
+ export function TooltipProvider({
7
+ delayDuration = 0,
8
+ ...props
9
+ }: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
10
+ return (
11
+ <TooltipPrimitive.Provider
12
+ delayDuration={delayDuration}
13
+ {...props}
14
+ />
15
+ )
16
+ }
17
+
18
+ export function Tooltip({
19
+ ...props
20
+ }: React.ComponentProps<typeof TooltipPrimitive.Root>) {
21
+ return <TooltipPrimitive.Root {...props} />
22
+ }
23
+
24
+ export function TooltipTrigger({
25
+ ...props
26
+ }: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
27
+ return <TooltipPrimitive.Trigger {...props} />
28
+ }
29
+
30
+ export function TooltipContent({
31
+ children,
32
+ className,
33
+ sideOffset = 0,
34
+ ...props
35
+ }: React.ComponentProps<typeof TooltipPrimitive.Content>) {
36
+ return (
37
+ <TooltipPrimitive.Portal>
38
+ <TooltipPrimitive.Content
39
+ className={cn('utopia-tooltip-content', className)}
40
+ sideOffset={sideOffset}
41
+ {...props}
42
+ >
43
+ {children}
44
+ <TooltipPrimitive.Arrow className="utopia-tooltip-arrow" />
45
+ </TooltipPrimitive.Content>
46
+ </TooltipPrimitive.Portal>
47
+ )
48
+ }
@@ -0,0 +1,59 @@
1
+ import { Button } from './button'
2
+
3
+ export type TopbarNavItem = {
4
+ href: string
5
+ label: string
6
+ }
7
+
8
+ export interface TopbarProps {
9
+ applyHref?: string
10
+ applyLabel?: string
11
+ brandHref?: string
12
+ brandLabel?: string
13
+ navItems?: TopbarNavItem[]
14
+ partnerHref?: string
15
+ partnerLabel?: string
16
+ }
17
+
18
+ const defaultNavItems: TopbarNavItem[] = [
19
+ { label: 'Fellows', href: '/fellows' },
20
+ { label: 'Startups', href: '/portfolio' },
21
+ { label: 'How We Build', href: '/how-we-build' },
22
+ { label: 'Team', href: '/team' },
23
+ { label: 'Resources', href: '/resources' },
24
+ ]
25
+
26
+ export function Topbar({
27
+ applyHref,
28
+ applyLabel = 'Apply',
29
+ brandHref = '/',
30
+ brandLabel = 'The Utopia Studio',
31
+ navItems = defaultNavItems,
32
+ partnerHref,
33
+ partnerLabel = 'Partner with us',
34
+ }: TopbarProps) {
35
+ return (
36
+ <header className="utopia-topbar">
37
+ <a className="utopia-topbar__brand" href={brandHref}>
38
+ {brandLabel}
39
+ </a>
40
+ <nav aria-label="Primary navigation" className="utopia-topbar__nav">
41
+ {navItems.map((item) => (
42
+ <a className="utopia-topbar__link" href={item.href} key={item.href}>
43
+ {item.label}
44
+ </a>
45
+ ))}
46
+ {partnerHref ? (
47
+ <Button asChild variant="outline">
48
+ <a href={partnerHref}>{partnerLabel}</a>
49
+ </Button>
50
+ ) : null}
51
+ {applyHref ? (
52
+ <Button asChild>
53
+ <a href={applyHref}>{applyLabel}</a>
54
+ </Button>
55
+ ) : null}
56
+ </nav>
57
+ </header>
58
+ )
59
+ }
@@ -0,0 +1,6 @@
1
+ import { clsx, type ClassValue } from 'clsx'
2
+ import { twMerge } from 'tailwind-merge'
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs))
6
+ }
@@ -0,0 +1,271 @@
1
+ @import './utopia-tokens.css';
2
+
3
+ * {
4
+ box-sizing: border-box;
5
+ }
6
+
7
+ body {
8
+ margin: 0;
9
+ min-width: 320px;
10
+ background: hsl(var(--background));
11
+ color: hsl(var(--foreground));
12
+ font-family: var(--font-sans);
13
+ font-weight: var(--font-weight-regular);
14
+ font-synthesis: none;
15
+ text-rendering: geometricPrecision;
16
+ -webkit-font-smoothing: antialiased;
17
+ -moz-osx-font-smoothing: grayscale;
18
+ }
19
+
20
+ a {
21
+ color: inherit;
22
+ text-decoration: none;
23
+ }
24
+
25
+ button,
26
+ a {
27
+ font: inherit;
28
+ }
29
+
30
+ .utopia-button {
31
+ align-items: center;
32
+ border-radius: 0;
33
+ display: inline-flex;
34
+ font-size: var(--button-font-size);
35
+ font-weight: var(--button-font-weight);
36
+ gap: var(--button-gap);
37
+ height: var(--button-height);
38
+ justify-content: center;
39
+ letter-spacing: var(--button-tracking);
40
+ padding-inline: var(--button-padding-x);
41
+ text-transform: uppercase;
42
+ transition: background 200ms ease, border-color 200ms ease, color 200ms ease;
43
+ white-space: nowrap;
44
+ }
45
+
46
+ .utopia-button:focus-visible,
47
+ .brand-action:focus-visible {
48
+ outline: 2px solid hsl(var(--ring));
49
+ outline-offset: 2px;
50
+ }
51
+
52
+ .utopia-button--primary {
53
+ background: hsl(var(--primary));
54
+ border: 1px solid hsl(var(--primary));
55
+ color: hsl(var(--primary-foreground));
56
+ }
57
+
58
+ .utopia-button--primary:hover {
59
+ background: var(--brand-brick-hover);
60
+ border-color: var(--brand-brick-hover);
61
+ }
62
+
63
+ .utopia-button--destructive {
64
+ background: hsl(var(--destructive));
65
+ border: 1px solid hsl(var(--destructive));
66
+ color: hsl(var(--destructive-foreground));
67
+ }
68
+
69
+ .utopia-button--destructive:hover {
70
+ background: var(--brand-brick-hover);
71
+ border-color: var(--brand-brick-hover);
72
+ }
73
+
74
+ .utopia-button--secondary {
75
+ background: hsl(var(--secondary));
76
+ border: 1px solid hsl(var(--secondary));
77
+ color: hsl(var(--secondary-foreground));
78
+ }
79
+
80
+ .utopia-button--outline,
81
+ .utopia-button--ghost {
82
+ background: transparent;
83
+ border: 1px solid hsl(var(--input));
84
+ color: hsl(var(--foreground));
85
+ }
86
+
87
+ .utopia-button--outline:hover,
88
+ .utopia-button--ghost:hover {
89
+ border-color: hsl(var(--primary));
90
+ color: hsl(var(--primary));
91
+ }
92
+
93
+ .utopia-card {
94
+ background: hsl(var(--card));
95
+ border: 1px solid var(--tint-black-20);
96
+ border-radius: 0;
97
+ box-shadow: none;
98
+ color: hsl(var(--card-foreground));
99
+ }
100
+
101
+ .utopia-icon-button {
102
+ padding: 0;
103
+ }
104
+
105
+ .utopia-tooltip-content {
106
+ background: hsl(var(--foreground));
107
+ color: hsl(var(--background));
108
+ font-size: 12px;
109
+ line-height: 1;
110
+ padding: 8px 10px;
111
+ z-index: 50;
112
+ }
113
+
114
+ .utopia-tooltip-arrow {
115
+ fill: hsl(var(--foreground));
116
+ }
117
+
118
+ .utopia-button-group {
119
+ align-items: center;
120
+ display: inline-flex;
121
+ }
122
+
123
+ .utopia-button-group__button {
124
+ border-right-width: 0;
125
+ }
126
+
127
+ .utopia-button-group__button:last-child {
128
+ border-right-width: 1px;
129
+ }
130
+
131
+ .brand-action {
132
+ align-items: center;
133
+ border: 1px solid hsl(var(--input));
134
+ border-radius: 0;
135
+ display: inline-flex;
136
+ font-size: var(--button-font-size);
137
+ font-weight: var(--button-font-weight);
138
+ gap: var(--button-gap);
139
+ min-height: var(--button-height);
140
+ padding-inline: var(--button-padding-x);
141
+ text-transform: uppercase;
142
+ letter-spacing: var(--button-tracking);
143
+ transition: background 200ms ease, border-color 200ms ease, color 200ms ease;
144
+ }
145
+
146
+ .brand-action--primary {
147
+ background: hsl(var(--primary));
148
+ border-color: hsl(var(--primary));
149
+ color: hsl(var(--primary-foreground));
150
+ }
151
+
152
+ .brand-action--secondary,
153
+ .brand-action--ghost {
154
+ background: transparent;
155
+ color: hsl(var(--foreground));
156
+ }
157
+
158
+ .brand-action--secondary:hover,
159
+ .brand-action--ghost:hover {
160
+ border-color: hsl(var(--primary));
161
+ color: hsl(var(--primary));
162
+ }
163
+
164
+ .brand-page-hero {
165
+ display: grid;
166
+ gap: clamp(28px, 5vw, 72px);
167
+ grid-template-columns: minmax(0, 1.15fr) minmax(280px, 0.85fr);
168
+ padding: var(--section-padding-y) var(--section-padding-x);
169
+ }
170
+
171
+ .brand-page-hero--stacked {
172
+ grid-template-columns: 1fr;
173
+ }
174
+
175
+ .brand-page-hero__eyebrow,
176
+ .brand-cta-section__eyebrow,
177
+ .utopia-topbar__link {
178
+ font-size: var(--fs-eyebrow);
179
+ font-weight: var(--font-weight-medium);
180
+ letter-spacing: var(--tracking-eyebrow);
181
+ text-transform: uppercase;
182
+ }
183
+
184
+ .brand-page-hero h1,
185
+ .brand-cta-section h2 {
186
+ font-family: var(--font-display);
187
+ font-size: var(--type-hero-title);
188
+ font-weight: var(--font-weight-strong);
189
+ letter-spacing: 0;
190
+ line-height: var(--lh-tight);
191
+ margin: 0;
192
+ }
193
+
194
+ .brand-page-hero__panel {
195
+ border-left: 1px solid hsl(var(--foreground) / 0.18);
196
+ padding-left: clamp(20px, 3vw, 40px);
197
+ }
198
+
199
+ .brand-page-hero__panel p {
200
+ font-size: var(--type-hero-description);
201
+ line-height: var(--lh-body);
202
+ margin: 0;
203
+ }
204
+
205
+ .brand-page-hero__actions,
206
+ .brand-cta-section__actions {
207
+ display: flex;
208
+ flex-wrap: wrap;
209
+ gap: 12px;
210
+ margin-top: 28px;
211
+ }
212
+
213
+ .brand-cta-section {
214
+ padding: var(--section-padding-y) var(--section-padding-x);
215
+ }
216
+
217
+ .brand-cta-section__inner {
218
+ border-top: 1px solid hsl(var(--foreground) / 0.18);
219
+ padding-top: clamp(24px, 4vw, 44px);
220
+ }
221
+
222
+ .utopia-topbar {
223
+ align-items: center;
224
+ border-bottom: 1px solid hsl(var(--foreground) / 0.14);
225
+ display: flex;
226
+ gap: 24px;
227
+ min-height: var(--topbar-height);
228
+ padding-inline: var(--section-padding-x);
229
+ }
230
+
231
+ .utopia-topbar__brand {
232
+ font-size: 14px;
233
+ font-weight: var(--font-weight-strong);
234
+ letter-spacing: 0.08em;
235
+ text-transform: uppercase;
236
+ }
237
+
238
+ .utopia-topbar__nav {
239
+ align-items: center;
240
+ display: flex;
241
+ gap: 18px;
242
+ margin-left: auto;
243
+ }
244
+
245
+ .utopia-topbar__link:hover {
246
+ color: hsl(var(--primary));
247
+ }
248
+
249
+ @media (max-width: 760px) {
250
+ .brand-page-hero {
251
+ grid-template-columns: 1fr;
252
+ }
253
+
254
+ .brand-page-hero__panel {
255
+ border-left: 0;
256
+ border-top: 1px solid hsl(var(--foreground) / 0.18);
257
+ padding-left: 0;
258
+ padding-top: 20px;
259
+ }
260
+
261
+ .utopia-topbar {
262
+ align-items: flex-start;
263
+ flex-direction: column;
264
+ padding-block: 18px;
265
+ }
266
+
267
+ .utopia-topbar__nav {
268
+ flex-wrap: wrap;
269
+ margin-left: 0;
270
+ }
271
+ }
@@ -0,0 +1,21 @@
1
+ :root {
2
+ --background: 342 9.1% 21.6%;
3
+ --foreground: 0 0% 100%;
4
+ --card: 0 0% 100%;
5
+ --card-foreground: 342 9.1% 21.6%;
6
+ --primary: 12 59.5% 50.6%;
7
+ --primary-foreground: 0 0% 100%;
8
+ --secondary: 0 0% 93.3%;
9
+ --secondary-foreground: 342 9.1% 21.6%;
10
+ --border: 0 0% 100% / 0.18;
11
+ --ring: 12 59.5% 50.6%;
12
+ --font-sans: 'TWK Lausanne';
13
+ --font-weight-regular: 350;
14
+ --font-weight-medium: 500;
15
+ --font-weight-strong: 700;
16
+ --radius: 0px;
17
+ --type-hero-title: clamp(54px, 6.1vw, 112px);
18
+ --button-height: 44px;
19
+ --button-font-size: 14px;
20
+ --button-tracking: 0.06em;
21
+ }
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "Utopia Studio Design System",
3
+ "version": "0.1.0",
4
+ "defaultTheme": "utopia",
5
+ "themeArchitecture": "Utopia is the default/reference theme. Components must consume semantic roles so other themes can be added without changing component APIs.",
6
+ "semanticRoles": [
7
+ "--background",
8
+ "--foreground",
9
+ "--card",
10
+ "--card-foreground",
11
+ "--primary",
12
+ "--primary-foreground",
13
+ "--secondary",
14
+ "--secondary-foreground",
15
+ "--muted",
16
+ "--muted-foreground",
17
+ "--border",
18
+ "--input",
19
+ "--ring",
20
+ "--radius"
21
+ ],
22
+ "color": {
23
+ "specialBlack": "hsl(342 9.1% 21.6%)",
24
+ "white": "hsl(0 0% 100%)",
25
+ "brickRed": "hsl(12 59.5% 50.6%)",
26
+ "brickHover": "hsl(12 61.4% 44.7%)",
27
+ "brickPress": "hsl(12 62.1% 38.2%)",
28
+ "lightGrey": "hsl(0 0% 93.3%)"
29
+ },
30
+ "radius": {
31
+ "base": "0px",
32
+ "max": "0px",
33
+ "button": "0px"
34
+ },
35
+ "font": {
36
+ "family": "TWK Lausanne",
37
+ "regular": 350,
38
+ "medium": 500,
39
+ "strong": 700
40
+ }
41
+ }