openuispec 0.2.1 → 0.2.3

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 (34) hide show
  1. package/README.md +15 -0
  2. package/examples/social-app/AGENTS.md +1 -1
  3. package/examples/social-app/CLAUDE.md +1 -1
  4. package/examples/social-app/generated/web/social-app/src/components/Shell.tsx +24 -24
  5. package/examples/social-app/generated/web/social-app/src/components/ui.tsx +1 -1
  6. package/examples/social-app/generated/web/social-app/src/styles.css +1 -0
  7. package/examples/social-app/openuispec/contracts/action_trigger.yaml +21 -0
  8. package/examples/social-app/openuispec/contracts/nav_container.yaml +21 -0
  9. package/examples/todo-orbit/openuispec/contracts/action_trigger.yaml +10 -0
  10. package/examples/todo-orbit/openuispec/contracts/nav_container.yaml +25 -5
  11. package/mcp-server/index.ts +52 -0
  12. package/package.json +1 -1
  13. package/schema/contract.schema.json +57 -7
  14. package/schema/custom-contract.schema.json +59 -8
  15. package/schema/defs/action.schema.json +1 -1
  16. package/schema/defs/adaptive.schema.json +1 -1
  17. package/schema/defs/common.schema.json +8 -8
  18. package/schema/defs/data-binding.schema.json +3 -3
  19. package/schema/defs/validation.schema.json +1 -1
  20. package/schema/flow.schema.json +14 -14
  21. package/schema/locale.schema.json +1 -1
  22. package/schema/openuispec.schema.json +5 -5
  23. package/schema/platform.schema.json +2 -2
  24. package/schema/screen.schema.json +23 -23
  25. package/schema/tokens/color.schema.json +9 -9
  26. package/schema/tokens/elevation.schema.json +2 -2
  27. package/schema/tokens/icons.schema.json +1 -1
  28. package/schema/tokens/layout.schema.json +9 -9
  29. package/schema/tokens/motion.schema.json +2 -2
  30. package/schema/tokens/spacing.schema.json +4 -4
  31. package/schema/tokens/themes.schema.json +2 -2
  32. package/schema/tokens/typography.schema.json +4 -4
  33. package/schema/validate.ts +1 -1
  34. package/spec/openuispec-v0.1.md +83 -4
package/README.md CHANGED
@@ -244,6 +244,21 @@ Validate with: `openuispec validate`
244
244
 
245
245
  Use `openuispec validate semantic` to run cross-reference linting for locale keys, formatter refs, mapper refs, contracts, icons, navigation targets, and API endpoint references.
246
246
 
247
+ ### Shared interactive state roles
248
+
249
+ Interactive contracts may optionally express state-specific visual roles inside their token maps with a nested `states:` object. This lets generators use explicit foreground/background/icon/border roles for `default`, `active`, `selected`, `pressed`, `focused`, `disabled`, `loading`, and `error` states instead of inferring them from container colors or platform defaults.
250
+
251
+ Allowed visual role keys (no others are permitted):
252
+
253
+ - `background`
254
+ - `text`
255
+ - `icon`
256
+ - `border`
257
+ - `badge_background`
258
+ - `badge_text`
259
+
260
+ When `states:` is omitted, generators fall back to the token values defined at that level plus the base contract semantics.
261
+
247
262
  ## Output directories
248
263
 
249
264
  By default, drift stores state in `generated/<target>/<project>/`. To point targets to your actual code directories, add `output_dir` to `openuispec.yaml`:
@@ -1,5 +1,5 @@
1
1
  <!-- openuispec-rules-start -->
2
- <!-- openuispec-rules-version: 0.1.47 -->
2
+ <!-- openuispec-rules-version: 0.2.2 -->
3
3
  # OpenUISpec — AI Assistant Rules
4
4
  # ================================
5
5
  # This project uses OpenUISpec to define UI as a semantic spec.
@@ -1,5 +1,5 @@
1
1
  <!-- openuispec-rules-start -->
2
- <!-- openuispec-rules-version: 0.1.47 -->
2
+ <!-- openuispec-rules-version: 0.2.2 -->
3
3
  # OpenUISpec — AI Assistant Rules
4
4
  # ================================
5
5
  # This project uses OpenUISpec to define UI as a semantic spec.
