openuispec 0.1.25 → 0.1.28

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.
Files changed (139) hide show
  1. package/README.md +63 -18
  2. package/cli/index.ts +21 -3
  3. package/cli/init.ts +27 -11
  4. package/docs/implementation-notes.md +119 -0
  5. package/docs/release-notes-v0.1.26.md +64 -0
  6. package/docs/release-notes-v0.1.27.md +28 -0
  7. package/docs/release-notes-v0.1.28.md +25 -0
  8. package/docs/stress-test-maturity-report.md +1 -1
  9. package/drift/index.ts +396 -22
  10. package/examples/taskflow/AGENTS.md +112 -0
  11. package/examples/taskflow/CLAUDE.md +112 -0
  12. package/examples/taskflow/generated/android/TaskFlow/README.md +43 -0
  13. package/examples/taskflow/generated/android/TaskFlow/app/build.gradle.kts +76 -0
  14. package/examples/taskflow/generated/android/TaskFlow/app/proguard-rules.pro +1 -0
  15. package/examples/taskflow/generated/android/TaskFlow/app/src/main/AndroidManifest.xml +21 -0
  16. package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/MainActivity.kt +19 -0
  17. package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/TaskFlowApp.kt +283 -0
  18. package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/model/DomainModels.kt +106 -0
  19. package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/model/SampleData.kt +57 -0
  20. package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/ui/components/Common.kt +109 -0
  21. package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/ui/screens/HomeScreen.kt +112 -0
  22. package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/ui/screens/ProjectsScreen.kt +61 -0
  23. package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/ui/screens/SettingsScreen.kt +82 -0
  24. package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/ui/screens/TaskDetailScreen.kt +111 -0
  25. package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/ui/sheets/Sheets.kt +77 -0
  26. package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/ui/theme/Color.kt +30 -0
  27. package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/ui/theme/Theme.kt +86 -0
  28. package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/ui/theme/Type.kt +57 -0
  29. package/examples/taskflow/generated/android/TaskFlow/app/src/main/res/values/strings.xml +155 -0
  30. package/examples/taskflow/generated/android/TaskFlow/app/src/main/res/values/themes.xml +4 -0
  31. package/examples/taskflow/generated/android/TaskFlow/build.gradle.kts +5 -0
  32. package/examples/taskflow/generated/android/TaskFlow/gradle/gradle-daemon-jvm.properties +12 -0
  33. package/examples/taskflow/generated/android/TaskFlow/gradle/wrapper/gradle-wrapper.jar +0 -0
  34. package/examples/taskflow/generated/android/TaskFlow/gradle/wrapper/gradle-wrapper.properties +7 -0
  35. package/examples/taskflow/generated/android/TaskFlow/gradle.properties +4 -0
  36. package/examples/taskflow/generated/android/TaskFlow/gradlew +18 -0
  37. package/examples/taskflow/generated/android/TaskFlow/gradlew.bat +12 -0
  38. package/examples/taskflow/generated/android/TaskFlow/settings.gradle.kts +18 -0
  39. package/examples/taskflow/generated/ios/TaskFlow/README.md +21 -0
  40. package/examples/taskflow/generated/ios/TaskFlow/Resources/en.lproj/Localizable.strings +115 -0
  41. package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/App/TaskFlowApp.swift +24 -0
  42. package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Components/AppChrome.swift +150 -0
  43. package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Flows/TaskEditorSheet.swift +220 -0
  44. package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Models/DomainModels.swift +122 -0
  45. package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Screens/CalendarView.swift +21 -0
  46. package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Screens/HomeView.swift +201 -0
  47. package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Screens/ProfileEditView.swift +48 -0
  48. package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Screens/ProjectDetailView.swift +59 -0
  49. package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Screens/ProjectsView.swift +63 -0
  50. package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Screens/SettingsView.swift +85 -0
  51. package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Screens/TaskDetailView.swift +219 -0
  52. package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Support/AppModel.swift +320 -0
  53. package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Support/AppSupport.swift +41 -0
  54. package/examples/taskflow/generated/ios/TaskFlow/project.yml +26 -0
  55. package/examples/taskflow/generated/web/TaskFlow/README.md +19 -0
  56. package/examples/taskflow/generated/web/TaskFlow/index.html +12 -0
  57. package/examples/taskflow/generated/web/TaskFlow/package-lock.json +1908 -0
  58. package/examples/taskflow/generated/web/TaskFlow/package.json +24 -0
  59. package/examples/taskflow/generated/web/TaskFlow/src/App.tsx +58 -0
  60. package/examples/taskflow/generated/web/TaskFlow/src/AppShell.tsx +55 -0
  61. package/examples/taskflow/generated/web/TaskFlow/src/components/Common.tsx +82 -0
  62. package/examples/taskflow/generated/web/TaskFlow/src/components/Modals.tsx +191 -0
  63. package/examples/taskflow/generated/web/TaskFlow/src/components/Nav.tsx +41 -0
  64. package/examples/taskflow/generated/web/TaskFlow/src/generated/messages.ts +131 -0
  65. package/examples/taskflow/generated/web/TaskFlow/src/hooks.ts +25 -0
  66. package/examples/taskflow/generated/web/TaskFlow/src/i18n.ts +39 -0
  67. package/examples/taskflow/generated/web/TaskFlow/src/locales.en.json +111 -0
  68. package/examples/taskflow/generated/web/TaskFlow/src/main.tsx +13 -0
  69. package/examples/taskflow/generated/web/TaskFlow/src/screens/HomeScreen.tsx +111 -0
  70. package/examples/taskflow/generated/web/TaskFlow/src/screens/ProjectsScreen.tsx +82 -0
  71. package/examples/taskflow/generated/web/TaskFlow/src/screens/SettingsScreens.tsx +132 -0
  72. package/examples/taskflow/generated/web/TaskFlow/src/screens/TaskDetail.tsx +105 -0
  73. package/examples/taskflow/generated/web/TaskFlow/src/store.ts +216 -0
  74. package/examples/taskflow/generated/web/TaskFlow/src/styles.css +617 -0
  75. package/examples/taskflow/generated/web/TaskFlow/src/types.ts +64 -0
  76. package/examples/taskflow/generated/web/TaskFlow/src/utils.ts +78 -0
  77. package/examples/taskflow/generated/web/TaskFlow/tsconfig.json +21 -0
  78. package/examples/taskflow/generated/web/TaskFlow/vite.config.ts +6 -0
  79. package/examples/taskflow/openuispec/README.md +49 -0
  80. package/examples/todo-orbit/AGENTS.md +46 -14
  81. package/examples/todo-orbit/CLAUDE.md +46 -14
  82. package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/ui/components/CommonComponents.kt +69 -18
  83. package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/ui/screens/TasksScreen.kt +5 -0
  84. package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/ui/sheets/EditorSheets.kt +5 -2
  85. package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/res/values/strings.xml +1 -0
  86. package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/res/values-ru/strings.xml +1 -0
  87. package/examples/todo-orbit/generated/ios/Todo Orbit/Resources/en.lproj/Localizable.strings +1 -0
  88. package/examples/todo-orbit/generated/ios/Todo Orbit/Resources/ru.lproj/Localizable.strings +1 -0
  89. package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Flows/RecurringRuleSheet.swift +3 -0
  90. package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Flows/TaskEditorSheet.swift +1 -0
  91. package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Screens/SettingsView.swift +2 -0
  92. package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Screens/TasksHomeView.swift +1 -0
  93. package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Support/AppModel.swift +1 -1
  94. package/examples/todo-orbit/generated/web/Todo Orbit/src/App.tsx +59 -6
  95. package/examples/todo-orbit/generated/web/Todo Orbit/src/styles.css +40 -0
  96. package/examples/todo-orbit/openuispec/README.md +24 -131
  97. package/examples/todo-orbit/openuispec/flows/create_recurring_rule.yaml +3 -0
  98. package/examples/todo-orbit/openuispec/flows/create_task.yaml +1 -0
  99. package/examples/todo-orbit/openuispec/flows/edit_task.yaml +1 -0
  100. package/examples/todo-orbit/openuispec/locales/en.json +1 -0
  101. package/examples/todo-orbit/openuispec/locales/ru.json +1 -0
  102. package/examples/todo-orbit/openuispec/screens/task_detail.yaml +1 -0
  103. package/examples/todo-orbit/openuispec/tokens/icons.yaml +6 -0
  104. package/package.json +6 -1
  105. package/prepare/index.ts +391 -0
  106. package/schema/semantic-lint.ts +592 -0
  107. package/schema/validate.ts +17 -13
  108. package/status/index.ts +200 -0
  109. /package/examples/taskflow/{contracts → openuispec/contracts}/README.md +0 -0
  110. /package/examples/taskflow/{contracts → openuispec/contracts}/action_trigger.yaml +0 -0
  111. /package/examples/taskflow/{contracts → openuispec/contracts}/collection.yaml +0 -0
  112. /package/examples/taskflow/{contracts → openuispec/contracts}/data_display.yaml +0 -0
  113. /package/examples/taskflow/{contracts → openuispec/contracts}/feedback.yaml +0 -0
  114. /package/examples/taskflow/{contracts → openuispec/contracts}/input_field.yaml +0 -0
  115. /package/examples/taskflow/{contracts → openuispec/contracts}/nav_container.yaml +0 -0
  116. /package/examples/taskflow/{contracts → openuispec/contracts}/surface.yaml +0 -0
  117. /package/examples/taskflow/{contracts → openuispec/contracts}/x_media_player.yaml +0 -0
  118. /package/examples/taskflow/{flows → openuispec/flows}/create_task.yaml +0 -0
  119. /package/examples/taskflow/{flows → openuispec/flows}/edit_task.yaml +0 -0
  120. /package/examples/taskflow/{locales → openuispec/locales}/en.json +0 -0
  121. /package/examples/taskflow/{openuispec.yaml → openuispec/openuispec.yaml} +0 -0
  122. /package/examples/taskflow/{platform → openuispec/platform}/android.yaml +0 -0
  123. /package/examples/taskflow/{platform → openuispec/platform}/ios.yaml +0 -0
  124. /package/examples/taskflow/{platform → openuispec/platform}/web.yaml +0 -0
  125. /package/examples/taskflow/{screens → openuispec/screens}/calendar.yaml +0 -0
  126. /package/examples/taskflow/{screens → openuispec/screens}/home.yaml +0 -0
  127. /package/examples/taskflow/{screens → openuispec/screens}/profile_edit.yaml +0 -0
  128. /package/examples/taskflow/{screens → openuispec/screens}/project_detail.yaml +0 -0
  129. /package/examples/taskflow/{screens → openuispec/screens}/projects.yaml +0 -0
  130. /package/examples/taskflow/{screens → openuispec/screens}/settings.yaml +0 -0
  131. /package/examples/taskflow/{screens → openuispec/screens}/task_detail.yaml +0 -0
  132. /package/examples/taskflow/{tokens → openuispec/tokens}/color.yaml +0 -0
  133. /package/examples/taskflow/{tokens → openuispec/tokens}/elevation.yaml +0 -0
  134. /package/examples/taskflow/{tokens → openuispec/tokens}/icons.yaml +0 -0
  135. /package/examples/taskflow/{tokens → openuispec/tokens}/layout.yaml +0 -0
  136. /package/examples/taskflow/{tokens → openuispec/tokens}/motion.yaml +0 -0
  137. /package/examples/taskflow/{tokens → openuispec/tokens}/spacing.yaml +0 -0
  138. /package/examples/taskflow/{tokens → openuispec/tokens}/themes.yaml +0 -0
  139. /package/examples/taskflow/{tokens → openuispec/tokens}/typography.yaml +0 -0
