@getrheo/rheo-skill 2.2.0 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @getrheo/rheo-skill
2
2
 
3
- Current release: **`2.1.0`** on npm. Public source: [getrheo/rheo-skill](https://github.com/getrheo/rheo-skill) (mirrored from this private monorepo via `pnpm extract:oss-repos`).
3
+ Current release: **`2.3.0`** on npm (`PLATFORM_SDK_VERSION` in `scripts/publish-package-registry.mjs`). Public source: [getrheo/rheo-skill](https://github.com/getrheo/rheo-skill) (mirrored from this private monorepo via `pnpm extract:oss-repos`).
4
4
 
5
5
  Source and build tooling for the **`rheo`** agent skill — a single, self-contained
6
6
  skill with two sub-skills:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getrheo/rheo-skill",
3
- "version": "2.2.0",
3
+ "version": "2.4.0",
4
4
  "type": "module",
5
5
  "description": "Rheo agent skill — SDK install guidance and flow manifest import tooling.",
6
6
  "main": "./src/index.ts",
@@ -9,8 +9,8 @@
9
9
  ".": "./src/index.ts"
10
10
  },
11
11
  "dependencies": {
12
- "@getrheo/contracts": "2.2.0",
13
- "@getrheo/flow-runtime": "2.2.0",
12
+ "@getrheo/contracts": "2.4.0",
13
+ "@getrheo/flow-runtime": "2.4.0",
14
14
  "zod": "^3.23.8"
15
15
  },
16
16
  "devDependencies": {
package/rheo/SKILL.md CHANGED
@@ -3,7 +3,7 @@ name: rheo
3
3
  description: Work with Rheo, the headless onboarding/paywall flow engine for mobile apps. Use when a user wants to install or wire the Rheo SDK (React Native, Expo, or SwiftUI), follow Rheo SDK best practices, configure integrations (RevenueCat, AppsFlyer), wire auth/permissions/terminal callbacks, OR import/migrate an existing mobile flow into a compliant Rheo FlowManifest and validate it. Routes to the `rheo-best-practices` and `rheo-flow-import` sub-skills.
4
4
  compatibility: Requires Node.js 20+. rheo-flow-import scripts are fully self-contained (no install step). Internet access fetches the latest Manifest Agent Profile; a bundled fallback works offline.
5
5
  metadata:
6
- rheo-version: "2.2.0"
6
+ rheo-version: "2.4.0"
7
7
  manifest-schema-version: "7"
8
8
  ---
9
9
 
@@ -57,7 +57,7 @@ All `<manifest.json>` / `<flow-spec.json>` paths and outputs resolve from the **
57
57
  - **Choice inputs:** `single_choice`/`multiple_choice` require `fieldKey` (snake_case), `children` (≥2 option stacks), `optionBindings` (one per option, `rootLayerId` = child stack `lyr_*` id), and `branching` (`{ "enabled": false, "conditions": [] }` when no branches). Never `"options"`/`"choices"`.
58
58
  - **Styling:** when the audit reports colors, populate `manifest.theme` and layer `style` (including `style.color` on every text layer and nested button label). No black-and-white defaults when color evidence exists.
59
59
  - **Gradients:** map `LinearGradient`/gradient stops to `screen.containerStyle.backgroundFill.color` as a `linear-gradient(...)` CSS string.
60
- - **Carousels:** pager/carousel evidence (`infoSteps`, horizontal pager, `pagingEnabled`) → `kind: "carousel"`, one slide per page, swipe-only (no in-pager buttons). See [references/carousel-import.md](references/carousel-import.md).
60
+ - **Carousels:** pager/carousel evidence (`infoSteps`, horizontal pager, `pagingEnabled`) → `kind: "carousel"`, one slide per page; swipe by default, or a `button` with `action.kind: "advance_carousel"` when the source has an explicit Next control. See [references/carousel-import.md](references/carousel-import.md).
61
61
  - **Fonts:** custom fonts go in `rheo-import.fonts.json` under `assets/fonts/` and `manifest.theme.fontFamily`; **never** in `rheo-import.assets.json`. See [references/font-import.md](references/font-import.md).
62
62
  - **Localization:** resolve **default-locale** strings into every `text.default` — never raw translation keys. Set `manifest.defaultLocale`. See [references/localization-import.md](references/localization-import.md).
63
63
  - **Animations:** map motion from the audit only when intake Q6 is yes and the plan includes animations ([references/animation-import.md](references/animation-import.md)); otherwise omit all `animations`, `stagger`, `restingMotion`.
@@ -26,6 +26,7 @@ Every layer `kind` accepted by the manifest:
26
26
  - `multiple_choice`
27
27
  - `text_input`
28
28
  - `scale_input`
29
+ - `wheel_picker`
29
30
  - `oauth_provider`
30
31
  - `oauth_login`
31
32
  - `email_password_auth`
@@ -34,10 +35,17 @@ Every layer `kind` accepted by the manifest:
34
35
  - `carousel`
35
36
  - `hyperlink`
36
37
  - `checkbox`
38
+ - `conditional`
37
39
 
38
40
  Container layers that **must** include a `children` array (or `slides` for carousel): `stack`, `carousel`,
39
41
  `button`, `back_button`, `hyperlink`, `single_choice`, `multiple_choice`, `oauth_login`,
40
- `oauth_provider` (custom variant), `email_password_auth`, `email_password_field`, `email_password_submit`.
42
+ `oauth_provider` (custom variant), `email_password_auth`, `email_password_field`, `email_password_submit`,
43
+ `conditional`.
44
+
45
+ `conditional` picks which of its child stacks renders: ordered `cases[]` (each with a `DecisionExpr`
46
+ and a `rootLayerId`) plus a required `elseRootLayerId`. Every bound id must be a distinct direct child
47
+ `stack`. Cases may only read fields answered above the conditional, every case needs at least one rule
48
+ before publish, and the one-input limit applies per active path so sibling branches may each own an input.
41
49
 
42
50
  ## Button / back_button variants
43
51
 
@@ -59,11 +67,13 @@ Valid `action.kind` values on `button` layers:
59
67
  - `request_os_permission`
60
68
  - `play_media`
61
69
  - `request_app_review`
70
+ - `advance_carousel`
62
71
 
63
72
  - `FlowGraphNodeJumpTarget` (`scr_*` | `dec_*` | `surf_*`): `go_to_step.screenId`, choice `branching.conditions[].goTo`, loader/lottie/video `onComplete` when mode is `screen`, and `request_os_permission` outcomes (except `continue`/`end`).
64
73
  - `go_back_one_screen` and `back_button` accept optional `fallbackScreenId` (`scr_*` only).
65
74
  - `request_os_permission` requires `permissionKey` and `outcomes` (`granted`/`denied`/`blocked`).
66
75
  - `play_media` requires `targetLayerIds` (≥1) pointing at Lottie/video layers on the same screen.
76
+ - `advance_carousel` requires `targetLayerId` (exactly one `carousel` layer on the same screen) and takes optional `onLast` (`noop` default, or `complete` to finish the carousel when already on the last slide).
67
77
  - `back_button` takes **no** `action` (back navigation is built in).
68
78
 
69
79
  ## OS permission keys
@@ -95,7 +105,7 @@ Valid `permissionKey` values for `request_os_permission`:
95
105
 
96
106
  ## Inputs and auth
97
107
 
98
- - `text_input` types: `plain`, `email`, `phone`, `url`, `multiline`.
108
+ - `text_input` types: `plain`, `email`, `phone`, `url`, `number`, `multiline`.
99
109
  - `text_input` classification: `safe`, `sensitive`.
100
110
  - `oauth_login` preset providers: `github`, `google`, `apple`.
101
111
  - `email_password_auth` modes: `sign_in`, `sign_up` (sign_up requires email + password + confirm fields).
@@ -2,15 +2,25 @@
2
2
 
3
3
  Use when the source flow has an **in-screen pager** (`infoSteps`, `currentInfoStep`, horizontal `FlatList` / `PagerView`, dot indicators).
4
4
 
5
- ## Rheo carousel behavior (swipe-only)
5
+ ## Rheo carousel behavior (swipe by default)
6
6
 
7
- - Users move between slides by **swiping** (horizontal scroll with snap).
7
+ - Users move between slides by **swiping** (horizontal scroll with snap). Swipe is always available.
8
8
  - Optional **`pageControl`** adds dot indicators — not buttons.
9
- - There is **no** Next / Continue button on the carousel layer in the manifest or at runtime.
10
- - On the **last slide** (when `loop` is false and there are 2+ slides), swiping to that slide emits a carousel completion so `screen.next` can run.
9
+ - On the **last slide** (when `loop` is false and there are 2+ slides), arriving at that slide emits a carousel completion so `screen.next` can run.
11
10
  - **Single-slide** carousels do not auto-complete; add a `regions.footer` **Continue** (or other) button to advance the flow.
12
11
  - **`loop: true`** carousels never auto-complete; pair with a separate screen-level CTA when the flow should move on.
13
12
 
13
+ ## Optional Next button (`advance_carousel`)
14
+
15
+ A button may page the carousel when the source screen genuinely has a Next control:
16
+
17
+ - Action shape: `{ "kind": "advance_carousel", "targetLayerId": "<carousel layer id>", "onLast": "noop" | "complete" }`.
18
+ - `targetLayerId` must name **exactly one** `carousel` layer on the **same screen**.
19
+ - `onLast` is optional and defaults to `"noop"` (stay on the last slide). Use `"complete"` when tapping Next on the last slide should finish the carousel and follow `screen.next`.
20
+ - `loop: true` and single-slide carousels never complete through this action — they wrap or do nothing.
21
+ - Advancing from second-to-last to last emits carousel completion just like a swipe, so a `"complete"` `onLast` only matters when the user is **already** on the last slide.
22
+ - Runtime support: **Web and React Native**. Flutter and SwiftUI decode the action but do nothing on tap, so do not rely on it for flow exit on those platforms — use a `continue` button instead.
23
+
14
24
  ## Manifest shape
15
25
 
16
26
  - One `kind: "carousel"` in `regions.body` (or inside a body stack).
@@ -24,8 +34,9 @@ Use when the source flow has an **in-screen pager** (`infoSteps`, `currentInfoSt
24
34
  | Pager pages | `carousel.slides[]` |
25
35
  | Dot indicators | `pageControl` only |
26
36
  | Swipe between pages | Default — no extra buttons |
27
- | Footer that only increments pager index | **Omit** — not `regions.footer` |
28
- | Footer / CTA that exits to **next route** | `regions.footer` with `continue` / `go_to_step`, or rely on last-slide swipe completion + `screen.next` |
37
+ | Footer that only increments pager index, with no visible Next affordance | **Omit** — swipe already covers it |
38
+ | Explicit **Next** button in the source pager | `button` with `advance_carousel` targeting the carousel |
39
+ | Footer / CTA that exits to **next route** | `regions.footer` with `continue` / `go_to_step`, or rely on last-slide completion + `screen.next` |
29
40
 
30
41
  ## Example (swipe + dots)
31
42
 
@@ -57,8 +68,54 @@ Use when the source flow has an **in-screen pager** (`infoSteps`, `currentInfoSt
57
68
  }
58
69
  ```
59
70
 
71
+ ## Example (explicit Next button)
72
+
73
+ ```json
74
+ {
75
+ "regions": {
76
+ "body": {
77
+ "kind": "stack",
78
+ "direction": "vertical",
79
+ "children": [
80
+ {
81
+ "id": "lyr_car_intro",
82
+ "kind": "carousel",
83
+ "slides": [
84
+ {
85
+ "kind": "stack",
86
+ "direction": "vertical",
87
+ "children": [
88
+ { "kind": "text", "text": { "default": "Slide 1" }, "style": { "color": "#111111" } }
89
+ ]
90
+ },
91
+ {
92
+ "kind": "stack",
93
+ "direction": "vertical",
94
+ "children": [
95
+ { "kind": "text", "text": { "default": "Slide 2" }, "style": { "color": "#111111" } }
96
+ ]
97
+ }
98
+ ],
99
+ "pageControl": { "position": "bottom" }
100
+ }
101
+ ]
102
+ },
103
+ "footer": {
104
+ "kind": "button",
105
+ "variant": "primary",
106
+ "action": { "kind": "advance_carousel", "targetLayerId": "lyr_car_intro", "onLast": "complete" },
107
+ "children": [
108
+ { "kind": "text", "text": { "default": "Next" }, "style": { "color": "#FFFFFF" } }
109
+ ]
110
+ }
111
+ },
112
+ "next": { "default": "scr_next" }
113
+ }
114
+ ```
115
+
60
116
  ## Completion gate
61
117
 
62
- - [ ] No `button` layers for in-pager next/continue.
118
+ - [ ] No `button` layers that only mimic paging without an `advance_carousel` action.
119
+ - [ ] Every `advance_carousel` button targets a `carousel` on the same screen.
63
120
  - [ ] `screen.next` targets the next **screen** in the flow, not “next slide”.
64
121
  - [ ] Single-slide carousel screens include a flow-level CTA if the flow must advance without swiping.
@@ -62,7 +62,7 @@ the scaffold expands into a real layer. `id` is always optional. `style` and
62
62
  { "kind": "text", "text": "Welcome to Rheo", "style": { "color": "#FFFFFF" } }
63
63
  { "kind": "text", "text": { "default": "Hi", "translations": { "es": "Hola" } } }
64
64
  { "kind": "image", "mediaAssetId": "00000000-0000-0000-0000-000000000101", "alt": "Hero" }
65
- { "kind": "icon", "iconName": "sparkles-outline" } // family defaults to ionicons
65
+ { "kind": "icon", "iconName": "sparkles-outline" } // family defaults to ionicons; size defaults to 24×24
66
66
  { "kind": "lottie", "mediaAssetId": "…", "loop": true, "autoPlay": true }
67
67
  { "kind": "video", "mediaAssetId": "…", "autoPlay": false } // needs a play_media button
68
68
  ```
@@ -91,23 +91,29 @@ Action shorthands: `"none"`, `"continue"`, `"skip"`, `"end_flow"`,
91
91
  `"go_back_one_screen"`, `"request_app_review"`. Object forms:
92
92
  `{ "kind": "go_to_step", "screenId": "scr_x" }` (or `dec_*` / `surf_*`),
93
93
  `{ "kind": "request_os_permission", "permissionKey": "…", "outcomes": { "granted": "scr_a", "denied": "dec_b", "blocked": "surf_c" } }` (targets may be `scr_*`, `dec_*`, `surf_*`, `"continue"`, or `"end"`),
94
- `{ "kind": "play_media", "targetLayerIds": ["lyr_video"] }`.
94
+ `{ "kind": "play_media", "targetLayerIds": ["lyr_video"] }`,
95
+ `{ "kind": "advance_carousel", "targetLayerId": "lyr_carousel", "onLast": "noop" }`
96
+ (`onLast` is optional and defaults to `"noop"`; use `"complete"` to finish the carousel when the
97
+ button is tapped on the last slide).
95
98
 
96
99
  ### Stacks (layout)
97
100
 
98
101
  ```jsonc
99
- { "kind": "stack", "direction": "vertical", "align": "center", "gap": 12,
102
+ { "kind": "stack", "direction": "vertical", "align": "center", "distribution": "center", "gap": 12,
100
103
  "style": { "padding": { "t": 24, "r": 16, "b": 24, "l": 16 } },
101
104
  "children": [ { "kind": "text", "text": "Centered hero copy" } ] }
102
105
  ```
103
106
 
107
+ Use `distribution` for main-axis packing (`start` \| `center` \| `end` \| `between` \| `around`). Legacy flow-spec `justify` is accepted by the scaffold and rewritten to `distribution`, but prefer writing `distribution` directly.
108
+
104
109
  Card chrome (border/shadow/background/radius) goes on a wrapping stack's `style`.
105
110
 
106
111
  ### Choices
107
112
 
108
113
  You provide `options`; the scaffold builds the option `stack` children,
109
114
  `optionBindings`, and `branching`. `optionId` defaults to a slug of the label.
110
- Map unselected chrome to `style` and selected chrome to `selectedStyle` per option.
115
+ Map unselected chrome to `style` and selected chrome to `selectedStyle` per option
116
+ (bake padding/radius/background/border into `style` — do not omit default chrome).
111
117
 
112
118
  ```jsonc
113
119
  {
@@ -136,11 +142,13 @@ option, set `branching: { "enabled": true, "conditions": [{ "choiceId": "male",
136
142
  "inputType": "plain", "required": true, "classification": "safe" }
137
143
  { "kind": "scale_input", "fieldKey": "fitness_level", "min": 1, "max": 5, "step": 1,
138
144
  "minLabel": "Beginner", "maxLabel": "Pro" }
145
+ { "kind": "wheel_picker", "fieldKey": "birth_year", "mode": "date", "datePart": "year",
146
+ "minYear": 1950, "maxYear": 2010, "defaultValue": "1990" }
139
147
  { "kind": "checkbox", "fieldKey": "accept_terms", "blocking": true }
140
148
  ```
141
149
 
142
150
  Use **at most one** input layer per screen, and add a `continue` button on
143
- screens with `text_input`, `multiple_choice`, or `scale_input`.
151
+ screens with `text_input`, `multiple_choice`, `scale_input`, or `wheel_picker`.
144
152
 
145
153
  ### Auth
146
154
 
@@ -156,7 +164,7 @@ screens with `text_input`, `multiple_choice`, or `scale_input`.
156
164
 
157
165
  Keep OAuth, email/password, and questionnaire inputs on **separate screens**.
158
166
 
159
- ### Carousel (swipe-only onboarding pager)
167
+ ### Carousel (onboarding pager)
160
168
 
161
169
  ```jsonc
162
170
  {
@@ -175,7 +183,9 @@ Keep OAuth, email/password, and questionnaire inputs on **separate screens**.
175
183
  }
176
184
  ```
177
185
 
178
- No buttons inside a carousel — paging is swipe-only ([carousel-import.md](carousel-import.md)).
186
+ Paging is swipe by default. When the source screen has an explicit Next control, add a
187
+ `button` with `action: { "kind": "advance_carousel", "targetLayerId": "<carousel id>" }`
188
+ instead of a plain footer CTA ([carousel-import.md](carousel-import.md)).
179
189
 
180
190
  ### Counter / progress / loader
181
191
 
@@ -63,12 +63,12 @@ If the user has not named an entry point, stop after question 1 and wait for an
63
63
  - When source uses `infoSteps`, `currentInfoStep`, horizontal `translateX` pagers, or `pagingEnabled` lists, emit `kind: "carousel"`.
64
64
  - Each slide is a vertical `stack` with image, title, and body text from that slide.
65
65
  - Add `pageControl: { "position": "bottom" }` when dot indicators exist (dots only).
66
- - Carousels are **swipe-only** — no Next/Continue button on the carousel layer. Do not add footer/body buttons for in-pager paging.
67
- - Use `regions.footer` only when the source CTA advances the **next screen in the flow**, or for single-slide carousels that need an explicit Continue.
66
+ - Carousels page by swipe. When the source has an explicit Next control, keep it as a `button` with `action.kind: "advance_carousel"` targeting that carousel; never add a plain footer/body button that only bumps the pager index.
67
+ - Use `regions.footer` when the source CTA advances the **next screen in the flow**, or for single-slide carousels that need an explicit Continue.
68
68
  - Bundle every slide image; every asset referenced in the carousel must appear in `rheo-import.assets.json`.
69
69
  - Do not collapse multi-slide routes into one static screen with a single image.
70
70
  11. Map layout, alignment, borders, and shadows:
71
- - Parent vertical stacks that center content need `align: "center"` (and `justify: "center"` when appropriate).
71
+ - Parent vertical stacks that center content need `align: "center"` (and `distribution: "center"` when appropriate).
72
72
  - Hero images belong inside centered stacks, not as lone siblings in an unaligned vertical stack.
73
73
  - Card rows (rating, testimonials, why-it-works cards) use wrapping stacks with `style.background`, `style.radius`, `style.padding`, `style.border`, and `style.shadow` from source.
74
74
  12. Map custom fonts from audit findings ([font-import.md](font-import.md)):
@@ -43,7 +43,7 @@ Before zipping, read [layer-schema-pitfalls.md](layer-schema-pitfalls.md) and ru
43
43
 
44
44
  ## Layer Kinds
45
45
 
46
- Use only: `stack`, `text`, `image`, `lottie`, `video`, `icon`, `button`, `back_button`, `progress`, `loader`, `counter`, `single_choice`, `multiple_choice`, `text_input`, `scale_input`, `oauth_provider`, `oauth_login`, `email_password_auth`, `email_password_field`, `email_password_submit`, `carousel`, `hyperlink`, `checkbox`.
46
+ Use only: `stack`, `text`, `image`, `lottie`, `video`, `icon`, `button`, `back_button`, `progress`, `loader`, `counter`, `single_choice`, `multiple_choice`, `text_input`, `scale_input`, `wheel_picker`, `oauth_provider`, `oauth_login`, `email_password_auth`, `email_password_field`, `email_password_submit`, `carousel`, `hyperlink`, `checkbox`, `conditional`.
47
47
 
48
48
  ## Rules
49
49
 
@@ -55,13 +55,17 @@ Use only: `stack`, `text`, `image`, `lottie`, `video`, `icon`, `button`, `back_b
55
55
  - Inspect theme/style/token files, StyleSheet, and Tailwind classes before using black-and-white defaults.
56
56
  - Set `style.color` on text for dark/saturated screen backgrounds.
57
57
  - Gradients: `screen.containerStyle.backgroundFill.color` as `linear-gradient(...)` CSS when `kind` is `color`.
58
- - In-screen pagers → `kind: "carousel"` with one slide per page; swipe-only; bundle every slide asset. See carousel-import.md in references.
58
+ - In-screen pagers → `kind: "carousel"` with one slide per page; swipe by default, or an `advance_carousel` button when the source has an explicit Next control; bundle every slide asset. See carousel-import.md in references.
59
59
  - Center images with parent stack `align: "center"`; map card borders/shadows to wrapping stacks.
60
60
  - Custom fonts: bundle files in `rheo-import.fonts.json` only (never `rheo-import.assets.json`), `manifest.theme.fontFamily`. See `font-import.md`.
61
- - Choice options: each option stack uses `style` (default) and `selectedStyle` (selected).
61
+ - Choice options: each option stack bakes default chrome into `style` and selected overrides into `selectedStyle` (optional `selectedStyleBreakpoints`).
62
+ - Stack packing: `distribution` — never `justify`. Width fill parent is `"full"` — never width `"fill"`.
63
+ - `text_input.fieldStyle` is typography only; chrome on outer `style`.
64
+ - `wheel_picker`: scroll wheel / date parts; `itemStyle` / `selectedItemStyle` typography only; branch later via `dec_*` with string predicates.
65
+ - Icons default to 24×24 unless source specifies another size.
62
66
  - Publish gates: explicit `style.color` on all text (including button labels), Continue on manual-submit screens, valid entry/completion path. Run `scripts/audit-publish-manifest.mjs` before finishing.
63
67
  - Black-and-white fallback is acceptable only when the audit finds no style/token evidence and the user confirms no theme source.
64
- - Use at most one input layer kind per screen.
68
+ - Use at most one input layer kind per **active path** (`single_choice`, `multiple_choice`, `text_input`, `scale_input`, `wheel_picker`) — one per screen unless sibling `conditional` branches split the path.
65
69
  - Non-reserved `sdk.*` decision keys must be listed in `sdkAttributeKeys`.
66
70
  - RevenueCat paywalls are external surface nodes and always need `fallback`.
67
71
  - Emit complete graph edges for imported flows.
@@ -23,7 +23,7 @@ See [layer-schema-pitfalls.md](layer-schema-pitfalls.md) for common id mistakes
23
23
 
24
24
  ## Layer Kinds
25
25
 
26
- Allowed kinds: `stack`, `text`, `image`, `lottie`, `video`, `icon`, `button`, `back_button`, `progress`, `loader`, `counter`, `single_choice`, `multiple_choice`, `text_input`, `scale_input`, `oauth_provider`, `oauth_login`, `email_password_auth`, `email_password_field`, `email_password_submit`, `carousel`, `hyperlink`, `checkbox`.
26
+ Allowed kinds: `stack`, `text`, `image`, `lottie`, `video`, `icon`, `button`, `back_button`, `progress`, `loader`, `counter`, `single_choice`, `multiple_choice`, `text_input`, `scale_input`, `wheel_picker`, `oauth_provider`, `oauth_login`, `email_password_auth`, `email_password_field`, `email_password_submit`, `carousel`, `hyperlink`, `checkbox`, `conditional`.
27
27
 
28
28
  ## Regions
29
29
 
@@ -56,27 +56,36 @@ Map clear brand values into `manifest.theme` and layer styles. Set `style.color`
56
56
 
57
57
  - In-screen pagers (`infoSteps`, horizontal pager, dot indicators) → `kind: "carousel"` with one slide stack per page.
58
58
  - Do not collapse multi-slide routes to one static screen.
59
- - `pageControl` is optional dot chrome only. Carousels are swipe-only (no pager buttons).
60
- - Do not duplicate paging with `regions.footer` Continue when the source footer only increments pager index. See [carousel-import.md](carousel-import.md).
59
+ - `pageControl` is optional dot chrome only; the carousel layer has no built-in paging buttons.
60
+ - An explicit source Next control becomes a `button` with `action: { "kind": "advance_carousel", "targetLayerId": "<carousel id>" }` on the same screen.
61
+ - Do not duplicate paging with a `regions.footer` Continue when the source footer only increments pager index. See [carousel-import.md](carousel-import.md).
61
62
 
62
63
  ## Layout
63
64
 
64
65
  - Center images: parent stack `align: "center"`.
66
+ - Main-axis packing: stack / button `distribution` (not `justify`).
65
67
  - Card chrome: wrapping stacks with `border`, `shadow`, `background`, `radius`, `padding`.
68
+ - Do not invent image `radius` unless the source has rounded corners.
66
69
 
67
70
  ## Explicit Sizing
68
71
 
69
- Set `style.width` and `style.height` on every layer — do not omit them. `width` accepts `"full"`, `"auto"`, a fraction (`"1/2"`, `"1/3"`, `"2/3"`, `"1/4"`, `"3/4"`), or a pixel number; `height` accepts `"fill"`, `"auto"`, or a pixel number. Use these per-kind defaults unless the design requires otherwise:
72
+ Set `style.width` and `style.height` on every layer — do not omit them. `width` accepts `"full"` (**never** `"fill"` on width), `"auto"`, a fraction (`"1/2"`, `"1/3"`, `"2/3"`, `"1/4"`, `"3/4"`), or a pixel number; `height` accepts `"fill"`, `"auto"`, or a pixel number. Optional clamps: `minWidth` / `maxWidth` / `minHeight` / `maxHeight`. Use these per-kind defaults unless the design requires otherwise:
70
73
 
71
74
  | Layer kinds | `width` | `height` |
72
75
  |-------------|---------|----------|
73
- | `stack`, `text_input`, `scale_input`, `oauth_login`, `email_password_auth`, `email_password_field`, `progress`, `loader` | `"full"` | `"fill"` |
76
+ | `stack`, `text_input`, `scale_input`, `wheel_picker`, `oauth_login`, `email_password_auth`, `email_password_field`, `progress`, `loader` | `"full"` | `"fill"` |
74
77
  | `button`, `back_button`, `oauth_provider`, `email_password_submit`, `checkbox`, `single_choice`, `multiple_choice` | `"full"` | `"auto"` |
75
- | `text`, `counter`, `icon`, `hyperlink` | `"auto"` | `"auto"` |
78
+ | `text`, `counter`, `hyperlink` | `"auto"` | `"auto"` |
79
+ | `icon` | `24` | `24` |
76
80
  | `image`, `lottie`, `video` | `"full"` | number (e.g. `160`) |
77
81
  | `carousel` | — (no outer `style` sizing) | — |
78
82
 
79
- Dashboard import backfills these defaults automatically, but emitting them explicitly produces higher-fidelity first drafts.
83
+ Stack packing uses **`distribution`** (`start` \| `center` \| `end` \| `between` \| `around`). Never emit legacy `justify`.
84
+
85
+ `text_input.fieldStyle` is typography only; outer chrome stays on `style` / `styleBreakpoints`.
86
+ `wheel_picker.itemStyle` / `selectedItemStyle` are row typography only; outer chrome stays on `style` / `styleBreakpoints`.
87
+
88
+ Dashboard import backfills sizing defaults automatically, but emitting them explicitly produces higher-fidelity first drafts.
80
89
 
81
90
  ## Container Layers (Required `children`)
82
91
 
@@ -135,6 +144,7 @@ When source uses a chevron-only back control, still nest an `icon` child (and op
135
144
  - **`back_button`** has no `action` field; navigation is built-in. Use in `regions.header` for back/close chrome.
136
145
  - Prefer `continue`, `skip`, and `end_flow` actions for imported first drafts (on `button` only).
137
146
  - **`request_app_review`** is allowed for human/explicit requests only (not default imports): empty action object, requires `screen.next.default`, single CTA after a positive moment.
147
+ - **`advance_carousel`** is for a source pager's own Next control: `targetLayerId` must be a `carousel` on the same screen, and optional `onLast` is `noop` (default) or `complete`.
138
148
 
139
149
  ## Custom Fonts
140
150
 
@@ -159,9 +169,10 @@ Full template: [layer-schema-pitfalls.md](layer-schema-pitfalls.md#single_choice
159
169
 
160
170
  ## Inputs
161
171
 
162
- - Use at most one input layer kind per screen.
172
+ - Use at most one input layer kind per **active path** (`single_choice`, `multiple_choice`, `text_input`, `scale_input`, `wheel_picker`) — one per screen unless sibling `conditional` branches split the path.
163
173
  - Use stable snake_case `fieldKey` values.
164
174
  - Mark text input classification as `safe` or `sensitive`.
175
+ - `wheel_picker` captures a string (options or date part); no layer branching — use `dec_*` with string predicates.
165
176
 
166
177
  ## Decisions
167
178
 
@@ -169,6 +180,19 @@ Full template: [layer-schema-pitfalls.md](layer-schema-pitfalls.md#single_choice
169
180
  - Non-reserved `sdk.*` keys used in decisions must be listed in `sdkAttributeKeys`.
170
181
  - Prefer source semantics over visual guessing.
171
182
 
183
+ ## Conditionals (same-screen variants)
184
+
185
+ Use `conditional` when the source renders different content on **one** screen based on locale, platform, an SDK attribute, or an answer already captured. Use `dec_*` when the source picks a different **screen**.
186
+
187
+ - Ordered `cases[]` (1–16), each with a `DecisionExpr` `expression` and a `rootLayerId`; first match wins.
188
+ - Required `elseRootLayerId` for the fallback. Every bound id is a distinct direct child `stack`.
189
+ - Cases read only fields answered **above** the conditional — upstream screens, or same-screen inputs earlier in tree order. Never a field captured inside its own branches.
190
+ - One input / `oauth_login` / `email_password_auth` per active path; sibling branches each get their own.
191
+ - `fieldKey` values stay unique across the whole screen, including across branches.
192
+ - Every case needs at least one rule before publish.
193
+ - No `style` or layout of its own; the winning branch renders in place.
194
+ - Do not use a conditional to reproduce a source screen that was genuinely a separate route.
195
+
172
196
  ## External Surfaces
173
197
 
174
198
  - RevenueCat paywalls become `externalSurfaceNodes` with provider `revenuecat`.
@@ -194,4 +218,4 @@ When source screens use i18n ([localization-import.md](localization-import.md)):
194
218
  - Avoid orphaned screens unless intentionally parked for later editing.
195
219
  - Run `scripts/audit-publish-manifest.mjs` before finishing — it enforces dashboard **Publish** rules (see [publish-gates.md](publish-gates.md)).
196
220
  - Every `text` and `icon` layer needs explicit `style.color` (including nested button label text).
197
- - Screens with `text_input`, `multiple_choice`, or `scale_input` need a `continue` button.
221
+ - Screens with `text_input`, `multiple_choice`, `scale_input`, or `wheel_picker` need a `continue` button.
@@ -26,11 +26,13 @@ These mirror `apps/web/src/features/builder/validateFlow.ts` and API `preflightP
26
26
  | **Choice input shape** | `single_choice` / `multiple_choice` missing `optionBindings` or `branching`, using `"options"` instead of `children`, or `fieldKey` not snake_case. |
27
27
  | **Container `children`** | `back_button`, `button`, or `hyperlink` emitted without a `children` array (or with label text on the parent instead of nested `text` children). Crashes import on Indie plans during motion strip; fails Zod validation. |
28
28
  | **Text/icon `style.color`** | Body text or **button label** (nested text child) left without `style.color` — native does not inherit CSS colors. |
29
- | **Continue button** | `text_input`, `multiple_choice`, or `scale_input` without a `button` with `action.kind: "continue"`. |
30
- | **One input per screen** | Multiple inputs, or OAuth/email-password combined with inputs. |
29
+ | **Continue button** | `text_input`, `multiple_choice`, `scale_input`, or `wheel_picker` without a `button` with `action.kind: "continue"`. |
30
+ | **One input per active path** | Multiple inputs on the same path, or OAuth/email-password combined with inputs. Sibling `conditional` branches each get their own budget. |
31
+ | **Conditional bindings** | `cases[].rootLayerId` / `elseRootLayerId` not pointing at distinct direct child stacks, or a case reading a field captured inside its own branch. |
31
32
  | **fieldKey** | Missing or non–snake_case on input layers. |
32
33
  | **Graph targets** | `go_to_step`, choice `goTo`, loader/lottie/video `onComplete` (screen mode), permission outcomes, and fallbacks point at missing `scr_*` / `dec_*` / `surf_*` ids. |
33
34
  | **Media triggers** | Lottie/video with `autoPlay: false` needs a `play_media` button targeting that layer. |
35
+ | **Carousel targets** | `advance_carousel` must point `targetLayerId` at a `carousel` layer on the same screen. |
34
36
  | **Screen backgrounds** | Image/video fills need `mediaAssetId`; manual background video needs trigger wiring. |
35
37
 
36
38
  ### Publishable graph (`validatePublishable`)
@@ -39,6 +41,7 @@ These mirror `apps/web/src/features/builder/validateFlow.ts` and API `preflightP
39
41
  - `entryScreenId` set and valid.
40
42
  - A **completion path** from entry (`end_flow`, terminal `next`, or external surface end).
41
43
  - Decision nodes: every case and `elseNext` connected.
44
+ - `conditional` layers: every case has at least one rule (`conditional.incomplete_cases`).
42
45
 
43
46
  ### Integrations (default: enabled)
44
47
 
@@ -38,7 +38,7 @@ Ask the user to confirm the entry file if it is not obvious; do not guess.
38
38
  | `react-native-video` / `expo-video` | `video` |
39
39
  | `<Pressable>`/`<TouchableOpacity>` acting as CTA | `button` (label = nested `text` child) |
40
40
  | Header back chevron / close (`<HeaderBackButton>`, a back `Pressable` in a top bar) | `back_button` in `regions.header`, `icon.family: "ionicons"`, no `action` |
41
- | `<View style={{ flexDirection, alignItems, gap }}>` | `stack` (`direction`, `align`, `justify`, `gap`) |
41
+ | `<View style={{ flexDirection, alignItems, gap }}>` | `stack` (`direction`, `align`, `distribution`, `gap`) |
42
42
  | Vector icons (`react-native-vector-icons`, `@expo/vector-icons` Ionicons) | `icon` (`family: "ionicons"`) |
43
43
  | External `Linking.openURL(...)` / `<A href>` | `hyperlink` |
44
44
  | Progress bar / step dots tied to step index | `progress` (header) |
@@ -55,6 +55,7 @@ Hero images that render through wrapper components (`<Illustration>`, `<Logo>`,
55
55
  - `<TextInput>` collecting a value → `text_input` with a snake_case `fieldKey`;
56
56
  mark password/email/PII as `classification: "sensitive"`.
57
57
  - Sliders / rating rows → `scale_input`. Consent toggles → `checkbox`.
58
+ - Native scroll wheels / `Picker` / year-month-day selectors → `wheel_picker` (not `single_choice`).
58
59
  - Screens with a manual-submit input need a `continue` button.
59
60
 
60
61
  ## Theme, colors, gradients, fonts
@@ -75,8 +76,9 @@ Hero images that render through wrapper components (`<Illustration>`, `<Logo>`,
75
76
  `react-native-pager-view`, a `FlatList horizontal pagingEnabled`, a
76
77
  `ScrollView horizontal` snap pager, `Animated` `translateX` paging, or an
77
78
  `infoSteps`/`currentStep` index → `kind: "carousel"` with one slide per page.
78
- Carousels are swipe-only; do not turn the pager's own next button into a footer
79
- button ([carousel-import.md](carousel-import.md)).
79
+ Paging is swipe by default; map the pager's own next button to a `button` with
80
+ `action.kind: "advance_carousel"` rather than a footer CTA
81
+ ([carousel-import.md](carousel-import.md)).
80
82
 
81
83
  ## Integrations and native steps
82
84
 
@@ -36,7 +36,7 @@ Ask the user to confirm the entry view/coordinator if it is not obvious.
36
36
  | `VideoPlayer` / `AVPlayer` | `video` |
37
37
  | `Button { … } label: { … }` acting as CTA | `button` (label = nested `text` child) |
38
38
  | Toolbar back/close (`.toolbar { ToolbarItem(placement: .navigationBarLeading) }`, a chevron `Button`) | `back_button` in `regions.header`, `icon.family: "ionicons"`, no `action` |
39
- | `VStack`/`HStack`/`ZStack` with `spacing`, `alignment` | `stack` (`direction`, `align`, `justify`, `gap`) |
39
+ | `VStack`/`HStack`/`ZStack` with `spacing`, `alignment` | `stack` (`direction`, `align`, `distribution`, `gap`) |
40
40
  | SF Symbol `Image(systemName:)` | `icon` — **remap to a valid `ionicons` name** (SF Symbols are not allowed on `icon`) |
41
41
  | `Link(destination:)` / `openURL` | `hyperlink` |
42
42
  | `ProgressView(value:)` / step dots | `progress` (header) |
@@ -53,6 +53,7 @@ real asset — follow the struct to its `Image("…")` to find the asset name.
53
53
  - `TextField`/`SecureField` → `text_input` with a snake_case `fieldKey`
54
54
  (`SecureField` / email / PII → `classification: "sensitive"`).
55
55
  - `Slider` / star rating → `scale_input`. `Toggle` consent → `checkbox`.
56
+ - `Picker` / `WheelPicker` / date-part wheels → `wheel_picker` (not `single_choice`).
56
57
  - Manual-submit input screens need a `continue` button.
57
58
 
58
59
  ## Theme, colors, gradients, fonts
@@ -73,7 +74,8 @@ real asset — follow the struct to its `Image("…")` to find the asset name.
73
74
 
74
75
  A paging `TabView { … }.tabViewStyle(.page)`, a horizontal `ScrollView` with snap
75
76
  paging, or an `infoSteps`/`currentStep` index → `kind: "carousel"`, one slide per
76
- page, swipe-only (no in-pager button) ([carousel-import.md](carousel-import.md)).
77
+ page. Paging is swipe by default; an explicit Next button becomes a `button` with
78
+ `action.kind: "advance_carousel"` ([carousel-import.md](carousel-import.md)).
77
79
 
78
80
  ## Integrations and native steps
79
81