@utopia-studio-design/design-system-cli 0.1.0 → 0.3.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/README.md +12 -41
- package/bin/utopia-ds-mcp.mjs +55 -0
- package/bin/utopia-ds.mjs +233 -318
- package/data/docs/ai-platform-plan.md +53 -0
- package/data/docs/arabic-friendly.md +72 -0
- package/data/docs/foundations.md +154 -0
- package/data/docs/guide.md +37 -0
- package/data/docs/quick-start-ai.md +122 -0
- package/data/docs/shadcn-conversion.md +35 -0
- package/data/docs/theme-authoring.md +46 -0
- package/data/manifests/catalog.json +547 -0
- package/data/manifests/components.json +4780 -0
- package/data/manifests/patterns.json +12 -0
- package/data/manifests/templates.json +96 -0
- package/data/manifests/theme-barrier-intelligence.json +123 -0
- package/data/manifests/theme-dextrum.json +255 -0
- package/data/manifests/theme-utopia-default.json +163 -0
- package/data/manifests/themes.json +417 -0
- package/data/templates/saas-solution-homepage/README.md +52 -0
- package/data/templates/saas-solution-homepage/agents/index.html +1 -0
- package/data/templates/saas-solution-homepage/changelog/index.html +1 -0
- package/data/templates/saas-solution-homepage/contact-sales/index.html +1 -0
- package/data/templates/saas-solution-homepage/customers/aster-labs/index.html +1 -0
- package/data/templates/saas-solution-homepage/customers/index.html +1 -0
- package/data/templates/saas-solution-homepage/index.html +17 -0
- package/data/templates/saas-solution-homepage/integrations/index.html +1 -0
- package/data/templates/saas-solution-homepage/integrations/slack/index.html +1 -0
- package/data/templates/saas-solution-homepage/main.tsx +642 -0
- package/data/templates/saas-solution-homepage/pricing/index.html +1 -0
- package/data/templates/saas-solution-homepage/product/index.html +1 -0
- package/data/templates/saas-solution-homepage/styles.css +863 -0
- package/data/templates/saas-solution-homepage/template.manifest.json +36 -0
- package/lib/api.mjs +152 -0
- package/package.json +17 -15
- package/LICENSE +0 -21
- package/templates/agent-docs/examples.md +0 -33
- package/templates/agent-docs/index.md +0 -78
- package/templates/agent-docs/llms.txt +0 -33
- package/templates/components/badge.tsx +0 -12
- package/templates/components/brand-action.tsx +0 -32
- package/templates/components/button-group.tsx +0 -30
- package/templates/components/button.tsx +0 -44
- package/templates/components/card.tsx +0 -23
- package/templates/components/cta-section.tsx +0 -34
- package/templates/components/icon-button.tsx +0 -44
- package/templates/components/page-hero.tsx +0 -47
- package/templates/components/tooltip.tsx +0 -48
- package/templates/components/topbar.tsx +0 -59
- package/templates/lib/utils.ts +0 -6
- package/templates/styles/styles.css +0 -271
- package/templates/styles/tokens.css +0 -21
- package/templates/tokens.json +0 -41
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "template-saas-solution-homepage",
|
|
3
|
+
"name": "SaaS Solution Website",
|
|
4
|
+
"category": "website-page",
|
|
5
|
+
"status": "available",
|
|
6
|
+
"purpose": "A complete product-led SaaS website family spanning marketing, product, proof, adoption, updates, integrations, and sales contact surfaces.",
|
|
7
|
+
"packageImport": "@utopia-studio-design/design-system",
|
|
8
|
+
"sourcePath": "templates/saas-solution-homepage/main.tsx",
|
|
9
|
+
"entryPath": "templates/saas-solution-homepage/index.html",
|
|
10
|
+
"shadcnFoundation": ["Navigation Menu", "Button", "Badge", "Card", "Input", "Select", "Textarea"],
|
|
11
|
+
"requiredTokens": ["background", "foreground", "muted-foreground", "surface", "surface-strong", "border", "primary", "ring", "radius-control", "radius-surface", "space-*", "motion-duration-*"],
|
|
12
|
+
"sections": ["Hero", "Product interface preview", "Core workflow", "Agents and automation", "Integrations", "Security and reliability", "Team proof", "Adoption CTA", "Footer"],
|
|
13
|
+
"pages": [
|
|
14
|
+
{ "id": "home", "path": "index.html", "purpose": "Primary narrative and product proof" },
|
|
15
|
+
{ "id": "product", "path": "product/index.html", "purpose": "Product overview and capability architecture" },
|
|
16
|
+
{ "id": "agents", "path": "agents/index.html", "purpose": "Automation and agent governance" },
|
|
17
|
+
{ "id": "integrations", "path": "integrations/index.html", "purpose": "Searchable integration directory" },
|
|
18
|
+
{ "id": "integration-detail", "path": "integrations/slack/index.html", "purpose": "Integration capabilities and setup" },
|
|
19
|
+
{ "id": "customers", "path": "customers/index.html", "purpose": "Customer proof library" },
|
|
20
|
+
{ "id": "customer-story", "path": "customers/aster-labs/index.html", "purpose": "Long-form customer case study" },
|
|
21
|
+
{ "id": "pricing", "path": "pricing/index.html", "purpose": "Plans, comparison, and FAQ" },
|
|
22
|
+
{ "id": "changelog", "path": "changelog/index.html", "purpose": "Product update timeline" },
|
|
23
|
+
{ "id": "contact-sales", "path": "contact-sales/index.html", "purpose": "Sales qualification form and success state" }
|
|
24
|
+
],
|
|
25
|
+
"features": { "lightDark": true, "rtl": true, "responsive": true, "seededDemoData": true, "scrollAwareNavigation": true, "reducedMotion": true },
|
|
26
|
+
"useWhen": ["A SaaS product needs a complete product-led marketing narrative", "A real interface preview can carry the product proof", "Automation, integrations, and reliability are material buying criteria"],
|
|
27
|
+
"avoidWhen": ["The page is a single-feature launch", "The primary experience is editorial", "There is no reviewed product model behind the interface claims"],
|
|
28
|
+
"neverInvent": ["Do not copy Linear branding, copy, assets, or proprietary interface details", "Do not hard-code brand primitives inside reusable components", "Do not use left/right-only layout APIs where logical properties work", "Do not invent production customer claims from seeded demo content"],
|
|
29
|
+
"queryParameters": { "seed": "Integer used to deterministically select product, company, people, pricing, and proof data." },
|
|
30
|
+
"defaultUrl": "/templates/saas-solution-homepage/index.html?seed=1974341818#agents",
|
|
31
|
+
"themeContract": {
|
|
32
|
+
"default": "utopia-default",
|
|
33
|
+
"generationFlag": "--theme <id>",
|
|
34
|
+
"ownership": "The active theme owns dark/light semantic mappings; the template owns no brand primitives."
|
|
35
|
+
}
|
|
36
|
+
}
|
package/lib/api.mjs
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync } from 'node:fs'
|
|
2
|
+
import { dirname, join, resolve } from 'node:path'
|
|
3
|
+
import { fileURLToPath } from 'node:url'
|
|
4
|
+
|
|
5
|
+
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
|
6
|
+
const workspaceRoot = resolve(packageRoot, '../..')
|
|
7
|
+
const packagedDataRoot = join(packageRoot, 'data')
|
|
8
|
+
const hasWorkspaceSource = existsSync(join(workspaceRoot, 'packages/design-system/src/manifests/components.json'))
|
|
9
|
+
|
|
10
|
+
export const apiVersion = 1
|
|
11
|
+
export const cliVersion = '0.3.0'
|
|
12
|
+
export const mcpLaunch = {
|
|
13
|
+
command: 'npx',
|
|
14
|
+
args: ['-y', '--package', '@utopia-studio-design/design-system-cli', 'utopia-ds', 'mcp'],
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function paths(root = hasWorkspaceSource ? workspaceRoot : packagedDataRoot) {
|
|
18
|
+
return {
|
|
19
|
+
root,
|
|
20
|
+
manifests: hasWorkspaceSource && root === workspaceRoot ? join(root, 'packages/design-system/src/manifests') : join(root, 'manifests'),
|
|
21
|
+
docs: hasWorkspaceSource && root === workspaceRoot ? join(root, 'docs/design-system') : join(root, 'docs'),
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function readJson(path) {
|
|
26
|
+
return JSON.parse(readFileSync(path, 'utf8'))
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function normalize(value) {
|
|
30
|
+
return String(value).toLowerCase().replace(/[^a-z0-9]/g, '')
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function envelope(type, data, meta = {}) {
|
|
34
|
+
return { apiVersion, type, data, meta: { version: cliVersion, ...meta } }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function getCatalog() {
|
|
38
|
+
return readJson(join(paths().manifests, 'catalog.json'))
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function listComponents() {
|
|
42
|
+
return [...readJson(join(paths().manifests, 'components.json')).components]
|
|
43
|
+
.sort((a, b) => a.name.localeCompare(b.name, undefined, { numeric: true, sensitivity: 'base' }))
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function getComponent(name) {
|
|
47
|
+
const key = normalize(name)
|
|
48
|
+
return listComponents().find((item) => normalize(item.name) === key) ?? null
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function listTemplates() {
|
|
52
|
+
return readJson(join(paths().manifests, 'templates.json')).templates
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function getTemplate(name) {
|
|
56
|
+
const key = normalize(name)
|
|
57
|
+
return listTemplates().find((item) => normalize(item.id) === key || normalize(item.title) === key) ?? null
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function listThemes() {
|
|
61
|
+
return readJson(join(paths().manifests, 'themes.json')).themes
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function getTheme(name) {
|
|
65
|
+
const key = normalize(name)
|
|
66
|
+
const entry = listThemes().find((item) => normalize(item.id) === key || normalize(item.name) === key)
|
|
67
|
+
if (!entry) return null
|
|
68
|
+
const policyPath = hasWorkspaceSource
|
|
69
|
+
? join(workspaceRoot, entry.policyManifest)
|
|
70
|
+
: join(paths().manifests, entry.policyManifest.split('/').at(-1))
|
|
71
|
+
return { ...entry, policy: readJson(policyPath) }
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function listDocs() {
|
|
75
|
+
return readdirSync(paths().docs)
|
|
76
|
+
.filter((name) => name.endsWith('.md'))
|
|
77
|
+
.map((name) => name.slice(0, -3))
|
|
78
|
+
.sort()
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function getDoc(topic) {
|
|
82
|
+
const match = listDocs().find((name) => normalize(name) === normalize(topic))
|
|
83
|
+
if (!match) return null
|
|
84
|
+
return { topic: match, content: readFileSync(join(paths().docs, `${match}.md`), 'utf8') }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function search(query) {
|
|
88
|
+
const terms = String(query).toLowerCase().split(/\s+/).filter(Boolean)
|
|
89
|
+
const score = (text) => terms.reduce((total, term) => total + (text.toLowerCase().includes(term) ? 1 : 0), 0)
|
|
90
|
+
const results = [
|
|
91
|
+
...listComponents().map((item) => ({ kind: 'component', id: item.name, title: item.name, summary: [...item.useWhen, item.category].join(' '), item })),
|
|
92
|
+
...listTemplates().map((item) => ({ kind: 'template', id: item.id, title: item.title, summary: `${item.category} ${item.purpose}`, item })),
|
|
93
|
+
...listThemes().map((item) => ({ kind: 'theme', id: item.id, title: item.name, summary: item.role, item })),
|
|
94
|
+
...listDocs().map((topic) => ({ kind: 'docs', id: topic, title: topic, summary: getDoc(topic)?.content.slice(0, 500) ?? '', item: { topic } })),
|
|
95
|
+
]
|
|
96
|
+
return results
|
|
97
|
+
.map((result) => ({ ...result, score: score(`${result.title} ${result.summary}`) }))
|
|
98
|
+
.filter((result) => result.score > 0)
|
|
99
|
+
.sort((a, b) => b.score - a.score || a.title.localeCompare(b.title))
|
|
100
|
+
.map(({ summary: _summary, ...result }) => result)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function capabilityManifest() {
|
|
104
|
+
return {
|
|
105
|
+
name: 'utopia-ds',
|
|
106
|
+
version: cliVersion,
|
|
107
|
+
description: 'Ceramic Design System interface for humans, agents, build tools, and MCP clients.',
|
|
108
|
+
transports: ['cli', 'json', 'mcp-stdio', 'programmatic-api'],
|
|
109
|
+
globalOptions: [
|
|
110
|
+
{ flag: '--json', type: 'boolean', description: 'Return a typed JSON envelope.' },
|
|
111
|
+
{ flag: '--dense', type: 'boolean', description: 'Return compact agent-readable text.' },
|
|
112
|
+
],
|
|
113
|
+
commands: [
|
|
114
|
+
{ name: 'init', args: ['[directory]'], flags: ['--yes', '--theme <id>'], responseType: 'init-result' },
|
|
115
|
+
{ name: 'search', args: ['<query>'], flags: [], responseType: 'search-results' },
|
|
116
|
+
{ name: 'component', args: ['<name>|--list'], flags: [], responseType: 'component|component-list' },
|
|
117
|
+
{ name: 'template', args: ['<id>|--list'], flags: ['--skeleton', '--copy <directory>', '--theme <id>', '--force'], responseType: 'template|template-list|template-copy-result' },
|
|
118
|
+
{ name: 'theme', args: ['<id>|--list', 'create <id> [directory]'], flags: ['--force'], responseType: 'theme|theme-list|theme-create-result' },
|
|
119
|
+
{ name: 'docs', args: ['<topic>|--list'], flags: [], responseType: 'docs|docs-list' },
|
|
120
|
+
{ name: 'manifest', args: [], flags: [], responseType: 'manifest' },
|
|
121
|
+
{ name: 'doctor', args: [], flags: [], responseType: 'doctor-result' },
|
|
122
|
+
{ name: 'mcp', args: [], flags: [], responseType: 'stdio-server' },
|
|
123
|
+
],
|
|
124
|
+
mcp: {
|
|
125
|
+
...mcpLaunch,
|
|
126
|
+
tools: ['search', 'list_components', 'get_component', 'list_templates', 'get_template', 'list_themes', 'get_theme', 'list_docs', 'get_docs', 'doctor'],
|
|
127
|
+
},
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function repositoryDoctor() {
|
|
132
|
+
const required = [
|
|
133
|
+
'catalog.json', 'components.json', 'templates.json', 'themes.json',
|
|
134
|
+
].map((name) => join(paths().manifests, name))
|
|
135
|
+
const docs = ['quick-start-ai', 'arabic-friendly', 'theme-authoring'].map((name) => join(paths().docs, `${name}.md`))
|
|
136
|
+
const missing = [...required, ...docs].filter((path) => !existsSync(path))
|
|
137
|
+
return {
|
|
138
|
+
ok: missing.length === 0,
|
|
139
|
+
checks: {
|
|
140
|
+
manifests: required.every(existsSync),
|
|
141
|
+
docs: docs.every(existsSync),
|
|
142
|
+
components: listComponents().length,
|
|
143
|
+
templates: listTemplates().length,
|
|
144
|
+
themes: listThemes().length,
|
|
145
|
+
mcp: existsSync(join(packageRoot, 'bin/utopia-ds-mcp.mjs'))
|
|
146
|
+
&& mcpLaunch.command === 'npx'
|
|
147
|
+
&& mcpLaunch.args.includes('utopia-ds')
|
|
148
|
+
&& mcpLaunch.args.at(-1) === 'mcp',
|
|
149
|
+
},
|
|
150
|
+
missing,
|
|
151
|
+
}
|
|
152
|
+
}
|
package/package.json
CHANGED
|
@@ -1,30 +1,32 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@utopia-studio-design/design-system-cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "CLI
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "AI-readable CLI and MCP server for Ceramic Design System",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"bin": {
|
|
8
|
-
"utopia-ds": "bin/utopia-ds.mjs"
|
|
8
|
+
"utopia-ds": "bin/utopia-ds.mjs",
|
|
9
|
+
"ceramic-mcp": "bin/utopia-ds-mcp.mjs"
|
|
10
|
+
},
|
|
11
|
+
"exports": {
|
|
12
|
+
".": "./lib/api.mjs",
|
|
13
|
+
"./api": "./lib/api.mjs",
|
|
14
|
+
"./mcp": "./bin/utopia-ds-mcp.mjs"
|
|
9
15
|
},
|
|
10
16
|
"files": [
|
|
11
17
|
"bin",
|
|
12
|
-
"
|
|
13
|
-
"
|
|
14
|
-
"
|
|
18
|
+
"lib",
|
|
19
|
+
"data",
|
|
20
|
+
"README.md"
|
|
15
21
|
],
|
|
16
|
-
"publishConfig": {
|
|
17
|
-
"access": "public"
|
|
18
|
-
},
|
|
19
22
|
"scripts": {
|
|
20
|
-
"
|
|
21
|
-
"
|
|
22
|
-
"prepublishOnly": "npm run check"
|
|
23
|
+
"sync-data": "node scripts/sync-data.mjs",
|
|
24
|
+
"prepack": "npm run sync-data"
|
|
23
25
|
},
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
+
"publishConfig": {
|
|
27
|
+
"access": "public"
|
|
26
28
|
},
|
|
27
29
|
"engines": {
|
|
28
|
-
"node": ">=
|
|
30
|
+
"node": ">=20"
|
|
29
31
|
}
|
|
30
32
|
}
|
package/LICENSE
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
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.
|
|
@@ -1,33 +0,0 @@
|
|
|
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.
|
|
@@ -1,78 +0,0 @@
|
|
|
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
|
-
```
|
|
@@ -1,33 +0,0 @@
|
|
|
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.
|
|
@@ -1,12 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,32 +0,0 @@
|
|
|
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">-></span> : null}
|
|
30
|
-
</a>
|
|
31
|
-
)
|
|
32
|
-
}
|
|
@@ -1,30 +0,0 @@
|
|
|
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'
|
|
@@ -1,44 +0,0 @@
|
|
|
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'
|
|
@@ -1,23 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,34 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,44 +0,0 @@
|
|
|
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'
|