@@ -0,0 +1,78 @@
1
+ import type { Filter, Project, Task } from "./types";
2
+
3
+ export function matchesFilter(task: Task, filter: Filter) {
4
+ const due = task.dueDate ? new Date(task.dueDate) : null;
5
+ const today = new Date();
6
+ today.setHours(0, 0, 0, 0);
7
+ if (filter === "all") return true;
8
+ if (filter === "done") return task.status === "done";
9
+ if (filter === "today") return Boolean(due && due.toDateString() === today.toDateString() && task.status !== "done");
10
+ return Boolean(due && due > today && task.status !== "done");
11
+ }
12
+
13
+ export function matchesQuery(task: Task, projects: Project[], query: string) {
14
+ if (!query.trim()) return true;
15
+ const projectName = projects.find((project) => project.id === task.projectId)?.name ?? "";
16
+ return [task.title, task.description ?? "", projectName, task.tags.join(" ")]
17
+ .join(" ")
18
+ .toLowerCase()
19
+ .includes(query.toLowerCase());
20
+ }
21
+
22
+ export function buildCounts(tasks: Task[]) {
23
+ return {
24
+ all: tasks.length,
25
+ today: tasks.filter((task) => matchesFilter(task, "today")).length,
26
+ upcoming: tasks.filter((task) => matchesFilter(task, "upcoming")).length,
27
+ done: tasks.filter((task) => task.status === "done").length
28
+ };
29
+ }
30
+
31
+ export function parseTags(value: string) {
32
+ return value
33
+ .split(",")
34
+ .map((item) => item.trim())
35
+ .filter(Boolean);
36
+ }
37
+
38
+ export function initials(name: string) {
39
+ return name
40
+ .split(" ")
41
+ .slice(0, 2)
42
+ .map((part) => part[0]?.toUpperCase() ?? "")
43
+ .join("");
44
+ }
45
+
46
+ export function relativeDate(value: string) {
47
+ const date = new Date(value);
48
+ const today = new Date();
49
+ today.setHours(0, 0, 0, 0);
50
+ const target = new Date(date);
51
+ target.setHours(0, 0, 0, 0);
52
+ const diff = Math.round((target.getTime() - today.getTime()) / 86400000);
53
+ if (diff === 0) return "Today";
54
+ if (diff === 1) return "Tomorrow";
55
+ if (diff === -1) return "Yesterday";
56
+ if (diff > 1) return `in ${diff} days`;
57
+ return `${Math.abs(diff)} days ago`;
58
+ }
59
+
60
+ export function formatDate(value: string) {
61
+ return new Date(value).toLocaleDateString("en-US", {
62
+ month: "short",
63
+ day: "numeric",
64
+ year: "numeric"
65
+ });
66
+ }
67
+
68
+ export function addDays(days: number) {
69
+ const next = new Date();
70
+ next.setDate(next.getDate() + days);
71
+ return next.toISOString();
72
+ }
73
+
74
+ export function daysAgo(days: number) {
75
+ const previous = new Date();
76
+ previous.setDate(previous.getDate() - days);
77
+ return previous.toISOString();
78
+ }
@@ -0,0 +1,21 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "useDefineForClassFields": true,
5
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
6
+ "allowJs": false,
7
+ "skipLibCheck": true,
8
+ "esModuleInterop": true,
9
+ "allowSyntheticDefaultImports": true,
10
+ "strict": true,
11
+ "forceConsistentCasingInFileNames": true,
12
+ "module": "ESNext",
13
+ "moduleResolution": "Node",
14
+ "resolveJsonModule": true,
15
+ "isolatedModules": true,
16
+ "noEmit": true,
17
+ "jsx": "react-jsx"
18
+ },
19
+ "include": ["src"],
20
+ "references": []
21
+ }
@@ -0,0 +1,6 @@
1
+ import { defineConfig } from "vite";
2
+ import react from "@vitejs/plugin-react";
3
+
4
+ export default defineConfig({
5
+ plugins: [react()]
6
+ });
@@ -0,0 +1,49 @@
1
+ # TaskFlow — OpenUISpec
2
+
3
+ This directory contains the **OpenUISpec** semantic UI specification for **TaskFlow**.
4
+
5
+ **Start here:** read `openuispec.yaml` — it defines the project structure, data model, API endpoints, and generation targets (**ios, android, web**).
6
+
7
+ ## Directory structure
8
+
9
+ | Directory | Contents |
10
+ |-----------|----------|
11
+ | `tokens/` | Design tokens — colors, typography, spacing, elevation, motion, icons, themes |
12
+ | `screens/` | Screen definitions — one YAML file per screen |
13
+ | `flows/` | Navigation flows — multi-step user journeys |
14
+ | `contracts/` | Component contracts — standard extensions and custom (`x_` prefixed) |
15
+ | `platform/` | Platform overrides — per-target (iOS, Android, Web) behaviors |
16
+ | `locales/` | Localization — i18n strings (JSON, ICU MessageFormat) |
17
+
18
+ ## IMPORTANT — Read the specification before working with spec files
19
+
20
+ The spec format, file schemas, and generation rules are defined in the installed `openuispec` package.
21
+ You MUST read these reference files before creating, editing, or generating from any spec file.
22
+ Do NOT guess the file format — skipping this step will produce invalid YAML that fails validation.
23
+
24
+ **Find the package in this order:**
25
+ 1. `node_modules/openuispec/` (project dependency)
26
+ 2. Run `npm root -g` → `<prefix>/openuispec/` (global install)
27
+ 3. Online: `https://openuispec.rsteam.uz/llms-full.txt` (if not installed)
28
+
29
+ **Reference files inside the package (read in this order):**
30
+ 1. `README.md` — schema tables, file format reference, root keys
31
+ 2. `spec/openuispec-v0.1.md` — full specification (contracts, layout, expressions, etc.)
32
+ 3. `examples/taskflow/openuispec/` — complete working example with all file types
33
+ 4. `schema/` — JSON Schemas for validation
34
+
35
+ ## CLI commands
36
+
37
+ ```bash
38
+ openuispec validate # Validate spec files against schemas
39
+ openuispec validate semantic # Check semantic cross-references
40
+ openuispec validate screens # Validate only screens
41
+ openuispec status # Show which targets are behind
42
+ openuispec drift --target ios --explain # Explain semantic spec drift since ios baseline
43
+ openuispec prepare --target ios # Build an AI-ready ios update bundle
44
+ openuispec drift --snapshot --target ios # Snapshot current state + git baseline after ios output exists
45
+ ```
46
+
47
+ ## Learn more
48
+
49
+ Docs: https://openuispec.rsteam.uz
@@ -1,5 +1,5 @@
1
1
  <!-- openuispec-rules-start -->