@@ -91,23 +91,11 @@ export function AppShell() {
91
91
 
92
92
  {sizeClass !== "expanded" ? <BottomTabBar unreadCount={unreadCount} /> : null}
93
93
 
94
- {location.pathname.startsWith("/home") || location.pathname === "/" ? (
95
- <Link
96
- to="/create"
97
- className={cn(
98
- "interactive-press fixed z-30 flex h-14 w-14 items-center justify-center rounded-cap-primary bg-[var(--color-brand-primary)] text-white shadow-md",
99
- sizeClass === "expanded" ? "bottom-8 right-8" : "bottom-20 right-4",
100
- )}
101
- aria-label={t("nav.create")}
102
- >
103
- <Icon name="create_post" className="h-6 w-6" />
104
- </Link>
105
- ) : null}
106
94
  </div>
107
95
 
108
96
  {toast ? (
109
97
  <div className="pointer-events-none fixed inset-x-0 top-4 z-50 flex justify-center px-4">
110
- <div className="rounded-cap-primary bg-[var(--color-brand-primary)] px-5 py-3 text-sm font-medium text-white shadow-md">
98
+ <div className="rounded-cap-primary bg-[var(--color-brand-primary)] px-5 py-3 text-sm font-medium text-[var(--color-brand-primary-on)] shadow-md">
111
99
  {toast.message}
112
100
  </div>
113
101
  </div>
@@ -160,7 +148,7 @@ function BottomTabBar({ unreadCount }: { unreadCount: number }) {
160
148
  <Icon name={route.icon} className="h-5 w-5" />
161
149
  <span>{t(route.labelKey)}</span>
162
150
  {route.to === "/notifications" && unreadCount > 0 ? (
163
- <span className="absolute right-3 top-2 inline-flex min-h-5 min-w-5 items-center justify-center rounded-full bg-[var(--color-brand-accent)] px-1.5 text-[10px] font-semibold text-white">
151
+ <span className="absolute right-3 top-2 inline-flex min-h-5 min-w-5 items-center justify-center rounded-full bg-[var(--color-brand-accent)] px-1.5 text-[10px] font-semibold text-[var(--color-brand-accent-on)]">
164
152
  {unreadCount}
165
153
  </span>
166
154
  ) : null}
@@ -187,20 +175,32 @@ function DesktopSidebar({ unreadCount }: { unreadCount: number }) {
187
175
  to={route.to}
188
176
  className={({ isActive }) =>
189
177
  cn(
190
- "relative flex items-center gap-3 rounded-cap-primary px-4 py-3 text-sm font-semibold transition",
178
+ "relative rounded-cap-primary px-4 py-3 text-sm font-semibold transition",
191
179
  isActive
192
- ? "bg-[var(--color-brand-primary)] text-white shadow-sm"
193
- : "text-[var(--color-text-secondary)] hover:bg-[var(--color-surface-tertiary)]",
180
+ ? "bg-[var(--color-brand-primary)] shadow-sm"
181
+ : "hover:bg-[var(--color-surface-tertiary)]",
194
182
  )
195
183
  }
196
184
  >
197
- <Icon name={route.icon} className="h-5 w-5" />
198
- <span>{t(route.labelKey)}</span>
199
- {route.to === "/notifications" && unreadCount > 0 ? (
200
- <span className="ml-auto rounded-full bg-[var(--color-brand-accent)] px-2 py-1 text-[10px] font-semibold text-white">
201
- {unreadCount}
202
- </span>
203
- ) : null}
185
+ {({ isActive }) => (
186
+ <div className="flex items-center gap-3">
187
+ <Icon
188
+ name={route.icon}
189
+ className={cn(
190
+ "h-5 w-5 shrink-0",
191
+ isActive ? "text-[var(--color-brand-primary-on)]" : "text-[var(--color-text-secondary)]",
192
+ )}
193
+ />
194
+ <span className={cn(isActive ? "text-[var(--color-brand-primary-on)]" : "text-[var(--color-text-secondary)]")}>
195
+ {t(route.labelKey)}
196
+ </span>
197
+ {route.to === "/notifications" && unreadCount > 0 ? (
198
+ <span className="ml-auto rounded-full bg-[var(--color-brand-accent)] px-2 py-1 text-[10px] font-semibold text-[var(--color-brand-accent-on)]">
199
+ {unreadCount}
200
+ </span>
201
+ ) : null}
202
+ </div>
203
+ )}
204
204
  </NavLink>
205
205
  ))}
206
206
  </nav>
@@ -73,7 +73,7 @@ export function ActionButton({
73
73
  : variant === "secondary"
74
74
  ? "rounded-cap-alternate border-[var(--color-border-strong)] bg-transparent text-[var(--color-text-primary)]"
75
75
  : variant === "destructive"
76
- ? "rounded-cap-primary border-transparent bg-[var(--color-semantic-danger)] text-white"
76
+ ? "rounded-cap-primary border-transparent bg-[var(--color-semantic-danger)] text-[var(--color-semantic-danger-on)]"
77
77
  : variant === "fab"
78
78
  ? "rounded-cap-primary border-transparent bg-[var(--color-brand-primary)] text-[var(--color-brand-primary-on)] shadow-md"
79
79
  : selected
@@ -16,6 +16,7 @@
16
16
  --color-border-default: #e0dcd6;
17
17
  --color-border-strong: #c5c0b8;
18
18
  --color-semantic-danger: #d43b3b;
19
+ --color-semantic-danger-on: #ffffff;
19
20
  color-scheme: light;
20
21
  }
21
22
 
@@ -15,6 +15,10 @@ action_trigger:
15
15
  text: "color.brand.primary.on_color"
16
16
  border_width: 0
17
17
  shape: "2px 24px 2px 24px"
18
+ states:
19
+ disabled:
20
+ background: "color.surface.tertiary"
21
+ text: "color.text.tertiary"
18
22
  platform_mapping:
19
23
  ios: { shape: "RoundedCapShape(diagonal: .primary)" }
20
24
  android: { shape: "RoundedCapShape(diagonal = Primary)" }
@@ -36,6 +40,10 @@ action_trigger:
36
40
  text: "color.text.primary"
37
41
  border: { color: "color.border.strong", width: 1 }
38
42
  shape: "24px 2px 24px 2px"
43
+ states:
44
+ disabled:
45
+ text: "color.text.tertiary"
46
+ border: { color: "color.border.default", width: 1 }
39
47
  platform_mapping:
40
48
  ios: { shape: "RoundedCapShape(diagonal: .alternate)" }
41
49
  android: { shape: "RoundedCapShape(diagonal = Alternate)" }
@@ -56,6 +64,15 @@ action_trigger:
56
64
  text_selected: "color.brand.accent.on_color"
57
65
  border: { color: "color.border.default", width: 1 }
58
66
  shape: "2px 24px 2px 24px"
67
+ states:
68
+ default:
69
+ background: "transparent"
70
+ text: "color.text.primary"
71
+ border: { color: "color.border.default", width: 1 }
72
+ selected:
73
+ background: "color.brand.accent"
74
+ text: "color.brand.accent.on_color"
75
+ border: { color: "color.brand.accent", width: 1 }
59
76
  platform_mapping:
60
77
  ios: { shape: "RoundedCapShape(diagonal: .primary)" }
61
78
  android: { shape: "RoundedCapShape(diagonal = Primary)" }
@@ -71,6 +88,10 @@ action_trigger:
71
88
  background: "color.semantic.danger"
72
89
  text: "color.semantic.danger.on_color"
73
90
  shape: "2px 24px 2px 24px"
91
+ states:
92
+ disabled:
93
+ background: "color.surface.tertiary"
94
+ text: "color.text.tertiary"
74
95
  platform_mapping:
75
96
  ios: { shape: "RoundedCapShape(diagonal: .primary)", role: "destructive" }
76
97
  android: { shape: "RoundedCapShape(diagonal = Primary)" }
@@ -9,6 +9,16 @@ nav_container:
9
9
  background: "color.surface.primary"
10
10
  tint_active: "color.brand.primary"
11
11
  tint_inactive: "color.text.tertiary"
12
+ item:
13
+ states:
14
+ default:
15
+ text: "color.text.tertiary"
16
+ icon: "color.text.tertiary"
17
+ active:
18
+ text: "color.brand.primary"
19
+ icon: "color.brand.primary"
20
+ badge_background: "color.brand.accent"
21
+ badge_text: "color.brand.accent.on_color"
12
22
  platform_mapping:
13
23
  ios: { uses_system_tab_bar: true, height: 49 }
14
24
  android: { component: "NavigationBar" }
@@ -23,6 +33,17 @@ nav_container:
23
33
  tokens:
24
34
  background: "color.surface.secondary"
25
35
  width: 280
36
+ item:
37
+ states:
38
+ default:
39
+ text: "color.text.secondary"
40
+ icon: "color.text.secondary"
41
+ active:
42
+ background: "color.brand.primary"
43
+ text: "color.brand.primary.on_color"
44
+ icon: "color.brand.primary.on_color"
45
+ badge_background: "color.brand.accent"
46
+ badge_text: "color.brand.accent.on_color"
26
47
  platform_mapping:
27
48
  ios: { style: "sidebar" }
28
49
  android: { component: "NavigationRail" }
@@ -5,11 +5,21 @@ action_trigger:
5
5
  text: "color.brand.primary.on_color"
6
6
  border: { width: 2, color: "color.brand.primary" }
7
7
  cut_size: "spacing.sm"
8
+ states:
9
+ disabled:
10
+ background: "color.surface.secondary"
11
+ text: "color.text.tertiary"
12
+ border: { width: 2, color: "color.border.default" }
8
13
  ghost:
9
14
  background: "color.surface.secondary"
10
15
  text: "color.text.primary"
11
16
  border: { width: 2, color: "color.brand.primary" }
12
17
  cut_size: "spacing.sm"
18
+ states:
19
+ disabled:
20
+ background: "color.surface.secondary"
21
+ text: "color.text.tertiary"
22
+ border: { width: 2, color: "color.border.default" }
13
23
  platform_mapping:
14
24
  ios:
15
25
  primary: { shape: "CutCornerShape", clip: true, style: ".plain" }
@@ -6,24 +6,44 @@ nav_container:
6
6
  border_top: { width: 1, color: "color.border.default" }
7
7
  icon_size: 22
8
8
  label_style: "typography.caption"
9
- active_color: "color.brand.primary"
10
- inactive_color: "color.text.tertiary"
9
+ item:
10
+ states:
11
+ default:
12
+ text: "color.text.tertiary"
13
+ icon: "color.text.tertiary"
14
+ active:
15
+ text: "color.brand.primary"
16
+ icon: "color.brand.primary"
11
17
  rail:
12
18
  width: 76
13
19
  background: "color.surface.secondary"
14
20
  icon_size: 22
15
21
  label_style: "typography.caption"
16
- active_color: "color.brand.primary"
22
+ item:
23
+ states:
24
+ default:
25
+ text: "color.text.secondary"
26
+ icon: "color.text.secondary"
27
+ active:
28
+ text: "color.brand.primary"
29
+ icon: "color.brand.primary"
17
30
  sidebar:
18
31
  width: { collapsed: 72, expanded: 240 }
19
32
  background: "color.surface.secondary"
20
33
  item_height: 48
21
34
  item_radius: "spacing.sm"
22
35
  item_padding_h: "spacing.md"
23
- active_background: "color.brand.primary"
24
- active_text: "color.brand.primary.on_color"
25
36
  icon_size: 20
26
37
  label_style: "typography.body_sm"
38
+ item:
39
+ states:
40
+ default:
41
+ text: "color.text.secondary"
42
+ icon: "color.text.secondary"
43
+ active:
44
+ background: "color.brand.primary"
45
+ text: "color.brand.primary.on_color"
46
+ icon: "color.brand.primary.on_color"
27
47
  platform_mapping:
28
48
  ios:
29
49
  tab_bar: { widget: "TabView" }
@@ -65,6 +65,39 @@ function toolError(err: unknown): { content: [{ type: "text"; text: string }]; i
65
65
  return { content: [{ type: "text" as const, text: `Error: ${formatError(err)}` }], isError: true };
66
66
  }
67
67
 
68
+ function formatAuditValue(value: unknown): string {
69
+ if (typeof value === "string") return value;
70
+ return JSON.stringify(value);
71
+ }
72
+
73
+ function collectStateRoleAuditItems(node: unknown, prefix = ""): string[] {
74
+ if (!node || typeof node !== "object" || Array.isArray(node)) {
75
+ return [];
76
+ }
77
+
78
+ const items: string[] = [];
79
+ const record = node as Record<string, unknown>;
80
+
81
+ const states = record.states;
82
+ if (states && typeof states === "object" && !Array.isArray(states)) {
83
+ for (const [stateName, roles] of Object.entries(states as Record<string, unknown>)) {
84
+ if (!roles || typeof roles !== "object" || Array.isArray(roles)) continue;
85
+ for (const [roleName, roleValue] of Object.entries(roles as Record<string, unknown>)) {
86
+ const statePath = prefix ? `${prefix}.states.${stateName}.${roleName}` : `states.${stateName}.${roleName}`;
87
+ items.push(`${statePath} = ${formatAuditValue(roleValue)}`);
88
+ }
89
+ }
90
+ }
91
+
92
+ for (const [key, value] of Object.entries(record)) {
93
+ if (key === "states" || !value || typeof value !== "object" || Array.isArray(value)) continue;
94
+ const childPrefix = prefix ? `${prefix}.${key}` : key;
95
+ items.push(...collectStateRoleAuditItems(value, childPrefix));
96
+ }
97
+
98
+ return items;
99
+ }
100
+
68
101
  // ── create server ────────────────────────────────────────────────────
69
102
 
70
103
  export const server = new McpServer(
@@ -173,6 +206,17 @@ function buildAuditChecklist(projectDir: string, target: string, screenFilter?:
173
206
  lines.push(`- [ ] ${item}`);
174
207
  }
175
208
  }
209
+
210
+ const stateRoleItems = collectStateRoleAuditItems(variant?.tokens);
211
+ if (stateRoleItems.length) {
212
+ if (!mustHandle?.length) {
213
+ lines.push(`\n### ${contractName}.${variantName}`);
214
+ }
215
+ lines.push(`- [ ] Explicit state-role tokens are implemented for ${contractName}.${variantName}`);
216
+ for (const item of stateRoleItems) {
217
+ lines.push(`- [ ] ${item}`);
218
+ }
219
+ }
176
220
  }
177
221
 
178
222
  // Top-level generation.must_handle
@@ -183,6 +227,14 @@ function buildAuditChecklist(projectDir: string, target: string, screenFilter?:
183
227
  lines.push(`- [ ] ${item}`);
184
228
  }
185
229
  }
230
+
231
+ const topLevelStateRoleItems = collectStateRoleAuditItems(contract?.tokens);
232
+ if (topLevelStateRoleItems.length) {
233
+ lines.push(`\n### ${contractName} (state-role tokens)`);
234
+ for (const item of topLevelStateRoleItems) {
235
+ lines.push(`- [ ] ${item}`);
236
+ }
237
+ }
186
238
  } catch { /* skip unparseable files */ }
