@utopia-studio-design/design-system-cli 0.3.0 → 0.3.1
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 +2 -0
- package/bin/utopia-ds-mcp.mjs +5 -3
- package/bin/utopia-ds.mjs +64 -24
- package/data/docs/community-template-contributions.md +85 -0
- package/data/docs/foundations.md +24 -4
- package/data/manifests/catalog.json +6 -3
- package/data/manifests/community-template.schema.json +51 -0
- package/data/manifests/components.json +72 -6
- package/data/manifests/motion-profiles.json +121 -0
- package/data/manifests/theme-barrier-intelligence.json +83 -4
- package/data/manifests/theme-dextrum.json +140 -180
- package/data/manifests/theme-utopia-default.json +40 -5
- package/data/manifests/themes.json +33 -16
- package/data/templates/saas-solution-homepage/main.tsx +6 -2
- package/lib/api.mjs +33 -6
- package/lib/template-submission.mjs +137 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -9,6 +9,8 @@ npx utopia-ds search "Arabic data table" --json
|
|
|
9
9
|
npx utopia-ds component DataTable --json
|
|
10
10
|
npx utopia-ds template template-saas-solution-homepage --copy ./saas-website
|
|
11
11
|
npx utopia-ds template template-saas-solution-homepage --theme dextrum --copy ./dextrum-website
|
|
12
|
+
npx utopia-ds template validate ./my-community-template
|
|
13
|
+
npx utopia-ds template submit ./my-community-template
|
|
12
14
|
npx utopia-ds theme create nova
|
|
13
15
|
npx utopia-ds manifest --json
|
|
14
16
|
npx utopia-ds mcp
|
package/bin/utopia-ds-mcp.mjs
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
-
capabilityManifest, getComponent, getDoc, getTemplate, getTheme,
|
|
4
|
-
listComponents, listDocs, listTemplates, listThemes, repositoryDoctor, search,
|
|
3
|
+
capabilityManifest, getComponent, getDoc, getMotionProfile, getTemplate, getTheme,
|
|
4
|
+
listComponents, listDocs, listMotionProfiles, listTemplates, listThemes, repositoryDoctor, search,
|
|
5
5
|
} from '../lib/api.mjs'
|
|
6
6
|
|
|
7
7
|
const tools = [
|
|
8
|
-
['search', 'Search components, docs, templates, and
|
|
8
|
+
['search', 'Search components, docs, templates, themes, and motion profiles.', { query: { type: 'string' } }, ({ query }) => search(query)],
|
|
9
9
|
['list_components', 'List every Ceramic component contract.', {}, () => listComponents()],
|
|
10
10
|
['get_component', 'Get one component including imports, tokens, usage, and Arabic rules.', { name: { type: 'string' } }, ({ name }) => getComponent(name)],
|
|
11
11
|
['list_templates', 'List Ceramic starter templates.', {}, () => listTemplates()],
|
|
12
12
|
['get_template', 'Get a template contract and sections.', { id: { type: 'string' } }, ({ id }) => getTemplate(id)],
|
|
13
13
|
['list_themes', 'List themes that implement the semantic contract.', {}, () => listThemes()],
|
|
14
14
|
['get_theme', 'Get a theme policy and semantic mappings.', { id: { type: 'string' } }, ({ id }) => getTheme(id)],
|
|
15
|
+
['list_motion_profiles', 'List theme-owned semantic motion personalities.', {}, () => listMotionProfiles()],
|
|
16
|
+
['get_motion_profile', 'Get a motion profile, its rules, contract, and compatible runtime adapters.', { id: { type: 'string' } }, ({ id }) => getMotionProfile(id)],
|
|
15
17
|
['list_docs', 'List AI-readable design-system documents.', {}, () => listDocs()],
|
|
16
18
|
['get_docs', 'Read one design-system document.', { topic: { type: 'string' } }, ({ topic }) => getDoc(topic)],
|
|
17
19
|
['doctor', 'Validate the Ceramic source of truth.', {}, () => repositoryDoctor()],
|
package/bin/utopia-ds.mjs
CHANGED
|
@@ -3,9 +3,10 @@ import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync, wri
|
|
|
3
3
|
import { dirname, join, resolve } from 'node:path'
|
|
4
4
|
import { spawn } from 'node:child_process'
|
|
5
5
|
import {
|
|
6
|
-
capabilityManifest, envelope, getComponent, getDoc, getTemplate, getTheme,
|
|
7
|
-
listComponents, listDocs, listTemplates, listThemes, mcpLaunch, repositoryDoctor, search,
|
|
6
|
+
capabilityManifest, envelope, getComponent, getDoc, getMotionProfile, getTemplate, getTheme,
|
|
7
|
+
listComponents, listDocs, listMotionProfiles, listTemplates, listThemes, mcpLaunch, repositoryDoctor, search,
|
|
8
8
|
} from '../lib/api.mjs'
|
|
9
|
+
import { createTemplateSubmissionUrl, validateTemplateSubmission } from '../lib/template-submission.mjs'
|
|
9
10
|
|
|
10
11
|
const args = process.argv.slice(2)
|
|
11
12
|
const command = args.find((arg) => !arg.startsWith('--')) ?? 'help'
|
|
@@ -43,8 +44,11 @@ Commands:
|
|
|
43
44
|
template <id>|--list [--skeleton] Inspect starter structures
|
|
44
45
|
template <id> --copy <directory> [--theme <id>]
|
|
45
46
|
Generate a themed runnable project
|
|
47
|
+
template validate [directory] Validate a community submission
|
|
48
|
+
template submit [directory] Open a validated GitHub review request
|
|
46
49
|
theme <id>|--list Inspect theme contracts
|
|
47
50
|
theme create <id> [directory] Scaffold and register a theme
|
|
51
|
+
motion <id>|--list Inspect motion personalities and adapters
|
|
48
52
|
docs <topic>|--list Read design-system guidance
|
|
49
53
|
manifest Print the self-describing CLI contract
|
|
50
54
|
doctor Validate the design-system source
|
|
@@ -96,40 +100,42 @@ function scaffoldTheme() {
|
|
|
96
100
|
}
|
|
97
101
|
|
|
98
102
|
const themeValues = {
|
|
99
|
-
'--background': '#
|
|
100
|
-
'--primary': '#
|
|
101
|
-
'--muted': '#
|
|
102
|
-
'--ring': '#
|
|
103
|
-
'--radius-chat-composer': '
|
|
104
|
-
'--motion-duration-page': '200ms', '--motion-duration-expand': '
|
|
103
|
+
'--background': '#F2EFE8', '--foreground': '#292725', '--card': '#FAF8F3', '--card-foreground': '#292725',
|
|
104
|
+
'--primary': '#4A514B', '--primary-foreground': '#FAF8F3', '--secondary': '#DFDBD1', '--secondary-foreground': '#292725',
|
|
105
|
+
'--muted': '#E8E4DB', '--muted-foreground': '#66625C', '--border': 'rgba(41,39,37,0.18)', '--input': 'rgba(41,39,37,0.28)',
|
|
106
|
+
'--ring': '#4A514B', '--radius': '2px', '--radius-control': '2px', '--radius-surface': '4px', '--radius-chat-bubble': '4px',
|
|
107
|
+
'--radius-chat-composer': '6px', '--radius-chat-token': '4px', '--shadow-control': 'none', '--motion-duration-press': '100ms',
|
|
108
|
+
'--motion-duration-page': '200ms', '--motion-duration-expand': '220ms', '--motion-duration-reveal': '240ms', '--motion-duration-icon': '320ms',
|
|
105
109
|
'--motion-ease-standard': 'cubic-bezier(0.2, 0, 0, 1)', '--motion-ease-emphasized': 'cubic-bezier(0.16, 1, 0.3, 1)',
|
|
106
|
-
'--motion-ease-icon': 'cubic-bezier(0.16, 1, 0.3, 1)', '--motion-press-scale': '0.
|
|
107
|
-
'--motion-distance-reveal': '
|
|
108
|
-
'--font-marketing-display': '
|
|
110
|
+
'--motion-ease-icon': 'cubic-bezier(0.16, 1, 0.3, 1)', '--motion-press-scale': '0.985', '--motion-distance-page': '0px',
|
|
111
|
+
'--motion-distance-reveal': '0px', '--font-sans': 'ui-sans-serif, sans-serif', '--font-display': 'ui-sans-serif, sans-serif', '--font-ui-support': 'ui-sans-serif, sans-serif',
|
|
112
|
+
'--font-marketing-display': 'ui-sans-serif, sans-serif', '--font-arabic': 'IBM Plex Sans Arabic', '--font-arabic-body': 'IBM Plex Sans Arabic',
|
|
109
113
|
'--font-arabic-display': 'IBM Plex Sans Arabic', '--font-weight-arabic-body': '400', '--font-weight-arabic-display': '700',
|
|
110
114
|
'--font-size-display': 'clamp(2.75rem, 7vw, 5.5rem)', '--font-size-arabic-body': '16px', '--font-size-arabic-body-lg': '18px',
|
|
111
115
|
'--font-size-arabic-display': 'clamp(2.625rem, 6.6vw, 5.25rem)', '--line-height-arabic': '1.75', '--line-height-arabic-body': '1.75',
|
|
112
116
|
'--line-height-arabic-display': '1.14', '--tracking-arabic': '0', '--tracking-arabic-display': '0', '--sidebar-width': '17rem',
|
|
113
117
|
'--control-height-sm': '2rem', '--sidebar-width-collapsed': '4rem', '--sidebar-min-block-size': '30rem', '--sidebar-rail-size': '8px',
|
|
114
|
-
'--button-height': '40px', '--card-padding': '24px', '--overlay': 'rgba(
|
|
118
|
+
'--button-height': '40px', '--card-padding': '24px', '--overlay': 'rgba(41,39,37,0.72)', '--modal-surface': '#FAF8F3', '--popover-surface': '#FAF8F3',
|
|
115
119
|
}
|
|
116
120
|
const declarations = Object.entries(themeValues).map(([token, value]) => ` ${token}: ${value};`).join('\n')
|
|
117
|
-
const css = `[data-theme="${id}"] {\n color-scheme:
|
|
121
|
+
const css = `[data-theme="${id}"] {\n color-scheme: light;\n${declarations}\n}\n\n[data-theme="${id}"][data-color-mode="dark"] {\n color-scheme: dark;\n --background: #252523;\n --foreground: #EEEAE1;\n --card: #2D2D2A;\n --card-foreground: #EEEAE1;\n --primary: #B8C0B8;\n --primary-foreground: #252523;\n --secondary: #3A3935;\n --secondary-foreground: #EEEAE1;\n --muted: #33322F;\n --muted-foreground: #B9B4AA;\n --border: rgb(238 234 225 / 18%);\n --input: rgb(238 234 225 / 28%);\n --surface: rgb(238 234 225 / 4%);\n --surface-strong: rgb(238 234 225 / 8%);\n --surface-elevated: #2D2D2A;\n --overlay: rgb(37 37 35 / 82%);\n --modal-surface: #2D2D2A;\n --popover-surface: #2D2D2A;\n}\n\n[data-theme="${id}"] :where(:lang(ar), [lang|="ar"]) {\n font-family: var(--font-arabic-body);\n line-height: var(--line-height-arabic-body);\n letter-spacing: var(--tracking-arabic);\n text-transform: none;\n}\n`
|
|
118
122
|
const policy = {
|
|
119
123
|
'$schema': 'https://utopia-studio.co/design-system/theme.schema.json', id, name, type: 'theme-policy', locked: false,
|
|
120
|
-
|
|
124
|
+
motionProfile: 'precise',
|
|
125
|
+
summary: `${name} neutral theme scaffold mapped to the Ceramic semantic contract; replace placeholder primitives before release.`,
|
|
121
126
|
sourceFiles: [`packages/design-system/src/themes/${id}.css`, `packages/design-system/src/manifests/theme-${id}.json`],
|
|
122
|
-
brandPrimitives: { colors: { background: themeValues['--background'], foreground: themeValues['--foreground'], accent: themeValues['--primary'] }, typography: { latin: '
|
|
127
|
+
brandPrimitives: { colors: { background: themeValues['--background'], foreground: themeValues['--foreground'], accent: themeValues['--primary'] }, typography: { latin: 'Unassigned', arabic: 'IBM Plex Sans Arabic' }, geometry: { controlRadius: themeValues['--radius-control'], surfaceRadius: themeValues['--radius-surface'] } },
|
|
123
128
|
semanticMappings: Object.fromEntries(Object.entries(themeValues).map(([token, value]) => [token, value])),
|
|
124
|
-
visualPolicy: { tone: ['
|
|
129
|
+
visualPolicy: { tone: ['neutral', 'restrained', 'unfinished-by-design'], allow: ['semantic color roles', 'logical layout properties', 'theme-owned brand expression'], avoid: ['shipping placeholder primitives', 'generic SaaS gradients', 'default purple accents', 'component-specific color literals', 'left/right-only layout APIs'] },
|
|
125
130
|
arabicFriendly: { direction: 'Support dir="rtl" with logical CSS properties.', typography: 'Use the declared Arabic family without Latin tracking or casing.' },
|
|
126
131
|
translations: { ar: { summary: `ثيم ${name} مبني على عقد Ceramic الدلالي.`, iconPolicy: { description: 'استخدم أيقونات واضحة ومحايدة الاتجاه، واعكس الأيقونات الاتجاهية في RTL.', allow: ['أيقونات Lucide', 'أيقونات محايدة الاتجاه', 'انعكاس الأيقونات الاتجاهية في RTL'] } } },
|
|
127
132
|
}
|
|
128
133
|
const entry = {
|
|
129
134
|
id, name, shortName: name, locked: false, role: 'brand theme', description: policy.summary,
|
|
135
|
+
motionProfile: 'precise',
|
|
130
136
|
bestFor: [`${name} product interfaces`], principles: policy.visualPolicy.tone,
|
|
131
137
|
translations: { ar: { name, role: 'ثيم علامة', description: `ثيم ${name} مبني على عقد Ceramic الدلالي.`, bestFor: [`واجهات ${name}`], principles: ['وضوح', 'اتساق', 'دعم العربية'] } },
|
|
132
|
-
policyManifest: `packages/design-system/src/manifests/theme-${id}.json`, css: `packages/design-system/src/themes/${id}.css`, iconSystem: '
|
|
138
|
+
policyManifest: `packages/design-system/src/manifests/theme-${id}.json`, css: `packages/design-system/src/themes/${id}.css`, iconSystem: 'unassigned', values: themeValues,
|
|
133
139
|
}
|
|
134
140
|
|
|
135
141
|
write(cssPath, css)
|
|
@@ -157,15 +163,16 @@ function init() {
|
|
|
157
163
|
2. Run \`npm run ceramic -- search <intent> --json\`, then inspect the selected component or template.
|
|
158
164
|
3. Prefer \`@utopia-studio-design/design-system\` exports over raw shadcn/ui source.
|
|
159
165
|
4. Components consume semantic tokens only. The active theme is \`${theme}\`.
|
|
160
|
-
5.
|
|
161
|
-
6.
|
|
162
|
-
7.
|
|
166
|
+
5. Request semantic motion only; inspect \`npm run ceramic -- motion ${getTheme(theme)?.motionProfile} --json\` before choosing an application runtime adapter.
|
|
167
|
+
6. Read \`npm run ceramic -- docs arabic-friendly --dense\` before Arabic or RTL work.
|
|
168
|
+
7. Never invent component props, import paths, tokens, Arabic product copy, or left/right-only APIs.
|
|
169
|
+
8. Validate with \`npm run ceramic:doctor\` before handoff.
|
|
163
170
|
`
|
|
164
171
|
write(join(target, 'AGENTS.md'), rules)
|
|
165
172
|
write(join(target, 'CLAUDE.md'), rules)
|
|
166
173
|
write(join(target, '.cursor/rules/ceramic-design-system.mdc'), `---\ndescription: Ceramic Design System rules\nalwaysApply: true\n---\n\n${rules}`)
|
|
167
174
|
write(join(target, '.github/copilot-instructions.md'), rules)
|
|
168
|
-
write(join(target, '.ceramic/config.json'), `${JSON.stringify({ apiVersion: 1, theme, arabicFriendly: true, source: '@utopia-studio-design/design-system' }, null, 2)}\n`)
|
|
175
|
+
write(join(target, '.ceramic/config.json'), `${JSON.stringify({ apiVersion: 1, theme, motionProfile: getTheme(theme)?.motionProfile, arabicFriendly: true, source: '@utopia-studio-design/design-system' }, null, 2)}\n`)
|
|
169
176
|
write(join(target, '.mcp.json'), `${JSON.stringify({ mcpServers: { ceramic: mcpLaunch } }, null, 2)}\n`)
|
|
170
177
|
output('init-result', { ok: true, target, theme, files: ['AGENTS.md', 'CLAUDE.md', '.cursor/rules/ceramic-design-system.mdc', '.github/copilot-instructions.md', '.ceramic/config.json', '.mcp.json'] },
|
|
171
178
|
(data) => `Ceramic initialized in ${data.target}.\nCreated ${data.files.join(', ')}.`)
|
|
@@ -226,13 +233,40 @@ function copyTemplateProject(entry) {
|
|
|
226
233
|
},
|
|
227
234
|
devDependencies: { '@utopia-studio-design/design-system-cli': cliDependency, '@vitejs/plugin-react': '^4.3.4', typescript: '^5.7.2', vite: '^6.0.7' },
|
|
228
235
|
}, null, 2)}\n`)
|
|
229
|
-
write(join(target, '.ceramic/config.json'), `${JSON.stringify({ apiVersion: 1, theme, arabicFriendly: true, source: '@utopia-studio-design/design-system' }, null, 2)}\n`)
|
|
236
|
+
write(join(target, '.ceramic/config.json'), `${JSON.stringify({ apiVersion: 1, theme, motionProfile: getTheme(theme)?.motionProfile, motionAdapter: 'framer-motion', arabicFriendly: true, source: '@utopia-studio-design/design-system' }, null, 2)}\n`)
|
|
230
237
|
write(join(target, 'vite.config.ts'), `import { resolve } from 'node:path'\nimport react from '@vitejs/plugin-react'\nimport { defineConfig } from 'vite'\n\nexport default defineConfig({\n plugins: [react()],\n build: {\n rollupOptions: {\n input: {\n home: resolve('index.html'),\n product: resolve('product/index.html'),\n agents: resolve('agents/index.html'),\n integrations: resolve('integrations/index.html'),\n integrationDetail: resolve('integrations/slack/index.html'),\n customers: resolve('customers/index.html'),\n customerStory: resolve('customers/aster-labs/index.html'),\n pricing: resolve('pricing/index.html'),\n changelog: resolve('changelog/index.html'),\n contactSales: resolve('contact-sales/index.html'),\n },\n },\n },\n})\n`)
|
|
231
238
|
const existingReadme = readFileSync(join(target, 'README.md'), 'utf8')
|
|
232
239
|
write(join(target, 'README.md'), `# Generated Ceramic SaaS website\n\nActive theme: \`${theme}\`\n\n\`\`\`sh\nnpm install\nnpm run dev\n\`\`\`\n\nOpen \`http://localhost:5173/?seed=1974341818\`. All ten page entries share the same seed, theme, and locale state.\n\n${existingReadme}`)
|
|
233
240
|
output('template-copy-result', { ok: true, id: entry.id, target, theme, pages: entry.pages ?? [] }, (data) => `Generated ${data.id} with theme ${data.theme} in ${data.target}.\nNext: cd ${data.target} && npm install && npm run dev`)
|
|
234
241
|
}
|
|
235
242
|
|
|
243
|
+
function validateCommunityTemplate({ submit = false } = {}) {
|
|
244
|
+
const target = resolve(values[1] ?? process.cwd())
|
|
245
|
+
const result = validateTemplateSubmission(target)
|
|
246
|
+
if (!result.ok) {
|
|
247
|
+
if (json) return fail('Community template validation failed.', 'ERR_TEMPLATE_SUBMISSION', result.errors)
|
|
248
|
+
console.error(`Community template validation failed with ${result.errors.length} error(s):`)
|
|
249
|
+
for (const item of result.errors) console.error(`- ${item.code}: ${item.message}${item.path ? ` (${item.path})` : ''}`)
|
|
250
|
+
for (const item of result.warnings) console.error(`- warning ${item.code}: ${item.message}`)
|
|
251
|
+
process.exitCode = 1
|
|
252
|
+
return
|
|
253
|
+
}
|
|
254
|
+
const submissionUrl = submit ? createTemplateSubmissionUrl(result) : null
|
|
255
|
+
output(submit ? 'template-submit-result' : 'template-validation-result', {
|
|
256
|
+
ok: true,
|
|
257
|
+
id: result.manifest.id,
|
|
258
|
+
version: result.manifest.version,
|
|
259
|
+
repository: result.manifest.repository,
|
|
260
|
+
files: result.files.length,
|
|
261
|
+
warnings: result.warnings,
|
|
262
|
+
submissionUrl,
|
|
263
|
+
}, (data) => [
|
|
264
|
+
`Community template ${data.id}@${data.version} passed validation (${data.files} files).`,
|
|
265
|
+
...data.warnings.map((item) => `Warning ${item.code}: ${item.message}`),
|
|
266
|
+
...(data.submissionUrl ? [`Submit for review: ${data.submissionUrl}`] : []),
|
|
267
|
+
].join('\n'))
|
|
268
|
+
}
|
|
269
|
+
|
|
236
270
|
if (command === 'help' || args.includes('--help')) help()
|
|
237
271
|
else if (command === 'init') init()
|
|
238
272
|
else if (command === 'manifest') output('manifest', capabilityManifest())
|
|
@@ -246,7 +280,9 @@ else if (command === 'search') {
|
|
|
246
280
|
else { const item = getComponent(name); item ? output('component', item, formatComponent) : fail(`Unknown component "${name}".`, 'ERR_COMPONENT', search(name).slice(0, 5).map((result) => result.id)) }
|
|
247
281
|
} else if (command === 'template') {
|
|
248
282
|
const name = values[0]
|
|
249
|
-
if (
|
|
283
|
+
if (name === 'validate') validateCommunityTemplate()
|
|
284
|
+
else if (name === 'submit') validateCommunityTemplate({ submit: true })
|
|
285
|
+
else if (!name || args.includes('--list')) output('template-list', listTemplates(), (rows) => rows.map((item) => `${item.id}|${item.category}|${item.title}|${item.purpose}`).join('\n'))
|
|
250
286
|
else {
|
|
251
287
|
const item = getTemplate(name)
|
|
252
288
|
if (!item) fail(`Unknown template "${name}".`, 'ERR_TEMPLATE')
|
|
@@ -258,11 +294,15 @@ else if (command === 'search') {
|
|
|
258
294
|
if (name === 'create') scaffoldTheme()
|
|
259
295
|
else if (!name || args.includes('--list')) output('theme-list', listThemes(), (rows) => rows.map((item) => `${item.id}|${item.name}|${item.role}|${item.policyManifest}`).join('\n'))
|
|
260
296
|
else { const item = getTheme(name); item ? output('theme', item) : fail(`Unknown theme "${name}".`, 'ERR_THEME') }
|
|
297
|
+
} else if (command === 'motion') {
|
|
298
|
+
const name = values[0]
|
|
299
|
+
if (!name || args.includes('--list')) output('motion-profile-list', listMotionProfiles(), (rows) => rows.map((item) => `${item.id}|${item.themes.join(',')}|${item.label}|${item.description}`).join('\n'))
|
|
300
|
+
else { const item = getMotionProfile(name); item ? output('motion-profile', item) : fail(`Unknown motion profile "${name}".`, 'ERR_MOTION_PROFILE', listMotionProfiles().map((item) => item.id)) }
|
|
261
301
|
} else if (command === 'docs') {
|
|
262
302
|
const topic = values[0]
|
|
263
303
|
if (!topic || args.includes('--list')) output('docs-list', listDocs(), (rows) => rows.join('\n'))
|
|
264
304
|
else { const item = getDoc(topic); item ? output('docs', item, (doc) => dense ? doc.content.replace(/\n{2,}/g, '\n').trim() : doc.content) : fail(`Unknown docs topic "${topic}".`, 'ERR_DOCS', listDocs()) }
|
|
265
305
|
} else if (command === 'doctor') {
|
|
266
|
-
const result = repositoryDoctor(); output('doctor-result', result, (data) => data.ok ? `Ceramic doctor passed. ${data.checks.components} components, ${data.checks.templates} templates, ${data.checks.themes} themes, MCP ready.` : `Ceramic doctor failed: ${data.missing.join(', ')}`); if (!result.ok) process.exitCode = 1
|
|
306
|
+
const result = repositoryDoctor(); output('doctor-result', result, (data) => data.ok ? `Ceramic doctor passed. ${data.checks.components} components, ${data.checks.templates} templates, ${data.checks.themes} themes, ${data.checks.motionProfiles} motion profiles, MCP ready.` : `Ceramic doctor failed: ${data.missing.join(', ')}`); if (!result.ok) process.exitCode = 1
|
|
267
307
|
} else if (command === 'mcp') runMcp()
|
|
268
308
|
else fail(`Unknown command "${command}".`, 'ERR_COMMAND', capabilityManifest().commands.map((item) => item.name))
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# Share a community template
|
|
2
|
+
|
|
3
|
+
Ceramic community templates stay in the creator's public GitHub repository. Ceramic reviews a pinned version before listing it in the catalog; submitting a repository does not execute its code on the documentation website.
|
|
4
|
+
|
|
5
|
+
## What the designer prepares
|
|
6
|
+
|
|
7
|
+
1. A public GitHub repository containing the runnable template source.
|
|
8
|
+
2. A working preview URL when possible.
|
|
9
|
+
3. A license covering the source and included assets.
|
|
10
|
+
4. A `ceramic.template.json` file at the repository root.
|
|
11
|
+
5. Responsive, dark-mode, keyboard, and RTL coverage.
|
|
12
|
+
|
|
13
|
+
Do not include credentials, `.env` files, private keys, production customer data, unlicensed media, or install lifecycle scripts.
|
|
14
|
+
|
|
15
|
+
## Manifest
|
|
16
|
+
|
|
17
|
+
Create `ceramic.template.json` in the repository root:
|
|
18
|
+
|
|
19
|
+
```json
|
|
20
|
+
{
|
|
21
|
+
"$schema": "https://raw.githubusercontent.com/The-Utopia-Studio/Ceramic-Design-System/main/packages/design-system/src/manifests/community-template.schema.json",
|
|
22
|
+
"schemaVersion": 1,
|
|
23
|
+
"id": "operations-dashboard",
|
|
24
|
+
"name": "Operations Dashboard",
|
|
25
|
+
"version": "1.0.0",
|
|
26
|
+
"summary": "A responsive operations dashboard for queues, incidents, and team handoffs.",
|
|
27
|
+
"category": "dashboard",
|
|
28
|
+
"author": {
|
|
29
|
+
"name": "Example Studio",
|
|
30
|
+
"url": "https://example.com"
|
|
31
|
+
},
|
|
32
|
+
"license": "MIT",
|
|
33
|
+
"repository": "https://github.com/example/operations-dashboard",
|
|
34
|
+
"preview": "https://operations-dashboard.example.com",
|
|
35
|
+
"designSystem": {
|
|
36
|
+
"package": "@utopia-studio-design/design-system",
|
|
37
|
+
"version": "^0.4.4"
|
|
38
|
+
},
|
|
39
|
+
"entry": "src/main.tsx",
|
|
40
|
+
"files": ["src", "index.html", "package.json"],
|
|
41
|
+
"themes": ["utopia-default", "dextrum"],
|
|
42
|
+
"features": {
|
|
43
|
+
"responsive": true,
|
|
44
|
+
"darkMode": true,
|
|
45
|
+
"rtl": true
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Validate
|
|
51
|
+
|
|
52
|
+
Install the public CLI and run validation from the template repository root:
|
|
53
|
+
|
|
54
|
+
```sh
|
|
55
|
+
npm install -D @utopia-studio-design/design-system-cli
|
|
56
|
+
npx utopia-ds template validate .
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Warnings identify raw color values or physical left/right CSS. Explain intentional theme-owned media values in the review request. Validation errors must be fixed before submission.
|
|
60
|
+
|
|
61
|
+
## Submit
|
|
62
|
+
|
|
63
|
+
Either use the form in the Ceramic Templates page or run:
|
|
64
|
+
|
|
65
|
+
```sh
|
|
66
|
+
npx utopia-ds template submit .
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
The command returns a GitHub review URL populated from the manifest. Open it, verify the declaration, and submit the issue. Pin a release or commit for review instead of asking maintainers to review a moving branch.
|
|
70
|
+
|
|
71
|
+
## Review and publication
|
|
72
|
+
|
|
73
|
+
Ceramic maintainers verify:
|
|
74
|
+
|
|
75
|
+
- public component APIs and package imports;
|
|
76
|
+
- semantic-token usage and theme compatibility;
|
|
77
|
+
- responsive, dark-mode, keyboard, reduced-motion, and RTL behavior;
|
|
78
|
+
- licensing, demo-data labeling, and asset ownership;
|
|
79
|
+
- the absence of secrets and install lifecycle scripts.
|
|
80
|
+
|
|
81
|
+
Approved submissions receive a catalog record under **Community**. The creator continues to own the source and releases. A new published version requires a new review; an existing version is never overwritten.
|
|
82
|
+
|
|
83
|
+
## Instructions to send to a designer
|
|
84
|
+
|
|
85
|
+
> Put the runnable template in a public GitHub repository and deploy a preview if possible. Add `ceramic.template.json` at the repository root using the example above. Confirm that the template uses `@utopia-studio-design/design-system`, semantic tokens, logical CSS properties, responsive layouts, dark mode, keyboard interaction, and RTL. Remove `.env` files, credentials, private keys, install scripts, private customer data, and unlicensed assets. Run `npx utopia-ds template validate .`, fix every error, then run `npx utopia-ds template submit .` or use the Submit a template form on the Ceramic Templates page. Send the generated GitHub review request with a pinned release or commit.
|
package/data/docs/foundations.md
CHANGED
|
@@ -69,26 +69,46 @@ Arabic display sizing should follow the Latin display scale at about 95%, rather
|
|
|
69
69
|
|
|
70
70
|
## Motion Contract
|
|
71
71
|
|
|
72
|
-
- Ceramic
|
|
72
|
+
- Ceramic separates motion policy from motion execution: the theme selects a personality, the application selects an adapter, and components request semantic motion.
|
|
73
|
+
- Component timing continues to use five semantic patterns: `press`, `page`, `expand`, `reveal`, and `icon`.
|
|
74
|
+
- Runtime recipes use four engine-neutral intents: `feedback`, `page`, `surface`, and `layout`.
|
|
73
75
|
- Components consume `--motion-duration-*` and `--motion-ease-*` roles rather than hardcoded milliseconds or easing curves.
|
|
74
|
-
- `MotionProvider` sets the subtree
|
|
76
|
+
- `MotionProvider` sets the theme profile and runtime adapter for a subtree. Motion-aware components expose `motion?: boolean` for a local override.
|
|
75
77
|
- `motion={false}` and `prefers-reduced-motion: reduce` disable decorative movement while preserving state changes and accessibility.
|
|
76
78
|
- Directional motion follows logical inline start/end and mirrors in RTL when direction carries meaning.
|
|
77
79
|
- Icon motion follows the action: a bell swings from its top, download moves downward, and copy snaps once. Do not apply a generic bounce.
|
|
80
|
+
- `utopia-default` uses the `ceremonial` profile, `dextrum` uses `swift`, and `barrier-intelligence` uses `precise`.
|
|
81
|
+
- WAAPI is built in. Motion for React, Anime.js, and GSAP are optional peer adapters, so unused engines do not need to ship with an application.
|
|
78
82
|
|
|
79
83
|
```tsx
|
|
80
|
-
import { MotionProvider } from '@utopia-studio-design/design-system/Motion'
|
|
84
|
+
import { getMotionThemeProfile, MotionProvider } from '@utopia-studio-design/design-system/Motion'
|
|
85
|
+
import { animeMotionAdapter } from '@utopia-studio-design/design-system/MotionAnime'
|
|
81
86
|
import { Button } from '@utopia-studio-design/design-system/Button'
|
|
82
87
|
|
|
83
88
|
export function App() {
|
|
84
89
|
return (
|
|
85
|
-
<MotionProvider
|
|
90
|
+
<MotionProvider
|
|
91
|
+
adapter={animeMotionAdapter}
|
|
92
|
+
themeProfile={getMotionThemeProfile(activeTheme.motionProfile)}
|
|
93
|
+
>
|
|
86
94
|
<Button motion={false}>Static local action</Button>
|
|
87
95
|
</MotionProvider>
|
|
88
96
|
)
|
|
89
97
|
}
|
|
90
98
|
```
|
|
91
99
|
|
|
100
|
+
Install only the engine selected by the application:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
npm install animejs
|
|
104
|
+
# or
|
|
105
|
+
npm install gsap
|
|
106
|
+
# or
|
|
107
|
+
npm install framer-motion
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
The engine-neutral registry is published as `manifests/motion-profiles.json`. It is the runtime, CLI, and MCP source of truth for timing, easing, semantic recipes, orchestration, theme mappings, and reduced-motion behavior. Resolve custom themes through their declared `motionProfile`; reduced motion applies each semantic final state immediately and clears transform and filter effects. Optional adapters should be loaded with `import()` when they are selected. Motion AI Kit and GSAP Skills may help agents author or audit engine-specific code, but they are authoring tools rather than runtime adapters.
|
|
111
|
+
|
|
92
112
|
## Elevation Contract
|
|
93
113
|
|
|
94
114
|
- Elevation is semantic hierarchy, not a mandatory shadow style.
|
|
@@ -194,7 +194,8 @@
|
|
|
194
194
|
"Skeleton",
|
|
195
195
|
"Spinner",
|
|
196
196
|
"Table",
|
|
197
|
-
"Typography"
|
|
197
|
+
"Typography",
|
|
198
|
+
"Utopia Wordmark Loader"
|
|
198
199
|
]
|
|
199
200
|
}
|
|
200
201
|
]
|
|
@@ -307,7 +308,8 @@
|
|
|
307
308
|
"Toggle Button",
|
|
308
309
|
"Toggle Button Group",
|
|
309
310
|
"Tooltip",
|
|
310
|
-
"Typography"
|
|
311
|
+
"Typography",
|
|
312
|
+
"Utopia Wordmark Loader"
|
|
311
313
|
],
|
|
312
314
|
"utopiaWrapped": [
|
|
313
315
|
"Accordion",
|
|
@@ -384,7 +386,8 @@
|
|
|
384
386
|
"Toggle Button",
|
|
385
387
|
"Toggle Button Group",
|
|
386
388
|
"Tooltip",
|
|
387
|
-
"Typography"
|
|
389
|
+
"Typography",
|
|
390
|
+
"Utopia Wordmark Loader"
|
|
388
391
|
],
|
|
389
392
|
"legacy": [
|
|
390
393
|
"Toast"
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://utopia-studio.co/design-system/community-template.schema.json",
|
|
4
|
+
"title": "Ceramic Community Template",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"additionalProperties": false,
|
|
7
|
+
"required": ["schemaVersion", "id", "name", "version", "summary", "category", "author", "license", "repository", "designSystem", "entry", "files", "themes", "features"],
|
|
8
|
+
"properties": {
|
|
9
|
+
"$schema": { "type": "string", "format": "uri" },
|
|
10
|
+
"schemaVersion": { "const": 1 },
|
|
11
|
+
"id": { "type": "string", "pattern": "^[a-z][a-z0-9-]*$" },
|
|
12
|
+
"name": { "type": "string", "minLength": 2 },
|
|
13
|
+
"version": { "type": "string", "pattern": "^\\d+\\.\\d+\\.\\d+(?:-[0-9A-Za-z.-]+)?$" },
|
|
14
|
+
"summary": { "type": "string", "minLength": 20 },
|
|
15
|
+
"category": { "type": "string", "enum": ["application", "dashboard", "website", "workflow", "commerce", "content", "other"] },
|
|
16
|
+
"author": {
|
|
17
|
+
"type": "object",
|
|
18
|
+
"additionalProperties": false,
|
|
19
|
+
"required": ["name"],
|
|
20
|
+
"properties": {
|
|
21
|
+
"name": { "type": "string", "minLength": 2 },
|
|
22
|
+
"url": { "type": "string", "format": "uri" }
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"license": { "type": "string", "minLength": 2 },
|
|
26
|
+
"repository": { "type": "string", "pattern": "^https://github\\.com/[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+/?$" },
|
|
27
|
+
"preview": { "type": "string", "format": "uri" },
|
|
28
|
+
"designSystem": {
|
|
29
|
+
"type": "object",
|
|
30
|
+
"additionalProperties": false,
|
|
31
|
+
"required": ["package", "version"],
|
|
32
|
+
"properties": {
|
|
33
|
+
"package": { "const": "@utopia-studio-design/design-system" },
|
|
34
|
+
"version": { "type": "string", "minLength": 1 }
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"entry": { "type": "string", "minLength": 1 },
|
|
38
|
+
"files": { "type": "array", "minItems": 1, "uniqueItems": true, "items": { "type": "string", "minLength": 1 } },
|
|
39
|
+
"themes": { "type": "array", "minItems": 1, "uniqueItems": true, "items": { "type": "string", "minLength": 1 } },
|
|
40
|
+
"features": {
|
|
41
|
+
"type": "object",
|
|
42
|
+
"additionalProperties": false,
|
|
43
|
+
"required": ["responsive", "darkMode", "rtl"],
|
|
44
|
+
"properties": {
|
|
45
|
+
"responsive": { "const": true },
|
|
46
|
+
"darkMode": { "const": true },
|
|
47
|
+
"rtl": { "const": true }
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -699,7 +699,10 @@
|
|
|
699
699
|
],
|
|
700
700
|
"ai": {
|
|
701
701
|
"props": {
|
|
702
|
-
"motion": "Optional boolean. Defaults to the MotionProvider policy and disables this component pattern when false."
|
|
702
|
+
"motion": "Optional boolean. Defaults to the MotionProvider policy and disables this component pattern when false.",
|
|
703
|
+
"contentAlign": "Use 'start' for leading-aligned labels, 'center' for the default action layout, or 'between' to pin endContent to the logical inline end.",
|
|
704
|
+
"startContent": "Optional leading icon or content slot with package-owned alignment.",
|
|
705
|
+
"endContent": "Optional trailing content slot. With contentAlign='between', it remains pinned while the label truncates."
|
|
703
706
|
}
|
|
704
707
|
}
|
|
705
708
|
},
|
|
@@ -2980,8 +2983,8 @@
|
|
|
2980
2983
|
"name": "Native Select",
|
|
2981
2984
|
"category": "Forms",
|
|
2982
2985
|
"status": "available",
|
|
2983
|
-
"packageImport": "import { NativeSelect, NativeSelectOptGroup, NativeSelectOption } from '@utopia-studio-design/design-system/
|
|
2984
|
-
"sourcePath": "packages/design-system/src/components/
|
|
2986
|
+
"packageImport": "import { NativeSelect, NativeSelectOptGroup, NativeSelectOption } from '@utopia-studio-design/design-system/Forms';",
|
|
2987
|
+
"sourcePath": "packages/design-system/src/components/Forms.tsx",
|
|
2985
2988
|
"shadcnFoundation": [
|
|
2986
2989
|
"native-select",
|
|
2987
2990
|
"shadcn/ui registry",
|
|
@@ -3008,10 +3011,12 @@
|
|
|
3008
3011
|
"avoidWhen": [
|
|
3009
3012
|
"Searchable or async option sets",
|
|
3010
3013
|
"Complex custom option rendering",
|
|
3014
|
+
"The opened option menu must match dark, light, or popover semantic tokens; use Select, SelectTrigger, SelectContent, and SelectItem",
|
|
3011
3015
|
"Product localization rules inside reusable component logic"
|
|
3012
3016
|
],
|
|
3013
3017
|
"neverInvent": [
|
|
3014
3018
|
"Product-owned option values or selection rules",
|
|
3019
|
+
"A promise that the opened native option menu can be fully themed; the OS or browser owns that surface",
|
|
3015
3020
|
"Production Arabic option labels not supplied by localization",
|
|
3016
3021
|
"Left/right-only layout rules",
|
|
3017
3022
|
"Utopia brand primitives inside reusable component logic"
|
|
@@ -3482,10 +3487,12 @@
|
|
|
3482
3487
|
"--background",
|
|
3483
3488
|
"--foreground",
|
|
3484
3489
|
"--muted-foreground",
|
|
3490
|
+
"--icon-size-sm",
|
|
3485
3491
|
"--button-height-sm"
|
|
3486
3492
|
],
|
|
3487
3493
|
"useWhen": [
|
|
3488
3494
|
"Compact option selection",
|
|
3495
|
+
"Dark, light, or branded surfaces where the opened menu must follow popover semantic tokens",
|
|
3489
3496
|
"Longer option sets than Radio Group",
|
|
3490
3497
|
"AI-generated forms that need explicit trigger, value, content, and item parts",
|
|
3491
3498
|
"Grouped option lists with labels",
|
|
@@ -3617,7 +3624,7 @@
|
|
|
3617
3624
|
"name": "Side Nav",
|
|
3618
3625
|
"category": "Navigation",
|
|
3619
3626
|
"status": "available",
|
|
3620
|
-
"packageImport": "import { PanelIcon, SideNav, SideNavCollapseButton, SideNavContent, SideNavHeading, SideNavItem, SideNavNestedGroup, SideNavNestedItems, SideNavSearch, SideNavSection } from '@utopia-studio-design/design-system/Navigation';",
|
|
3627
|
+
"packageImport": "import { PanelIcon, SideNav, SideNavAccount, SideNavAuxiliary, SideNavCollapseButton, SideNavCommand, SideNavContent, SideNavFooter, SideNavHeader, SideNavHeading, SideNavItem, SideNavMain, SideNavNestedGroup, SideNavNestedItems, SideNavSearch, SideNavSection, SideNavStatus, SideNavWorkspaceSwitcher } from '@utopia-studio-design/design-system/Navigation';",
|
|
3621
3628
|
"sourcePath": "packages/design-system/src/components/Navigation.tsx",
|
|
3622
3629
|
"shadcnFoundation": [
|
|
3623
3630
|
"sidebar",
|
|
@@ -3641,6 +3648,8 @@
|
|
|
3641
3648
|
"useWhen": [
|
|
3642
3649
|
"Persistent docs or app navigation",
|
|
3643
3650
|
"Grouped navigation links with current page state",
|
|
3651
|
+
"An application shell needs fixed header and footer regions with only the main navigation scrolling",
|
|
3652
|
+
"Search, AI commands, status, and account controls must adapt to expanded and collapsed states without app CSS overrides",
|
|
3644
3653
|
"A navigation surface needs package-owned content, search, section, nested group, item, heading, account, and collapse composition parts",
|
|
3645
3654
|
"A navigation surface should receive heading and search as SideNav props instead of hardcoding local wrappers",
|
|
3646
3655
|
"AI-generated app shells that need explicit section, heading, item, nested item, search, and collapse button parts"
|
|
@@ -3654,7 +3663,7 @@
|
|
|
3654
3663
|
"Raw shadcn theme colors",
|
|
3655
3664
|
"Left/right-only layout rules",
|
|
3656
3665
|
"Utopia brand primitives inside reusable component logic",
|
|
3657
|
-
"Local CSS-only search,
|
|
3666
|
+
"Local CSS-only search, command, account, footer, or content wrappers when the corresponding SideNav composition parts are available"
|
|
3658
3667
|
],
|
|
3659
3668
|
"ai": {
|
|
3660
3669
|
"props": {
|
|
@@ -3662,7 +3671,15 @@
|
|
|
3662
3671
|
"density": "Use default for app navigation; use compact for docs/library sidebars with dense catalog lists.",
|
|
3663
3672
|
"heading": "Optional SideNavHeading node rendered before search and navigation content.",
|
|
3664
3673
|
"search": "Optional SideNavSearch node rendered after heading and before navigation content.",
|
|
3665
|
-
"SideNavContent": "
|
|
3674
|
+
"SideNavContent": "Compatibility body region. Set layout='stack' for a flex column that supports an auto-positioned footer, or layout='grid' for the legacy grid flow.",
|
|
3675
|
+
"SideNavHeader": "Fixed header region for brand, workspace, and collapse controls.",
|
|
3676
|
+
"SideNavMain": "The only independently scrolling navigation region. It defaults to layout='stack' and fills remaining height.",
|
|
3677
|
+
"SideNavFooter": "Bottom-pinned, bounded footer region for status and account controls.",
|
|
3678
|
+
"SideNavCommand": "Full-width icon, label, and shortcut action. It derives collapsed state, accessible name, and tooltip title from SideNav context.",
|
|
3679
|
+
"SideNavAccount": "Stable avatar, single-line identity, and action layout. Use asChild or trigger to compose an app-owned popover/auth trigger.",
|
|
3680
|
+
"SideNavStatus": "Status indicator with label and optional description. Collapsed navigation retains the label for assistive technology and exposes a tooltip title.",
|
|
3681
|
+
"SideNavWorkspaceSwitcher": "Fixed region for an app-owned organization or workspace switcher.",
|
|
3682
|
+
"SideNavAuxiliary": "Fixed auxiliary action region outside the independently scrolling main navigation.",
|
|
3666
3683
|
"SideNavSearch": "Package-owned search field for filtering navigation lists without local input styling.",
|
|
3667
3684
|
"SideNavNestedGroup": "Groups a parent SideNavItem and its nested child collection.",
|
|
3668
3685
|
"SideNavNestedItems": "Indented child item collection using logical inline-start spacing and border.",
|
|
@@ -3823,6 +3840,7 @@
|
|
|
3823
3840
|
],
|
|
3824
3841
|
"ai": {
|
|
3825
3842
|
"props": {
|
|
3843
|
+
"asChild": "Use with a framework-native link such as Next.js Link so the link remains the single interactive element while SideNavItem supplies styling and state.",
|
|
3826
3844
|
"icon": "Optional leading visual slot. Omit for text-only rows; do not add placeholder marks.",
|
|
3827
3845
|
"depth": "Use 0 for root rows, 1 for items inside a top-level section, 2 for nested section children, and 3 only for rare deep navigation.",
|
|
3828
3846
|
"badge": "Optional trailing count or status pill aligned to the inline end.",
|
|
@@ -4089,6 +4107,54 @@
|
|
|
4089
4107
|
"arabicFriendly": "Loading labels come from product localization. The spinner is direction-neutral and must not be mirrored."
|
|
4090
4108
|
}
|
|
4091
4109
|
},
|
|
4110
|
+
{
|
|
4111
|
+
"name": "Utopia Wordmark Loader",
|
|
4112
|
+
"category": "Data Display",
|
|
4113
|
+
"status": "available",
|
|
4114
|
+
"packageImport": "import { UtopiaWordmarkLoader } from '@utopia-studio-design/design-system/UtopiaWordmarkLoader';",
|
|
4115
|
+
"sourcePath": "packages/design-system/src/components/UtopiaWordmarkLoader.tsx",
|
|
4116
|
+
"shadcnFoundation": [
|
|
4117
|
+
"spinner status semantics",
|
|
4118
|
+
"full-screen loading state"
|
|
4119
|
+
],
|
|
4120
|
+
"fallbackToShadcn": "spinner",
|
|
4121
|
+
"requiredTokens": [
|
|
4122
|
+
"--brand-loader-background",
|
|
4123
|
+
"--brand-loader-accent",
|
|
4124
|
+
"--brand-loader-foreground",
|
|
4125
|
+
"--brand-loader-size",
|
|
4126
|
+
"--brand-loader-duration-enter",
|
|
4127
|
+
"--brand-loader-duration-cycle",
|
|
4128
|
+
"--brand-loader-duration-stagger",
|
|
4129
|
+
"--brand-loader-duration-exit",
|
|
4130
|
+
"--brand-loader-distance-exit"
|
|
4131
|
+
],
|
|
4132
|
+
"useWhen": [
|
|
4133
|
+
"A Utopia product needs a branded full-page or contained loading transition",
|
|
4134
|
+
"Workspace or route loading should end as soon as the consumer loading state resolves",
|
|
4135
|
+
"The Utopia Default theme is active and the stacked studio wordmark is appropriate"
|
|
4136
|
+
],
|
|
4137
|
+
"avoidWhen": [
|
|
4138
|
+
"Short inline pending states; use Spinner",
|
|
4139
|
+
"Layout-preserving content loading; use Skeleton",
|
|
4140
|
+
"A non-Utopia product theme needs its own brand mark"
|
|
4141
|
+
],
|
|
4142
|
+
"neverInvent": [
|
|
4143
|
+
"A timer that delays navigation after the real loading state resolves",
|
|
4144
|
+
"Left/right-only layout rules",
|
|
4145
|
+
"Utopia brand primitives outside this explicitly branded component contract"
|
|
4146
|
+
],
|
|
4147
|
+
"ai": {
|
|
4148
|
+
"props": {
|
|
4149
|
+
"label": "Required localized loading text announced through role=status.",
|
|
4150
|
+
"phase": "Use intro while loading and exit after the consumer loading state resolves.",
|
|
4151
|
+
"fullscreen": "Defaults to true. Set false for a contained loading region.",
|
|
4152
|
+
"size": "sm is the production default; md and lg are available for larger brand moments.",
|
|
4153
|
+
"onExitComplete": "Called after the exit motion so the consumer can unmount the overlay."
|
|
4154
|
+
},
|
|
4155
|
+
"arabicFriendly": "The mark is direction-neutral and must not mirror. The localized status label is visually hidden and remains consumer-owned."
|
|
4156
|
+
}
|
|
4157
|
+
},
|
|
4092
4158
|
{
|
|
4093
4159
|
"name": "Switch",
|
|
4094
4160
|
"category": "Forms",
|