2
- <!-- openuispec-rules-version: 0.1.22 -->
2
+ <!-- openuispec-rules-version: 0.1.28 -->
3
3
  # OpenUISpec — AI Assistant Rules
4
4
  # ================================
5
5
  # This project uses OpenUISpec to define UI as a semantic spec.
@@ -20,7 +20,7 @@ Do NOT guess the file format — skipping this step will produce invalid YAML th
20
20
  **Reference files inside the package (read in this order):**
21
21
  1. `README.md` — schema tables, file format reference, root wrapper keys
22
22
  2. `spec/openuispec-v0.1.md` — full specification (contracts, layout, expressions, adaptive, etc.)
23
- 3. `examples/taskflow/` — complete working example with all file types
23
+ 3. `examples/taskflow/openuispec/` — complete working example with all file types
24
24
  4. `schema/` — JSON Schemas for every file type
25
25
 
26
26
  These files are updated with each package version. Always read from the installed package,
@@ -57,24 +57,56 @@ This means the project has existing UI code but hasn't been specced yet. Your jo
57
57
  4. **Extract tokens** — scan for colors, fonts, spacing and create files in `openuispec/tokens/`.
58
58
  5. **Update the manifest** — fill in `data_model` and `api.endpoints` in `openuispec/openuispec.yaml`.