187
239
  }
188
240
  lines.push("");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openuispec",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "description": "A semantic UI specification format for AI-native, platform-native app development",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
- "$id": "https://openuispec.org/schema/contract.schema.json",
3
+ "$id": "https://openuispec.rsteam.uz/schema/contract.schema.json",
4
4
  "title": "OpenUISpec Standard Contract Extension",
5
5
  "description": "Extension file for a standard contract — add variants, override tokens, platform mapping, and generation hints",
6
6
  "type": "object",
@@ -36,13 +36,12 @@
36
36
  "type": "object",
37
37
  "description": "Additional props beyond the spec definition",
38
38
  "additionalProperties": {
39
- "$ref": "https://openuispec.org/schema/custom-contract.schema.json#/$defs/prop_def"
39
+ "$ref": "https://openuispec.rsteam.uz/schema/custom-contract.schema.json#/$defs/prop_def"
40
40
  }
41
41
  },
42
42
  "tokens": {
43
- "type": "object",
44
43
  "description": "Token overrides at the contract level",
45
- "additionalProperties": true
44
+ "$ref": "#/$defs/token_object"
46
45
  },
47
46
  "platform_mapping": {
48
47
  "type": "object",
@@ -80,7 +79,7 @@
80
79
  "type": "array",
81
80
  "description": "Additional behavioral test cases",
82
81
  "items": {
83
- "$ref": "https://openuispec.org/schema/custom-contract.schema.json#/$defs/test_case"
82
+ "$ref": "https://openuispec.rsteam.uz/schema/custom-contract.schema.json#/$defs/test_case"
84
83
  }
85
84
  }
