openuispec 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +214 -0
- package/cli/index.ts +49 -0
- package/cli/init.ts +390 -0
- package/drift/index.ts +398 -0
- package/examples/taskflow/README.md +103 -0
- package/examples/taskflow/contracts/README.md +18 -0
- package/examples/taskflow/contracts/action_trigger.yaml +7 -0
- package/examples/taskflow/contracts/collection.yaml +7 -0
- package/examples/taskflow/contracts/data_display.yaml +7 -0
- package/examples/taskflow/contracts/feedback.yaml +7 -0
- package/examples/taskflow/contracts/input_field.yaml +7 -0
- package/examples/taskflow/contracts/nav_container.yaml +7 -0
- package/examples/taskflow/contracts/surface.yaml +7 -0
- package/examples/taskflow/contracts/x_media_player.yaml +185 -0
- package/examples/taskflow/flows/create_task.yaml +171 -0
- package/examples/taskflow/flows/edit_task.yaml +131 -0
- package/examples/taskflow/locales/en.json +158 -0
- package/examples/taskflow/openuispec.yaml +144 -0
- package/examples/taskflow/platform/android.yaml +32 -0
- package/examples/taskflow/platform/ios.yaml +39 -0
- package/examples/taskflow/platform/web.yaml +35 -0
- package/examples/taskflow/screens/calendar.yaml +23 -0
- package/examples/taskflow/screens/home.yaml +220 -0
- package/examples/taskflow/screens/profile_edit.yaml +70 -0
- package/examples/taskflow/screens/project_detail.yaml +65 -0
- package/examples/taskflow/screens/projects.yaml +142 -0
- package/examples/taskflow/screens/settings.yaml +178 -0
- package/examples/taskflow/screens/task_detail.yaml +317 -0
- package/examples/taskflow/tokens/color.yaml +88 -0
- package/examples/taskflow/tokens/elevation.yaml +27 -0
- package/examples/taskflow/tokens/icons.yaml +189 -0
- package/examples/taskflow/tokens/layout.yaml +156 -0
- package/examples/taskflow/tokens/motion.yaml +41 -0
- package/examples/taskflow/tokens/spacing.yaml +23 -0
- package/examples/taskflow/tokens/themes.yaml +34 -0
- package/examples/taskflow/tokens/typography.yaml +61 -0
- package/package.json +43 -0
- package/schema/custom-contract.schema.json +257 -0
- package/schema/defs/action.schema.json +272 -0
- package/schema/defs/adaptive.schema.json +13 -0
- package/schema/defs/common.schema.json +330 -0
- package/schema/defs/data-binding.schema.json +119 -0
- package/schema/defs/validation.schema.json +121 -0
- package/schema/flow.schema.json +164 -0
- package/schema/locale.schema.json +26 -0
- package/schema/openuispec.schema.json +287 -0
- package/schema/platform.schema.json +95 -0
- package/schema/screen.schema.json +346 -0
- package/schema/tokens/color.schema.json +104 -0
- package/schema/tokens/elevation.schema.json +84 -0
- package/schema/tokens/icons.schema.json +149 -0
- package/schema/tokens/layout.schema.json +170 -0
- package/schema/tokens/motion.schema.json +83 -0
- package/schema/tokens/spacing.schema.json +93 -0
- package/schema/tokens/themes.schema.json +92 -0
- package/schema/tokens/typography.schema.json +106 -0
- package/schema/validate.ts +258 -0
- package/spec/openuispec-v0.1.md +3677 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Rustam Samandarov
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
# OpenUISpec
|
|
2
|
+
|
|
3
|
+
> A single source of truth design language for AI-native, platform-native app development.
|
|
4
|
+
|
|
5
|
+
OpenUISpec is a **semantic UI specification format** that replaces cross-platform frameworks with a declarative design language. Instead of sharing runtime code across platforms, you share the *spec* — and AI generates native SwiftUI, Jetpack Compose, and React code from it.
|
|
6
|
+
|
|
7
|
+
## Why
|
|
8
|
+
|
|
9
|
+
Cross-platform frameworks (Flutter, React Native, KMP/CMP) solve code duplication by sharing a runtime. OpenUISpec solves it by sharing **intent**:
|
|
10
|
+
|
|
11
|
+
| Approach | Shares | Runs |
|
|
12
|
+
|----------|--------|------|
|
|
13
|
+
| Cross-platform framework | Runtime code | Abstraction layer |
|
|
14
|
+
| **OpenUISpec** | **Semantic spec** | **Native per platform** |
|
|
15
|
+
|
|
16
|
+
The result: each platform feels native, but every app stays consistent because it's generated from the same source of truth.
|
|
17
|
+
|
|
18
|
+
## How it works
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
┌─────────────────────────┐
|
|
22
|
+
│ OpenUISpec (YAML) │ ← Single source of truth
|
|
23
|
+
│ Tokens + Contracts │
|
|
24
|
+
│ Screens + Flows │
|
|
25
|
+
└────────┬────────────────┘
|
|
26
|
+
│
|
|
27
|
+
AI Generation Layer
|
|
28
|
+
│
|
|
29
|
+
┌────┴─────┬──────────┐
|
|
30
|
+
▼ ▼ ▼
|
|
31
|
+
SwiftUI Compose React
|
|
32
|
+
(iOS) (Android) (Web)
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Workflows
|
|
36
|
+
|
|
37
|
+
OpenUISpec supports two complementary workflows — one for designing new features, another for day-to-day development.
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
DESIGN MODE (spec-first) DEVELOPMENT MODE (platform-first)
|
|
41
|
+
For new features & design changes For iteration, tweaks & bug fixes
|
|
42
|
+
|
|
43
|
+
Spec (YAML) Xcode / Android Studio
|
|
44
|
+
│ │
|
|
45
|
+
▼ ▼
|
|
46
|
+
AI generates native code Developer edits native code
|
|
47
|
+
│ │
|
|
48
|
+
▼ ▼
|
|
49
|
+
Refine per platform AI syncs changes back to spec
|
|
50
|
+
│
|
|
51
|
+
▼
|
|
52
|
+
Other platforms see spec diff
|
|
53
|
+
and update their code
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
**Design mode** is the starting point: you write (or generate) spec YAML, then AI produces native SwiftUI, Compose, or React code. This is how new screens, flows, and design system changes enter the project.
|
|
57
|
+
|
|
58
|
+
**Development mode** is where most time is spent: a developer works in their IDE with live preview, iterating on layout, interactions, and fixes. When they're done, AI reads the code changes and updates the spec YAML. Other platform teams see the spec diff in version control and propagate the changes to their codebase. The spec acts as a shared sync layer — a UI changelog that keeps all platforms consistent.
|
|
59
|
+
|
|
60
|
+
## Key concepts
|
|
61
|
+
|
|
62
|
+
- **Tokens**: Design values (color, typography, spacing, elevation, motion) with semantic names and constrained ranges
|
|
63
|
+
- **Contracts**: 7 behavioral component families defined by role, props, state machines, and accessibility
|
|
64
|
+
- **Screens**: Compositions of contracts with data bindings, adaptive layout, and conditional rendering
|
|
65
|
+
- **Flows**: Multi-screen navigation journeys, intent-based and platform-agnostic
|
|
66
|
+
- **Actions**: 13 typed action types with composition, error handling, and optimistic updates
|
|
67
|
+
- **Data binding**: Reactive state, format expressions, caching, and loading/error/empty states
|
|
68
|
+
- **Adaptive layout**: Size classes (compact/regular/expanded) with per-section overrides
|
|
69
|
+
- **Platform adaptation**: Per-target overrides for iOS, Android, and Web behaviors
|
|
70
|
+
|
|
71
|
+
## Quick start
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
npm install -g openuispec
|
|
75
|
+
cd your-project
|
|
76
|
+
openuispec init
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
This scaffolds a spec directory, starter tokens, and adds rules to `CLAUDE.md` / `AGENTS.md` so AI assistants track spec changes automatically.
|
|
80
|
+
|
|
81
|
+
Then hand your spec to any AI code generator:
|
|
82
|
+
|
|
83
|
+
> Generate a native iOS app from this OpenUISpec. Follow all contract state machines, apply token ranges for iOS, and implement navigation flows as defined. Use `platform/ios.yaml` for SwiftUI-specific overrides.
|
|
84
|
+
|
|
85
|
+
See the [TaskFlow example](./examples/taskflow/) for a complete spec covering all 7 contract families.
|
|
86
|
+
|
|
87
|
+
## Repository structure
|
|
88
|
+
|
|
89
|
+
```
|
|
90
|
+
openuispec/
|
|
91
|
+
├── spec/
|
|
92
|
+
│ └── openuispec-v0.1.md # Full specification (14 sections)
|
|
93
|
+
├── schema/ # JSON Schema for validation (draft 2020-12)
|
|
94
|
+
│ ├── openuispec.schema.json # Root manifest schema
|
|
95
|
+
│ ├── screen.schema.json # Screen composition schema
|
|
96
|
+
│ ├── flow.schema.json # Navigation flow schema
|
|
97
|
+
│ ├── platform.schema.json # Platform adaptation schema
|
|
98
|
+
│ ├── locale.schema.json # Locale file schema
|
|
99
|
+
│ ├── custom-contract.schema.json # Custom contract extension schema
|
|
100
|
+
│ ├── tokens/
|
|
101
|
+
│ │ ├── color.schema.json # Color token schema
|
|
102
|
+
│ │ ├── typography.schema.json # Typography token schema
|
|
103
|
+
│ │ ├── spacing.schema.json # Spacing token schema
|
|
104
|
+
│ │ ├── elevation.schema.json # Elevation token schema
|
|
105
|
+
│ │ ├── motion.schema.json # Motion token schema
|
|
106
|
+
│ │ ├── layout.schema.json # Layout token schema
|
|
107
|
+
│ │ ├── themes.schema.json # Theme token schema
|
|
108
|
+
│ │ └── icons.schema.json # Icon token schema
|
|
109
|
+
│ ├── defs/
|
|
110
|
+
│ │ ├── common.schema.json # Shared types (icons, badges, etc.)
|
|
111
|
+
│ │ ├── action.schema.json # 13 action types (discriminated union)
|
|
112
|
+
│ │ ├── data-binding.schema.json # Data sources, state, params
|
|
113
|
+
│ │ ├── adaptive.schema.json # Adaptive override pattern
|
|
114
|
+
│ │ └── validation.schema.json # Validation rule definitions
|
|
115
|
+
│ └── validate.ts # Validation script (npm run validate)
|
|
116
|
+
├── examples/
|
|
117
|
+
│ └── taskflow/ # Complete example app
|
|
118
|
+
│ ├── openuispec.yaml # Root manifest + data model + API endpoints
|
|
119
|
+
│ ├── tokens/
|
|
120
|
+
│ │ ├── color.yaml # Brand + semantic + status colors
|
|
121
|
+
│ │ ├── typography.yaml # Font family + 8-step type scale
|
|
122
|
+
│ │ ├── spacing.yaml # 4px base unit, 9-step scale
|
|
123
|
+
│ │ ├── elevation.yaml # 4-level elevation (none/sm/md/lg)
|
|
124
|
+
│ │ ├── motion.yaml # Durations, easings, patterns
|
|
125
|
+
│ │ ├── layout.yaml # Size classes, primitives, reflow rules
|
|
126
|
+
│ │ ├── themes.yaml # Light, dark, warm variants
|
|
127
|
+
│ │ └── icons.yaml # Icon registry with platform mappings
|
|
128
|
+
│ ├── contracts/ # 7 contract family stubs + custom contracts
|
|
129
|
+
│ │ └── x_media_player.yaml # Custom media player contract (Section 12)
|
|
130
|
+
│ ├── screens/
|
|
131
|
+
│ │ ├── home.yaml # Task list with search, filters, FAB, adaptive nav
|
|
132
|
+
│ │ ├── task_detail.yaml # Full task view with actions + assignee sheet
|
|
133
|
+
│ │ ├── projects.yaml # Project grid + new project dialog
|
|
134
|
+
│ │ ├── project_detail.yaml # Single project with task list (stub)
|
|
135
|
+
│ │ ├── settings.yaml # Preferences, toggles, account management
|
|
136
|
+
│ │ ├── profile_edit.yaml # Edit profile form (stub)
|
|
137
|
+
│ │ └── calendar.yaml # Calendar view (stub)
|
|
138
|
+
│ ├── flows/
|
|
139
|
+
│ │ ├── create_task.yaml # Task creation form (sheet presentation)
|
|
140
|
+
│ │ └── edit_task.yaml # Task editing flow
|
|
141
|
+
│ ├── locales/
|
|
142
|
+
│ │ └── en.json # English locale (ICU MessageFormat)
|
|
143
|
+
│ └── platform/
|
|
144
|
+
│ ├── ios.yaml # SwiftUI overrides + behaviors
|
|
145
|
+
│ ├── android.yaml # Compose overrides + behaviors
|
|
146
|
+
│ └── web.yaml # React overrides + responsive rules
|
|
147
|
+
├── cli/ # CLI tool (openuispec init, drift, validate)
|
|
148
|
+
│ ├── index.ts # Entry point
|
|
149
|
+
│ └── init.ts # Project scaffolding + AI rules
|
|
150
|
+
├── drift/ # Drift detection (spec change tracking)
|
|
151
|
+
│ └── index.ts # Hash-based drift checker
|
|
152
|
+
├── LICENSE
|
|
153
|
+
└── README.md
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## Spec at a glance
|
|
157
|
+
|
|
158
|
+
| Section | What it defines |
|
|
159
|
+
|---------|----------------|
|
|
160
|
+
| 1. Philosophy | Core principles: semantic, constrained, contract-driven, AI-first |
|
|
161
|
+
| 2. Document structure | Project layout and root manifest |
|
|
162
|
+
| 3. Token layer | Color, typography, spacing, elevation, motion, layout, themes, icons |
|
|
163
|
+
| 4. Component contracts | 7 behavioral families (action_trigger, data_display, input_field, nav_container, feedback, surface, collection) |
|
|
164
|
+
| 5. Screen composition | Contract-based layouts, screen-level keys, adaptive layout system |
|
|
165
|
+
| 6. Navigation flows | Multi-screen journeys with transitions and progress |
|
|
166
|
+
| 7. Platform adaptation | Per-target overrides for iOS, Android, Web |
|
|
167
|
+
| 8. AI generation contract | Compliance levels (MUST/SHOULD/MAY), validation, drift detection |
|
|
168
|
+
| 9. Action system | 13 action types, composition, optimistic updates |
|
|
169
|
+
| 10. Data binding & state | Sources, paths, format expressions, reactivity, caching |
|
|
170
|
+
| 11. Internationalization | Locale files, `$t:` references, ICU MessageFormat, RTL, platform mapping |
|
|
171
|
+
| 12. Custom contract extensions | `x_` prefixed domain-specific contracts, registration, dependencies |
|
|
172
|
+
| 13. Form validation & field dependencies | Validation rules, field dependencies, cross-field checks, async validation |
|
|
173
|
+
| 14. Development workflow | Dual-workflow model, drift detection, spec as sync layer |
|
|
174
|
+
|
|
175
|
+
## The 7 contract families
|
|
176
|
+
|
|
177
|
+
| Contract | Role | Maps to |
|
|
178
|
+
|----------|------|---------|
|
|
179
|
+
| `action_trigger` | Initiates actions | Button, FAB, link |
|
|
180
|
+
| `data_display` | Shows read-only info | Card, list item, stat |
|
|
181
|
+
| `input_field` | Captures user input | TextField, toggle, picker |
|
|
182
|
+
| `nav_container` | Persistent navigation | Tab bar, sidebar, drawer |
|
|
183
|
+
| `feedback` | System status messages | Toast, dialog, banner |
|
|
184
|
+
| `surface` | Contains other components | Sheet, modal, popover |
|
|
185
|
+
| `collection` | Repeating data sets | List, grid, table |
|
|
186
|
+
|
|
187
|
+
## Status
|
|
188
|
+
|
|
189
|
+
**v0.1 — Draft**. The spec covers all foundational layers. One complete example app (TaskFlow) demonstrates full coverage.
|
|
190
|
+
|
|
191
|
+
### Roadmap
|
|
192
|
+
|
|
193
|
+
- [x] Token system with constrained ranges
|
|
194
|
+
- [x] 7 component contract families
|
|
195
|
+
- [x] Adaptive layout system (size classes + reflow rules)
|
|
196
|
+
- [x] Action system (13 types, composition, optimistic updates)
|
|
197
|
+
- [x] Data binding & state management (sources, expressions, caching)
|
|
198
|
+
- [x] Format expression grammar with computed expressions
|
|
199
|
+
- [x] Internationalization (i18n) with ICU MessageFormat and `$t:` references
|
|
200
|
+
- [x] JSON Schema for spec validation
|
|
201
|
+
- [x] Custom contract extension mechanism
|
|
202
|
+
- [x] Icon system definition
|
|
203
|
+
- [x] Form system (validation rules, field dependencies)
|
|
204
|
+
- [x] Drift detection (spec change tracking per platform)
|
|
205
|
+
- [x] CLI tool (`openuispec init` for project scaffolding + AI rules)
|
|
206
|
+
- [ ] More example apps (e-commerce, social, dashboard)
|
|
207
|
+
|
|
208
|
+
## Contributing
|
|
209
|
+
|
|
210
|
+
OpenUISpec is in early development. If you're interested in contributing — especially around AI code generation, platform-specific expertise, or additional example apps — open an issue to start the conversation.
|
|
211
|
+
|
|
212
|
+
## License
|
|
213
|
+
|
|
214
|
+
MIT
|
package/cli/index.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
#!/usr/bin/env -S npx tsx
|
|
2
|
+
/**
|
|
3
|
+
* OpenUISpec CLI
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* openuispec init # scaffold a new spec project
|
|
7
|
+
* openuispec drift # check for spec drift (all targets)
|
|
8
|
+
* openuispec drift --target ios
|
|
9
|
+
* openuispec drift --snapshot --target ios
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { init } from "./init.js";
|
|
13
|
+
|
|
14
|
+
async function main(): Promise<void> {
|
|
15
|
+
const [command] = process.argv.slice(2);
|
|
16
|
+
|
|
17
|
+
switch (command) {
|
|
18
|
+
case "init":
|
|
19
|
+
await init();
|
|
20
|
+
break;
|
|
21
|
+
|
|
22
|
+
case "drift":
|
|
23
|
+
console.error("drift command coming soon — use npm run drift for now");
|
|
24
|
+
process.exit(1);
|
|
25
|
+
break;
|
|
26
|
+
|
|
27
|
+
case undefined:
|
|
28
|
+
case "--help":
|
|
29
|
+
case "-h":
|
|
30
|
+
console.log(`
|
|
31
|
+
OpenUISpec CLI v0.1
|
|
32
|
+
|
|
33
|
+
Usage:
|
|
34
|
+
openuispec init Create a new spec project
|
|
35
|
+
openuispec drift [--target <t>] Check for spec drift
|
|
36
|
+
openuispec drift --snapshot --target <t> Snapshot current state
|
|
37
|
+
|
|
38
|
+
Learn more: https://github.com/anthropics/openuispec
|
|
39
|
+
`);
|
|
40
|
+
break;
|
|
41
|
+
|
|
42
|
+
default:
|
|
43
|
+
console.error(`Unknown command: ${command}`);
|
|
44
|
+
console.error(`Run "openuispec --help" for usage.`);
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
main();
|
package/cli/init.ts
ADDED
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* openuispec init — interactive project scaffolding
|
|
3
|
+
*
|
|
4
|
+
* Creates folder structure, manifest, and AI assistant rules
|
|
5
|
+
* (CLAUDE.md / AGENTS.md) so AI tools track spec changes properly.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { createInterface } from "node:readline/promises";
|
|
9
|
+
import { stdin, stdout } from "node:process";
|
|
10
|
+
import {
|
|
11
|
+
mkdirSync,
|
|
12
|
+
writeFileSync,
|
|
13
|
+
readFileSync,
|
|
14
|
+
readdirSync,
|
|
15
|
+
existsSync,
|
|
16
|
+
appendFileSync,
|
|
17
|
+
} from "node:fs";
|
|
18
|
+
import { join, relative } from "node:path";
|
|
19
|
+
|
|
20
|
+
// ── prompts ──────────────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
async function ask(
|
|
23
|
+
rl: ReturnType<typeof createInterface>,
|
|
24
|
+
question: string,
|
|
25
|
+
fallback?: string
|
|
26
|
+
): Promise<string> {
|
|
27
|
+
const suffix = fallback ? ` (${fallback})` : "";
|
|
28
|
+
const answer = (await rl.question(`${question}${suffix}: `)).trim();
|
|
29
|
+
return answer || fallback || "";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function askList(
|
|
33
|
+
rl: ReturnType<typeof createInterface>,
|
|
34
|
+
question: string,
|
|
35
|
+
options: string[],
|
|
36
|
+
defaults: string[]
|
|
37
|
+
): Promise<string[]> {
|
|
38
|
+
console.log(`${question}`);
|
|
39
|
+
for (const opt of options) {
|
|
40
|
+
const mark = defaults.includes(opt) ? "[x]" : "[ ]";
|
|
41
|
+
console.log(` ${mark} ${opt}`);
|
|
42
|
+
}
|
|
43
|
+
const raw = (
|
|
44
|
+
await rl.question(`Enter choices (comma-separated, enter for defaults): `)
|
|
45
|
+
).trim();
|
|
46
|
+
if (!raw) return defaults;
|
|
47
|
+
return raw
|
|
48
|
+
.split(",")
|
|
49
|
+
.map((s) => s.trim().toLowerCase())
|
|
50
|
+
.filter((s) => options.includes(s));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ── scaffold ─────────────────────────────────────────────────────────
|
|
54
|
+
|
|
55
|
+
function ensureDir(path: string): void {
|
|
56
|
+
if (!existsSync(path)) {
|
|
57
|
+
mkdirSync(path, { recursive: true });
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function writeIfMissing(path: string, content: string): boolean {
|
|
62
|
+
if (existsSync(path)) {
|
|
63
|
+
console.log(` skip ${relative(process.cwd(), path)} (exists)`);
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
writeFileSync(path, content);
|
|
67
|
+
console.log(` create ${relative(process.cwd(), path)}`);
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ── templates ────────────────────────────────────────────────────────
|
|
72
|
+
|
|
73
|
+
function manifestTemplate(
|
|
74
|
+
name: string,
|
|
75
|
+
targets: string[],
|
|
76
|
+
specDir: string
|
|
77
|
+
): string {
|
|
78
|
+
const targetList = targets.join(", ");
|
|
79
|
+
const outputLines = targets
|
|
80
|
+
.map((t) => {
|
|
81
|
+
if (t === "ios")
|
|
82
|
+
return ` ios: { language: swift, framework: swiftui, min_version: "17.0" }`;
|
|
83
|
+
if (t === "android")
|
|
84
|
+
return ` android: { language: kotlin, framework: compose, min_sdk: 26 }`;
|
|
85
|
+
if (t === "web")
|
|
86
|
+
return ` web: { language: typescript, framework: react, bundler: vite }`;
|
|
87
|
+
return ` ${t}: {}`;
|
|
88
|
+
})
|
|
89
|
+
.join("\n");
|
|
90
|
+
|
|
91
|
+
return `# ${name} — OpenUISpec v0.1
|
|
92
|
+
spec_version: "0.1"
|
|
93
|
+
|
|
94
|
+
project:
|
|
95
|
+
name: "${name}"
|
|
96
|
+
description: ""
|
|
97
|
+
|
|
98
|
+
includes:
|
|
99
|
+
tokens: "./tokens/"
|
|
100
|
+
contracts: "./contracts/"
|
|
101
|
+
screens: "./screens/"
|
|
102
|
+
flows: "./flows/"
|
|
103
|
+
platform: "./platform/"
|
|
104
|
+
locales: "./locales/"
|
|
105
|
+
|
|
106
|
+
i18n:
|
|
107
|
+
default_locale: "en"
|
|
108
|
+
supported_locales: [en]
|
|
109
|
+
fallback_strategy: "default"
|
|
110
|
+
|
|
111
|
+
generation:
|
|
112
|
+
targets: [${targetList}]
|
|
113
|
+
output_format:
|
|
114
|
+
${outputLines}
|
|
115
|
+
|
|
116
|
+
data_model: {}
|
|
117
|
+
|
|
118
|
+
api:
|
|
119
|
+
base_url: "/api/v1"
|
|
120
|
+
auth: "bearer_token"
|
|
121
|
+
endpoints: {}
|
|
122
|
+
`;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function starterColorTokens(): string {
|
|
126
|
+
return `# Design tokens: Color palette
|
|
127
|
+
brand:
|
|
128
|
+
primary:
|
|
129
|
+
"50": "#EEF2FF"
|
|
130
|
+
"100": "#E0E7FF"
|
|
131
|
+
"500": "#6366F1"
|
|
132
|
+
"600": "#4F46E5"
|
|
133
|
+
"900": "#312E81"
|
|
134
|
+
|
|
135
|
+
surface:
|
|
136
|
+
background: "#FFFFFF"
|
|
137
|
+
card: "#F9FAFB"
|
|
138
|
+
elevated: "#FFFFFF"
|
|
139
|
+
|
|
140
|
+
text:
|
|
141
|
+
primary: "#111827"
|
|
142
|
+
secondary: "#6B7280"
|
|
143
|
+
disabled: "#D1D5DB"
|
|
144
|
+
|
|
145
|
+
semantic:
|
|
146
|
+
success: "#10B981"
|
|
147
|
+
warning: "#F59E0B"
|
|
148
|
+
error: "#EF4444"
|
|
149
|
+
info: "#3B82F6"
|
|
150
|
+
`;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function starterTypographyTokens(): string {
|
|
154
|
+
return `# Design tokens: Typography
|
|
155
|
+
font_families:
|
|
156
|
+
sans: { default: "System" }
|
|
157
|
+
|
|
158
|
+
type_scale:
|
|
159
|
+
title_lg: { size: 28, weight: bold, tracking: 0, line_height: 1.2 }
|
|
160
|
+
title_md: { size: 22, weight: semibold, tracking: 0, line_height: 1.3 }
|
|
161
|
+
title_sm: { size: 17, weight: semibold, tracking: 0, line_height: 1.3 }
|
|
162
|
+
body_lg: { size: 17, weight: regular, tracking: 0, line_height: 1.5 }
|
|
163
|
+
body_md: { size: 15, weight: regular, tracking: 0, line_height: 1.5 }
|
|
164
|
+
body_sm: { size: 13, weight: regular, tracking: 0, line_height: 1.4 }
|
|
165
|
+
label_lg: { size: 15, weight: medium, tracking: 0.02, line_height: 1.3 }
|
|
166
|
+
label_md: { size: 13, weight: medium, tracking: 0.02, line_height: 1.3 }
|
|
167
|
+
caption: { size: 11, weight: regular, tracking: 0.02, line_height: 1.3 }
|
|
168
|
+
`;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function starterSpacingTokens(): string {
|
|
172
|
+
return `# Design tokens: Spacing
|
|
173
|
+
base_unit: 4
|
|
174
|
+
platform_flex: "10%"
|
|
175
|
+
|
|
176
|
+
scale:
|
|
177
|
+
"0": 0
|
|
178
|
+
"1": 4
|
|
179
|
+
"2": 8
|
|
180
|
+
"3": 12
|
|
181
|
+
"4": 16
|
|
182
|
+
"5": 20
|
|
183
|
+
"6": 24
|
|
184
|
+
"8": 32
|
|
185
|
+
"10": 40
|
|
186
|
+
"12": 48
|
|
187
|
+
"16": 64
|
|
188
|
+
`;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function starterLocale(): string {
|
|
192
|
+
return JSON.stringify(
|
|
193
|
+
{
|
|
194
|
+
app: {
|
|
195
|
+
name: "My App",
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
null,
|
|
199
|
+
2
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function aiRulesBlock(specDir: string, targets: string[]): string {
|
|
204
|
+
const targetList = targets.map((t) => `"${t}"`).join(", ");
|
|
205
|
+
return `
|
|
206
|
+
# OpenUISpec — AI Assistant Rules
|
|
207
|
+
# ================================
|
|
208
|
+
# This project uses OpenUISpec to define UI as a semantic spec.
|
|
209
|
+
# Spec files are the single source of truth for all UI across platforms.
|
|
210
|
+
|
|
211
|
+
## Spec location
|
|
212
|
+
- Spec root: \`${specDir}/\`
|
|
213
|
+
- Manifest: \`${specDir}/openuispec.yaml\`
|
|
214
|
+
- Always read \`openuispec.yaml\` first to understand the project structure.
|
|
215
|
+
|
|
216
|
+
## Before making UI changes
|
|
217
|
+
1. Read the relevant spec files (screens, tokens, flows) before modifying any UI code.
|
|
218
|
+
2. If the change requires a spec update, modify the spec files FIRST, then update generated code.
|
|
219
|
+
3. Never modify generated code directly — change the spec and regenerate.
|
|
220
|
+
|
|
221
|
+
## After modifying spec files
|
|
222
|
+
1. Run \`openuispec drift --snapshot --target <target>\` for each affected platform.
|
|
223
|
+
2. Targets in this project: ${targetList}.
|
|
224
|
+
3. Run \`openuispec drift\` to verify no untracked drift remains.
|
|
225
|
+
|
|
226
|
+
## Spec file conventions
|
|
227
|
+
- Tokens (colors, typography, spacing, motion, icons) live in \`${specDir}/tokens/\`.
|
|
228
|
+
- Contracts (UI component definitions) live in \`${specDir}/contracts/\`.
|
|
229
|
+
- Screens live in \`${specDir}/screens/\`. One screen per file.
|
|
230
|
+
- Navigation flows live in \`${specDir}/flows/\`.
|
|
231
|
+
- Platform overrides live in \`${specDir}/platform/\`.
|
|
232
|
+
- Localization strings live in \`${specDir}/locales/\`.
|
|
233
|
+
|
|
234
|
+
## Key rules
|
|
235
|
+
- The spec uses 7 contract families: nav_container, surface, action_trigger, input_field, data_display, collection, feedback.
|
|
236
|
+
- Custom contracts are prefixed with \`x_\` (e.g., \`x_media_player\`).
|
|
237
|
+
- Data binding uses \`$data:\`, \`$state:\`, \`$param:\`, \`$t:\` prefixes.
|
|
238
|
+
- Actions use typed action objects (navigate, api_call, set_state, confirm, etc.).
|
|
239
|
+
- When adding a new screen, also create the corresponding spec file.
|
|
240
|
+
- When renaming or removing a screen, update the spec and all flow references.
|
|
241
|
+
`;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// ── main ─────────────────────────────────────────────────────────────
|
|
245
|
+
|
|
246
|
+
export async function init(): Promise<void> {
|
|
247
|
+
const rl = createInterface({ input: stdin, output: stdout });
|
|
248
|
+
|
|
249
|
+
console.log("\nOpenUISpec — Project Setup\n");
|
|
250
|
+
|
|
251
|
+
try {
|
|
252
|
+
// 1. Project name
|
|
253
|
+
const cwd = process.cwd();
|
|
254
|
+
const defaultName =
|
|
255
|
+
cwd.split("/").pop()?.replace(/[^a-zA-Z0-9]/g, "") || "MyApp";
|
|
256
|
+
const name = await ask(rl, "Project name", defaultName);
|
|
257
|
+
|
|
258
|
+
// 2. Spec directory
|
|
259
|
+
const specDir = await ask(rl, "Spec directory", "openuispec");
|
|
260
|
+
|
|
261
|
+
// 3. Platforms
|
|
262
|
+
const allTargets = ["ios", "android", "web"];
|
|
263
|
+
const targets = await askList(
|
|
264
|
+
rl,
|
|
265
|
+
"\nWhich platforms?",
|
|
266
|
+
allTargets,
|
|
267
|
+
["ios", "android"]
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
if (targets.length === 0) {
|
|
271
|
+
console.error("At least one target is required.");
|
|
272
|
+
process.exit(1);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// 4. Starter tokens?
|
|
276
|
+
const wantTokens = await ask(rl, "Include starter tokens? (y/n)", "y");
|
|
277
|
+
|
|
278
|
+
// 5. AI rules?
|
|
279
|
+
const wantRules = await ask(
|
|
280
|
+
rl,
|
|
281
|
+
"Add rules to CLAUDE.md and AGENTS.md? (y/n)",
|
|
282
|
+
"y"
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
rl.close();
|
|
286
|
+
|
|
287
|
+
// ── create folders ─────────────────────────────────────────────
|
|
288
|
+
|
|
289
|
+
console.log("\nScaffolding...\n");
|
|
290
|
+
|
|
291
|
+
const root = join(cwd, specDir);
|
|
292
|
+
const dirs = [
|
|
293
|
+
"tokens",
|
|
294
|
+
"contracts",
|
|
295
|
+
"screens",
|
|
296
|
+
"flows",
|
|
297
|
+
"platform",
|
|
298
|
+
"locales",
|
|
299
|
+
];
|
|
300
|
+
|
|
301
|
+
ensureDir(root);
|
|
302
|
+
for (const d of dirs) {
|
|
303
|
+
ensureDir(join(root, d));
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// ── manifest ───────────────────────────────────────────────────
|
|
307
|
+
|
|
308
|
+
writeIfMissing(
|
|
309
|
+
join(root, "openuispec.yaml"),
|
|
310
|
+
manifestTemplate(name, targets, specDir)
|
|
311
|
+
);
|
|
312
|
+
|
|
313
|
+
// ── starter tokens ─────────────────────────────────────────────
|
|
314
|
+
|
|
315
|
+
if (wantTokens.toLowerCase().startsWith("y")) {
|
|
316
|
+
writeIfMissing(join(root, "tokens", "color.yaml"), starterColorTokens());
|
|
317
|
+
writeIfMissing(
|
|
318
|
+
join(root, "tokens", "typography.yaml"),
|
|
319
|
+
starterTypographyTokens()
|
|
320
|
+
);
|
|
321
|
+
writeIfMissing(
|
|
322
|
+
join(root, "tokens", "spacing.yaml"),
|
|
323
|
+
starterSpacingTokens()
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// ── starter locale ─────────────────────────────────────────────
|
|
328
|
+
|
|
329
|
+
writeIfMissing(
|
|
330
|
+
join(root, "locales", "en.json"),
|
|
331
|
+
starterLocale() + "\n"
|
|
332
|
+
);
|
|
333
|
+
|
|
334
|
+
// ── .gitkeep for empty dirs ────────────────────────────────────
|
|
335
|
+
|
|
336
|
+
for (const d of dirs) {
|
|
337
|
+
const dir = join(root, d);
|
|
338
|
+
const entries = existsSync(dir)
|
|
339
|
+
? readdirSync(dir).filter((f) => f !== ".gitkeep")
|
|
340
|
+
: [];
|
|
341
|
+
if (entries.length === 0) {
|
|
342
|
+
const gk = join(dir, ".gitkeep");
|
|
343
|
+
if (!existsSync(gk)) {
|
|
344
|
+
writeFileSync(gk, "");
|
|
345
|
+
console.log(` create ${relative(cwd, gk)}`);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// ── AI assistant rules ─────────────────────────────────────────
|
|
351
|
+
|
|
352
|
+
if (wantRules.toLowerCase().startsWith("y")) {
|
|
353
|
+
const rules = aiRulesBlock(specDir, targets);
|
|
354
|
+
|
|
355
|
+
for (const file of ["CLAUDE.md", "AGENTS.md"]) {
|
|
356
|
+
const filePath = join(cwd, file);
|
|
357
|
+
if (existsSync(filePath)) {
|
|
358
|
+
const existing = readFileSync(filePath, "utf-8");
|
|
359
|
+
if (existing.includes("OpenUISpec")) {
|
|
360
|
+
console.log(` skip ${file} (already has OpenUISpec rules)`);
|
|
361
|
+
continue;
|
|
362
|
+
}
|
|
363
|
+
appendFileSync(filePath, "\n" + rules);
|
|
364
|
+
console.log(` update ${file} (appended rules)`);
|
|
365
|
+
} else {
|
|
366
|
+
writeFileSync(filePath, rules.trimStart());
|
|
367
|
+
console.log(` create ${file}`);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// ── done ───────────────────────────────────────────────────────
|
|
373
|
+
|
|
374
|
+
console.log(`
|
|
375
|
+
Done! Your spec project is ready at ./${specDir}/
|
|
376
|
+
|
|
377
|
+
Next steps:
|
|
378
|
+
1. Edit ${specDir}/openuispec.yaml to define your data model and API
|
|
379
|
+
2. Add screens in ${specDir}/screens/
|
|
380
|
+
3. Add flows in ${specDir}/flows/
|
|
381
|
+
4. Generate code for your target platform
|
|
382
|
+
5. Run \`openuispec drift --snapshot --target ${targets[0]}\` to baseline
|
|
383
|
+
|
|
384
|
+
Learn more: https://github.com/anthropics/openuispec
|
|
385
|
+
`);
|
|
386
|
+
} catch (err) {
|
|
387
|
+
rl.close();
|
|
388
|
+
throw err;
|
|
389
|
+
}
|
|
390
|
+
}
|