59
59
 
60
- ## Making UI changes
61
- 1. Read the relevant spec files before modifying any UI code.
62
- 2. If the change requires a spec update, modify the spec FIRST, then update code.
63
- 3. Never modify generated code without updating the spec.
64
- 4. When adding a new screen, create the corresponding spec file.
65
- 5. When removing a screen, remove the spec file and update flow references.
60
+ ## OpenUISpec Source Of Truth
66
61
 
67
- ## After modifying spec files
68
- 1. Run `openuispec validate` to check specs against the schema.
69
- 2. **Update the generated code** for each affected platform to match the new spec.
70
- 3. Run `openuispec drift --snapshot --target <target>` to baseline the updated state.
71
- 4. Run `openuispec drift` to verify no untracked drift remains.
62
+ OpenUISpec spec files are the primary source of truth for UI behavior across platforms.
63
+
64
+ ### Start from spec when:
65
+ - the request changes screen structure
66
+ - the request changes navigation
67
+ - the request changes fields, actions, validation, or data binding
68
+ - the request changes tokens, variants, contracts, flows, or localization
69
+ - the request affects more than one platform
70
+ - the request is phrased in product/UI terms rather than platform-code terms
71
+
72
+ Spec-first workflow:
73
+ 1. Read `openuispec/openuispec.yaml` and the relevant spec files first.
74
+ 2. Update the spec first.
75
+ 3. Update the affected generated/native UI code to match the spec.
76
+ 4. Run `openuispec validate`.
77
+ 5. Run `openuispec validate semantic`.
78
+ 6. Run `openuispec drift --target <target> --explain` to inspect semantic changes since that target's baseline.
79
+ 7. Run `openuispec prepare --target <target>` to build the AI/developer work bundle for that target.
80
+ 8. Verify the affected UI targets build/run if possible.
81
+ 9. Only then run `openuispec drift --snapshot --target <target>` for affected targets, after that target output directory exists.
82
+ 10. Run `openuispec drift --target <target> --explain` again to confirm no spec changes remain for that target.
83
+ 11. Use `openuispec status` to see which other targets are still behind the updated spec.
84
+
85
+ ### Start from platform code when:
86
+ - the change is platform-specific polish
87
+ - the change is a local bug fix that does not alter shared semantic behavior
88
+ - the request explicitly asks for an iOS-only, Android-only, or web-only adjustment
89
+
90
+ Platform-first workflow:
91
+ 1. Update native/platform code.
92
+ 2. If the change affects shared semantics, sync the spec afterward.
93
+ 3. If the change is intentionally platform-specific, document it in `platform/*.yaml` when appropriate.
94
+
95
+ ### Never do this:
96
+ - Do not snapshot drift immediately after changing spec unless the UI code has also been updated.
97
+ - Do not treat `openuispec drift` as proof that generated UI matches the spec.
98
+ - Do not skip `--explain` / `prepare` when another platform needs to catch up with shared spec changes.
99
+ - Do not modify generated UI without checking whether the spec must change first.
72
100
 