86
85
  },
@@ -95,9 +94,8 @@
95
94
  "description": "Human-readable description of this variant"
96
95
  },
97
96
  "tokens": {
98
- "type": "object",
99
97
  "description": "Visual token bindings for this variant",
100
- "additionalProperties": true
98
+ "$ref": "#/$defs/token_object"
101
99
  },
102
100
  "platform_mapping": {
103
101
  "type": "object",
@@ -124,6 +122,58 @@
124
122
  }
125
123
  },
126
124
  "additionalProperties": false
125
+ },
126
+ "token_object": {
127
+ "type": "object",
128
+ "description": "Recursive token object. If a `states` key is present, it must use the documented interactive state-role shape.",
129
+ "properties": {
130
+ "states": {
131
+ "$ref": "#/$defs/state_role_map"
132
+ }
133
+ },
134
+ "additionalProperties": {
135
+ "$ref": "#/$defs/token_value"
136
+ }
137
+ },
138
+ "token_value": {
139
+ "description": "Any token value, including nested token objects and arrays.",
140
+ "oneOf": [
141
+ {
142
+ "type": ["string", "number", "boolean", "null"]
143
+ },
144
+ {
145
+ "type": "array",
146
+ "items": {
147
+ "$ref": "#/$defs/token_value"
148
+ }
149
+ },
150
+ {
151
+ "$ref": "#/$defs/token_object"
152
+ }
153
+ ]
154
+ },
155
+ "state_role_map": {
156
+ "type": "object",
157
+ "description": "Map of state names to visual-role overrides.",
158
+ "propertyNames": {
159
+ "pattern": "^[a-z][a-z0-9_]*$"
160
+ },
161
+ "additionalProperties": {
162
+ "$ref": "#/$defs/state_visual_roles"
163
+ }
164
+ },
165
+ "state_visual_roles": {
166
+ "type": "object",
167
+ "description": "Visual-role overrides available inside token `states` maps.",
168
+ "properties": {
169
+ "background": { "$ref": "#/$defs/token_value" },
170
+ "text": { "$ref": "#/$defs/token_value" },
171
+ "icon": { "$ref": "#/$defs/token_value" },
172
+ "border": { "$ref": "#/$defs/token_value" },
173
+ "badge_background": { "$ref": "#/$defs/token_value" },
174
+ "badge_text": { "$ref": "#/$defs/token_value" }
175
+ },
176
+ "additionalProperties": false
127
177
  }
