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.
- package/README.md +63 -18
- package/cli/index.ts +21 -3
- package/cli/init.ts +27 -11
- package/docs/implementation-notes.md +119 -0
- package/docs/release-notes-v0.1.26.md +64 -0
- package/docs/release-notes-v0.1.27.md +28 -0
- package/docs/release-notes-v0.1.28.md +25 -0
- package/docs/stress-test-maturity-report.md +1 -1
- package/drift/index.ts +396 -22
- package/examples/taskflow/AGENTS.md +112 -0
- package/examples/taskflow/CLAUDE.md +112 -0
- package/examples/taskflow/generated/android/TaskFlow/README.md +43 -0
- package/examples/taskflow/generated/android/TaskFlow/app/build.gradle.kts +76 -0
- package/examples/taskflow/generated/android/TaskFlow/app/proguard-rules.pro +1 -0
- package/examples/taskflow/generated/android/TaskFlow/app/src/main/AndroidManifest.xml +21 -0
- package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/MainActivity.kt +19 -0
- package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/TaskFlowApp.kt +283 -0
- package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/model/DomainModels.kt +106 -0
- package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/model/SampleData.kt +57 -0
- package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/ui/components/Common.kt +109 -0
- package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/ui/screens/HomeScreen.kt +112 -0
- package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/ui/screens/ProjectsScreen.kt +61 -0
- package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/ui/screens/SettingsScreen.kt +82 -0
- package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/ui/screens/TaskDetailScreen.kt +111 -0
- package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/ui/sheets/Sheets.kt +77 -0
- package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/ui/theme/Color.kt +30 -0
- package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/ui/theme/Theme.kt +86 -0
- package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/ui/theme/Type.kt +57 -0
- package/examples/taskflow/generated/android/TaskFlow/app/src/main/res/values/strings.xml +155 -0
- package/examples/taskflow/generated/android/TaskFlow/app/src/main/res/values/themes.xml +4 -0
- package/examples/taskflow/generated/android/TaskFlow/build.gradle.kts +5 -0
- package/examples/taskflow/generated/android/TaskFlow/gradle/gradle-daemon-jvm.properties +12 -0
- package/examples/taskflow/generated/android/TaskFlow/gradle/wrapper/gradle-wrapper.jar +0 -0
- package/examples/taskflow/generated/android/TaskFlow/gradle/wrapper/gradle-wrapper.properties +7 -0
- package/examples/taskflow/generated/android/TaskFlow/gradle.properties +4 -0
- package/examples/taskflow/generated/android/TaskFlow/gradlew +18 -0
- package/examples/taskflow/generated/android/TaskFlow/gradlew.bat +12 -0
- package/examples/taskflow/generated/android/TaskFlow/settings.gradle.kts +18 -0
- package/examples/taskflow/generated/ios/TaskFlow/README.md +21 -0
- package/examples/taskflow/generated/ios/TaskFlow/Resources/en.lproj/Localizable.strings +115 -0
- package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/App/TaskFlowApp.swift +24 -0
- package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Components/AppChrome.swift +150 -0
- package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Flows/TaskEditorSheet.swift +220 -0
- package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Models/DomainModels.swift +122 -0
- package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Screens/CalendarView.swift +21 -0
- package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Screens/HomeView.swift +201 -0
- package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Screens/ProfileEditView.swift +48 -0
- package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Screens/ProjectDetailView.swift +59 -0
- package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Screens/ProjectsView.swift +63 -0
- package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Screens/SettingsView.swift +85 -0
- package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Screens/TaskDetailView.swift +219 -0
- package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Support/AppModel.swift +320 -0
- package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Support/AppSupport.swift +41 -0
- package/examples/taskflow/generated/ios/TaskFlow/project.yml +26 -0
- package/examples/taskflow/generated/web/TaskFlow/README.md +19 -0
- package/examples/taskflow/generated/web/TaskFlow/index.html +12 -0
- package/examples/taskflow/generated/web/TaskFlow/package-lock.json +1908 -0
- package/examples/taskflow/generated/web/TaskFlow/package.json +24 -0
- package/examples/taskflow/generated/web/TaskFlow/src/App.tsx +58 -0
- package/examples/taskflow/generated/web/TaskFlow/src/AppShell.tsx +55 -0
- package/examples/taskflow/generated/web/TaskFlow/src/components/Common.tsx +82 -0
- package/examples/taskflow/generated/web/TaskFlow/src/components/Modals.tsx +191 -0
- package/examples/taskflow/generated/web/TaskFlow/src/components/Nav.tsx +41 -0
- package/examples/taskflow/generated/web/TaskFlow/src/generated/messages.ts +131 -0
- package/examples/taskflow/generated/web/TaskFlow/src/hooks.ts +25 -0
- package/examples/taskflow/generated/web/TaskFlow/src/i18n.ts +39 -0
- package/examples/taskflow/generated/web/TaskFlow/src/locales.en.json +111 -0
- package/examples/taskflow/generated/web/TaskFlow/src/main.tsx +13 -0
- package/examples/taskflow/generated/web/TaskFlow/src/screens/HomeScreen.tsx +111 -0
- package/examples/taskflow/generated/web/TaskFlow/src/screens/ProjectsScreen.tsx +82 -0
- package/examples/taskflow/generated/web/TaskFlow/src/screens/SettingsScreens.tsx +132 -0
- package/examples/taskflow/generated/web/TaskFlow/src/screens/TaskDetail.tsx +105 -0
- package/examples/taskflow/generated/web/TaskFlow/src/store.ts +216 -0
- package/examples/taskflow/generated/web/TaskFlow/src/styles.css +617 -0
- package/examples/taskflow/generated/web/TaskFlow/src/types.ts +64 -0
- package/examples/taskflow/generated/web/TaskFlow/src/utils.ts +78 -0
- package/examples/taskflow/generated/web/TaskFlow/tsconfig.json +21 -0
- package/examples/taskflow/generated/web/TaskFlow/vite.config.ts +6 -0
- package/examples/taskflow/openuispec/README.md +49 -0
- package/examples/todo-orbit/AGENTS.md +46 -14
- package/examples/todo-orbit/CLAUDE.md +46 -14
- package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/ui/components/CommonComponents.kt +69 -18
- package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/ui/screens/TasksScreen.kt +5 -0
- package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/ui/sheets/EditorSheets.kt +5 -2
- package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/res/values/strings.xml +1 -0
- package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/res/values-ru/strings.xml +1 -0
- package/examples/todo-orbit/generated/ios/Todo Orbit/Resources/en.lproj/Localizable.strings +1 -0
- package/examples/todo-orbit/generated/ios/Todo Orbit/Resources/ru.lproj/Localizable.strings +1 -0
- package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Flows/RecurringRuleSheet.swift +3 -0
- package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Flows/TaskEditorSheet.swift +1 -0
- package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Screens/SettingsView.swift +2 -0
- package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Screens/TasksHomeView.swift +1 -0
- package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Support/AppModel.swift +1 -1
- package/examples/todo-orbit/generated/web/Todo Orbit/src/App.tsx +59 -6
- package/examples/todo-orbit/generated/web/Todo Orbit/src/styles.css +40 -0
- package/examples/todo-orbit/openuispec/README.md +24 -131
- package/examples/todo-orbit/openuispec/flows/create_recurring_rule.yaml +3 -0
- package/examples/todo-orbit/openuispec/flows/create_task.yaml +1 -0
- package/examples/todo-orbit/openuispec/flows/edit_task.yaml +1 -0
- package/examples/todo-orbit/openuispec/locales/en.json +1 -0
- package/examples/todo-orbit/openuispec/locales/ru.json +1 -0
- package/examples/todo-orbit/openuispec/screens/task_detail.yaml +1 -0
- package/examples/todo-orbit/openuispec/tokens/icons.yaml +6 -0
- package/package.json +6 -1
- package/prepare/index.ts +391 -0
- package/schema/semantic-lint.ts +592 -0
- package/schema/validate.ts +17 -13
- package/status/index.ts +200 -0
- /package/examples/taskflow/{contracts → openuispec/contracts}/README.md +0 -0
- /package/examples/taskflow/{contracts → openuispec/contracts}/action_trigger.yaml +0 -0
- /package/examples/taskflow/{contracts → openuispec/contracts}/collection.yaml +0 -0
- /package/examples/taskflow/{contracts → openuispec/contracts}/data_display.yaml +0 -0
- /package/examples/taskflow/{contracts → openuispec/contracts}/feedback.yaml +0 -0
- /package/examples/taskflow/{contracts → openuispec/contracts}/input_field.yaml +0 -0
- /package/examples/taskflow/{contracts → openuispec/contracts}/nav_container.yaml +0 -0
- /package/examples/taskflow/{contracts → openuispec/contracts}/surface.yaml +0 -0
- /package/examples/taskflow/{contracts → openuispec/contracts}/x_media_player.yaml +0 -0
- /package/examples/taskflow/{flows → openuispec/flows}/create_task.yaml +0 -0
- /package/examples/taskflow/{flows → openuispec/flows}/edit_task.yaml +0 -0
- /package/examples/taskflow/{locales → openuispec/locales}/en.json +0 -0
- /package/examples/taskflow/{openuispec.yaml → openuispec/openuispec.yaml} +0 -0
- /package/examples/taskflow/{platform → openuispec/platform}/android.yaml +0 -0
- /package/examples/taskflow/{platform → openuispec/platform}/ios.yaml +0 -0
- /package/examples/taskflow/{platform → openuispec/platform}/web.yaml +0 -0
- /package/examples/taskflow/{screens → openuispec/screens}/calendar.yaml +0 -0
- /package/examples/taskflow/{screens → openuispec/screens}/home.yaml +0 -0
- /package/examples/taskflow/{screens → openuispec/screens}/profile_edit.yaml +0 -0
- /package/examples/taskflow/{screens → openuispec/screens}/project_detail.yaml +0 -0
- /package/examples/taskflow/{screens → openuispec/screens}/projects.yaml +0 -0
- /package/examples/taskflow/{screens → openuispec/screens}/settings.yaml +0 -0
- /package/examples/taskflow/{screens → openuispec/screens}/task_detail.yaml +0 -0
- /package/examples/taskflow/{tokens → openuispec/tokens}/color.yaml +0 -0
- /package/examples/taskflow/{tokens → openuispec/tokens}/elevation.yaml +0 -0
- /package/examples/taskflow/{tokens → openuispec/tokens}/icons.yaml +0 -0
- /package/examples/taskflow/{tokens → openuispec/tokens}/layout.yaml +0 -0
- /package/examples/taskflow/{tokens → openuispec/tokens}/motion.yaml +0 -0
- /package/examples/taskflow/{tokens → openuispec/tokens}/spacing.yaml +0 -0
- /package/examples/taskflow/{tokens → openuispec/tokens}/themes.yaml +0 -0
- /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,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.
|
|
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
|
-
##
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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 --
|
|
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.
|
|
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
|
-
##
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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 --
|
|
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.
|
|
22
|
-
import androidx.compose.material3.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
}
|
package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/res/values/strings.xml
CHANGED
|
@@ -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>
|
package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/res/values-ru/strings.xml
CHANGED
|
@@ -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
|
}
|
package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Screens/SettingsView.swift
CHANGED
|
@@ -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)
|
package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Support/AppModel.swift
CHANGED
|
@@ -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 = .
|
|
289
|
+
formatter.unitsStyle = .abbreviated
|
|
290
290
|
return formatter.localizedString(for: date, relativeTo: .now)
|
|
291
291
|
}
|
|
292
292
|
|