73
101
  ## CLI commands
74
102
  - `openuispec init` — scaffold a new spec project
75
103
  - `openuispec validate [group...]` — validate spec files against schemas
104
+ - `openuispec validate semantic` — run semantic cross-reference linting
76
105
  - `openuispec drift --target <t>` — check for spec drift
77
- - `openuispec drift --snapshot --target <t>`snapshot current state
106
+ - `openuispec drift --target <t> --explain` explain semantic spec drift since the target baseline
107
+ - `openuispec drift --snapshot --target <t>` — snapshot current state after the target output exists
108
+ - `openuispec prepare --target <t>` — build an AI-ready target update bundle
109
+ - `openuispec status` — show cross-target baseline/drift status
78
110
  - `openuispec update-rules` — update AI rules to match installed package version
79
111
  - `openuispec drift --all` — include stubs in drift check
80
112
  <!-- openuispec-rules-end -->
@@ -1,5 +1,5 @@
1
1
  <!-- openuispec-rules-start -->
2
- <!-- openuispec-rules-version: 0.1.22 -->
2
+ <!-- openuispec-rules-version: 0.1.28 -->
3
3
  # OpenUISpec — AI Assistant Rules
4
4
  # ================================
5
5
  # This project uses OpenUISpec to define UI as a semantic spec.
@@ -20,7 +20,7 @@ Do NOT guess the file format — skipping this step will produce invalid YAML th
20
20
  **Reference files inside the package (read in this order):**
21
21
  1. `README.md` — schema tables, file format reference, root wrapper keys
22
22
  2. `spec/openuispec-v0.1.md` — full specification (contracts, layout, expressions, adaptive, etc.)
23
- 3. `examples/taskflow/` — complete working example with all file types
23
+ 3. `examples/taskflow/openuispec/` — complete working example with all file types
24
24
  4. `schema/` — JSON Schemas for every file type
25
25
 
26
26
  These files are updated with each package version. Always read from the installed package,
@@ -57,24 +57,56 @@ This means the project has existing UI code but hasn't been specced yet. Your jo
57
57
  4. **Extract tokens** — scan for colors, fonts, spacing and create files in `openuispec/tokens/`.
58
58
  5. **Update the manifest** — fill in `data_model` and `api.endpoints` in `openuispec/openuispec.yaml`.
59
59
 
60
- ## Making UI changes
61
- 1. Read the relevant spec files before modifying any UI code.
62
- 2. If the change requires a spec update, modify the spec FIRST, then update code.
63
- 3. Never modify generated code without updating the spec.
64
- 4. When adding a new screen, create the corresponding spec file.
65
- 5. When removing a screen, remove the spec file and update flow references.
60
+ ## OpenUISpec Source Of Truth
66
61
 
67
- ## After modifying spec files
68
- 1. Run `openuispec validate` to check specs against the schema.
69
- 2. **Update the generated code** for each affected platform to match the new spec.
70
- 3. Run `openuispec drift --snapshot --target <target>` to baseline the updated state.
71
- 4. Run `openuispec drift` to verify no untracked drift remains.
62
+ OpenUISpec spec files are the primary source of truth for UI behavior across platforms.
63
+
64
+ ### Start from spec when:
65
+ - the request changes screen structure
66
+ - the request changes navigation
67
+ - the request changes fields, actions, validation, or data binding
68
+ - the request changes tokens, variants, contracts, flows, or localization
69
+ - the request affects more than one platform
70
+ - the request is phrased in product/UI terms rather than platform-code terms
71
+
72
+ Spec-first workflow:
73
+ 1. Read `openuispec/openuispec.yaml` and the relevant spec files first.
74
+ 2. Update the spec first.
75
+ 3. Update the affected generated/native UI code to match the spec.
76
+ 4. Run `openuispec validate`.
77
+ 5. Run `openuispec validate semantic`.
78
+ 6. Run `openuispec drift --target <target> --explain` to inspect semantic changes since that target's baseline.
79
+ 7. Run `openuispec prepare --target <target>` to build the AI/developer work bundle for that target.
80
+ 8. Verify the affected UI targets build/run if possible.
81
+ 9. Only then run `openuispec drift --snapshot --target <target>` for affected targets, after that target output directory exists.
82
+ 10. Run `openuispec drift --target <target> --explain` again to confirm no spec changes remain for that target.
83
+ 11. Use `openuispec status` to see which other targets are still behind the updated spec.
84
+
85
+ ### Start from platform code when:
86
+ - the change is platform-specific polish
87
+ - the change is a local bug fix that does not alter shared semantic behavior
88
+ - the request explicitly asks for an iOS-only, Android-only, or web-only adjustment
89
+
90
+ Platform-first workflow:
91
+ 1. Update native/platform code.
92
+ 2. If the change affects shared semantics, sync the spec afterward.
93
+ 3. If the change is intentionally platform-specific, document it in `platform/*.yaml` when appropriate.
94
+
95
+ ### Never do this:
96
+ - Do not snapshot drift immediately after changing spec unless the UI code has also been updated.
97
+ - Do not treat `openuispec drift` as proof that generated UI matches the spec.
98
+ - Do not skip `--explain` / `prepare` when another platform needs to catch up with shared spec changes.
99
+ - Do not modify generated UI without checking whether the spec must change first.
72
100
 