128
178
  }
129
179
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
- "$id": "https://openuispec.org/schema/custom-contract.schema.json",
3
+ "$id": "https://openuispec.rsteam.uz/schema/custom-contract.schema.json",
4
4
  "title": "OpenUISpec Custom Contract",
5
5
  "description": "Custom contract extension — root key must be x_ prefixed, value follows the standard contract anatomy",
6
6
  "type": "object",
@@ -10,7 +10,7 @@
10
10
  "pattern": "^x_[a-z][a-z0-9_]*$"
11
11
  },
12
12
  "additionalProperties": {
13
- "$ref": "https://openuispec.org/schema/custom-contract.schema.json#/$defs/contract_def"
13
+ "$ref": "https://openuispec.rsteam.uz/schema/custom-contract.schema.json#/$defs/contract_def"
14
14
  },
15
15
  "$defs": {
16
16
  "contract_def": {
@@ -25,14 +25,14 @@
25
25
  "type": "object",
26
26
  "description": "Typed property definitions for this contract",
27
27
  "additionalProperties": {
28
- "$ref": "https://openuispec.org/schema/custom-contract.schema.json#/$defs/prop_def"
28
+ "$ref": "https://openuispec.rsteam.uz/schema/custom-contract.schema.json#/$defs/prop_def"
29
29
  }
30
30
  },
31
31
  "states": {
32
32
  "type": "object",
33
33
  "description": "State machine definition — each key is a state name",
34
34
  "additionalProperties": {
35
- "$ref": "https://openuispec.org/schema/custom-contract.schema.json#/$defs/state_def"
35
+ "$ref": "https://openuispec.rsteam.uz/schema/custom-contract.schema.json#/$defs/state_def"
36
36
  }
37
37
  },
38
38
  "a11y": {
@@ -62,9 +62,8 @@
62
62
  "additionalProperties": true
63
63
  },
64
64
  "tokens": {
65
- "type": "object",
66
65
  "description": "Per-variant visual token bindings",
67
- "additionalProperties": true
66
+ "$ref": "https://openuispec.rsteam.uz/schema/custom-contract.schema.json#/$defs/token_object"
68
67
  },
69
68
  "platform_mapping": {
70
69
  "type": "object",
@@ -92,7 +91,7 @@
92
91
  "type": "object",
93
92
  "description": "Per-platform library/framework requirements",
94
93
  "additionalProperties": {
95
- "$ref": "https://openuispec.org/schema/custom-contract.schema.json#/$defs/dependency_def"
94
+ "$ref": "https://openuispec.rsteam.uz/schema/custom-contract.schema.json#/$defs/dependency_def"
96
95
  }
97
96
  },
98
97
  "generation": {
@@ -121,7 +120,7 @@
121
120
  "type": "array",
122
121
  "description": "Behavioral verification scenarios",
123
122
  "items": {
124
- "$ref": "https://openuispec.org/schema/custom-contract.schema.json#/$defs/test_case"
123
+ "$ref": "https://openuispec.rsteam.uz/schema/custom-contract.schema.json#/$defs/test_case"
125
124
  }
126
125
  }
127
126
  },
