openuispec 0.1.17 → 0.1.19
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 +52 -34
- package/cli/index.ts +1 -1
- package/docs/stress-test-maturity-report.md +97 -0
- package/examples/taskflow/screens/profile_edit.yaml +2 -0
- package/examples/todo-orbit/AGENTS.md +127 -0
- package/examples/todo-orbit/CLAUDE.md +127 -0
- package/examples/todo-orbit/README.md +62 -0
- package/examples/todo-orbit/generated/android/Todo Orbit/README.md +14 -0
- package/examples/todo-orbit/generated/android/Todo Orbit/app/build.gradle.kts +58 -0
- package/examples/todo-orbit/generated/android/Todo Orbit/app/proguard-rules.pro +1 -0
- package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/AndroidManifest.xml +20 -0
- package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/MainActivity.kt +14 -0
- package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/TodoOrbitApp.kt +345 -0
- package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/support/AppLogic.kt +231 -0
- package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/support/Models.kt +169 -0
- package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/support/Strings.kt +8 -0
- package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/ui/components/CommonComponents.kt +185 -0
- package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/ui/screens/AnalyticsScreen.kt +193 -0
- package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/ui/screens/SettingsScreen.kt +102 -0
- package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/ui/screens/TasksScreen.kt +342 -0
- package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/ui/sheets/EditorSheets.kt +344 -0
- package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/ui/theme/TodoOrbitTheme.kt +59 -0
- package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/res/values/strings.xml +148 -0
- package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/res/values-ru/strings.xml +154 -0
- package/examples/todo-orbit/generated/android/Todo Orbit/build.gradle.kts +4 -0
- package/examples/todo-orbit/generated/android/Todo Orbit/gradle/wrapper/gradle-wrapper.jar +0 -0
- package/examples/todo-orbit/generated/android/Todo Orbit/gradle/wrapper/gradle-wrapper.properties +7 -0
- package/examples/todo-orbit/generated/android/Todo Orbit/gradle.properties +4 -0
- package/examples/todo-orbit/generated/android/Todo Orbit/gradlew +248 -0
- package/examples/todo-orbit/generated/android/Todo Orbit/gradlew.bat +93 -0
- package/examples/todo-orbit/generated/android/Todo Orbit/settings.gradle.kts +18 -0
- package/examples/todo-orbit/generated/ios/Todo Orbit/README.md +29 -0
- package/examples/todo-orbit/generated/ios/Todo Orbit/Resources/en.lproj/Localizable.strings +118 -0
- package/examples/todo-orbit/generated/ios/Todo Orbit/Resources/ru.lproj/Localizable.strings +118 -0
- package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/App/TodoOrbitApp.swift +50 -0
- package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Components/OrbitChrome.swift +204 -0
- package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Components/SchedulePreviewView.swift +126 -0
- package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Components/TrendChartView.swift +70 -0
- package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Flows/RecurringRuleSheet.swift +123 -0
- package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Flows/TaskEditorSheet.swift +60 -0
- package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Models/DomainModels.swift +238 -0
- package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Screens/AnalyticsView.swift +94 -0
- package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Screens/SettingsView.swift +74 -0
- package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Screens/TasksHomeView.swift +363 -0
- package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Support/AppModel.swift +324 -0
- package/examples/todo-orbit/generated/ios/Todo Orbit/TodoOrbit.xcodeproj/project.pbxproj +408 -0
- package/examples/todo-orbit/generated/ios/Todo Orbit/TodoOrbit.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
- package/examples/todo-orbit/generated/ios/Todo Orbit/TodoOrbit.xcodeproj/xcshareddata/xcschemes/TodoOrbit.xcscheme +79 -0
- package/examples/todo-orbit/generated/ios/Todo Orbit/project.yml +24 -0
- package/examples/todo-orbit/generated/web/Todo Orbit/index.html +16 -0
- package/examples/todo-orbit/generated/web/Todo Orbit/package-lock.json +1087 -0
- package/examples/todo-orbit/generated/web/Todo Orbit/package.json +24 -0
- package/examples/todo-orbit/generated/web/Todo Orbit/src/App.tsx +2114 -0
- package/examples/todo-orbit/generated/web/Todo Orbit/src/main.tsx +13 -0
- package/examples/todo-orbit/generated/web/Todo Orbit/src/styles.css +886 -0
- package/examples/todo-orbit/generated/web/Todo Orbit/tsconfig.json +19 -0
- package/examples/todo-orbit/generated/web/Todo Orbit/vite.config.ts +6 -0
- package/examples/todo-orbit/openuispec/README.md +158 -0
- package/examples/todo-orbit/openuispec/contracts/.gitkeep +0 -0
- package/examples/todo-orbit/openuispec/contracts/action_trigger.yaml +28 -0
- package/examples/todo-orbit/openuispec/contracts/collection.yaml +32 -0
- package/examples/todo-orbit/openuispec/contracts/data_display.yaml +38 -0
- package/examples/todo-orbit/openuispec/contracts/feedback.yaml +32 -0
- package/examples/todo-orbit/openuispec/contracts/input_field.yaml +52 -0
- package/examples/todo-orbit/openuispec/contracts/nav_container.yaml +47 -0
- package/examples/todo-orbit/openuispec/contracts/surface.yaml +28 -0
- package/examples/todo-orbit/openuispec/contracts/x_schedule_preview.yaml +134 -0
- package/examples/todo-orbit/openuispec/contracts/x_task_trend_chart.yaml +139 -0
- package/examples/todo-orbit/openuispec/flows/.gitkeep +0 -0
- package/examples/todo-orbit/openuispec/flows/create_recurring_rule.yaml +253 -0
- package/examples/todo-orbit/openuispec/flows/create_task.yaml +118 -0
- package/examples/todo-orbit/openuispec/flows/edit_task.yaml +126 -0
- package/examples/todo-orbit/openuispec/locales/.gitkeep +0 -0
- package/examples/todo-orbit/openuispec/locales/en.json +150 -0
- package/examples/todo-orbit/openuispec/locales/ru.json +150 -0
- package/examples/todo-orbit/openuispec/openuispec.yaml +122 -0
- package/examples/todo-orbit/openuispec/platform/.gitkeep +0 -0
- package/examples/todo-orbit/openuispec/platform/android.yaml +19 -0
- package/examples/todo-orbit/openuispec/platform/ios.yaml +20 -0
- package/examples/todo-orbit/openuispec/platform/web.yaml +22 -0
- package/examples/todo-orbit/openuispec/screens/.gitkeep +0 -0
- package/examples/todo-orbit/openuispec/screens/analytics.yaml +139 -0
- package/examples/todo-orbit/openuispec/screens/home.yaml +172 -0
- package/examples/todo-orbit/openuispec/screens/settings.yaml +148 -0
- package/examples/todo-orbit/openuispec/screens/task_detail.yaml +223 -0
- package/examples/todo-orbit/openuispec/tokens/.gitkeep +0 -0
- package/examples/todo-orbit/openuispec/tokens/color.yaml +93 -0
- package/examples/todo-orbit/openuispec/tokens/elevation.yaml +25 -0
- package/examples/todo-orbit/openuispec/tokens/icons.yaml +92 -0
- package/examples/todo-orbit/openuispec/tokens/layout.yaml +107 -0
- package/examples/todo-orbit/openuispec/tokens/motion.yaml +39 -0
- package/examples/todo-orbit/openuispec/tokens/spacing.yaml +18 -0
- package/examples/todo-orbit/openuispec/tokens/themes.yaml +23 -0
- package/examples/todo-orbit/openuispec/tokens/typography.yaml +52 -0
- package/package.json +1 -1
- package/schema/validate.ts +271 -4
- package/spec/openuispec-v0.1.md +80 -13
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
icons:
|
|
2
|
+
sizes:
|
|
3
|
+
sm:
|
|
4
|
+
semantic: "Small inline icon size"
|
|
5
|
+
value: 16
|
|
6
|
+
md:
|
|
7
|
+
semantic: "Default icon size for actions and list items"
|
|
8
|
+
value: 20
|
|
9
|
+
lg:
|
|
10
|
+
semantic: "Prominent icon size for empty states and hero elements"
|
|
11
|
+
value: 24
|
|
12
|
+
|
|
13
|
+
variants:
|
|
14
|
+
default: "outline"
|
|
15
|
+
suffixes:
|
|
16
|
+
_fill: "Filled variant for active or completed states"
|
|
17
|
+
|
|
18
|
+
fallback:
|
|
19
|
+
strategy: "name_passthrough"
|
|
20
|
+
missing_icon: "questionmark_circle"
|
|
21
|
+
|
|
22
|
+
registry:
|
|
23
|
+
actions:
|
|
24
|
+
plus:
|
|
25
|
+
semantic: "Create a new task or item"
|
|
26
|
+
platform:
|
|
27
|
+
ios: "plus"
|
|
28
|
+
android: "add"
|
|
29
|
+
web: "plus"
|
|
30
|
+
search:
|
|
31
|
+
semantic: "Search tasks"
|
|
32
|
+
platform:
|
|
33
|
+
ios: "magnifyingglass"
|
|
34
|
+
android: "search"
|
|
35
|
+
web: "search"
|
|
36
|
+
trash:
|
|
37
|
+
semantic: "Delete a task"
|
|
38
|
+
platform:
|
|
39
|
+
ios: "trash"
|
|
40
|
+
android: "delete"
|
|
41
|
+
web: "trash-2"
|
|
42
|
+
checkmark:
|
|
43
|
+
semantic: "Confirm completion"
|
|
44
|
+
variants: ["circle", "circle_fill", "list"]
|
|
45
|
+
platform:
|
|
46
|
+
ios: "checkmark"
|
|
47
|
+
android: "check"
|
|
48
|
+
web: "check"
|
|
49
|
+
|
|
50
|
+
objects:
|
|
51
|
+
calendar:
|
|
52
|
+
semantic: "Date and scheduling"
|
|
53
|
+
platform:
|
|
54
|
+
ios: "calendar"
|
|
55
|
+
android: "calendar_today"
|
|
56
|
+
web: "calendar"
|
|
57
|
+
gear:
|
|
58
|
+
semantic: "Settings and preferences"
|
|
59
|
+
variants: ["fill"]
|
|
60
|
+
platform:
|
|
61
|
+
ios: "gear"
|
|
62
|
+
android: "settings"
|
|
63
|
+
web: "settings"
|
|
64
|
+
globe:
|
|
65
|
+
semantic: "Language and locale selection"
|
|
66
|
+
platform:
|
|
67
|
+
ios: "globe"
|
|
68
|
+
android: "language"
|
|
69
|
+
web: "languages"
|
|
70
|
+
flag:
|
|
71
|
+
semantic: "Priority marker"
|
|
72
|
+
variants: ["fill"]
|
|
73
|
+
platform:
|
|
74
|
+
ios: "flag"
|
|
75
|
+
android: "flag"
|
|
76
|
+
web: "flag"
|
|
77
|
+
|
|
78
|
+
custom:
|
|
79
|
+
todo_orbit:
|
|
80
|
+
checkmark_list:
|
|
81
|
+
semantic: "Todo list navigation icon"
|
|
82
|
+
platform:
|
|
83
|
+
ios: "checklist"
|
|
84
|
+
android: "checklist"
|
|
85
|
+
web: "list-checks"
|
|
86
|
+
chart_line:
|
|
87
|
+
semantic: "Analytics and trend visualization icon"
|
|
88
|
+
variants: ["fill"]
|
|
89
|
+
platform:
|
|
90
|
+
ios: "chart.line.uptrend.xyaxis"
|
|
91
|
+
android: "insert_chart"
|
|
92
|
+
web: "chart-line"
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
layout:
|
|
2
|
+
size_classes:
|
|
3
|
+
compact:
|
|
4
|
+
semantic: "Phone-first single-column layouts"
|
|
5
|
+
width: { max: 640 }
|
|
6
|
+
columns: 4
|
|
7
|
+
margin: "spacing.md"
|
|
8
|
+
content_max_width: null
|
|
9
|
+
examples: "Phones in portrait, narrow browser windows"
|
|
10
|
+
regular:
|
|
11
|
+
semantic: "Tablet portrait and large phone landscape layouts"
|
|
12
|
+
width: { min: 641, max: 1024 }
|
|
13
|
+
columns: 8
|
|
14
|
+
margin: "spacing.lg"
|
|
15
|
+
content_max_width: 860
|
|
16
|
+
examples: "Tablets in portrait, wide phones in landscape"
|
|
17
|
+
expanded:
|
|
18
|
+
semantic: "Desktop and large-tablet multi-column layouts"
|
|
19
|
+
width: { min: 1025 }
|
|
20
|
+
columns: 12
|
|
21
|
+
margin: "spacing.xl"
|
|
22
|
+
content_max_width: 1240
|
|
23
|
+
examples: "Desktop browsers, tablets in landscape"
|
|
24
|
+
|
|
25
|
+
platform_mapping:
|
|
26
|
+
ios:
|
|
27
|
+
uses: "UIUserInterfaceSizeClass"
|
|
28
|
+
compact: ".compact horizontal"
|
|
29
|
+
regular: ".regular horizontal"
|
|
30
|
+
expanded: ".regular horizontal + width > 1024"
|
|
31
|
+
android:
|
|
32
|
+
uses: "WindowSizeClass"
|
|
33
|
+
compact: "WindowWidthSizeClass.Compact"
|
|
34
|
+
regular: "WindowWidthSizeClass.Medium"
|
|
35
|
+
expanded: "WindowWidthSizeClass.Expanded"
|
|
36
|
+
web:
|
|
37
|
+
uses: "media queries"
|
|
38
|
+
compact: "@media (max-width: 640px)"
|
|
39
|
+
regular: "@media (min-width: 641px) and (max-width: 1024px)"
|
|
40
|
+
expanded: "@media (min-width: 1025px)"
|
|
41
|
+
|
|
42
|
+
primitives:
|
|
43
|
+
stack:
|
|
44
|
+
semantic: "Vertical content flow"
|
|
45
|
+
props:
|
|
46
|
+
spacing: { type: token_ref, default: "spacing.md" }
|
|
47
|
+
align: { type: enum, values: [leading, center, trailing, stretch], default: stretch }
|
|
48
|
+
padding: { type: token_ref, required: false }
|
|
49
|
+
platform_mapping:
|
|
50
|
+
ios: "VStack"
|
|
51
|
+
android: "Column"
|
|
52
|
+
web: "flex-direction: column"
|
|
53
|
+
row:
|
|
54
|
+
semantic: "Horizontal content flow"
|
|
55
|
+
props:
|
|
56
|
+
spacing: { type: token_ref, default: "spacing.sm" }
|
|
57
|
+
align: { type: enum, values: [top, center, bottom, baseline, stretch], default: center }
|
|
58
|
+
justify: { type: enum, values: [start, center, end, space-between, space-around], default: start }
|
|
59
|
+
wrap: { type: bool, default: false }
|
|
60
|
+
platform_mapping:
|
|
61
|
+
ios: "HStack"
|
|
62
|
+
android: "Row"
|
|
63
|
+
web: "flex-direction: row"
|
|
64
|
+
grid:
|
|
65
|
+
semantic: "Responsive 2D arrangement"
|
|
66
|
+
props:
|
|
67
|
+
columns: { type: "int or adaptive_map", default: 2 }
|
|
68
|
+
gap: { type: token_ref, default: "spacing.md" }
|
|
69
|
+
platform_mapping:
|
|
70
|
+
ios: "LazyVGrid"
|
|
71
|
+
android: "LazyVerticalGrid"
|
|
72
|
+
web: "display: grid"
|
|
73
|
+
scroll_vertical:
|
|
74
|
+
semantic: "Scrollable vertical canvas"
|
|
75
|
+
props:
|
|
76
|
+
safe_area: { type: bool, default: true }
|
|
77
|
+
padding: { type: token_ref, required: false }
|
|
78
|
+
platform_mapping:
|
|
79
|
+
ios: "ScrollView(.vertical)"
|
|
80
|
+
android: "LazyColumn or verticalScroll"
|
|
81
|
+
web: "overflow-y: auto"
|
|
82
|
+
split_view:
|
|
83
|
+
semantic: "Master-detail two-pane layout"
|
|
84
|
+
props:
|
|
85
|
+
primary_width: { type: "fraction or token_ref", default: 0.36 }
|
|
86
|
+
divider: { type: bool, default: true }
|
|
87
|
+
collapse_at: { type: size_class, default: "compact" }
|
|
88
|
+
platform_mapping:
|
|
89
|
+
ios: "NavigationSplitView"
|
|
90
|
+
android: "ListDetailPaneScaffold"
|
|
91
|
+
web: "CSS Grid with two columns"
|
|
92
|
+
|
|
93
|
+
reflow_rules:
|
|
94
|
+
action_trigger:
|
|
95
|
+
compact: { full_width: true }
|
|
96
|
+
regular: { full_width: false }
|
|
97
|
+
nav_container:
|
|
98
|
+
compact: { variant: "tab_bar" }
|
|
99
|
+
regular: { variant: "rail" }
|
|
100
|
+
expanded: { variant: "sidebar" }
|
|
101
|
+
task_actions:
|
|
102
|
+
compact: { layout: stack }
|
|
103
|
+
regular: { layout: row }
|
|
104
|
+
stat_group:
|
|
105
|
+
compact: { layout: grid, columns: 2 }
|
|
106
|
+
regular: { layout: row }
|
|
107
|
+
expanded: { layout: row }
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
motion:
|
|
2
|
+
duration:
|
|
3
|
+
instant: 100
|
|
4
|
+
quick: 180
|
|
5
|
+
normal: 280
|
|
6
|
+
slow: 420
|
|
7
|
+
|
|
8
|
+
easing:
|
|
9
|
+
default: "ease-out"
|
|
10
|
+
enter: "ease-out"
|
|
11
|
+
exit: "ease-in"
|
|
12
|
+
emphasis: "cubic-bezier(0.2, 0, 0, 1)"
|
|
13
|
+
|
|
14
|
+
reduced_motion: "remove-animation"
|
|
15
|
+
|
|
16
|
+
patterns:
|
|
17
|
+
press_feedback:
|
|
18
|
+
duration: "instant"
|
|
19
|
+
property: "scale"
|
|
20
|
+
value: 0.98
|
|
21
|
+
state_change:
|
|
22
|
+
duration: "quick"
|
|
23
|
+
property: "opacity, border-color, background"
|
|
24
|
+
screen_enter:
|
|
25
|
+
duration: "normal"
|
|
26
|
+
easing: "enter"
|
|
27
|
+
pattern: "slide-from-trailing"
|
|
28
|
+
screen_exit:
|
|
29
|
+
duration: "quick"
|
|
30
|
+
easing: "exit"
|
|
31
|
+
pattern: "slide-to-leading"
|
|
32
|
+
checkbox_check:
|
|
33
|
+
duration: "quick"
|
|
34
|
+
easing: "emphasis"
|
|
35
|
+
pattern: "scale-bounce"
|
|
36
|
+
modal_present:
|
|
37
|
+
duration: "normal"
|
|
38
|
+
easing: "enter"
|
|
39
|
+
pattern: "fade-and-lift"
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
spacing:
|
|
2
|
+
base_unit: 4
|
|
3
|
+
platform_flex: 0.15
|
|
4
|
+
|
|
5
|
+
scale:
|
|
6
|
+
none: 0
|
|
7
|
+
xs: 4
|
|
8
|
+
sm: 8
|
|
9
|
+
md: { base: 16, range: [12, 16] }
|
|
10
|
+
lg: { base: 24, range: [20, 24] }
|
|
11
|
+
xl: 32
|
|
12
|
+
xxl: 40
|
|
13
|
+
|
|
14
|
+
aliases:
|
|
15
|
+
page_margin: { horizontal: md, vertical: md }
|
|
16
|
+
card_padding: { all: md }
|
|
17
|
+
section_gap: lg
|
|
18
|
+
inline_gap: sm
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
themes:
|
|
2
|
+
default: "light"
|
|
3
|
+
|
|
4
|
+
variants:
|
|
5
|
+
light:
|
|
6
|
+
surface.primary: { lightness: [97, 100] }
|
|
7
|
+
surface.secondary: { lightness: [93, 97] }
|
|
8
|
+
surface.tertiary: { lightness: [89, 94] }
|
|
9
|
+
text.primary: { lightness: [8, 14] }
|
|
10
|
+
text.secondary: { lightness: [36, 46] }
|
|
11
|
+
border.default: { opacity: 0.22 }
|
|
12
|
+
dark:
|
|
13
|
+
surface.primary: { lightness: [10, 14] }
|
|
14
|
+
surface.secondary: { lightness: [14, 18] }
|
|
15
|
+
surface.tertiary: { lightness: [18, 24] }
|
|
16
|
+
text.primary: { lightness: [90, 95] }
|
|
17
|
+
text.secondary: { lightness: [62, 70] }
|
|
18
|
+
border.default: { opacity: 0.18 }
|
|
19
|
+
|
|
20
|
+
platform:
|
|
21
|
+
ios: { supports_dynamic: true }
|
|
22
|
+
android: { dynamic_color: true }
|
|
23
|
+
web: { prefers_color_scheme: true, css_custom_properties: true }
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
typography:
|
|
2
|
+
font_family:
|
|
3
|
+
primary:
|
|
4
|
+
semantic: "Main UI typeface for the todo app"
|
|
5
|
+
value: "Manrope"
|
|
6
|
+
fallback_strategy: "modern-sans"
|
|
7
|
+
platform:
|
|
8
|
+
ios: { system_alternative: "SF Pro" }
|
|
9
|
+
android: { system_alternative: "Google Sans" }
|
|
10
|
+
web: { load_strategy: "swap", source: "google_fonts" }
|
|
11
|
+
|
|
12
|
+
scale:
|
|
13
|
+
display:
|
|
14
|
+
semantic: "Large empty-state and hero copy"
|
|
15
|
+
size: { base: 30, range: [28, 34] }
|
|
16
|
+
weight: 700
|
|
17
|
+
tracking: -0.02
|
|
18
|
+
line_height: 1.15
|
|
19
|
+
heading_lg:
|
|
20
|
+
semantic: "Primary screen titles"
|
|
21
|
+
size: { base: 24, range: [22, 28] }
|
|
22
|
+
weight: 700
|
|
23
|
+
tracking: -0.02
|
|
24
|
+
line_height: 1.2
|
|
25
|
+
heading:
|
|
26
|
+
semantic: "Section headings and modal titles"
|
|
27
|
+
size: { base: 18, range: [17, 20] }
|
|
28
|
+
weight: 600
|
|
29
|
+
tracking: -0.01
|
|
30
|
+
line_height: 1.3
|
|
31
|
+
heading_sm:
|
|
32
|
+
semantic: "Compact emphasized labels"
|
|
33
|
+
size: { base: 15, range: [14, 17] }
|
|
34
|
+
weight: 600
|
|
35
|
+
line_height: 1.35
|
|
36
|
+
body:
|
|
37
|
+
semantic: "Primary body copy"
|
|
38
|
+
size: { base: 16, range: [15, 17] }
|
|
39
|
+
weight: 400
|
|
40
|
+
line_height: 1.5
|
|
41
|
+
body_sm:
|
|
42
|
+
semantic: "Supporting text and subtitles"
|
|
43
|
+
size: { base: 14, range: [13, 15] }
|
|
44
|
+
weight: 400
|
|
45
|
+
tracking: 0.004
|
|
46
|
+
line_height: 1.45
|
|
47
|
+
caption:
|
|
48
|
+
semantic: "Tiny labels and metadata"
|
|
49
|
+
size: { base: 12, range: [11, 13] }
|
|
50
|
+
weight: 500
|
|
51
|
+
tracking: 0.015
|
|
52
|
+
line_height: 1.35
|
package/package.json
CHANGED
package/schema/validate.ts
CHANGED
|
@@ -23,6 +23,18 @@ const __dirname = fileURLToPath(new URL(".", import.meta.url));
|
|
|
23
23
|
const SCHEMA_DIR = resolve(__dirname);
|
|
24
24
|
|
|
25
25
|
type AjvInstance = InstanceType<typeof Ajv2020>;
|
|
26
|
+
type UnknownRecord = Record<string, unknown>;
|
|
27
|
+
|
|
28
|
+
interface UsageLint {
|
|
29
|
+
path: string;
|
|
30
|
+
message: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface StandardContractRule {
|
|
34
|
+
requiredProps?: string[];
|
|
35
|
+
nonEmptyStringProps?: string[];
|
|
36
|
+
validate?: (node: UnknownRecord, props: UnknownRecord, path: string) => UsageLint[];
|
|
37
|
+
}
|
|
26
38
|
|
|
27
39
|
// ── helpers ──────────────────────────────────────────────────────────
|
|
28
40
|
|
|
@@ -49,6 +61,255 @@ function listFiles(dir: string, ext: string): string[] {
|
|
|
49
61
|
}
|
|
50
62
|
}
|
|
51
63
|
|
|
64
|
+
function isRecord(value: unknown): value is UnknownRecord {
|
|
65
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function isNonEmptyString(value: unknown): value is string {
|
|
69
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function getSingleRootValue(data: unknown): unknown {
|
|
73
|
+
if (!isRecord(data)) return undefined;
|
|
74
|
+
const values = Object.values(data);
|
|
75
|
+
return values.length === 1 ? values[0] : undefined;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const STANDARD_CONTRACT_RULES: Record<string, StandardContractRule> = {
|
|
79
|
+
action_trigger: {
|
|
80
|
+
requiredProps: ["label"],
|
|
81
|
+
nonEmptyStringProps: ["label"],
|
|
82
|
+
},
|
|
83
|
+
data_display: {
|
|
84
|
+
requiredProps: ["title"],
|
|
85
|
+
nonEmptyStringProps: ["title"],
|
|
86
|
+
},
|
|
87
|
+
input_field: {
|
|
88
|
+
requiredProps: ["label"],
|
|
89
|
+
nonEmptyStringProps: ["label"],
|
|
90
|
+
validate(node, props, path) {
|
|
91
|
+
const inputType = node.input_type;
|
|
92
|
+
if (inputType === "select" || inputType === "radio") {
|
|
93
|
+
return hasOwnProp(props, "options")
|
|
94
|
+
? []
|
|
95
|
+
: [{
|
|
96
|
+
path,
|
|
97
|
+
message: `contract "input_field" with input_type="${String(inputType)}" requires props.options`,
|
|
98
|
+
}];
|
|
99
|
+
}
|
|
100
|
+
if (inputType === "slider") {
|
|
101
|
+
return hasOwnProp(props, "range")
|
|
102
|
+
? []
|
|
103
|
+
: [{
|
|
104
|
+
path,
|
|
105
|
+
message: 'contract "input_field" with input_type="slider" requires props.range',
|
|
106
|
+
}];
|
|
107
|
+
}
|
|
108
|
+
return [];
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
nav_container: {
|
|
112
|
+
requiredProps: ["items"],
|
|
113
|
+
validate(_node, props, path) {
|
|
114
|
+
const items = props.items;
|
|
115
|
+
if (!Array.isArray(items)) {
|
|
116
|
+
return [];
|
|
117
|
+
}
|
|
118
|
+
const errors: UsageLint[] = [];
|
|
119
|
+
for (const [index, item] of items.entries()) {
|
|
120
|
+
const itemPath = `${path}/props/items/${index}`;
|
|
121
|
+
if (!isRecord(item)) {
|
|
122
|
+
errors.push({
|
|
123
|
+
path: itemPath,
|
|
124
|
+
message: "nav_container items must be objects",
|
|
125
|
+
});
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
for (const key of ["id", "label", "icon", "destination"]) {
|
|
129
|
+
if (!hasOwnProp(item, key)) {
|
|
130
|
+
errors.push({
|
|
131
|
+
path: itemPath,
|
|
132
|
+
message: `nav_container item requires "${key}"`,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (hasOwnProp(item, "label") && !isNonEmptyString(item.label)) {
|
|
137
|
+
errors.push({
|
|
138
|
+
path: `${itemPath}/label`,
|
|
139
|
+
message: 'nav_container item "label" must be a non-empty string',
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return errors;
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
feedback: {
|
|
147
|
+
requiredProps: ["message"],
|
|
148
|
+
nonEmptyStringProps: ["message"],
|
|
149
|
+
},
|
|
150
|
+
surface: {
|
|
151
|
+
requiredProps: ["content"],
|
|
152
|
+
},
|
|
153
|
+
collection: {
|
|
154
|
+
requiredProps: ["data", "item_contract", "item_props_map"],
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
function hasOwnProp(obj: UnknownRecord, key: string): boolean {
|
|
159
|
+
return Object.prototype.hasOwnProperty.call(obj, key);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function validateStandardContractUsage(
|
|
163
|
+
node: UnknownRecord,
|
|
164
|
+
path: string,
|
|
165
|
+
): UsageLint[] {
|
|
166
|
+
const contract = node.contract;
|
|
167
|
+
if (typeof contract !== "string") return [];
|
|
168
|
+
|
|
169
|
+
const rule = STANDARD_CONTRACT_RULES[contract];
|
|
170
|
+
if (!rule) return [];
|
|
171
|
+
|
|
172
|
+
const props = isRecord(node.props) ? node.props : {};
|
|
173
|
+
const errors: UsageLint[] = [];
|
|
174
|
+
|
|
175
|
+
for (const prop of rule.requiredProps ?? []) {
|
|
176
|
+
if (!hasOwnProp(props, prop)) {
|
|
177
|
+
errors.push({
|
|
178
|
+
path,
|
|
179
|
+
message: `contract "${contract}" requires props.${prop}`,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
for (const prop of rule.nonEmptyStringProps ?? []) {
|
|
185
|
+
if (hasOwnProp(props, prop) && !isNonEmptyString(props[prop])) {
|
|
186
|
+
errors.push({
|
|
187
|
+
path: `${path}/props/${prop}`,
|
|
188
|
+
message: `props.${prop} for contract "${contract}" must be a non-empty string`,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
errors.push(...(rule.validate?.(node, props, path) ?? []));
|
|
194
|
+
return errors;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function lintSectionItems(items: unknown, path: string): UsageLint[] {
|
|
198
|
+
if (!Array.isArray(items)) return [];
|
|
199
|
+
const errors: UsageLint[] = [];
|
|
200
|
+
|
|
201
|
+
for (const [index, item] of items.entries()) {
|
|
202
|
+
const itemPath = `${path}/${index}`;
|
|
203
|
+
if (!isRecord(item)) {
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
errors.push(...validateStandardContractUsage(item, itemPath));
|
|
208
|
+
|
|
209
|
+
if (Array.isArray(item.children)) {
|
|
210
|
+
errors.push(...lintSectionItems(item.children, `${itemPath}/children`));
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return errors;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function lintScreenLikeDefinition(screenDef: unknown, path: string): UsageLint[] {
|
|
218
|
+
if (!isRecord(screenDef)) return [];
|
|
219
|
+
const errors: UsageLint[] = [];
|
|
220
|
+
|
|
221
|
+
if (isRecord(screenDef.layout)) {
|
|
222
|
+
errors.push(
|
|
223
|
+
...lintSectionItems(screenDef.layout.sections, `${path}/layout/sections`),
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (isRecord(screenDef.navigation)) {
|
|
228
|
+
errors.push(
|
|
229
|
+
...validateStandardContractUsage(
|
|
230
|
+
screenDef.navigation,
|
|
231
|
+
`${path}/navigation`,
|
|
232
|
+
),
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (isRecord(screenDef.surfaces)) {
|
|
237
|
+
for (const [surfaceId, surfaceDef] of Object.entries(screenDef.surfaces)) {
|
|
238
|
+
const surfacePath = `${path}/surfaces/${surfaceId}`;
|
|
239
|
+
if (!isRecord(surfaceDef)) {
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
errors.push(...validateStandardContractUsage(surfaceDef, surfacePath));
|
|
244
|
+
|
|
245
|
+
const props = isRecord(surfaceDef.props) ? surfaceDef.props : {};
|
|
246
|
+
if (Array.isArray(props.content)) {
|
|
247
|
+
errors.push(
|
|
248
|
+
...lintSectionItems(props.content, `${surfacePath}/props/content`),
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return errors;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function lintScreenFile(dataPath: string): number {
|
|
258
|
+
const root = getSingleRootValue(loadData(dataPath));
|
|
259
|
+
const errors = lintScreenLikeDefinition(root, basename(dataPath));
|
|
260
|
+
if (errors.length === 0) {
|
|
261
|
+
return 0;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
console.log(` FAIL ${basename(dataPath)} (${errors.length} contract usage error(s))`);
|
|
265
|
+
for (const error of errors.slice(0, 5)) {
|
|
266
|
+
console.log(` [${error.path}] ${error.message}`);
|
|
267
|
+
}
|
|
268
|
+
if (errors.length > 5) {
|
|
269
|
+
console.log(` ... and ${errors.length - 5} more`);
|
|
270
|
+
}
|
|
271
|
+
console.log(
|
|
272
|
+
" Hint: built-in contract instances inherit required props from the spec even when contracts/<name>.yaml does not restate them.",
|
|
273
|
+
);
|
|
274
|
+
return errors.length;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function lintFlowFile(dataPath: string): number {
|
|
278
|
+
const root = getSingleRootValue(loadData(dataPath));
|
|
279
|
+
if (!isRecord(root) || !isRecord(root.screens)) {
|
|
280
|
+
return 0;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const errors: UsageLint[] = [];
|
|
284
|
+
for (const [screenId, screenEntry] of Object.entries(root.screens)) {
|
|
285
|
+
if (!isRecord(screenEntry) || !isRecord(screenEntry.screen_inline)) {
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
errors.push(
|
|
289
|
+
...lintScreenLikeDefinition(
|
|
290
|
+
screenEntry.screen_inline,
|
|
291
|
+
`${basename(dataPath)}/screens/${screenId}/screen_inline`,
|
|
292
|
+
),
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (errors.length === 0) {
|
|
297
|
+
return 0;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
console.log(` FAIL ${basename(dataPath)} (${errors.length} contract usage error(s))`);
|
|
301
|
+
for (const error of errors.slice(0, 5)) {
|
|
302
|
+
console.log(` [${error.path}] ${error.message}`);
|
|
303
|
+
}
|
|
304
|
+
if (errors.length > 5) {
|
|
305
|
+
console.log(` ... and ${errors.length - 5} more`);
|
|
306
|
+
}
|
|
307
|
+
console.log(
|
|
308
|
+
" Hint: flow screen_inline sections follow the same built-in contract requirements as screens/*.yaml.",
|
|
309
|
+
);
|
|
310
|
+
return errors.length;
|
|
311
|
+
}
|
|
312
|
+
|
|
52
313
|
// ── build Ajv instance with all schemas ──────────────────────────────
|
|
53
314
|
|
|
54
315
|
function buildAjv(): AjvInstance {
|
|
@@ -146,8 +407,6 @@ function validateFile(
|
|
|
146
407
|
return errors.length;
|
|
147
408
|
}
|
|
148
409
|
|
|
149
|
-
// ── validation groups ────────────────────────────────────────────────
|
|
150
|
-
|
|
151
410
|
// ── includes resolution ──────────────────────────────────────────────
|
|
152
411
|
|
|
153
412
|
interface Includes {
|
|
@@ -233,7 +492,11 @@ const GROUPS: Record<string, ValidationGroup> = {
|
|
|
233
492
|
let errors = 0;
|
|
234
493
|
const dir = resolveInclude(projectDir, includes.screens);
|
|
235
494
|
for (const f of listFiles(dir, ".yaml")) {
|
|
236
|
-
|
|
495
|
+
const schemaErrors = validateFile(ajv, f, `${BASE}screen.schema.json`);
|
|
496
|
+
errors += schemaErrors;
|
|
497
|
+
if (schemaErrors === 0) {
|
|
498
|
+
errors += lintScreenFile(f);
|
|
499
|
+
}
|
|
237
500
|
}
|
|
238
501
|
return errors;
|
|
239
502
|
},
|
|
@@ -245,7 +508,11 @@ const GROUPS: Record<string, ValidationGroup> = {
|
|
|
245
508
|
let errors = 0;
|
|
246
509
|
const dir = resolveInclude(projectDir, includes.flows);
|
|
247
510
|
for (const f of listFiles(dir, ".yaml")) {
|
|
248
|
-
|
|
511
|
+
const schemaErrors = validateFile(ajv, f, `${BASE}flow.schema.json`);
|
|
512
|
+
errors += schemaErrors;
|
|
513
|
+
if (schemaErrors === 0) {
|
|
514
|
+
errors += lintFlowFile(f);
|
|
515
|
+
}
|
|
249
516
|
}
|
|
250
517
|
return errors;
|
|
251
518
|
},
|