73
101
  ## CLI commands
74
102
  - `openuispec init` — scaffold a new spec project
75
103
  - `openuispec validate [group...]` — validate spec files against schemas
104
+ - `openuispec validate semantic` — run semantic cross-reference linting
76
105
  - `openuispec drift --target <t>` — check for spec drift
77
- - `openuispec drift --snapshot --target <t>`snapshot current state
106
+ - `openuispec drift --target <t> --explain` explain semantic spec drift since the target baseline
107
+ - `openuispec drift --snapshot --target <t>` — snapshot current state after the target output exists
108
+ - `openuispec prepare --target <t>` — build an AI-ready target update bundle
109
+ - `openuispec status` — show cross-target baseline/drift status
78
110
  - `openuispec update-rules` — update AI rules to match installed package version
79
111
  - `openuispec drift --all` — include stubs in drift check
80
112
  <!-- openuispec-rules-end -->
@@ -4,8 +4,6 @@ import androidx.compose.foundation.background
4
4
  import androidx.compose.foundation.layout.Arrangement
5
5
  import androidx.compose.foundation.layout.Box
6
6
  import androidx.compose.foundation.layout.Column
7
- import androidx.compose.foundation.layout.ExperimentalLayoutApi
8
- import androidx.compose.foundation.layout.FlowRow
9
7
  import androidx.compose.foundation.layout.Row
10
8
  import androidx.compose.foundation.layout.Spacer
11
9
  import androidx.compose.foundation.layout.fillMaxWidth
@@ -13,19 +11,27 @@ import androidx.compose.foundation.layout.padding
13
11
  import androidx.compose.foundation.layout.size
14
12
  import androidx.compose.foundation.layout.width
15
13
  import androidx.compose.foundation.shape.CircleShape
16
- import androidx.compose.material.icons.Icons
17
- import androidx.compose.material.icons.outlined.Translate
18
14
  import androidx.compose.material3.Card
19
15
  import androidx.compose.material3.CardDefaults
16
+ import androidx.compose.material3.DropdownMenu
17
+ import androidx.compose.material3.DropdownMenuItem
20
18
  import androidx.compose.material3.ElevatedCard
21
- import androidx.compose.material3.FilterChip
22
- import androidx.compose.material3.Icon
19
+ import androidx.compose.material3.ExperimentalMaterial3Api
20
+ import androidx.compose.material3.ExposedDropdownMenuBox
21
+ import androidx.compose.material3.ExposedDropdownMenuDefaults
23
22
  import androidx.compose.material3.MaterialTheme
24
23
  import androidx.compose.material3.OutlinedCard
25
24
  import androidx.compose.material3.OutlinedTextField
25
+ import androidx.compose.material3.SegmentedButton
26
+ import androidx.compose.material3.SegmentedButtonDefaults
27
+ import androidx.compose.material3.SingleChoiceSegmentedButtonRow
26
28
  import androidx.compose.material3.Switch
27
29
  import androidx.compose.material3.Text
28
30
  import androidx.compose.runtime.Composable
31
+ import androidx.compose.runtime.getValue
32
+ import androidx.compose.runtime.mutableStateOf
33
+ import androidx.compose.runtime.remember
34
+ import androidx.compose.runtime.setValue
29
35
  import androidx.compose.ui.Alignment
30
36
  import androidx.compose.ui.Modifier
31
37
  import androidx.compose.ui.graphics.Color
@@ -75,9 +81,7 @@ fun LanguageSelector(current: UiLocale, onSelected: (UiLocale) -> Unit) {
75
81
  UiLocale.En.name to stringResource(R.string.settings_language_en),
76
82
  UiLocale.Ru.name to stringResource(R.string.settings_language_ru)
77
83
  ),
78
- leadingIcon = {
79
- Icon(Icons.Outlined.Translate, contentDescription = null, tint = MaterialTheme.colorScheme.onSurfaceVariant)
80
- },
84
+ style = EnumSelectorStyle.Segmented,
81
85
  onSelected = { selected -> onSelected(UiLocale.entries.first { it.name == selected }) }
82
86
  )
83
87
  }
@@ -91,29 +95,76 @@ fun ThemeSelector(current: ThemeMode, onSelected: (ThemeMode) -> Unit) {
91
95
  ThemeMode.Light.name to stringResource(R.string.settings_theme_light),
92
96
  ThemeMode.Dark.name to stringResource(R.string.settings_theme_dark)
93
97
  ),