@@ -208,6 +207,58 @@
208
207
  },
209
208
  "additionalProperties": false
210
209
  },
210
+ "token_object": {
211
+ "type": "object",
212
+ "description": "Recursive token object. If a `states` key is present, it must use the documented interactive state-role shape.",
213
+ "properties": {
214
+ "states": {
215
+ "$ref": "https://openuispec.rsteam.uz/schema/custom-contract.schema.json#/$defs/state_role_map"
216
+ }
217
+ },
218
+ "additionalProperties": {
219
+ "$ref": "https://openuispec.rsteam.uz/schema/custom-contract.schema.json#/$defs/token_value"
220
+ }
221
+ },
222
+ "token_value": {
223
+ "description": "Any token value, including nested token objects and arrays.",
224
+ "oneOf": [
225
+ {
226
+ "type": ["string", "number", "boolean", "null"]
227
+ },
228
+ {
229
+ "type": "array",
230
+ "items": {
231
+ "$ref": "https://openuispec.rsteam.uz/schema/custom-contract.schema.json#/$defs/token_value"
232
+ }
233
+ },
234
+ {
235
+ "$ref": "https://openuispec.rsteam.uz/schema/custom-contract.schema.json#/$defs/token_object"
236
+ }
237
+ ]
238
+ },
239
+ "state_role_map": {
240
+ "type": "object",
241
+ "description": "Map of state names to visual-role overrides.",
242
+ "propertyNames": {
243
+ "pattern": "^[a-z][a-z0-9_]*$"
244
+ },
245
+ "additionalProperties": {
246
+ "$ref": "https://openuispec.rsteam.uz/schema/custom-contract.schema.json#/$defs/state_visual_roles"
247
+ }
248
+ },
249
+ "state_visual_roles": {
250
+ "type": "object",
251
+ "description": "Visual-role overrides available inside token `states` maps.",
252
+ "properties": {
253
+ "background": { "$ref": "https://openuispec.rsteam.uz/schema/custom-contract.schema.json#/$defs/token_value" },
254
+ "text": { "$ref": "https://openuispec.rsteam.uz/schema/custom-contract.schema.json#/$defs/token_value" },
255
+ "icon": { "$ref": "https://openuispec.rsteam.uz/schema/custom-contract.schema.json#/$defs/token_value" },
256
+ "border": { "$ref": "https://openuispec.rsteam.uz/schema/custom-contract.schema.json#/$defs/token_value" },
257
+ "badge_background": { "$ref": "https://openuispec.rsteam.uz/schema/custom-contract.schema.json#/$defs/token_value" },
258
+ "badge_text": { "$ref": "https://openuispec.rsteam.uz/schema/custom-contract.schema.json#/$defs/token_value" }
259
+ },
260
+ "additionalProperties": false
261
+ },
211
262
  "dependency_def": {
212
263
  "type": "object",
213
264
  "description": "Platform-specific dependencies",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
- "$id": "https://openuispec.org/schema/defs/action.schema.json",
3
+ "$id": "https://openuispec.rsteam.uz/schema/defs/action.schema.json",
4
4
  "title": "OpenUISpec Action",
5
5
  "description": "Discriminated union of the 14 action types in OpenUISpec",
6
6
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
- "$id": "https://openuispec.org/schema/defs/adaptive.schema.json",
3
+ "$id": "https://openuispec.rsteam.uz/schema/defs/adaptive.schema.json",
4
4
  "title": "OpenUISpec Adaptive Override",
5
5
  "description": "Adaptive overrides keyed by size class (compact, regular, expanded). Values are override objects whose shape depends on context.",
6
6
  "type": "object",