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/drift/index.ts
ADDED
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* Hash-based drift detection for OpenUISpec projects.
|
|
4
|
+
*
|
|
5
|
+
* Tracks spec changes via content hashes so you know what to
|
|
6
|
+
* re-generate or review after editing spec files.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* npm run drift -- --target ios # check drift for ios
|
|
10
|
+
* npm run drift # check all targets with snapshots
|
|
11
|
+
* npm run drift -- --snapshot --target ios # snapshot for ios
|
|
12
|
+
* npm run drift -- --json --target ios # machine-readable output
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { readFileSync, writeFileSync, readdirSync, existsSync, mkdirSync } from "node:fs";
|
|
16
|
+
import { resolve, join, relative, basename, dirname } from "node:path";
|
|
17
|
+
import { fileURLToPath } from "node:url";
|
|
18
|
+
import { createHash } from "node:crypto";
|
|
19
|
+
import YAML from "yaml";
|
|
20
|
+
|
|
21
|
+
const __dirname = fileURLToPath(new URL(".", import.meta.url));
|
|
22
|
+
const REPO_ROOT = resolve(__dirname, "..");
|
|
23
|
+
const PROJECT_DIR = resolve(REPO_ROOT, "examples", "taskflow");
|
|
24
|
+
const GENERATED_DIR = resolve(REPO_ROOT, "generated");
|
|
25
|
+
const STATE_FILE = ".openuispec-state.json";
|
|
26
|
+
|
|
27
|
+
// ── types ─────────────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
interface StateFile {
|
|
30
|
+
spec_version: string;
|
|
31
|
+
snapshot_at: string;
|
|
32
|
+
target: string;
|
|
33
|
+
files: Record<string, string>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface DriftResult {
|
|
37
|
+
changed: string[];
|
|
38
|
+
added: string[];
|
|
39
|
+
removed: string[];
|
|
40
|
+
unchanged: string[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ── helpers ───────────────────────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
function listFiles(dir: string, ext: string): string[] {
|
|
46
|
+
try {
|
|
47
|
+
return readdirSync(dir)
|
|
48
|
+
.filter((f) => f.endsWith(ext))
|
|
49
|
+
.sort()
|
|
50
|
+
.map((f) => join(dir, f));
|
|
51
|
+
} catch {
|
|
52
|
+
return [];
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function hashFile(filePath: string): string {
|
|
57
|
+
const content = readFileSync(filePath);
|
|
58
|
+
const hash = createHash("sha256").update(content).digest("hex");
|
|
59
|
+
return `sha256:${hash}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function discoverSpecFiles(projectDir: string): string[] {
|
|
63
|
+
const manifest = join(projectDir, "openuispec.yaml");
|
|
64
|
+
if (!existsSync(manifest)) {
|
|
65
|
+
console.error(`Error: No openuispec.yaml found in ${projectDir}`);
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const doc = YAML.parse(readFileSync(manifest, "utf-8"));
|
|
70
|
+
const includes = doc.includes ?? {};
|
|
71
|
+
const files: string[] = [manifest];
|
|
72
|
+
|
|
73
|
+
// Tokens
|
|
74
|
+
if (includes.tokens) {
|
|
75
|
+
files.push(...listFiles(resolve(projectDir, includes.tokens), ".yaml"));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Screens
|
|
79
|
+
if (includes.screens) {
|
|
80
|
+
files.push(...listFiles(resolve(projectDir, includes.screens), ".yaml"));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Flows
|
|
84
|
+
if (includes.flows) {
|
|
85
|
+
files.push(...listFiles(resolve(projectDir, includes.flows), ".yaml"));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Platform
|
|
89
|
+
if (includes.platform) {
|
|
90
|
+
files.push(...listFiles(resolve(projectDir, includes.platform), ".yaml"));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Locales
|
|
94
|
+
if (includes.locales) {
|
|
95
|
+
files.push(...listFiles(resolve(projectDir, includes.locales), ".json"));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Custom contracts
|
|
99
|
+
if (includes.contracts) {
|
|
100
|
+
files.push(...listFiles(resolve(projectDir, includes.contracts), ".yaml"));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return files;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function categorize(relPath: string): string {
|
|
107
|
+
if (relPath === "openuispec.yaml") return "Manifest";
|
|
108
|
+
const dir = dirname(relPath);
|
|
109
|
+
if (dir === "tokens") return "Tokens";
|
|
110
|
+
if (dir === "screens") return "Screens";
|
|
111
|
+
if (dir === "flows") return "Flows";
|
|
112
|
+
if (dir === "platform") return "Platform";
|
|
113
|
+
if (dir === "locales") return "Locales";
|
|
114
|
+
if (dir === "contracts") return "Contracts";
|
|
115
|
+
return "Other";
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function resolveOutputDir(target: string): string {
|
|
119
|
+
return resolve(GENERATED_DIR, target, "TaskFlow");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function stateFilePath(target: string): string {
|
|
123
|
+
return join(resolveOutputDir(target), STATE_FILE);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Discover all targets that have an existing snapshot. */
|
|
127
|
+
function discoverTargets(): string[] {
|
|
128
|
+
if (!existsSync(GENERATED_DIR)) return [];
|
|
129
|
+
try {
|
|
130
|
+
return readdirSync(GENERATED_DIR)
|
|
131
|
+
.filter((entry) => existsSync(stateFilePath(entry)))
|
|
132
|
+
.sort();
|
|
133
|
+
} catch {
|
|
134
|
+
return [];
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ── snapshot ──────────────────────────────────────────────────────────
|
|
139
|
+
|
|
140
|
+
function snapshot(projectDir: string, target: string): void {
|
|
141
|
+
const outDir = resolveOutputDir(target);
|
|
142
|
+
if (!existsSync(outDir)) {
|
|
143
|
+
console.error(
|
|
144
|
+
`Error: Output directory not found: ${relative(REPO_ROOT, outDir)}\n` +
|
|
145
|
+
`Run code generation for "${target}" first.`
|
|
146
|
+
);
|
|
147
|
+
process.exit(1);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const files = discoverSpecFiles(projectDir);
|
|
151
|
+
const doc = YAML.parse(readFileSync(join(projectDir, "openuispec.yaml"), "utf-8"));
|
|
152
|
+
|
|
153
|
+
const hashes: Record<string, string> = {};
|
|
154
|
+
for (const f of files) {
|
|
155
|
+
const rel = relative(projectDir, f);
|
|
156
|
+
hashes[rel] = hashFile(f);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const state: StateFile = {
|
|
160
|
+
spec_version: doc.spec_version ?? "0.1",
|
|
161
|
+
snapshot_at: new Date().toISOString(),
|
|
162
|
+
target,
|
|
163
|
+
files: hashes,
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const outPath = stateFilePath(target);
|
|
167
|
+
writeFileSync(outPath, JSON.stringify(state, null, 2) + "\n");
|
|
168
|
+
console.log(`Snapshot saved: ${relative(REPO_ROOT, outPath)}`);
|
|
169
|
+
console.log(` ${Object.keys(hashes).length} files hashed`);
|
|
170
|
+
console.log(` target: ${target}`);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ── check ─────────────────────────────────────────────────────────────
|
|
174
|
+
|
|
175
|
+
function check(projectDir: string, target: string, jsonOutput: boolean): void {
|
|
176
|
+
const statePath = stateFilePath(target);
|
|
177
|
+
if (!existsSync(statePath)) {
|
|
178
|
+
console.error(
|
|
179
|
+
`No snapshot found for target "${target}".\n` +
|
|
180
|
+
`Run: npm run drift -- --snapshot --target ${target}`
|
|
181
|
+
);
|
|
182
|
+
process.exit(1);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const state: StateFile = JSON.parse(readFileSync(statePath, "utf-8"));
|
|
186
|
+
const files = discoverSpecFiles(projectDir);
|
|
187
|
+
|
|
188
|
+
const currentHashes: Record<string, string> = {};
|
|
189
|
+
for (const f of files) {
|
|
190
|
+
currentHashes[relative(projectDir, f)] = hashFile(f);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const result: DriftResult = {
|
|
194
|
+
changed: [],
|
|
195
|
+
added: [],
|
|
196
|
+
removed: [],
|
|
197
|
+
unchanged: [],
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
// Check current files against snapshot
|
|
201
|
+
for (const [rel, hash] of Object.entries(currentHashes)) {
|
|
202
|
+
if (!(rel in state.files)) {
|
|
203
|
+
result.added.push(rel);
|
|
204
|
+
} else if (state.files[rel] !== hash) {
|
|
205
|
+
result.changed.push(rel);
|
|
206
|
+
} else {
|
|
207
|
+
result.unchanged.push(rel);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Check for removed files
|
|
212
|
+
for (const rel of Object.keys(state.files)) {
|
|
213
|
+
if (!(rel in currentHashes)) {
|
|
214
|
+
result.removed.push(rel);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (jsonOutput) {
|
|
219
|
+
printJson(state, result);
|
|
220
|
+
} else {
|
|
221
|
+
printReport(projectDir, state, result);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const hasDrift =
|
|
225
|
+
result.changed.length > 0 ||
|
|
226
|
+
result.added.length > 0 ||
|
|
227
|
+
result.removed.length > 0;
|
|
228
|
+
process.exit(hasDrift ? 1 : 0);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function checkAll(projectDir: string, jsonOutput: boolean): void {
|
|
232
|
+
const targets = discoverTargets();
|
|
233
|
+
if (targets.length === 0) {
|
|
234
|
+
console.error(
|
|
235
|
+
"No snapshots found. Run: npm run drift -- --snapshot --target <target>"
|
|
236
|
+
);
|
|
237
|
+
process.exit(1);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
let anyDrift = false;
|
|
241
|
+
|
|
242
|
+
for (const target of targets) {
|
|
243
|
+
const statePath = stateFilePath(target);
|
|
244
|
+
const state: StateFile = JSON.parse(readFileSync(statePath, "utf-8"));
|
|
245
|
+
const files = discoverSpecFiles(projectDir);
|
|
246
|
+
|
|
247
|
+
const currentHashes: Record<string, string> = {};
|
|
248
|
+
for (const f of files) {
|
|
249
|
+
currentHashes[relative(projectDir, f)] = hashFile(f);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const result: DriftResult = {
|
|
253
|
+
changed: [],
|
|
254
|
+
added: [],
|
|
255
|
+
removed: [],
|
|
256
|
+
unchanged: [],
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
for (const [rel, hash] of Object.entries(currentHashes)) {
|
|
260
|
+
if (!(rel in state.files)) {
|
|
261
|
+
result.added.push(rel);
|
|
262
|
+
} else if (state.files[rel] !== hash) {
|
|
263
|
+
result.changed.push(rel);
|
|
264
|
+
} else {
|
|
265
|
+
result.unchanged.push(rel);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
for (const rel of Object.keys(state.files)) {
|
|
270
|
+
if (!(rel in currentHashes)) {
|
|
271
|
+
result.removed.push(rel);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (jsonOutput) {
|
|
276
|
+
printJson(state, result);
|
|
277
|
+
} else {
|
|
278
|
+
printReport(projectDir, state, result);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const hasDrift =
|
|
282
|
+
result.changed.length > 0 ||
|
|
283
|
+
result.added.length > 0 ||
|
|
284
|
+
result.removed.length > 0;
|
|
285
|
+
if (hasDrift) anyDrift = true;
|
|
286
|
+
|
|
287
|
+
if (targets.length > 1 && target !== targets[targets.length - 1]) {
|
|
288
|
+
console.log("");
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
process.exit(anyDrift ? 1 : 0);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// ── output ────────────────────────────────────────────────────────────
|
|
296
|
+
|
|
297
|
+
function printJson(state: StateFile, result: DriftResult): void {
|
|
298
|
+
console.log(
|
|
299
|
+
JSON.stringify(
|
|
300
|
+
{
|
|
301
|
+
snapshot_at: state.snapshot_at,
|
|
302
|
+
target: state.target,
|
|
303
|
+
...result,
|
|
304
|
+
},
|
|
305
|
+
null,
|
|
306
|
+
2
|
|
307
|
+
)
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function printReport(
|
|
312
|
+
projectDir: string,
|
|
313
|
+
state: StateFile,
|
|
314
|
+
result: DriftResult
|
|
315
|
+
): void {
|
|
316
|
+
const doc = YAML.parse(
|
|
317
|
+
readFileSync(join(projectDir, "openuispec.yaml"), "utf-8")
|
|
318
|
+
);
|
|
319
|
+
const projectName = doc.project?.name ?? basename(projectDir);
|
|
320
|
+
|
|
321
|
+
console.log("OpenUISpec Drift Check");
|
|
322
|
+
console.log("======================");
|
|
323
|
+
console.log(`Project: ${projectName}`);
|
|
324
|
+
console.log(`Snapshot: ${state.snapshot_at}`);
|
|
325
|
+
console.log(`Target: ${state.target}`);
|
|
326
|
+
|
|
327
|
+
// Group all files by category
|
|
328
|
+
const allFiles = new Set([
|
|
329
|
+
...result.unchanged,
|
|
330
|
+
...result.changed,
|
|
331
|
+
...result.added,
|
|
332
|
+
...result.removed,
|
|
333
|
+
]);
|
|
334
|
+
|
|
335
|
+
const categories = new Map<string, string[]>();
|
|
336
|
+
for (const f of allFiles) {
|
|
337
|
+
const cat = categorize(f);
|
|
338
|
+
if (!categories.has(cat)) categories.set(cat, []);
|
|
339
|
+
categories.get(cat)!.push(f);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const order = [
|
|
343
|
+
"Manifest",
|
|
344
|
+
"Tokens",
|
|
345
|
+
"Screens",
|
|
346
|
+
"Flows",
|
|
347
|
+
"Platform",
|
|
348
|
+
"Locales",
|
|
349
|
+
"Contracts",
|
|
350
|
+
"Other",
|
|
351
|
+
];
|
|
352
|
+
|
|
353
|
+
for (const cat of order) {
|
|
354
|
+
const files = categories.get(cat);
|
|
355
|
+
if (!files) continue;
|
|
356
|
+
files.sort();
|
|
357
|
+
|
|
358
|
+
console.log(`\n${cat}`);
|
|
359
|
+
for (const f of files) {
|
|
360
|
+
const name = basename(f);
|
|
361
|
+
if (result.changed.includes(f)) {
|
|
362
|
+
console.log(` ✗ ${name} (changed)`);
|
|
363
|
+
} else if (result.added.includes(f)) {
|
|
364
|
+
console.log(` + ${name} (added)`);
|
|
365
|
+
} else if (result.removed.includes(f)) {
|
|
366
|
+
console.log(` - ${name} (removed)`);
|
|
367
|
+
} else {
|
|
368
|
+
console.log(` ✓ ${name}`);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
console.log(
|
|
374
|
+
`\nSummary: ${result.changed.length} changed, ${result.added.length} added, ${result.removed.length} removed`
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// ── main ──────────────────────────────────────────────────────────────
|
|
379
|
+
|
|
380
|
+
const args = process.argv.slice(2);
|
|
381
|
+
const isSnapshot = args.includes("--snapshot");
|
|
382
|
+
const isJson = args.includes("--json");
|
|
383
|
+
|
|
384
|
+
const targetIdx = args.indexOf("--target");
|
|
385
|
+
const target = targetIdx !== -1 && args[targetIdx + 1] ? args[targetIdx + 1] : null;
|
|
386
|
+
|
|
387
|
+
if (isSnapshot) {
|
|
388
|
+
if (!target) {
|
|
389
|
+
console.error("Error: --target is required for --snapshot");
|
|
390
|
+
console.error("Usage: npm run drift -- --snapshot --target <target>");
|
|
391
|
+
process.exit(1);
|
|
392
|
+
}
|
|
393
|
+
snapshot(PROJECT_DIR, target);
|
|
394
|
+
} else if (target) {
|
|
395
|
+
check(PROJECT_DIR, target, isJson);
|
|
396
|
+
} else {
|
|
397
|
+
checkAll(PROJECT_DIR, isJson);
|
|
398
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# TaskFlow — OpenUISpec Example Application
|
|
2
|
+
|
|
3
|
+
A complete task management app defined entirely in OpenUISpec v0.1, demonstrating how a single source of truth generates native iOS, Android, and Web applications.
|
|
4
|
+
|
|
5
|
+
## What this demonstrates
|
|
6
|
+
|
|
7
|
+
### All 7 contract families in use
|
|
8
|
+
|
|
9
|
+
| Contract | Where it's used | Variants exercised |
|
|
10
|
+
|----------|----------------|-------------------|
|
|
11
|
+
| `action_trigger` | FAB, form buttons, destructive delete, ghost cancel | primary, secondary, tertiary, destructive, ghost |
|
|
12
|
+
| `data_display` | Task items, stat cards, hero header, profile card, settings rows | card, compact, hero, stat, inline |
|
|
13
|
+
| `input_field` | Search, task form (text, multiline, select, date, toggle, checkbox) | text, multiline, select, date, toggle, checkbox |
|
|
14
|
+
| `nav_container` | Main tab bar (4 tabs with badge), adaptive rail/sidebar | tab_bar, rail, sidebar |
|
|
15
|
+
| `feedback` | Success toasts, error banners, delete confirmation dialog | toast, banner, dialog |
|
|
16
|
+
| `surface` | Assignee picker sheet/panel, new project modal, create task sheet | sheet, modal, panel |
|
|
17
|
+
| `collection` | Task list, filter chips, project grid, settings list, tag chips | list, grid, chips |
|
|
18
|
+
|
|
19
|
+
### Adaptive layout
|
|
20
|
+
|
|
21
|
+
- **Navigation**: tab_bar (compact) → rail (regular) → sidebar (expanded)
|
|
22
|
+
- **Task detail**: stacked stat cards → horizontal row, stacked buttons → inline row
|
|
23
|
+
- **Projects**: 1-column grid → 2-column → 3-column
|
|
24
|
+
- **Surfaces**: assignee picker renders as sheet (compact) or side panel (expanded)
|
|
25
|
+
- **Home**: single-column scroll (compact) → split_view master-detail (expanded)
|
|
26
|
+
|
|
27
|
+
### Action system
|
|
28
|
+
|
|
29
|
+
- **navigate**: push to detail, present sheet/modal, `$back` destination
|
|
30
|
+
- **api_call**: with `on_success`/`on_error` handlers, optimistic updates on toggle
|
|
31
|
+
- **confirm**: destructive delete with confirmation dialog
|
|
32
|
+
- **sequence**: dismiss + feedback + refresh composed in order
|
|
33
|
+
- **feedback**: success toasts, error banners as side-effect actions
|
|
34
|
+
- **set_state**: filter changes, form submission loading state
|
|
35
|
+
|
|
36
|
+
### Data binding
|
|
37
|
+
|
|
38
|
+
- **API sources**: `api.tasks.list`, `api.auth.currentUser`, etc.
|
|
39
|
+
- **Local state**: `state.active_filter`, `state.search_query`, `state.is_submitting`
|
|
40
|
+
- **Format expressions**: `{task.due_date | format:date_relative}`, `{item.priority | map:priority_to_severity}`
|
|
41
|
+
- **Computed expressions**: `{task.status == done ? 'Reopen task' : 'Mark complete'}`
|
|
42
|
+
- **Two-way binding**: `data_binding: "form.title"` on input fields
|
|
43
|
+
- **Conditional rendering**: `condition: "task.description != null"`
|
|
44
|
+
|
|
45
|
+
### Token system
|
|
46
|
+
|
|
47
|
+
- **7 token categories**: color, typography, spacing, elevation, motion, layout, themes
|
|
48
|
+
- **Constrained ranges**: Every token uses Level 2 (range-based) constraints
|
|
49
|
+
- **Platform flex**: ±15% spacing adaptation, system font fallbacks, dynamic color support
|
|
50
|
+
|
|
51
|
+
### Platform adaptation
|
|
52
|
+
|
|
53
|
+
- **iOS**: System tab bar, `.insetGrouped` list style, haptics, swipe actions, sheet detents
|
|
54
|
+
- **Android**: Material 3 NavigationBar, ripple, predictive back, edge-to-edge, dynamic color
|
|
55
|
+
- **Web**: Keyboard shortcuts (⌘N, ⌘K, ⌘B), native `<dialog>`, CSS custom properties
|
|
56
|
+
|
|
57
|
+
## File structure
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
taskflow/
|
|
61
|
+
├── openuispec.yaml # Root manifest + data model + formatters + API endpoints
|
|
62
|
+
├── tokens/
|
|
63
|
+
│ ├── color.yaml # Brand + semantic + status colors
|
|
64
|
+
│ ├── typography.yaml # Font family + 9-step type scale
|
|
65
|
+
│ ├── spacing.yaml # 4px base unit, 10-step scale
|
|
66
|
+
│ ├── elevation.yaml # 4-level elevation (none/sm/md/lg)
|
|
67
|
+
│ ├── motion.yaml # Durations, easings, patterns
|
|
68
|
+
│ ├── layout.yaml # Size classes, primitives, reflow rules
|
|
69
|
+
│ └── themes.yaml # Light, dark, warm variants
|
|
70
|
+
├── contracts/ # 7 contract family stubs (see spec Section 4)
|
|
71
|
+
├── screens/
|
|
72
|
+
│ ├── home.yaml # Task list with search, filters, FAB, adaptive nav
|
|
73
|
+
│ ├── task_detail.yaml # Full task view with actions + assignee sheet
|
|
74
|
+
│ ├── projects.yaml # Project grid + new project dialog
|
|
75
|
+
│ ├── project_detail.yaml # Single project with task list (stub)
|
|
76
|
+
│ ├── settings.yaml # Preferences, toggles, account management
|
|
77
|
+
│ └── calendar.yaml # Calendar view (stub)
|
|
78
|
+
├── flows/
|
|
79
|
+
│ └── create_task.yaml # Task creation form (sheet presentation)
|
|
80
|
+
└── platform/
|
|
81
|
+
├── ios.yaml # SwiftUI overrides + behaviors
|
|
82
|
+
├── android.yaml # Compose overrides + behaviors
|
|
83
|
+
└── web.yaml # React overrides + responsive rules
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## How to use this with AI
|
|
87
|
+
|
|
88
|
+
Pass the entire `taskflow/` directory as context to an AI code generator with the prompt:
|
|
89
|
+
|
|
90
|
+
> Generate a native {ios|android|web} application from this OpenUISpec. Follow all contract state machines, apply token ranges for the target platform, and implement the navigation flows as defined. Use the platform adaptation file for target-specific overrides.
|
|
91
|
+
|
|
92
|
+
The AI should produce:
|
|
93
|
+
1. Compilable platform code (Swift/Kotlin/TypeScript)
|
|
94
|
+
2. All screens with correct contract-to-widget mapping
|
|
95
|
+
3. Navigation wiring matching the flow definitions
|
|
96
|
+
4. Theme support (light/dark at minimum)
|
|
97
|
+
5. Accessibility labels and roles per contract a11y specs
|
|
98
|
+
6. Loading, empty, and error states for all collections
|
|
99
|
+
7. Adaptive layout responding to size class changes
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
*TaskFlow is a reference implementation of OpenUISpec v0.1, not a production application.*
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# TaskFlow — Contract Files
|
|
2
|
+
|
|
3
|
+
These files define the 7 standard OpenUISpec contract families used by TaskFlow.
|
|
4
|
+
They are extracted from the OpenUISpec v0.1 specification (Section 4) for use as
|
|
5
|
+
standalone, machine-readable contract definitions.
|
|
6
|
+
|
|
7
|
+
For the full specification of each contract including generation hints and test cases,
|
|
8
|
+
see `spec/openuispec-v0.1.md` Section 4.
|
|
9
|
+
|
|
10
|
+
| File | Contract | Section |
|
|
11
|
+
|------|----------|---------|
|
|
12
|
+
| `action_trigger.yaml` | Initiates actions | 4.1 |
|
|
13
|
+
| `data_display.yaml` | Presents read-only info | 4.2 |
|
|
14
|
+
| `input_field.yaml` | Captures user input | 4.3 |
|
|
15
|
+
| `nav_container.yaml` | Persistent navigation | 4.4 |
|
|
16
|
+
| `feedback.yaml` | System status messages | 4.5 |
|
|
17
|
+
| `surface.yaml` | Contains other components | 4.6 |
|
|
18
|
+
| `collection.yaml` | Repeating data sets | 4.7 |
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# action_trigger contract — see spec Section 4 for full definition
|
|
2
|
+
# This file serves as a machine-readable reference for AI generators.
|
|
3
|
+
# The canonical definition lives in spec/openuispec-v0.1.md.
|
|
4
|
+
|
|
5
|
+
contract: action_trigger
|
|
6
|
+
spec_section: "4"
|
|
7
|
+
source: "spec/openuispec-v0.1.md"
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# collection contract — see spec Section 4 for full definition
|
|
2
|
+
# This file serves as a machine-readable reference for AI generators.
|
|
3
|
+
# The canonical definition lives in spec/openuispec-v0.1.md.
|
|
4
|
+
|
|
5
|
+
contract: collection
|
|
6
|
+
spec_section: "4"
|
|
7
|
+
source: "spec/openuispec-v0.1.md"
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# data_display contract — see spec Section 4 for full definition
|
|
2
|
+
# This file serves as a machine-readable reference for AI generators.
|
|
3
|
+
# The canonical definition lives in spec/openuispec-v0.1.md.
|
|
4
|
+
|
|
5
|
+
contract: data_display
|
|
6
|
+
spec_section: "4"
|
|
7
|
+
source: "spec/openuispec-v0.1.md"
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# feedback contract — see spec Section 4 for full definition
|
|
2
|
+
# This file serves as a machine-readable reference for AI generators.
|
|
3
|
+
# The canonical definition lives in spec/openuispec-v0.1.md.
|
|
4
|
+
|
|
5
|
+
contract: feedback
|
|
6
|
+
spec_section: "4"
|
|
7
|
+
source: "spec/openuispec-v0.1.md"
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# input_field contract — see spec Section 4 for full definition
|
|
2
|
+
# This file serves as a machine-readable reference for AI generators.
|
|
3
|
+
# The canonical definition lives in spec/openuispec-v0.1.md.
|
|
4
|
+
|
|
5
|
+
contract: input_field
|
|
6
|
+
spec_section: "4"
|
|
7
|
+
source: "spec/openuispec-v0.1.md"
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# nav_container contract — see spec Section 4 for full definition
|
|
2
|
+
# This file serves as a machine-readable reference for AI generators.
|
|
3
|
+
# The canonical definition lives in spec/openuispec-v0.1.md.
|
|
4
|
+
|
|
5
|
+
contract: nav_container
|
|
6
|
+
spec_section: "4"
|
|
7
|
+
source: "spec/openuispec-v0.1.md"
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# surface contract — see spec Section 4 for full definition
|
|
2
|
+
# This file serves as a machine-readable reference for AI generators.
|
|
3
|
+
# The canonical definition lives in spec/openuispec-v0.1.md.
|
|
4
|
+
|
|
5
|
+
contract: surface
|
|
6
|
+
spec_section: "4"
|
|
7
|
+
source: "spec/openuispec-v0.1.md"
|