@utopia-studio-design/design-system-cli 0.3.4 → 0.4.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 +1 -1
- package/bin/utopia-ds.mjs +35 -2
- package/data/docs/foundations.md +29 -0
- package/data/docs/guide.md +22 -0
- package/data/docs/observability.md +165 -0
- package/data/docs/quick-start-ai.md +34 -2
- package/data/manifests/catalog.json +9 -0
- package/data/manifests/components.json +84 -6
- package/lib/api.mjs +1 -1
- package/lib/telemetry.mjs +74 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ The same structured API powers terminal output, JSON automation, generated agent
|
|
|
4
4
|
|
|
5
5
|
```sh
|
|
6
6
|
npm install -D @utopia-studio-design/design-system-cli
|
|
7
|
-
npx utopia-ds init
|
|
7
|
+
npx utopia-ds init . --theme utopia-default --yes
|
|
8
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
|
package/bin/utopia-ds.mjs
CHANGED
|
@@ -7,13 +7,36 @@ import {
|
|
|
7
7
|
listComponents, listDocs, listMotionProfiles, listTemplates, listThemes, mcpLaunch, repositoryDoctor, search,
|
|
8
8
|
} from '../lib/api.mjs'
|
|
9
9
|
import { createTemplateSubmissionUrl, validateTemplateSubmission } from '../lib/template-submission.mjs'
|
|
10
|
+
import { installCliTelemetry } from '../lib/telemetry.mjs'
|
|
10
11
|
|
|
11
12
|
const args = process.argv.slice(2)
|
|
12
13
|
const command = args.find((arg) => !arg.startsWith('--')) ?? 'help'
|
|
13
14
|
const commandIndex = args.indexOf(command)
|
|
14
|
-
const
|
|
15
|
+
const optionsWithValues = new Set(['--copy', '--theme'])
|
|
16
|
+
const values = commandIndex >= 0 ? positionalValues(args.slice(commandIndex + 1)) : []
|
|
15
17
|
const json = args.includes('--json')
|
|
16
18
|
const dense = args.includes('--dense')
|
|
19
|
+
const cliVersion = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version
|
|
20
|
+
let telemetryErrorCode
|
|
21
|
+
|
|
22
|
+
installCliTelemetry({
|
|
23
|
+
command,
|
|
24
|
+
packageVersion: cliVersion,
|
|
25
|
+
getErrorCode: () => telemetryErrorCode,
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
function positionalValues(input) {
|
|
29
|
+
const positionals = []
|
|
30
|
+
for (let index = 0; index < input.length; index += 1) {
|
|
31
|
+
const argument = input[index]
|
|
32
|
+
if (optionsWithValues.has(argument)) {
|
|
33
|
+
index += 1
|
|
34
|
+
continue
|
|
35
|
+
}
|
|
36
|
+
if (!argument.startsWith('--')) positionals.push(argument)
|
|
37
|
+
}
|
|
38
|
+
return positionals
|
|
39
|
+
}
|
|
17
40
|
|
|
18
41
|
function output(type, data, format = null) {
|
|
19
42
|
if (json) return console.log(JSON.stringify(envelope(type, data), null, 2))
|
|
@@ -22,6 +45,7 @@ function output(type, data, format = null) {
|
|
|
22
45
|
}
|
|
23
46
|
|
|
24
47
|
function fail(message, code = 'ERR_INPUT', suggestions = []) {
|
|
48
|
+
telemetryErrorCode = code
|
|
25
49
|
if (json) console.error(JSON.stringify({ apiVersion: 1, error: message, code, suggestions }, null, 2))
|
|
26
50
|
else console.error(message)
|
|
27
51
|
process.exitCode = 1
|
|
@@ -29,7 +53,13 @@ function fail(message, code = 'ERR_INPUT', suggestions = []) {
|
|
|
29
53
|
|
|
30
54
|
function argAfter(flag, fallback) {
|
|
31
55
|
const index = args.indexOf(flag)
|
|
32
|
-
|
|
56
|
+
if (index < 0) return fallback
|
|
57
|
+
const value = args[index + 1]
|
|
58
|
+
if (!value || value.startsWith('--')) {
|
|
59
|
+
fail(`${flag} requires a value.`, 'ERR_ARGUMENT')
|
|
60
|
+
return null
|
|
61
|
+
}
|
|
62
|
+
return value
|
|
33
63
|
}
|
|
34
64
|
|
|
35
65
|
function help() {
|
|
@@ -150,6 +180,7 @@ function scaffoldTheme() {
|
|
|
150
180
|
function init() {
|
|
151
181
|
const target = resolve(values[0] ?? process.cwd())
|
|
152
182
|
const theme = argAfter('--theme', 'utopia-default')
|
|
183
|
+
if (!theme) return
|
|
153
184
|
if (!getTheme(theme)) return fail(`Unknown theme "${theme}".`, 'ERR_THEME', listThemes().map((item) => item.id))
|
|
154
185
|
const pkgPath = join(target, 'package.json')
|
|
155
186
|
if (!existsSync(pkgPath)) return fail(`No package.json found in ${target}.`, 'ERR_PROJECT')
|
|
@@ -186,8 +217,10 @@ function runMcp() {
|
|
|
186
217
|
function copyTemplateProject(entry) {
|
|
187
218
|
if (!entry.bundlePath) return fail(`Template "${entry.id}" is a blueprint contract and has no runnable bundle.`, 'ERR_TEMPLATE_BUNDLE')
|
|
188
219
|
const requestedTarget = argAfter('--copy', entry.id.replace(/^template-/, ''))
|
|
220
|
+
if (!requestedTarget) return
|
|
189
221
|
const target = resolve(requestedTarget)
|
|
190
222
|
const theme = argAfter('--theme', 'utopia-default')
|
|
223
|
+
if (!theme) return
|
|
191
224
|
if (!getTheme(theme)) return fail(`Unknown theme "${theme}".`, 'ERR_THEME', listThemes().map((item) => item.id))
|
|
192
225
|
if (existsSync(target) && !args.includes('--force')) return fail(`Target already exists: ${target}. Pass --force to overwrite it.`, 'ERR_TARGET_EXISTS')
|
|
193
226
|
const source = resolve(dirname(new URL(import.meta.url).pathname), '..', 'data', entry.bundlePath)
|
package/data/docs/foundations.md
CHANGED
|
@@ -74,6 +74,7 @@ Arabic display sizing should follow the Latin display scale at about 95%, rather
|
|
|
74
74
|
- Runtime recipes use four engine-neutral intents: `feedback`, `page`, `surface`, and `layout`.
|
|
75
75
|
- Components consume `--motion-duration-*` and `--motion-ease-*` roles rather than hardcoded milliseconds or easing curves.
|
|
76
76
|
- `MotionProvider` sets the theme profile and runtime adapter for a subtree. Motion-aware components expose `motion?: boolean` for a local override.
|
|
77
|
+
- `MotionProvider asChild` applies that policy to exactly one semantic subtree root without adding a wrapper. The consumer root must accept `className`, `style`, `data-*`, and its children.
|
|
77
78
|
- `motion={false}` and `prefers-reduced-motion: reduce` disable decorative movement while preserving state changes and accessibility.
|
|
78
79
|
- Directional motion follows logical inline start/end and mirrors in RTL when direction carries meaning.
|
|
79
80
|
- Icon motion follows the action: a bell swings from its top, download moves downward, and copy snaps once. Do not apply a generic bounce.
|
|
@@ -121,9 +122,37 @@ The engine-neutral registry is published as `manifests/motion-profiles.json`. It
|
|
|
121
122
|
- Core owns icon slots, icon-only controls, labels, and accessibility.
|
|
122
123
|
- Theme manifests own icon philosophy and icon style.
|
|
123
124
|
- Use `lucide-react` as the default shadcn/ui icon baseline for examples and previews.
|
|
125
|
+
- Use `PhosphorIcon` when a product selects the Phosphor family. Import individual
|
|
126
|
+
`*Icon` exports from `@phosphor-icons/react` so bundlers can tree-shake unused icons.
|
|
127
|
+
- `PhosphorIcon` uses `currentColor` and `--icon-size-xs|sm|md`; do not pass raw
|
|
128
|
+
color or pixel sizes.
|
|
129
|
+
- Mark visual-only icons with `decorative`. Give meaningful standalone icons a
|
|
130
|
+
localized `label`. Put interactive icons inside `IconButton` and label the button.
|
|
124
131
|
- Mirror arrows and chevrons when they mean previous/next, open/close, or inline movement.
|
|
125
132
|
- Do not mirror direction-neutral icons such as settings, add, download, home, camera, or panel icons unless a theme says otherwise.
|
|
126
133
|
|
|
134
|
+
```tsx
|
|
135
|
+
import { ArrowRightIcon, HouseIcon } from '@phosphor-icons/react'
|
|
136
|
+
import { PhosphorIcon } from '@utopia-studio-design/design-system/PhosphorIcon'
|
|
137
|
+
|
|
138
|
+
export function IconExamples() {
|
|
139
|
+
return (
|
|
140
|
+
<>
|
|
141
|
+
<PhosphorIcon decorative icon={HouseIcon} size="sm" />
|
|
142
|
+
<PhosphorIcon
|
|
143
|
+
direction="directional"
|
|
144
|
+
icon={ArrowRightIcon}
|
|
145
|
+
label="Continue"
|
|
146
|
+
/>
|
|
147
|
+
</>
|
|
148
|
+
)
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Phosphor is an additional supported family, not a silent replacement for
|
|
153
|
+
Lucide-based component internals. Breadcrumb separators and existing Ceramic
|
|
154
|
+
navigation defaults keep their documented icon contracts.
|
|
155
|
+
|
|
127
156
|
## Illustration Contract
|
|
128
157
|
|
|
129
158
|
- Illustrations are theme and product media, not core component requirements.
|
package/data/docs/guide.md
CHANGED
|
@@ -87,6 +87,28 @@ import {
|
|
|
87
87
|
- `BreadcrumbSeparator` provides an RTL-aware Lucide `ChevronRight` by default. Breadcrumb does not add a home icon.
|
|
88
88
|
- Breadcrumb links own their no-underline treatment and compact typography, so consumer-level anchor styles do not require an override.
|
|
89
89
|
|
|
90
|
+
## Phosphor Icon Composition
|
|
91
|
+
|
|
92
|
+
Phosphor is an optional supported icon family. It does not replace Lucide-based
|
|
93
|
+
defaults inside existing Ceramic components.
|
|
94
|
+
|
|
95
|
+
```tsx
|
|
96
|
+
import { GearIcon } from '@phosphor-icons/react'
|
|
97
|
+
import { IconButton } from '@utopia-studio-design/design-system/IconButton'
|
|
98
|
+
import { PhosphorIcon } from '@utopia-studio-design/design-system/PhosphorIcon'
|
|
99
|
+
|
|
100
|
+
<IconButton label="Settings">
|
|
101
|
+
<PhosphorIcon decorative icon={GearIcon} size="sm" />
|
|
102
|
+
</IconButton>
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
- Install `@phosphor-icons/react` in an application that imports Phosphor icons directly.
|
|
106
|
+
- Import named `*Icon` exports so unused icons can be removed from the bundle.
|
|
107
|
+
- Use `decorative` when the surrounding control or text already supplies the accessible name.
|
|
108
|
+
- Use `label` for a meaningful standalone icon.
|
|
109
|
+
- Set `direction="directional"` only for arrows, chevrons, and other meanings that follow reading direction.
|
|
110
|
+
- Keep color inherited and select `xs`, `sm`, or `md`; do not pass raw color or pixel size values.
|
|
111
|
+
|
|
90
112
|
## AI Rule
|
|
91
113
|
|
|
92
114
|
Before generating UI, read:
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# Ceramic Observability
|
|
2
|
+
|
|
3
|
+
Ceramic observability has two explicit modes. It is not a hidden backdoor:
|
|
4
|
+
|
|
5
|
+
- `operational` starts when a host configures and enables a collector. It
|
|
6
|
+
records only minimized error diagnostics, uses an in-memory page-session ID,
|
|
7
|
+
ignores actor/account IDs and custom properties, and does not use telemetry
|
|
8
|
+
cookies, `sessionStorage`, `localStorage`, usage events, or page-view events.
|
|
9
|
+
It needs no consent click, but hosts must publish notice, assess their lawful
|
|
10
|
+
basis, and provide an immediate opt-out.
|
|
11
|
+
- `consented` records the allow-listed usage plan and optional opaque cohorts
|
|
12
|
+
only after explicit consent.
|
|
13
|
+
|
|
14
|
+
## Decisions this data supports
|
|
15
|
+
|
|
16
|
+
- Which Ceramic versions, components, and public composition paths are in use?
|
|
17
|
+
- Which anonymous app/account cohort is encountering a repeated error?
|
|
18
|
+
- Which route, viewport class, locale, direction, and release are affected?
|
|
19
|
+
- Did a CLI command succeed or fail, without collecting its arguments or path?
|
|
20
|
+
- Did a fixed error fingerprint stop recurring after a release?
|
|
21
|
+
|
|
22
|
+
## Data minimization contract
|
|
23
|
+
|
|
24
|
+
Consented Ceramic telemetry may collect:
|
|
25
|
+
|
|
26
|
+
- event name and timestamp;
|
|
27
|
+
- opaque app, account, actor, session, and event IDs;
|
|
28
|
+
- component and operation names;
|
|
29
|
+
- package version and application release;
|
|
30
|
+
- route template supplied by the host;
|
|
31
|
+
- locale, direction, and viewport bucket;
|
|
32
|
+
- stable error code, error class, and non-reversible fingerprint;
|
|
33
|
+
- allow-listed short boolean, numeric, or non-email string properties.
|
|
34
|
+
|
|
35
|
+
Ceramic does not collect:
|
|
36
|
+
|
|
37
|
+
- names, email addresses, raw database IDs, or authentication identifiers;
|
|
38
|
+
- DOM, form values, user copy, component props, command arguments, or cwd;
|
|
39
|
+
- error messages or raw stacks;
|
|
40
|
+
- IP addresses in retained records;
|
|
41
|
+
- session replay, screenshots, keystrokes, or network payloads.
|
|
42
|
+
|
|
43
|
+
Operational mode accepts only `component_error` and `runtime_error`, and removes
|
|
44
|
+
`accountId`, `actorId`, custom properties, and persistent telemetry storage even
|
|
45
|
+
if the host passes them.
|
|
46
|
+
|
|
47
|
+
In consented mode, pass pre-hashed or otherwise opaque `accountId` and `actorId` values. The SDK
|
|
48
|
+
rejects identifiers with whitespace or email syntax. Route resolvers should
|
|
49
|
+
return route templates such as `/projects/:projectId`, not raw URLs containing
|
|
50
|
+
customer identifiers.
|
|
51
|
+
|
|
52
|
+
## Start the private collector and dashboard
|
|
53
|
+
|
|
54
|
+
Create two different random secrets:
|
|
55
|
+
|
|
56
|
+
```sh
|
|
57
|
+
export CERAMIC_TELEMETRY_INGEST_KEY="$(openssl rand -hex 32)"
|
|
58
|
+
export CERAMIC_TELEMETRY_ADMIN_TOKEN="$(openssl rand -hex 32)"
|
|
59
|
+
export CERAMIC_TELEMETRY_ALLOWED_ORIGINS="http://localhost:5173"
|
|
60
|
+
npm run observability:start
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Open `http://127.0.0.1:4318` and enter the admin token. The collector binds to
|
|
64
|
+
loopback by default, retains data for 30 days, and stores newline-delimited JSON
|
|
65
|
+
under `.ceramic/observability/`, which is ignored by Git.
|
|
66
|
+
|
|
67
|
+
The dashboard shows aggregate event, error, app, opaque actor/account,
|
|
68
|
+
component, release, and fingerprint counts plus the latest sanitized events.
|
|
69
|
+
The admin can export the aggregate response or delete all retained events.
|
|
70
|
+
|
|
71
|
+
For a shared deployment, place the collector behind TLS, SSO or a private
|
|
72
|
+
network, persistent encrypted storage, rate limiting, and backups appropriate
|
|
73
|
+
to the retention policy. Do not expose the Node reference collector directly
|
|
74
|
+
to the public internet. The admin token must never be put in a `VITE_*`
|
|
75
|
+
variable or browser bundle.
|
|
76
|
+
|
|
77
|
+
## Configure a React/browser consumer
|
|
78
|
+
|
|
79
|
+
```tsx
|
|
80
|
+
import {
|
|
81
|
+
configureCeramicTelemetry,
|
|
82
|
+
installCeramicGlobalErrorTracking,
|
|
83
|
+
trackCeramicEvent,
|
|
84
|
+
} from '@utopia-studio-design/design-system/Telemetry'
|
|
85
|
+
|
|
86
|
+
configureCeramicTelemetry({
|
|
87
|
+
enabled: diagnosticsPreference !== 'disabled',
|
|
88
|
+
consent: 'unknown',
|
|
89
|
+
mode: 'operational',
|
|
90
|
+
appId: 'renacore-dashboard',
|
|
91
|
+
endpoint: 'https://observability.example.com',
|
|
92
|
+
ingestKey: publicWriteOnlyIngestKey,
|
|
93
|
+
packageVersion: '0.6.0',
|
|
94
|
+
release: appRelease,
|
|
95
|
+
routeResolver: () => currentRouteTemplate,
|
|
96
|
+
sampleRate: 0.1,
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
const removeGlobalTracking = installCeramicGlobalErrorTracking()
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Global error listeners are never installed automatically by the package. The
|
|
103
|
+
host owns notice, lawful-basis assessment, opt-out, route normalization,
|
|
104
|
+
installation, and cleanup. Errors are sampled at 100%. Repeated reports with the same
|
|
105
|
+
fingerprint inside two seconds are deduplicated so a component boundary and a
|
|
106
|
+
global listener do not create duplicate incidents.
|
|
107
|
+
|
|
108
|
+
Ceramic's validated `asChild` paths report `ERR_COMPOSITION_CHILD` through the
|
|
109
|
+
configured client before throwing their explanatory development error. With no
|
|
110
|
+
configured and enabled client, this call is a no-op.
|
|
111
|
+
|
|
112
|
+
To enable usage events, obtain separate explicit consent and configure
|
|
113
|
+
`mode: "consented"`, `consent: "granted"`, and any opaque cohort IDs.
|
|
114
|
+
|
|
115
|
+
## Configure CLI diagnostics
|
|
116
|
+
|
|
117
|
+
CLI telemetry is also disabled by default and never sends command arguments,
|
|
118
|
+
queries, target directories, generated file names, or cwd.
|
|
119
|
+
|
|
120
|
+
```sh
|
|
121
|
+
export CERAMIC_TELEMETRY_ENABLED=true
|
|
122
|
+
export CERAMIC_TELEMETRY_MODE=operational
|
|
123
|
+
export CERAMIC_TELEMETRY_ENDPOINT=https://observability.example.com
|
|
124
|
+
export CERAMIC_TELEMETRY_INGEST_KEY=replace-with-write-only-key
|
|
125
|
+
export CERAMIC_TELEMETRY_APP_ID=renacore-dashboard
|
|
126
|
+
npx utopia-ds doctor --json
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Operational CLI mode sends failed completion only, without actor/account IDs.
|
|
130
|
+
Consented mode additionally requires `CERAMIC_TELEMETRY_MODE=consented` and
|
|
131
|
+
`CERAMIC_TELEMETRY_CONSENT=granted`, and may send successful completion and
|
|
132
|
+
opaque cohorts. Network failure never changes CLI output or exit status.
|
|
133
|
+
|
|
134
|
+
## Event plan
|
|
135
|
+
|
|
136
|
+
| Event | Trigger | Decision fields |
|
|
137
|
+
| --- | --- | --- |
|
|
138
|
+
| `app_started` | Consented SDK initialization | app, release, package version |
|
|
139
|
+
| `component_used` | Explicit host instrumentation | component, operation, route |
|
|
140
|
+
| `component_error` | Ceramic component contract failure | component, operation, error code/fingerprint |
|
|
141
|
+
| `runtime_error` | Explicit boundary/global error capture | route, release, error code/fingerprint |
|
|
142
|
+
| `cli_command_completed` | Operational failure or consented CLI process exit | command, success, version |
|
|
143
|
+
|
|
144
|
+
## Public notice and controls
|
|
145
|
+
|
|
146
|
+
The Ceramic documentation site publishes `#/terms` and `#/privacy`. Its privacy
|
|
147
|
+
page includes a browser-level operational-diagnostics opt-out and honors Global
|
|
148
|
+
Privacy Control and Do Not Track. Consumer applications must publish their own
|
|
149
|
+
notice and control because they determine the collector, purpose, retention,
|
|
150
|
+
recipients, and jurisdiction.
|
|
151
|
+
|
|
152
|
+
## Operations and deletion
|
|
153
|
+
|
|
154
|
+
- Default retention: 30 days; configure 1–365 days.
|
|
155
|
+
- `GET /health`: unauthenticated liveness only.
|
|
156
|
+
- `POST /v1/events`: write-only ingest-key authentication.
|
|
157
|
+
- `GET /v1/summary`: admin Bearer authentication.
|
|
158
|
+
- `DELETE /v1/events`: admin Bearer authentication and full retained-data deletion.
|
|
159
|
+
- `DELETE /v1/events?actorId=<opaque-id>` or `?accountId=<opaque-id>`:
|
|
160
|
+
selective deletion for a consent withdrawal or data-subject request.
|
|
161
|
+
- Rotate ingest and admin secrets independently.
|
|
162
|
+
- Treat the ingest key as public/write-only when used in browsers.
|
|
163
|
+
- Treat the admin token as a server-side secret.
|
|
164
|
+
- Publish a privacy notice before enabling identifiable account or actor
|
|
165
|
+
cohorts in a production consumer.
|
|
@@ -5,7 +5,7 @@ Ceramic follows the Astryx principle that humans, coding agents, build tools, an
|
|
|
5
5
|
## Paste This Into Your AI
|
|
6
6
|
|
|
7
7
|
```text
|
|
8
|
-
Install @utopia-studio-design/design-system and @utopia-studio-design/design-system-cli. Run `npx utopia-ds init --theme utopia-default`. Read the generated AGENTS.md and active theme config. Before editing UI, run `npx utopia-ds manifest --json`, search for the intended pattern, inspect its component or template contract, and read the Arabic-friendly guide when the product supports Arabic or RTL. Do not invent props, imports, tokens, or localized copy.
|
|
8
|
+
Install @utopia-studio-design/design-system and @utopia-studio-design/design-system-cli. Run `npx utopia-ds init . --theme utopia-default --yes`. Read the generated AGENTS.md and active theme config. Before editing UI, run `npx utopia-ds manifest --json`, search for the intended pattern, inspect its component or template contract, and read the Arabic-friendly guide when the product supports Arabic or RTL. Do not invent props, imports, tokens, or localized copy.
|
|
9
9
|
```
|
|
10
10
|
|
|
11
11
|
## Install
|
|
@@ -13,7 +13,7 @@ Install @utopia-studio-design/design-system and @utopia-studio-design/design-sys
|
|
|
13
13
|
```sh
|
|
14
14
|
npm install @utopia-studio-design/design-system
|
|
15
15
|
npm install -D @utopia-studio-design/design-system-cli
|
|
16
|
-
npx utopia-ds init --theme utopia-default
|
|
16
|
+
npx utopia-ds init . --theme utopia-default --yes
|
|
17
17
|
```
|
|
18
18
|
|
|
19
19
|
The init command creates:
|
|
@@ -42,6 +42,30 @@ export function SaveAction() {
|
|
|
42
42
|
}
|
|
43
43
|
```
|
|
44
44
|
|
|
45
|
+
For navigation, keep the anchor or framework link as the single interactive
|
|
46
|
+
element:
|
|
47
|
+
|
|
48
|
+
```tsx
|
|
49
|
+
import Link from 'next/link'
|
|
50
|
+
import { Button } from '@utopia-studio-design/design-system/Button'
|
|
51
|
+
|
|
52
|
+
<Button asChild variant="outline" size="sm">
|
|
53
|
+
<a href="/docs">Docs</a>
|
|
54
|
+
</Button>
|
|
55
|
+
|
|
56
|
+
<Button asChild startContent={<FolderIcon />}>
|
|
57
|
+
<Link href="/settings">Settings</Link>
|
|
58
|
+
</Button>
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
`asChild` accepts exactly one non-Fragment React element. The child must forward
|
|
62
|
+
`ref`, `className`, events, `children`, and accessibility props to one
|
|
63
|
+
interactive element. Anchor and Link props such as `href`, `target`, and
|
|
64
|
+
`aria-*` are preserved. When `disabled` or `loading` is true, Ceramic does not
|
|
65
|
+
pass the invalid `disabled` attribute to a link: it applies
|
|
66
|
+
`aria-disabled="true"`, `tabIndex={-1}`, and blocks click activation while
|
|
67
|
+
keeping the original `href`.
|
|
68
|
+
|
|
45
69
|
## Set Motion Policy
|
|
46
70
|
|
|
47
71
|
Ceramic maps BeUI-inspired interaction patterns to semantic roles: `press`, `page`, `expand`, `reveal`, and `icon`. Set the application default once and override only when a local workflow needs to be static.
|
|
@@ -96,6 +120,14 @@ The MCP server exposes the same API as the CLI. A generated `.mcp.json` uses:
|
|
|
96
120
|
|
|
97
121
|
Available tools include search, component/template/theme/doc discovery, and doctor. An MCP client must never receive capabilities that the CLI cannot expose.
|
|
98
122
|
|
|
123
|
+
## Optional diagnostics
|
|
124
|
+
|
|
125
|
+
Ceramic telemetry is disabled by default. If a product has an approved
|
|
126
|
+
diagnostics consent flow, configure the write-only SDK and private dashboard
|
|
127
|
+
using `npx utopia-ds docs observability --dense`. Never collect names, emails,
|
|
128
|
+
DOM content, component props, command arguments, filesystem paths, error
|
|
129
|
+
messages, or raw stacks.
|
|
130
|
+
|
|
99
131
|
## Agent Decision Loop
|
|
100
132
|
|
|
101
133
|
1. Run `manifest --json` to discover supported operations.
|
|
@@ -177,6 +177,13 @@
|
|
|
177
177
|
"Separator"
|
|
178
178
|
]
|
|
179
179
|
},
|
|
180
|
+
{
|
|
181
|
+
"id": "utilities",
|
|
182
|
+
"label": "Utilities",
|
|
183
|
+
"items": [
|
|
184
|
+
"Phosphor Icon"
|
|
185
|
+
]
|
|
186
|
+
},
|
|
180
187
|
{
|
|
181
188
|
"id": "data",
|
|
182
189
|
"label": "Data Display",
|
|
@@ -293,6 +300,7 @@
|
|
|
293
300
|
"Native Select",
|
|
294
301
|
"Navigation Menu",
|
|
295
302
|
"Pagination",
|
|
303
|
+
"Phosphor Icon",
|
|
296
304
|
"Popover",
|
|
297
305
|
"Progress",
|
|
298
306
|
"Radio Group",
|
|
@@ -373,6 +381,7 @@
|
|
|
373
381
|
"Native Select",
|
|
374
382
|
"Navigation Menu",
|
|
375
383
|
"Pagination",
|
|
384
|
+
"Phosphor Icon",
|
|
376
385
|
"Popover",
|
|
377
386
|
"Progress",
|
|
378
387
|
"Radio Group",
|
|
@@ -671,7 +671,7 @@
|
|
|
671
671
|
"sourcePath": "packages/design-system/src/components/Button.tsx",
|
|
672
672
|
"shadcnFoundation": [
|
|
673
673
|
"button",
|
|
674
|
-
"
|
|
674
|
+
"single-element asChild composition",
|
|
675
675
|
"cva"
|
|
676
676
|
],
|
|
677
677
|
"fallbackToShadcn": "button",
|
|
@@ -709,6 +709,7 @@
|
|
|
709
709
|
"New button colors",
|
|
710
710
|
"Gradient states",
|
|
711
711
|
"Unlabeled icon-only buttons",
|
|
712
|
+
"Fragments or multiple root children inside Button asChild",
|
|
712
713
|
"Left/right-only layout rules",
|
|
713
714
|
"Utopia brand primitives inside reusable component logic",
|
|
714
715
|
"Motion that bypasses MotionProvider or prefers-reduced-motion"
|
|
@@ -718,7 +719,10 @@
|
|
|
718
719
|
"motion": "Optional boolean. Defaults to the MotionProvider policy and disables this component pattern when false.",
|
|
719
720
|
"contentAlign": "Use 'start' for leading-aligned labels, 'center' for the default action layout, or 'between' to pin endContent to the logical inline end.",
|
|
720
721
|
"startContent": "Optional leading icon or content slot with package-owned alignment.",
|
|
721
|
-
"endContent": "Optional trailing content slot. With contentAlign='between', it remains pinned while the label truncates."
|
|
722
|
+
"endContent": "Optional trailing content slot. With contentAlign='between', it remains pinned while the label truncates.",
|
|
723
|
+
"asChild": "Use with exactly one non-Fragment anchor or framework Link that forwards ref, className, events, children, and accessibility props to one interactive element. Button preserves the child href, target, aria attributes, className, and ref while composing its internal slots inside that element.",
|
|
724
|
+
"disabled": "Native buttons receive disabled. With asChild, disabled links keep href, receive aria-disabled=true and tabIndex=-1, and block click activation without receiving a disabled attribute.",
|
|
725
|
+
"loading": "Shows the spinner/loadingText and sets aria-busy. Native buttons are disabled; asChild links use the same aria-disabled, tabIndex, and activation-blocking contract as disabled."
|
|
722
726
|
}
|
|
723
727
|
}
|
|
724
728
|
},
|
|
@@ -776,8 +780,9 @@
|
|
|
776
780
|
"label",
|
|
777
781
|
"children",
|
|
778
782
|
"ButtonGroupSeparator.orientation",
|
|
779
|
-
|
|
783
|
+
"ButtonGroupText.asChild"
|
|
780
784
|
],
|
|
785
|
+
"composition": "ButtonGroupText.asChild accepts exactly one text-compatible consumer element and preserves it as the semantic root while applying the group text class and props.",
|
|
781
786
|
"shadcnReferencePatterns": [
|
|
782
787
|
"button group",
|
|
783
788
|
"button group with dropdown menu",
|
|
@@ -2319,7 +2324,8 @@
|
|
|
2319
2324
|
"useWhen": [
|
|
2320
2325
|
"Structured tabular data with headers and comparable rows",
|
|
2321
2326
|
"AI-generated admin/product views that need accessible table semantics",
|
|
2322
|
-
"Arabic-friendly data displays that must preserve table meaning in dir='rtl'"
|
|
2327
|
+
"Arabic-friendly data displays that must preserve table meaning in dir='rtl'",
|
|
2328
|
+
"Responsive dashboards where wide tables must scroll inside the component instead of expanding the page"
|
|
2323
2329
|
],
|
|
2324
2330
|
"avoidWhen": [
|
|
2325
2331
|
"Card lists are easier to scan",
|
|
@@ -2330,7 +2336,14 @@
|
|
|
2330
2336
|
"Raw shadcn theme colors",
|
|
2331
2337
|
"Left/right-only layout rules",
|
|
2332
2338
|
"Utopia brand primitives inside reusable component logic"
|
|
2333
|
-
]
|
|
2339
|
+
],
|
|
2340
|
+
"ai": {
|
|
2341
|
+
"props": {
|
|
2342
|
+
"DataTableShell": "Full-width bounded shell with min-inline-size: 0 and component-owned horizontal scrolling. Place DataTableToolbar, DataTable, and DataTableFooter inside it.",
|
|
2343
|
+
"DataTable": "Semantic table content may exceed the available inline size; its nearest DataTableShell owns horizontal overflow."
|
|
2344
|
+
},
|
|
2345
|
+
"responsive": "Keep DataTableShell inside a min-inline-size: 0 grid or flex child. The shell prevents intrinsic table width from creating page-level horizontal scrolling."
|
|
2346
|
+
}
|
|
2334
2347
|
},
|
|
2335
2348
|
{
|
|
2336
2349
|
"name": "Date Picker",
|
|
@@ -2796,6 +2809,65 @@
|
|
|
2796
2809
|
]
|
|
2797
2810
|
}
|
|
2798
2811
|
},
|
|
2812
|
+
{
|
|
2813
|
+
"name": "Phosphor Icon",
|
|
2814
|
+
"category": "Utilities",
|
|
2815
|
+
"status": "available",
|
|
2816
|
+
"packageImport": "import { PhosphorIcon } from '@utopia-studio-design/design-system/PhosphorIcon';",
|
|
2817
|
+
"sourcePath": "packages/design-system/src/components/PhosphorIcon.tsx",
|
|
2818
|
+
"shadcnFoundation": [
|
|
2819
|
+
"@phosphor-icons/react",
|
|
2820
|
+
"named icon imports",
|
|
2821
|
+
"semantic icon size tokens",
|
|
2822
|
+
"currentColor"
|
|
2823
|
+
],
|
|
2824
|
+
"fallbackToShadcn": "lucide-react icon composed through the same semantic slot",
|
|
2825
|
+
"requiredTokens": [
|
|
2826
|
+
"--icon-size-xs",
|
|
2827
|
+
"--icon-size-sm",
|
|
2828
|
+
"--icon-size-md"
|
|
2829
|
+
],
|
|
2830
|
+
"useWhen": [
|
|
2831
|
+
"A product selects the Phosphor icon family",
|
|
2832
|
+
"Ceramic must own icon sizing, accessibility, and RTL behavior",
|
|
2833
|
+
"A named Phosphor icon is imported directly for tree-shaking"
|
|
2834
|
+
],
|
|
2835
|
+
"avoidWhen": [
|
|
2836
|
+
"The icon itself is interactive; compose it inside IconButton",
|
|
2837
|
+
"An existing Ceramic component already owns its Lucide-based icon contract",
|
|
2838
|
+
"Replacing Breadcrumb or Navigation defaults without a component-specific decision"
|
|
2839
|
+
],
|
|
2840
|
+
"neverInvent": [
|
|
2841
|
+
"Raw SVG paths copied into product code",
|
|
2842
|
+
"Raw color or pixel icon sizes",
|
|
2843
|
+
"Unlabeled meaningful standalone icons",
|
|
2844
|
+
"Mirroring direction-neutral icons",
|
|
2845
|
+
"Namespace imports that prevent per-icon tree-shaking",
|
|
2846
|
+
"Left/right-only layout rules",
|
|
2847
|
+
"Utopia brand primitives inside reusable component logic"
|
|
2848
|
+
],
|
|
2849
|
+
"ai": {
|
|
2850
|
+
"props": [
|
|
2851
|
+
"icon",
|
|
2852
|
+
"size",
|
|
2853
|
+
"weight",
|
|
2854
|
+
"decorative",
|
|
2855
|
+
"label",
|
|
2856
|
+
"direction"
|
|
2857
|
+
],
|
|
2858
|
+
"rules": [
|
|
2859
|
+
"Import individual *Icon exports from @phosphor-icons/react.",
|
|
2860
|
+
"Use decorative for visual-only icons and a localized label for meaningful standalone icons.",
|
|
2861
|
+
"Use direction=\"directional\" only when the icon meaning follows reading direction.",
|
|
2862
|
+
"Color is inherited through currentColor and size comes from Ceramic semantic tokens."
|
|
2863
|
+
],
|
|
2864
|
+
"arabicFriendly": [
|
|
2865
|
+
"Directional arrows and chevrons mirror when the wrapper is inside dir=\"rtl\".",
|
|
2866
|
+
"Home, settings, search, download, and other direction-neutral icons do not mirror.",
|
|
2867
|
+
"Use localized accessible labels supplied by the product."
|
|
2868
|
+
]
|
|
2869
|
+
}
|
|
2870
|
+
},
|
|
2799
2871
|
{
|
|
2800
2872
|
"name": "Input",
|
|
2801
2873
|
"category": "Forms",
|
|
@@ -4511,7 +4583,8 @@
|
|
|
4511
4583
|
"useWhen": [
|
|
4512
4584
|
"Peer views",
|
|
4513
4585
|
"Same-context panels",
|
|
4514
|
-
"Keyboard-switchable content sections"
|
|
4586
|
+
"Keyboard-switchable content sections",
|
|
4587
|
+
"Full-width application workspaces when layout='fluid'"
|
|
4515
4588
|
],
|
|
4516
4589
|
"avoidWhen": [
|
|
4517
4590
|
"Page navigation",
|
|
@@ -4525,6 +4598,7 @@
|
|
|
4525
4598
|
],
|
|
4526
4599
|
"ai": {
|
|
4527
4600
|
"props": {
|
|
4601
|
+
"layout": "Use 'contained' for compact content panels (the default 42rem measure) or 'fluid' for dashboards and application workspaces that should fill the available inline size.",
|
|
4528
4602
|
"motion": "Optional boolean. Defaults to the MotionProvider policy and disables this component pattern when false."
|
|
4529
4603
|
}
|
|
4530
4604
|
}
|
|
@@ -4602,6 +4676,7 @@
|
|
|
4602
4676
|
"shadcnFoundation": [
|
|
4603
4677
|
"toggle",
|
|
4604
4678
|
"Radix Toggle",
|
|
4679
|
+
"Slottable single-button asChild composition",
|
|
4605
4680
|
"cva"
|
|
4606
4681
|
],
|
|
4607
4682
|
"fallbackToShadcn": "shadcn/ui toggle",
|
|
@@ -4628,6 +4703,7 @@
|
|
|
4628
4703
|
"neverInvent": [
|
|
4629
4704
|
"New pressed colors",
|
|
4630
4705
|
"Checkbox behavior",
|
|
4706
|
+
"Fragments, multiple roots, anchors, or navigation links inside ToggleButton asChild",
|
|
4631
4707
|
"Left/right-only state names",
|
|
4632
4708
|
"Utopia brand primitives inside reusable component logic"
|
|
4633
4709
|
],
|
|
@@ -4642,9 +4718,11 @@
|
|
|
4642
4718
|
"isIconOnly",
|
|
4643
4719
|
"isDisabled",
|
|
4644
4720
|
"isLoading",
|
|
4721
|
+
"asChild",
|
|
4645
4722
|
"variant",
|
|
4646
4723
|
"size"
|
|
4647
4724
|
],
|
|
4725
|
+
"composition": "ToggleButton.asChild requires exactly one non-Fragment button-compatible component that forwards ref, className, events, children, disabled, and accessibility props to one button. Ceramic keeps spinner, icon, pressed icon, and label slots inside that root.",
|
|
4648
4726
|
"arabicFriendly": [
|
|
4649
4727
|
"Use logical start/end language for labels and adjacent content.",
|
|
4650
4728
|
"In RTL, keep persistent pressed state behavior identical; only directional layout and directional icons may mirror.",
|
package/lib/api.mjs
CHANGED
|
@@ -8,7 +8,7 @@ const packagedDataRoot = join(packageRoot, 'data')
|
|
|
8
8
|
const hasWorkspaceSource = existsSync(join(workspaceRoot, 'packages/design-system/src/manifests/components.json'))
|
|
9
9
|
|
|
10
10
|
export const apiVersion = 1
|
|
11
|
-
export const cliVersion = '0.
|
|
11
|
+
export const cliVersion = '0.4.0'
|
|
12
12
|
export const mcpLaunch = {
|
|
13
13
|
command: 'npx',
|
|
14
14
|
args: ['-y', '--package', '@utopia-studio-design/design-system-cli', 'utopia-ds', 'mcp'],
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
function safeId(value) {
|
|
2
|
+
return typeof value === 'string' && /^[a-zA-Z0-9][a-zA-Z0-9._:-]{0,127}$/.test(value)
|
|
3
|
+
? value
|
|
4
|
+
: undefined
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function randomId() {
|
|
8
|
+
return globalThis.crypto?.randomUUID?.() ?? `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 14)}`
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const SAFE_COMMANDS = new Set([
|
|
12
|
+
'component',
|
|
13
|
+
'doctor',
|
|
14
|
+
'docs',
|
|
15
|
+
'help',
|
|
16
|
+
'init',
|
|
17
|
+
'manifest',
|
|
18
|
+
'motion',
|
|
19
|
+
'search',
|
|
20
|
+
'template',
|
|
21
|
+
'theme',
|
|
22
|
+
])
|
|
23
|
+
|
|
24
|
+
export function installCliTelemetry({ command, packageVersion, getErrorCode }) {
|
|
25
|
+
const endpoint = process.env.CERAMIC_TELEMETRY_ENDPOINT?.replace(/\/$/, '')
|
|
26
|
+
const ingestKey = process.env.CERAMIC_TELEMETRY_INGEST_KEY
|
|
27
|
+
const appId = safeId(process.env.CERAMIC_TELEMETRY_APP_ID)
|
|
28
|
+
const consented = process.env.CERAMIC_TELEMETRY_MODE === 'consented'
|
|
29
|
+
&& process.env.CERAMIC_TELEMETRY_CONSENT === 'granted'
|
|
30
|
+
const enabled = process.env.CERAMIC_TELEMETRY_ENABLED === 'true'
|
|
31
|
+
&& endpoint
|
|
32
|
+
&& ingestKey
|
|
33
|
+
&& appId
|
|
34
|
+
&& command !== 'mcp'
|
|
35
|
+
if (!enabled) return
|
|
36
|
+
|
|
37
|
+
let sent = false
|
|
38
|
+
process.on('beforeExit', async () => {
|
|
39
|
+
if (sent) return
|
|
40
|
+
sent = true
|
|
41
|
+
const errorCode = getErrorCode()
|
|
42
|
+
const success = !errorCode && (process.exitCode ?? 0) === 0
|
|
43
|
+
if (!consented && success) return
|
|
44
|
+
const event = {
|
|
45
|
+
schemaVersion: 1,
|
|
46
|
+
id: randomId(),
|
|
47
|
+
occurredAt: new Date().toISOString(),
|
|
48
|
+
name: 'cli_command_completed',
|
|
49
|
+
appId,
|
|
50
|
+
source: 'cli',
|
|
51
|
+
sessionId: randomId(),
|
|
52
|
+
accountId: consented ? safeId(process.env.CERAMIC_TELEMETRY_ACCOUNT_ID) : undefined,
|
|
53
|
+
actorId: consented ? safeId(process.env.CERAMIC_TELEMETRY_ACTOR_ID) : undefined,
|
|
54
|
+
errorCode,
|
|
55
|
+
operation: SAFE_COMMANDS.has(command) ? command : 'unknown',
|
|
56
|
+
packageVersion,
|
|
57
|
+
release: process.env.CERAMIC_RELEASE?.slice(0, 128),
|
|
58
|
+
properties: consented ? { success } : undefined,
|
|
59
|
+
}
|
|
60
|
+
try {
|
|
61
|
+
await fetch(`${endpoint}/v1/events`, {
|
|
62
|
+
method: 'POST',
|
|
63
|
+
headers: {
|
|
64
|
+
'content-type': 'application/json',
|
|
65
|
+
'x-ceramic-ingest-key': ingestKey,
|
|
66
|
+
},
|
|
67
|
+
body: JSON.stringify(event),
|
|
68
|
+
signal: AbortSignal.timeout(750),
|
|
69
|
+
})
|
|
70
|
+
} catch {
|
|
71
|
+
// Telemetry is best effort and must never change CLI behavior.
|
|
72
|
+
}
|
|
73
|
+
})
|
|
74
|
+
}
|