98
+ style = EnumSelectorStyle.Segmented,
94
99
  onSelected = { selected -> onSelected(ThemeMode.entries.first { it.name == selected }) }
95
100
  )
96
101
  }
97
102
 
98
- @OptIn(ExperimentalLayoutApi::class)
103
+ enum class EnumSelectorStyle {
104
+ Segmented,
105
+ Dropdown
106
+ }
107
+
108
+ @OptIn(ExperimentalMaterial3Api::class)
99
109
  @Composable
100
110
  fun EnumSelector(
101
111
  title: String,
102
112
  current: String,
103
113
  options: List<Pair<String, String>>,
114
+ style: EnumSelectorStyle = EnumSelectorStyle.Segmented,
104
115
  leadingIcon: @Composable (() -> Unit)? = null,
105
116
  onSelected: (String) -> Unit
106
117
  ) {
107
118
  Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
108
119
  Text(title, style = MaterialTheme.typography.titleSmall)
109
- FlowRow(horizontalArrangement = Arrangement.spacedBy(8.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
110
- options.forEach { (value, label) ->
111
- FilterChip(
112
- selected = value == current,
113
- onClick = { onSelected(value) },
114
- label = { Text(label) },
115
- leadingIcon = if (value == current && leadingIcon != null) leadingIcon else null
116
- )
120
+ when (style) {
121
+ EnumSelectorStyle.Segmented -> {
122
+ SingleChoiceSegmentedButtonRow(modifier = Modifier.fillMaxWidth()) {
123
+ options.forEachIndexed { index, (value, label) ->
124
+ SegmentedButton(
125
+ selected = value == current,
126
+ onClick = { onSelected(value) },
127
+ shape = SegmentedButtonDefaults.itemShape(index = index, count = options.size),
128
+ label = { Text(label) }
129
+ )
130
+ }
131
+ }
132
+ }
133
+
134
+ EnumSelectorStyle.Dropdown -> {
135
+ var expanded by remember { mutableStateOf(false) }
136
+ val selectedLabel = options.firstOrNull { it.first == current }?.second.orEmpty()
137
+ ExposedDropdownMenuBox(
138
+ expanded = expanded,
139
+ onExpandedChange = { expanded = !expanded }
140
+ ) {
141
+ OutlinedTextField(
142
+ value = selectedLabel,
143
+ onValueChange = {},
144
+ modifier = Modifier
145
+ .menuAnchor()
146
+ .fillMaxWidth(),
147
+ readOnly = true,
148
+ label = { Text(title) },
149
+ leadingIcon = leadingIcon,
150
+ trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) },
151
+ shape = MaterialTheme.shapes.medium
152
+ )
153
+ DropdownMenu(
154
+ expanded = expanded,
155
+ onDismissRequest = { expanded = false }
156
+ ) {
157
+ options.forEach { (value, label) ->
158
+ DropdownMenuItem(
159
+ text = { Text(label) },
160
+ onClick = {
161
+ expanded = false
162
+ onSelected(value)
163
+ }
164
+ )
165
+ }
166
+ }
167
+ }
117
168
  }
118
169
  }
119
170
  }
@@ -235,6 +235,11 @@ fun TaskDetailPane(
235
235
  item {
236
236
  Row(horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth()) {
237
237
  Column(modifier = Modifier.fillMaxWidth(0.72f)) {
238
+ Text(
239
+ stringResource(R.string.task_detail_title),
240
+ style = MaterialTheme.typography.labelLarge,
241
+ color = MaterialTheme.colorScheme.onSurfaceVariant
242
+ )
238
243
  Text(task.title, style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold)
239
244
  Text(
240
245
  task.dueDate?.let { formatAbsolute(it, locale) } ?: stringResource(R.string.task_detail_no_due_date),
@@ -80,6 +80,7 @@ fun TaskEditorSheet(
80
80
  title = stringResource(R.string.create_task_field_priority),
81
81
  current = localDraft.priority.name,
82
82
  options = priorityOptions,
83
+ style = EnumSelectorStyle.Segmented,
83
84
  onSelected = { selected ->
84
85
  localDraft = localDraft.copy(priority = Priority.entries.first { it.name == selected })
85
86
  }
@@ -185,11 +186,11 @@ fun RecurringRuleSheet(
185
186
  title = stringResource(R.string.recurring_rule_field_cadence),
186
187
  current = draft.cadence?.name.orEmpty(),
187
188
  options = listOf(
188
- "" to "—",
189
189
  Cadence.Daily.name to dailyCadenceLabel,
190
190
  Cadence.Weekly.name to weeklyCadenceLabel,
191
191
  Cadence.Monthly.name to monthlyCadenceLabel
192
192
  ),
193
+ style = EnumSelectorStyle.Segmented,
193
194
  onSelected = {
194
195
  draft = draft.copy(
195
196
  cadence = Cadence.entries.firstOrNull { entry -> entry.name == it },
@@ -211,6 +212,7 @@ fun RecurringRuleSheet(
211
212
  title = stringResource(R.string.recurring_rule_field_weekday),
212
213
  current = draft.weekday?.name.orEmpty(),
213
214
  options = weekdayOptions,
215
+ style = EnumSelectorStyle.Dropdown,
214
216
  onSelected = { draft = draft.copy(weekday = Weekday.entries.firstOrNull { day -> day.name == it }) }
215
217
  )
216
218
  }
@@ -266,7 +268,8 @@ fun RecurringRuleSheet(
266
268
  EnumSelector(
267
269
  title = stringResource(R.string.recurring_rule_field_summary_channel),
268
270
  current = draft.summaryChannel?.name.orEmpty(),
269
- options = summaryChannelOptions,
271
+ options = summaryChannelOptions.drop(1),
272
+ style = EnumSelectorStyle.Segmented,
270
273
  onSelected = {
271
274
  draft = draft.copy(summaryChannel = SummaryChannel.entries.firstOrNull { item -> item.name == it })
272
275
  }
@@ -18,6 +18,7 @@
18
18
  <string name="home_empty_title">Nothing to do</string>
19
19
  <string name="home_empty_body">Add a task or switch filters to see more items.</string>
20
20
  <string name="task_detail_no_due_date">No deadline</string>
21
+ <string name="task_detail_title">Task details</string>
21
22
  <string name="task_detail_status">Status</string>
22
23
  <string name="task_detail_priority">Priority</string>
23
24
  <string name="task_detail_notes">Notes</string>
@@ -18,6 +18,7 @@
18
18
  <string name="home_empty_title">Список пуст</string>
19
19
  <string name="home_empty_body">Добавьте задачу или смените фильтр, чтобы увидеть элементы.</string>
20
20
  <string name="task_detail_no_due_date">Без срока</string>
21
+ <string name="task_detail_title">Детали задачи</string>
21
22
  <string name="task_detail_status">Статус</string>
22
23
  <string name="task_detail_priority">Приоритет</string>
23
24
  <string name="task_detail_notes">Заметки</string>
@@ -26,6 +26,7 @@
26
26
  "analytics.overdue_section" = "Overdue review";
27
27
  "analytics.overdue_subtitle" = "Tasks that need attention first.";
28
28
  "analytics.empty_overdue_body" = "Everything important is on track.";
29
+ "task_detail.title" = "Task details";
29
30
  "task_detail.status" = "Status";
30
31
  "task_detail.priority" = "Priority";
31
32
  "task_detail.notes" = "Notes";
@@ -26,6 +26,7 @@
26
26
  "analytics.overdue_section" = "Просроченные задачи";
27
27
  "analytics.overdue_subtitle" = "Задачи, которым нужно уделить внимание в первую очередь.";
28
28
  "analytics.empty_overdue_body" = "Все важные задачи идут по плану.";
29
+ "task_detail.title" = "Детали задачи";
29
30
  "task_detail.status" = "Статус";
30
31
  "task_detail.priority" = "Приоритет";
31
32
  "task_detail.notes" = "Заметки";
@@ -34,6 +34,7 @@ struct RecurringRuleSheet: View {
34
34
  .tag(RecurrenceCadence?.some(cadence))
35
35
  }
36
36
  }
37
+ .pickerStyle(.segmented)
37
38
  errorText("cadence")
38
39
 
39
40
  TextField(model.string("recurring_rule.field_interval"), text: $draft.interval)
@@ -47,6 +48,7 @@ struct RecurringRuleSheet: View {
47
48
  Text(model.label(for: weekday)).tag(Weekday?.some(weekday))
48
49
  }
49
50
  }
51
+ .pickerStyle(.menu)
50
52
  errorText("weekday")
51
53
  }
52
54
 
@@ -79,6 +81,7 @@ struct RecurringRuleSheet: View {
79
81
  .tag(SummaryChannel?.some(channel))
80
82
  }
81
83
  }
84
+ .pickerStyle(.segmented)
82
85
  errorText("summaryChannel")
83
86
  }
84
87
  }
@@ -27,6 +27,7 @@ struct TaskEditorSheet: View {
27
27
  Text(model.label(for: priority)).tag(priority)
28
28
  }
29
29
  }
30
+ .pickerStyle(.segmented)
30
31
  DatePicker(
31
32
  model.string(editingTaskID == nil ? "create_task.field_due_date" : "edit_task.field_due_date"),
32
33
  selection: Binding(
@@ -20,11 +20,13 @@ struct SettingsView: View {
20
20
  Text(model.string("settings.language_en")).tag(AppLocale.en)
21
21
  Text(model.string("settings.language_ru")).tag(AppLocale.ru)
22
22
  }
23
+ .pickerStyle(.segmented)
23
24
 
24
25
  Picker(model.string("settings.theme"), selection: $draft.theme) {
25
26
  Text(model.string("settings.theme_light")).tag(ThemePreference.light)
26
27
  Text(model.string("settings.theme_dark")).tag(ThemePreference.dark)
27
28
  }
29
+ .pickerStyle(.segmented)
28
30
 
29
31
  Toggle(model.string("settings.reminders"), isOn: $draft.remindersEnabled)
30
32
  Toggle(model.string("settings.daily_summary"), isOn: $draft.dailySummaryEnabled)
@@ -312,6 +312,7 @@ private struct TaskDetailPanel: View {
312
312
  }
313
313
  .padding()
314
314
  }
315
+ .navigationTitle(model.string("task_detail.title"))
315
316
  }
316
317
 
317
318
  private func stat(_ title: String, value: String) -> some View {
@@ -286,7 +286,7 @@ final class AppModel: ObservableObject {
286
286
  guard let date else { return string("task_detail.no_due_date") }
287
287
  let formatter = RelativeDateTimeFormatter()
288
288
  formatter.locale = locale
289
- formatter.unitsStyle = .full
289
+ formatter.unitsStyle = .abbreviated
290
290
  return formatter.localizedString(for: date, relativeTo: .now)
291
291
  }
292
292