@mmerterden/multi-agent-pipeline 10.0.6 → 10.3.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/CHANGELOG.md +143 -0
- package/README.md +5 -2
- package/docs/FIGMA_PIPELINE.md +12 -0
- package/docs/features.md +2 -0
- package/package.json +1 -1
- package/pipeline/agents/android-architect.md +3 -3
- package/pipeline/agents/backend-architect.md +3 -3
- package/pipeline/agents/code-reviewer.md +4 -4
- package/pipeline/agents/ios-architect.md +3 -3
- package/pipeline/agents/security-auditor.md +3 -3
- package/pipeline/commands/multi-agent/dev-autopilot.md +3 -3
- package/pipeline/commands/multi-agent/dev-local-autopilot.md +2 -2
- package/pipeline/commands/multi-agent/dev-local.md +3 -3
- package/pipeline/commands/multi-agent/dev.md +9 -9
- package/pipeline/commands/multi-agent/help.md +10 -10
- package/pipeline/commands/multi-agent/refs/channels/jira.md +1 -0
- package/pipeline/commands/multi-agent/refs/cross-cli-contract.md +7 -3
- package/pipeline/commands/multi-agent/refs/features/model-fallback.md +36 -18
- package/pipeline/commands/multi-agent/refs/phases/operations.md +15 -5
- package/pipeline/commands/multi-agent/refs/phases/phase-0-init.md +16 -2
- package/pipeline/commands/multi-agent/refs/phases/phase-1-analysis.md +8 -3
- package/pipeline/commands/multi-agent/refs/phases/phase-2-planning.md +1 -1
- package/pipeline/commands/multi-agent/refs/phases/phase-4-review.md +30 -8
- package/pipeline/commands/multi-agent/resume.md +4 -1
- package/pipeline/commands/multi-agent.md +5 -5
- package/pipeline/lib/fetch-confluence.sh +2 -2
- package/pipeline/lib/fetch-crashlytics.sh +2 -2
- package/pipeline/lib/fetch-fortify.sh +1 -1
- package/pipeline/lib/fetch-swagger.sh +1 -1
- package/pipeline/lib/figma-screenshot.sh +2 -2
- package/pipeline/preferences-template.json +8 -1
- package/pipeline/schemas/agent-state.schema.json +8 -0
- package/pipeline/schemas/figma-project-config.schema.json +39 -0
- package/pipeline/schemas/prefs.schema.json +3 -3
- package/pipeline/scripts/cost-table.json +0 -6
- package/pipeline/scripts/fixtures/install-layout.tsv +2 -2
- package/pipeline/scripts/phase-tracker.sh +7 -0
- package/pipeline/scripts/smoke-model-fallback.sh +20 -12
- package/pipeline/scripts/validate-state.mjs +108 -0
- package/pipeline/scripts/write-state.mjs +15 -4
- package/pipeline/skills/figma-android/README.md +3 -1
- package/pipeline/skills/figma-common/README.md +8 -1
- package/pipeline/skills/figma-common/figma-bottom-sheets/SKILL.md +152 -0
- package/pipeline/skills/figma-common/figma-evolve-component/SKILL.md +61 -0
- package/pipeline/skills/figma-common/figma-navigation/SKILL.md +156 -0
- package/pipeline/skills/figma-common/figma-overlays/SKILL.md +142 -0
- package/pipeline/skills/figma-common/figma-ui-patterns/SKILL.md +1 -0
- package/pipeline/skills/figma-common/figma-ui-patterns/patterns/animated-gradient-border.md +116 -0
- package/pipeline/skills/figma-ios/figma-to-component/SKILL.md +15 -0
- package/pipeline/skills/figma-ios/figma-to-component/phases/phase-3d-patterns.md +31 -0
- package/pipeline/skills/figma-ios/figma-to-component/phases/phase-4b-view.md +10 -0
- package/pipeline/skills/figma-ios/figma-to-component/reference/accessibility.md +55 -1
- package/pipeline/skills/figma-ios/figma-to-component/reference/orchestrator-discipline.md +1 -1
|
@@ -74,6 +74,13 @@ if [ -z "$TRACKER_FILE" ]; then
|
|
|
74
74
|
TASK_ID_FROM_ENV="$1"
|
|
75
75
|
fi
|
|
76
76
|
if [ -z "$TASK_ID_FROM_ENV" ]; then
|
|
77
|
+
# Strict mode: refuse the global pointer fallback entirely. Concurrent-safe by
|
|
78
|
+
# construction - the orchestrator must export MULTI_AGENT_TASK_ID. Opt in with
|
|
79
|
+
# MULTI_AGENT_STRICT_TASK_ID=1 (recommended whenever more than one run can be live).
|
|
80
|
+
if [ "${MULTI_AGENT_STRICT_TASK_ID:-0}" = "1" ]; then
|
|
81
|
+
echo "phase-tracker: MULTI_AGENT_STRICT_TASK_ID=1 but no MULTI_AGENT_TASK_ID in scope; refusing global pointer fallback. Export MULTI_AGENT_TASK_ID=<id>." >&2
|
|
82
|
+
exit 64
|
|
83
|
+
fi
|
|
77
84
|
# Fallback to the file written by the most recent init.
|
|
78
85
|
# WARNING: this pointer is global and flips when ANY shell on this user account runs
|
|
79
86
|
# `phase-tracker.sh init <other-task>`. In concurrent multi-agent runs (two terminals,
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# smoke-model-fallback.sh - lint the model fallback contract wiring (
|
|
3
|
-
# The contract lets
|
|
4
|
-
# a dispatch error, or a budget ceiling - via
|
|
5
|
-
# editing persona files. This smoke asserts the
|
|
6
|
-
# phase wiring all exist and agree. No network,
|
|
2
|
+
# smoke-model-fallback.sh - lint the model fallback contract wiring (v10.1.0).
|
|
3
|
+
# The contract lets preferredModel (opus) personas degrade to sonnet on a
|
|
4
|
+
# plan-window date gate, a dispatch error, or a budget ceiling - via
|
|
5
|
+
# PHASE_MODEL_OVERRIDE, never by editing persona files. This smoke asserts the
|
|
6
|
+
# doc, the prefs knob, and the phase wiring all exist and agree. No network,
|
|
7
|
+
# no credentials.
|
|
7
8
|
|
|
8
9
|
set -uo pipefail
|
|
9
10
|
|
|
@@ -26,7 +27,7 @@ if [ -f "$DOC" ]; then
|
|
|
26
27
|
grep -q "onDispatchError" "$DOC" && ok "doc documents dispatch-error retry" || fail "doc missing onDispatchError"
|
|
27
28
|
grep -q "cost-budget-check" "$DOC" && ok "doc documents budget-ceiling trigger" || fail "doc missing budget trigger"
|
|
28
29
|
grep -q "PHASE_MODEL_OVERRIDE" "$DOC" && ok "doc uses the existing override mechanism" || fail "doc missing PHASE_MODEL_OVERRIDE"
|
|
29
|
-
grep -q "
|
|
30
|
+
grep -q "opus -> sonnet" "$DOC" && ok "tier ladder declared" || fail "tier ladder missing"
|
|
30
31
|
else
|
|
31
32
|
fail "model-fallback.md missing"
|
|
32
33
|
fi
|
|
@@ -42,7 +43,7 @@ if not isinstance(mf, dict):
|
|
|
42
43
|
checks = [
|
|
43
44
|
mf.get("enabled") is True,
|
|
44
45
|
"premiumTierUntil" in mf and mf["premiumTierUntil"] is None,
|
|
45
|
-
mf.get("fallbackModel") == "
|
|
46
|
+
mf.get("fallbackModel") == "sonnet",
|
|
46
47
|
mf.get("onDispatchError") is True,
|
|
47
48
|
]
|
|
48
49
|
print("ok" if all(checks) else "bad-defaults")
|
|
@@ -61,12 +62,19 @@ echo "→ 3. Phase wiring references the contract"
|
|
|
61
62
|
grep -q "model-fallback.md" "$P0" && ok "phase-0-init wires the date gate" || fail "phase-0-init missing fallback wiring"
|
|
62
63
|
grep -q "model-fallback.md" "$P4" && ok "phase-4-review wires the per-dispatch triggers" || fail "phase-4-review missing fallback wiring"
|
|
63
64
|
|
|
64
|
-
echo "→ 4. Personas
|
|
65
|
-
|
|
66
|
-
if [ "$
|
|
67
|
-
ok "
|
|
65
|
+
echo "→ 4. Personas declare opus as preferred (fallback is dispatch-time, not file-time)"
|
|
66
|
+
OPUS_COUNT=$(grep -l "^preferredModel: opus" "$REPO_ROOT"/pipeline/agents/*.md 2>/dev/null | wc -l | tr -d ' ')
|
|
67
|
+
if [ "$OPUS_COUNT" -ge 5 ]; then
|
|
68
|
+
ok "opus personas intact ($OPUS_COUNT files)"
|
|
68
69
|
else
|
|
69
|
-
fail "expected >=5
|
|
70
|
+
fail "expected >=5 opus personas, found $OPUS_COUNT"
|
|
71
|
+
fi
|
|
72
|
+
# Fable 5 is retired - no persona should still pin it.
|
|
73
|
+
FABLE_LEFT=$(grep -l "^preferredModel: fable" "$REPO_ROOT"/pipeline/agents/*.md 2>/dev/null | wc -l | tr -d ' ')
|
|
74
|
+
if [ "$FABLE_LEFT" -eq 0 ]; then
|
|
75
|
+
ok "no persona still pins retired fable tier"
|
|
76
|
+
else
|
|
77
|
+
fail "expected 0 fable personas (fable retired), found $FABLE_LEFT"
|
|
70
78
|
fi
|
|
71
79
|
|
|
72
80
|
echo ""
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// validate-state.mjs - resume-safety check for agent-state.json.
|
|
3
|
+
//
|
|
4
|
+
// agent-state.json is the canonical per-run state read by every phase. A
|
|
5
|
+
// half-written or corrupt state (crash mid-write, truncated JSON, missing
|
|
6
|
+
// currentPhase) makes resume silently re-enter the wrong phase. Phase 0 and
|
|
7
|
+
// resume run this before trusting the file.
|
|
8
|
+
//
|
|
9
|
+
// This is deliberately a RESUME-SAFETY check, not strict conformance to
|
|
10
|
+
// pipeline/schemas/agent-state.schema.json. Real state files written by older
|
|
11
|
+
// pipeline versions omit schemaVersion, use a string shortId ("#2"), and write
|
|
12
|
+
// status "completed" - they are still perfectly resumable, so rejecting them
|
|
13
|
+
// here would break every legacy resume. We fail only on the conditions that
|
|
14
|
+
// actually make a resume unsafe:
|
|
15
|
+
// - the file does not parse (truncated / corrupt)
|
|
16
|
+
// - root is not an object
|
|
17
|
+
// - currentPhase is missing or out of the 0..7 range (resume keys on it)
|
|
18
|
+
// - phases is present but not an object, or an entry is not an object
|
|
19
|
+
// Phase status strings are NOT enum-checked: real writers use "completed"
|
|
20
|
+
// alongside the schema's "done", and an odd status does not make a resume
|
|
21
|
+
// unsafe (resume keys on currentPhase, not phase status). Zero deps, same
|
|
22
|
+
// style as the other validators.
|
|
23
|
+
//
|
|
24
|
+
// Usage:
|
|
25
|
+
// node validate-state.mjs path/to/agent-state.json
|
|
26
|
+
// cat agent-state.json | node validate-state.mjs -
|
|
27
|
+
//
|
|
28
|
+
// Exit codes:
|
|
29
|
+
// 0 - safe to resume
|
|
30
|
+
// 1 - unsafe: unparseable, or missing/invalid currentPhase, or malformed phases
|
|
31
|
+
// 64 - usage error
|
|
32
|
+
|
|
33
|
+
import { readFileSync } from "node:fs";
|
|
34
|
+
|
|
35
|
+
function readInput() {
|
|
36
|
+
const arg = process.argv[2];
|
|
37
|
+
if (!arg) {
|
|
38
|
+
console.error("usage: validate-state.mjs <path|->");
|
|
39
|
+
process.exit(64);
|
|
40
|
+
}
|
|
41
|
+
if (arg === "-") {
|
|
42
|
+
const chunks = [];
|
|
43
|
+
process.stdin.setEncoding("utf-8");
|
|
44
|
+
return new Promise((resolve) => {
|
|
45
|
+
process.stdin.on("data", (c) => chunks.push(c));
|
|
46
|
+
process.stdin.on("end", () => resolve(chunks.join("")));
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
return Promise.resolve(readFileSync(arg, "utf-8"));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function main() {
|
|
53
|
+
const raw = await readInput();
|
|
54
|
+
let s;
|
|
55
|
+
try {
|
|
56
|
+
s = JSON.parse(raw);
|
|
57
|
+
} catch (err) {
|
|
58
|
+
emit({ ok: false, code: 1, errors: [`invalid JSON: ${err.message}`] });
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const errors = [];
|
|
63
|
+
|
|
64
|
+
if (typeof s !== "object" || s === null || Array.isArray(s)) {
|
|
65
|
+
emit({ ok: false, code: 1, errors: ["root must be an object"] });
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// currentPhase is the field resume keys on - it must be present and in range.
|
|
70
|
+
if (!Number.isInteger(s.currentPhase) || s.currentPhase < 0 || s.currentPhase > 7) {
|
|
71
|
+
errors.push(`currentPhase must be integer 0..7, got ${JSON.stringify(s.currentPhase)}`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// phases is optional (some early/aborted states omit it), but if present it
|
|
75
|
+
// must be a well-formed map so sub-step resume can read it.
|
|
76
|
+
if (s.phases !== undefined) {
|
|
77
|
+
if (typeof s.phases !== "object" || s.phases === null || Array.isArray(s.phases)) {
|
|
78
|
+
errors.push("phases, when present, must be an object keyed by phase number");
|
|
79
|
+
} else {
|
|
80
|
+
for (const [k, v] of Object.entries(s.phases)) {
|
|
81
|
+
if (!/^[0-7]$/.test(k)) {
|
|
82
|
+
errors.push(`phases key invalid: "${k}" (expected "0".."7")`);
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (typeof v !== "object" || v === null) {
|
|
86
|
+
errors.push(`phases["${k}"] must be an object`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (errors.length > 0) {
|
|
93
|
+
emit({ ok: false, code: 1, errors });
|
|
94
|
+
process.exit(1);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
emit({ ok: true, code: 0, errors: [] });
|
|
98
|
+
process.exit(0);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function emit(result) {
|
|
102
|
+
process.stdout.write(JSON.stringify(result) + "\n");
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
main().catch((err) => {
|
|
106
|
+
emit({ ok: false, code: 1, errors: [`unhandled: ${err.message}`] });
|
|
107
|
+
process.exit(1);
|
|
108
|
+
});
|
|
@@ -20,8 +20,13 @@
|
|
|
20
20
|
* Exit codes:
|
|
21
21
|
* 0 - write succeeded
|
|
22
22
|
* 1 - invalid JSON on stdin
|
|
23
|
-
* 2 - lock timeout (another writer held the lock
|
|
23
|
+
* 2 - lock timeout (another writer held the lock past the acquire window)
|
|
24
24
|
* 3 - I/O error (disk full, permission denied, parent dir missing)
|
|
25
|
+
*
|
|
26
|
+
* Tunables (env):
|
|
27
|
+
* WRITE_STATE_LOCK_TIMEOUT_MS acquire window before exit 2 (default 15000)
|
|
28
|
+
* WRITE_STATE_LOCK_STALE_MS age past which a lock is reclaimed (default 30000)
|
|
29
|
+
* A dead holder (PID no longer alive) is reclaimed immediately regardless of age.
|
|
25
30
|
*/
|
|
26
31
|
|
|
27
32
|
import {
|
|
@@ -65,9 +70,9 @@ function parseArgs() {
|
|
|
65
70
|
* @param {number} timeoutMs
|
|
66
71
|
* @returns {Promise<void>}
|
|
67
72
|
*/
|
|
68
|
-
async function acquireLock(path, timeoutMs =
|
|
73
|
+
async function acquireLock(path, timeoutMs = Number(process.env.WRITE_STATE_LOCK_TIMEOUT_MS) || 15_000) {
|
|
69
74
|
const lockPath = `${path}.lock`;
|
|
70
|
-
const staleMs =
|
|
75
|
+
const staleMs = Number(process.env.WRITE_STATE_LOCK_STALE_MS) || 30_000;
|
|
71
76
|
const deadline = Date.now() + timeoutMs;
|
|
72
77
|
while (Date.now() < deadline) {
|
|
73
78
|
try {
|
|
@@ -89,7 +94,13 @@ async function acquireLock(path, timeoutMs = 5000) {
|
|
|
89
94
|
await new Promise((r) => setTimeout(r, 50));
|
|
90
95
|
}
|
|
91
96
|
}
|
|
92
|
-
throw Object.assign(
|
|
97
|
+
throw Object.assign(
|
|
98
|
+
new Error(
|
|
99
|
+
`lock timeout on ${lockPath} after ${timeoutMs}ms. If no other writer is running, ` +
|
|
100
|
+
`the lock is stale - remove it (rm "${lockPath}") and retry, or raise WRITE_STATE_LOCK_TIMEOUT_MS.`,
|
|
101
|
+
),
|
|
102
|
+
{ code: "LOCK_TIMEOUT" },
|
|
103
|
+
);
|
|
93
104
|
}
|
|
94
105
|
|
|
95
106
|
/**
|
|
@@ -16,7 +16,9 @@ Platform-specific skills for generating Jetpack Compose components from Figma de
|
|
|
16
16
|
|
|
17
17
|
## What's NOT here (yet)
|
|
18
18
|
|
|
19
|
-
The platform-**agnostic** skills - `figma-commit`, `figma-iterate`, `figma-issue`, `figma-review`, `figma-setup`, `figma-validate`, `figma-utility`, `figma-fix`, `figma-mend`, `figma-ui-patterns`, `figma-remote-mcp-auth`, `figma-cli-*`, `performance-*`, etc. - live under [`pipeline/skills/figma-common/`](../figma-common/) (WS-7b ✓). Both iOS and Android orchestrators dispatch there without duplication.
|
|
19
|
+
The platform-**agnostic** skills - `figma-commit`, `figma-iterate`, `figma-issue`, `figma-review`, `figma-setup`, `figma-validate`, `figma-utility`, `figma-fix`, `figma-mend`, `figma-evolve-component`, `figma-ui-patterns`, `figma-form-integration`, `figma-price-integration`, `figma-navigation`, `figma-overlays`, `figma-bottom-sheets`, `figma-remote-mcp-auth`, `figma-cli-*`, `performance-*`, etc. - live under [`pipeline/skills/figma-common/`](../figma-common/) (WS-7b ✓). Both iOS and Android orchestrators dispatch there without duplication.
|
|
20
|
+
|
|
21
|
+
The cross-cutting **integration adapters** (`figma-form-integration`, `figma-price-integration`, `figma-navigation`, `figma-overlays`, `figma-bottom-sheets`) carry both an iOS (SwiftUI) and an Android (Jetpack Compose) section - the Android orchestrator uses the Compose section; the same emit-intent / caller-owns rules and the `figma-config` `ui.*` hooks apply on both platforms.
|
|
20
22
|
|
|
21
23
|
## Routing
|
|
22
24
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Shared skills that both `pipeline/skills/figma-ios/` (SwiftUI) and `pipeline/skills/figma-android/` (Jetpack Compose) dispatch. Moved here by WS-7b of the v5.0.0 rebuild so the platform trees stay lean and there's no duplication.
|
|
4
4
|
|
|
5
|
-
## What lives here (
|
|
5
|
+
## What lives here (31 skills)
|
|
6
6
|
|
|
7
7
|
**Workflow orchestration:**
|
|
8
8
|
|
|
@@ -28,6 +28,7 @@ Shared skills that both `pipeline/skills/figma-ios/` (SwiftUI) and `pipeline/ski
|
|
|
28
28
|
- `figma-validate` - Pre-implementation validation
|
|
29
29
|
- `figma-fix` - Targeted bug fix from review findings
|
|
30
30
|
- `figma-mend` - Re-implement component from scratch
|
|
31
|
+
- `figma-evolve-component` - Reconcile drift + additively extend an existing component (human-gated)
|
|
31
32
|
|
|
32
33
|
**Setup + utility:**
|
|
33
34
|
|
|
@@ -38,6 +39,12 @@ Shared skills that both `pipeline/skills/figma-ios/` (SwiftUI) and `pipeline/ski
|
|
|
38
39
|
- `figma-form-integration` - Form protocol adapter (shared UX)
|
|
39
40
|
- `figma-price-integration` - Price protocol adapter (shared UX)
|
|
40
41
|
|
|
42
|
+
**Cross-cutting interaction adapters (dual-platform: iOS SwiftUI + Android Jetpack Compose; native-first; project system via `figma-config` `ui.*`):**
|
|
43
|
+
|
|
44
|
+
- `figma-navigation` - Navigation pattern: headless Output vs self-route; native `NavigationStack` or `ui.navigationSystem`
|
|
45
|
+
- `figma-overlays` - Overlay pattern: toast/HUD/alert/data-modal; native `.alert`/`.sheet(item:)` or `ui.overlaySystem`
|
|
46
|
+
- `figma-bottom-sheets` - Bottom-sheet/detent pattern: native `.presentationDetents` or `ui.sheetSystem`
|
|
47
|
+
|
|
41
48
|
**Performance batch:**
|
|
42
49
|
|
|
43
50
|
- `performance-start`, `performance-swiftui`, `performance-tour`, `performance-review-next`, `performance-iteration-commit-all`
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: figma-bottom-sheets
|
|
3
|
+
description: "Bottom-sheet / detent pattern integration for Figma-to-UI components (iOS SwiftUI + Android Jetpack Compose) — half-sheets, drawers, pinned bottom CTAs, resizable pull-up panels, content-sized sheets, and multi-step in-sheet flows. Native by default (SwiftUI .sheet+presentationDetents; Compose ModalBottomSheet + rememberModalBottomSheetState); an optional project-supplied detent-sheet system when figma-config declares one. Reach for this when a design shows a bottom sheet, half-sheet, bottom drawer, snap points, a sticky footer button, or a wizard-in-a-sheet."
|
|
4
|
+
user-invocable: true
|
|
5
|
+
allowed-tools: Read, Glob, Grep
|
|
6
|
+
status: wip-v5
|
|
7
|
+
sourced-from: upstream/figma-bottom-sheets
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# /figma-bottom-sheets - Bottom-Sheet / Detent Pattern Integration
|
|
11
|
+
|
|
12
|
+
## Purpose
|
|
13
|
+
|
|
14
|
+
Consumed by the figma-to-swiftui pipeline (Phase 3D detection, Phase 4B view) and available ad-hoc, this skill defines how **bottom-anchored, detent-based surfaces** are presented. Sibling of `/figma-overlays` (transient/blocking overlays) and `/figma-navigation` (routing): native-SwiftUI default + a config-driven hook for projects that ship a custom detent-sheet engine.
|
|
15
|
+
|
|
16
|
+
**Generic by design.** Default is stock SwiftUI `.sheet` + `.presentationDetents`. If `figma-config.json` declares `ui.sheetSystem`, route through that engine by name - same rules, project vocabulary. (Some apps replace system sheets because iOS 26 forces a Liquid-Glass floating inset on partial detents with no removal API; that replacement is the custom-system case, not the default.)
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Strict Rules (non-negotiable)
|
|
21
|
+
|
|
22
|
+
1. **The sheet surface is a caller concern; the component is the content.** A reusable component authored from Figma is the sheet's **content**, not the presenter. The caller owns the `isPresented`/`item` binding and the detents. A component that presents itself can't be reused elsewhere.
|
|
23
|
+
2. **No magic numbers.** Detents, corner radius, grabber size, insets come from tokens / figma-config token patterns, never hard-coded literals.
|
|
24
|
+
3. **Detents ascend, max 3.** Pass `[detent]` lowest→highest; the sheet opens at the lowest. Prefer semantic detents (`.medium`, `.large`, `.fraction`, content-height) over pixel heights where possible.
|
|
25
|
+
4. **A bottom sheet is not a modal card and not a navigation push.** Data alert/modal card → `/figma-overlays`; screen routing → `/figma-navigation` (a routed modal may still *render* as a sheet, but navigation owns the routing).
|
|
26
|
+
5. **Native SwiftUI is the default; a custom sheet engine is opt-in via config.** Only use a project detent-sheet system when `ui.sheetSystem.mode == "custom"`.
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## When to Invoke This Skill
|
|
31
|
+
|
|
32
|
+
1. **Phase 3D - sheet affordance/pattern:** the design is (or contains) a bottom sheet / half-sheet / drawer, a **pinned bottom CTA** bar, a resizable pull-up panel, a **content-sized** sheet, or a **multi-step** in-sheet flow (in-sheet back/next that resizes per step).
|
|
33
|
+
2. **Phase 4B - View:** the view is authored as sheet content, or presents a child as a sheet.
|
|
34
|
+
3. **Ad-hoc:** "half sheet", "bottom drawer", "pull-up panel", "snap points", "sticky footer button", "multi-step sheet", "sheet sized to its content".
|
|
35
|
+
|
|
36
|
+
**Not a bottom sheet (guards):**
|
|
37
|
+
- Toast / HUD / alert / data modal card → `/figma-overlays`.
|
|
38
|
+
- Full screen navigation / tab / deep link → `/figma-navigation`.
|
|
39
|
+
- An inline expandable section within the page flow (not an anchored surface) → plain component content.
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## Native-first architecture (default)
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
Caller
|
|
47
|
+
| owns isPresented / item + detents; presents the component as content
|
|
48
|
+
v
|
|
49
|
+
.sheet(item:/isPresented:) {
|
|
50
|
+
ComponentContent() // the Figma-authored component
|
|
51
|
+
.presentationDetents([.medium, .large])
|
|
52
|
+
.presentationDragIndicator(.visible)
|
|
53
|
+
.presentationBackgroundInteraction(.enabled(upThrough: .medium)) // passthrough-ish
|
|
54
|
+
.presentationCornerRadius(<token>)
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
- **Half / expandable:** `.presentationDetents([.medium, .large])` or `[.fraction(0.25), .fraction(0.6), .large]`.
|
|
59
|
+
- **Content-sized:** `.presentationDetents([.height(measuredHeight)])` using a height read via a preference key (a `sizeThatFits`/`GeometryReader` measured content height), then scroll past the cap.
|
|
60
|
+
- **Pinned bottom CTA (part of the screen, not a modal):** `.safeAreaInset(edge: .bottom) { CTA() }` - reserves space, screen stays interactive. This is NOT a presented sheet.
|
|
61
|
+
- **Backdrop behavior:** dim+block is the `.sheet` default; passthrough/interactive-behind uses `.presentationBackgroundInteraction(.enabled(...))`.
|
|
62
|
+
- **Multi-step in-sheet flow:** drive a `NavigationStack(path:)` *inside* the sheet content for push/pop, resizing detents per step; each step is a headless view emitting `Output` (see `/figma-navigation`). If the target project's custom engine forbids a nested `NavigationStack`, use its engine (config hook).
|
|
63
|
+
- **Overlays over a sheet:** mount the toast/HUD host on the sheet content (`/figma-overlays`), not the root.
|
|
64
|
+
|
|
65
|
+
Tokens per `reference/ui` + figma-config token patterns.
|
|
66
|
+
|
|
67
|
+
## Config hook - project-supplied detent-sheet engine
|
|
68
|
+
|
|
69
|
+
When `figma-config.json` declares:
|
|
70
|
+
```jsonc
|
|
71
|
+
"ui": { "sheetSystem": {
|
|
72
|
+
"mode": "custom",
|
|
73
|
+
"brandedModifier": "bottomSheet", // .bottomSheet(isPresented:title:) branded entry
|
|
74
|
+
"headlessModifiers": ["modalSheet", "dockedSheet", "expandableSheet", "detentNavigationSheet"],
|
|
75
|
+
"detentType": "SheetDetentType", // detent enum
|
|
76
|
+
"backdropType": "Backdrop", // backdrop enum
|
|
77
|
+
"cornerRadiusToken": ".CornerRadius.radius12" // example: your branded top-corner radius token
|
|
78
|
+
}}
|
|
79
|
+
```
|
|
80
|
+
apply the SAME rules via the project's engine (names from config, do NOT hardcode):
|
|
81
|
+
- Use `.{brandedModifier}` for product sheets needing brand chrome; the headless modifiers for unbranded surfaces / CTAs / resizable panels / multi-step flows.
|
|
82
|
+
- Detents/backdrops use `{detentType}`/`{backdropType}` values; corner radius from `{cornerRadiusToken}`.
|
|
83
|
+
- Multi-step flows use the engine's navigation sheet (e.g. `detentNavigationSheet` with a `Navigator<Route>`) rather than a raw nested `NavigationStack`.
|
|
84
|
+
- One container-owned grabber (pass custom headers grabberless if the engine draws the grabber).
|
|
85
|
+
- Present only from the surface's own host - never walk the scene/window hierarchy for a "top" controller.
|
|
86
|
+
|
|
87
|
+
If `mode` is absent or `native`, use the native-first section.
|
|
88
|
+
|
|
89
|
+
## Android (Jetpack Compose)
|
|
90
|
+
|
|
91
|
+
Same rules, Compose vocabulary. Default is Material3; a project sheet engine is used only when `ui.sheetSystem.mode == "custom"`.
|
|
92
|
+
|
|
93
|
+
- **Half / expandable / drawer:** `ModalBottomSheet(onDismissRequest = { showSheet = false }, sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = false)) { Content() }`. The caller owns `var showSheet by remember` and the `SheetState`; the Figma-authored composable is the **content**.
|
|
94
|
+
- **Detents / snap points:** Material3 exposes partial + expanded (`skipPartiallyExpanded`, `SheetValue`); for finer snap points use an `AnchoredDraggable`-based sheet. Don't hard-code pixel heights - derive from content / fractions.
|
|
95
|
+
- **Content-sized:** let the content wrap-height; the sheet grows to content then scrolls (inner `Column`/`LazyColumn` with `verticalScroll`).
|
|
96
|
+
- **Pinned bottom CTA (part of the screen, not a sheet):** `Scaffold(bottomBar = { CTA() })` - reserves space, screen stays interactive. Not a `ModalBottomSheet`.
|
|
97
|
+
- **Multi-step in-sheet flow:** host a nested `NavHost` (or step state) inside the sheet content, resizing per step; each step is a headless composable emitting events (see `/figma-navigation`).
|
|
98
|
+
- **Overlays over the sheet:** show `Snackbar`/`AlertDialog` scoped to the sheet content (`/figma-overlays`), not the root.
|
|
99
|
+
|
|
100
|
+
**Config hook:** `ui.sheetSystem.mode == "custom"` → use the project's `{brandedModifier}` / headless composables + `{detentType}` / `{backdropType}` and radius from `{cornerRadiusToken}`, resolved from config.
|
|
101
|
+
|
|
102
|
+
Corner radius / grabber / insets from tokens (`MaterialTheme.shapes` / figma-config), not literals.
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## Integration with Phase 3D
|
|
107
|
+
|
|
108
|
+
Record in `04b_component_architecture.md`:
|
|
109
|
+
|
|
110
|
+
```markdown
|
|
111
|
+
## Bottom Sheet
|
|
112
|
+
| Signal | Surface | Detents | Owner |
|
|
113
|
+
|---|---|---|---|
|
|
114
|
+
| Half-sheet picker | presented sheet | [.medium, .large] | caller owns isPresented |
|
|
115
|
+
| Sticky "Devam Et" bar | pinned bottom CTA | n/a (safeAreaInset) | part of screen |
|
|
116
|
+
| 3-step add flow | multi-step in-sheet | per-step (content height) | caller drives path |
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Property classification addition:
|
|
120
|
+
|
|
121
|
+
| Classification | When | Swift Pattern |
|
|
122
|
+
|---|---|---|
|
|
123
|
+
| Sheet content | Component is authored to live inside a sheet | plain component; caller adds `.presentationDetents(...)` |
|
|
124
|
+
| Present-child-as-sheet | Component reveals a child surface | caller-owned `isPresented`/`item` + `.sheet` (component emits the trigger intent) |
|
|
125
|
+
| Pinned CTA | Sticky bottom action bar | `.safeAreaInset(edge:.bottom)` at the screen, not a modal |
|
|
126
|
+
|
|
127
|
+
## Integration with Phase 4B
|
|
128
|
+
- Author the component as sheet **content**; let the caller own presentation + detents.
|
|
129
|
+
- Prefer semantic detents; content-sized sheets measure height via a preference key.
|
|
130
|
+
- Pinned CTA = `.safeAreaInset(edge:.bottom)`, not a presented sheet.
|
|
131
|
+
- Multi-step flow = `NavigationStack` inside the sheet (native) or the engine's nav sheet (custom).
|
|
132
|
+
- If `ui.sheetSystem.mode == custom`, use `{brandedModifier}`/headless modifiers + `{detentType}`/`{backdropType}` per config.
|
|
133
|
+
- Verify on the simulator across detents, backdrop, and drag/dismiss on the project's min + current iOS.
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## When NOT to Use Bottom-Sheet Integration
|
|
138
|
+
- Toast/HUD/alert/data modal card → `/figma-overlays`.
|
|
139
|
+
- Screen routing/tabs/deep links → `/figma-navigation`.
|
|
140
|
+
- An inline expandable/disclosure within page flow → plain component content.
|
|
141
|
+
|
|
142
|
+
## Checklist - Before Leaving the Skill
|
|
143
|
+
```
|
|
144
|
+
[ ] Component is sheet CONTENT; caller owns isPresented/item + detents
|
|
145
|
+
[ ] Detents ascend, max 3; semantic detents preferred; no magic-number heights
|
|
146
|
+
[ ] Pinned CTA uses safeAreaInset(edge:.bottom), not a presented sheet
|
|
147
|
+
[ ] Content-sized sheet measures height via preference key, then scrolls
|
|
148
|
+
[ ] Multi-step flow uses NavigationStack-in-sheet (native) or engine nav sheet (custom)
|
|
149
|
+
[ ] Corner radius / grabber / insets from tokens, not literals
|
|
150
|
+
[ ] Default native SwiftUI; custom engine only when figma-config ui.sheetSystem.mode == custom
|
|
151
|
+
[ ] Overlays over the sheet mount on the sheet content surface
|
|
152
|
+
```
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: figma-evolve-component
|
|
3
|
+
description: "On-demand reconcile-and-extend for an ALREADY-implemented UI component (iOS SwiftUI or Android Jetpack Compose). Heals drift from current Figma (stale variants/props/structure, design-token drift, a detached/overridden deviation) AND extends the component non-destructively to cover a need the design can't express — always behind a mandatory human gate. Reach for this when an existing component drifted from its current design or doesn't cover a need. Not for building from scratch (figma-to-component / figma-mend) or fixing one review bug (figma-fix)."
|
|
4
|
+
user-invocable: true
|
|
5
|
+
argument-hint: <component | figma-url> [--report-only]
|
|
6
|
+
allowed-tools: Task, Read, Glob, Grep, Bash, Edit, Write, AskUserQuestion
|
|
7
|
+
status: wip-v5
|
|
8
|
+
sourced-from: upstream/figma-evolve-component
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# /figma-evolve-component - reconcile & extend an existing component
|
|
12
|
+
|
|
13
|
+
> **Use when:** an existing UI component drifted from its current Figma design, OR it doesn't cover a need you now have.
|
|
14
|
+
> **Don't use when:** building a brand-new component → `figma-to-swiftui`; re-implementing a discarded component from scratch → `figma-mend`; fixing a single bug filed in review → `figma-fix`.
|
|
15
|
+
|
|
16
|
+
## Scope
|
|
17
|
+
Reconcile **one** existing component against current Figma and extend it for a real need, then propagate the change to its Code Connect mapping + docs + wiki. **Out:** new components, networking, navigation graphs, batch/all-component sweeps.
|
|
18
|
+
|
|
19
|
+
## Invariants
|
|
20
|
+
- **Two truths.** Design is truth for **heal** (drift); **the user** is truth for **extend** (a need/divergence design can't supply). Design cannot tell us which gap to close - the user does.
|
|
21
|
+
- **Human gate is mandatory.** Never write before the user's decisions (Procedure step 3). This is prompt-driven, not autonomous. `--report-only` stops after the diff.
|
|
22
|
+
- **Additive / non-destructive.** Preserve the public API, init signatures, and variant cases. Never remove or rename them without explicit confirmation - it breaks callers.
|
|
23
|
+
- **Detached / overridden node = suspect truth.** If the Figma node is a detached instance or has local overrides, surface it and reconcile against the **main** component - don't auto-heal toward a deviated instance.
|
|
24
|
+
- **Recorded divergences are respected.** A `// evolve-keep: <reason>` marker in the code means the audit skips it and apply never reverts it.
|
|
25
|
+
- **Not done until it builds + existing call sites still compile + tests/snapshots pass** (`figma-to-swiftui` Phase 5 / `reference/build-and-test`).
|
|
26
|
+
|
|
27
|
+
## Conventions & golden paths (point, don't re-implement)
|
|
28
|
+
- **Design truth** → `/figma-utility` (`properties`, `tokens`, `context`, `metadata`, `screenshot`, `code-connect`). Analysis phase only - same MCP/REST rules as the pipeline.
|
|
29
|
+
- **Locate the component + its Figma node** → `FigmaRegistryUtility` lookup / repo grep for the `{ComponentName}` files and its `*.figma.swift`, or a URL the user gives. Paths resolve from `figma-config.json` (`repos.*`, component roots) - never hardcode.
|
|
30
|
+
- **Component structure + tokens** → `reference/ui` (`figma-to-component/reference/`), `figma-to-swiftui-effects.md`, `macros.md`. Cross-cutting patterns → `/figma-ui-patterns`, `/figma-form-integration`, `/figma-price-integration`, `/figma-navigation`, `/figma-overlays`, `/figma-bottom-sheets`.
|
|
31
|
+
- **After any prop/variant change, regenerate the derived artifacts - don't hand-roll:** Code Connect → `figma-to-component` Phase 6; local `.figma.md` doc → Phase 4C / `figma-component-docs`; public wiki → Phase 7 / `figma-component-wiki`.
|
|
32
|
+
- Build/run/test gate → `reference/build-and-test`; ship → `figma-commit` / `figma-iteration-commit`.
|
|
33
|
+
|
|
34
|
+
## Decision rules - classify each finding heal / extend / keep
|
|
35
|
+
- **stale** — variant/prop/structure differs from current Figma → **heal** toward design.
|
|
36
|
+
- **token drift** — hardcoded or old color/spacing/typography/radius vs the current token → **heal** to the current token (resolve the token name via the registry / `reference/ui`; don't guess).
|
|
37
|
+
- **detached / override** — node deviates from its main → **stop, ask the user** which is truth.
|
|
38
|
+
- **coverage gap** — design doesn't cover the need, or the impl is wrong → **extend**; the user says what to add.
|
|
39
|
+
- Removing/renaming public API or a variant case is **never** automatic → explicit confirmation.
|
|
40
|
+
|
|
41
|
+
## Procedure (the gate is non-negotiable)
|
|
42
|
+
1. **Resolve & gather** — locate the component file(s) + its Figma node (registry / Code Connect map / user URL). Pull design truth via `/figma-utility`.
|
|
43
|
+
- If the node is unresolved, or **detached/overridden** → ask the user (which node is truth?).
|
|
44
|
+
2. **Diff & present** — list findings per drift axis, each tagged **heal / extend / keep**. **No writes.** (`--report-only` stops here.)
|
|
45
|
+
3. **GATE — user decides** (`AskUserQuestion`): which to heal, which to keep (→ add `// evolve-keep:` marker), what to extend (the need design can't supply). Do not proceed without this.
|
|
46
|
+
4. **Apply** — only the approved changes, additively / non-destructively. When healing token drift, resolve the current token name via the registry / `reference/ui` - don't guess it.
|
|
47
|
+
5. **Propagate** — regenerate Code Connect (Phase 6), the local `.figma.md` doc (Phase 4C), and the wiki page (Phase 7) for the changed props/variants. If keys changed (testing/loc/a11y/analytics), update them through the normal utility path.
|
|
48
|
+
6. **Verify** — build + snapshot/preview; confirm existing call sites still compile (no API break); run affected tests.
|
|
49
|
+
|
|
50
|
+
## Verification
|
|
51
|
+
Build (project scheme from figma-config) + snapshot/preview render; callers compile; Code Connect + `.figma.md` + wiki reflect the new props/variants.
|
|
52
|
+
|
|
53
|
+
## Pitfalls
|
|
54
|
+
- Applying without the step-3 gate - this skill is human-gated by design.
|
|
55
|
+
- Healing toward a detached/overridden node (deviated truth).
|
|
56
|
+
- Removing/renaming a variant or init param → breaks callers; stay additive.
|
|
57
|
+
- Skipping Propagate → Code Connect + docs silently go stale after a prop/variant change.
|
|
58
|
+
|
|
59
|
+
## Escape hatches
|
|
60
|
+
- Build new from scratch → `figma-to-swiftui`. Re-implement a discarded component → `figma-mend`. Fix one review bug → `figma-fix`.
|
|
61
|
+
- Just fetch design data → `/figma-utility`. Build/test → `reference/build-and-test`. Commit/PR → `figma-commit`.
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: figma-navigation
|
|
3
|
+
description: "Navigation pattern integration for Figma-to-UI components (iOS SwiftUI + Android Jetpack Compose). Guides how a component reaches other screens, tabs, or deep links without coupling to a router — headless screens that emit a typed Output/event, native NavigationStack (iOS) / Navigation Compose (Android) by default, an optional project-supplied navigation system when figma-config declares one. Reach for this when a design embeds a tab bar, nav bar / back button, a push/detail affordance, or a deep-link entry point."
|
|
4
|
+
user-invocable: true
|
|
5
|
+
allowed-tools: Read, Glob, Grep
|
|
6
|
+
status: wip-v5
|
|
7
|
+
sourced-from: upstream/figma-navigation
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# /figma-navigation - Navigation Pattern Integration
|
|
11
|
+
|
|
12
|
+
## Purpose
|
|
13
|
+
|
|
14
|
+
Consumed by the figma-to-swiftui pipeline (Phase 3D pattern detection, Phase 4B view implementation) and available for ad-hoc/vibe-coding, this skill defines how a component participates in navigation **without owning routing**. It is the navigation counterpart of `/figma-form-integration` and `/figma-price-integration`: a cross-cutting integration with a **native-SwiftUI default** and a **config-driven hook** for projects that ship their own navigation system.
|
|
15
|
+
|
|
16
|
+
**Generic by design.** Nothing here assumes a specific app. The default is stock SwiftUI (`NavigationStack`, `navigationDestination`, `.toolbar`, `.sheet`). If the target project declares a custom navigation system in its `figma-config.json` (`ui.navigationSystem`), this skill routes to that system's types by name instead — same rules, project-specific vocabulary.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Strict Rules (non-negotiable)
|
|
21
|
+
|
|
22
|
+
1. **A component never names a route, a sibling screen, or a router.** A component/screen takes its arrival data as init params and emits intent through a typed **`Output`** closure (`onSelect`, `onSubmit`, `output:`). The *caller* (scene/coordinator/parent) maps each output to a navigation edge. This keeps the component reusable and testable in isolation.
|
|
23
|
+
2. **No navigation side effects inside a component body.** No `NavigationLink(destination:)` hardcoding a concrete next screen, no router calls, no global singletons touched from `body`. Navigation is a caller concern expressed via the emitted output.
|
|
24
|
+
3. **Selection/tab state is observable data, not an imperative bridge.** A selected tab/section is a bound value (`@Binding`/observable), written by the caller; the component reflects it. A selection set before the container mounts is simply rendered on appear - no cold-start race.
|
|
25
|
+
4. **Deep links reduce to state and never crash.** A URL parses to a plan/value; malformed input is dropped (logged, no trap). The component is never the deep-link parser - it only exposes the entry state the parser can set.
|
|
26
|
+
5. **Native SwiftUI is the default; a custom system is opt-in via config.** Do NOT invent a bespoke router for a project that hasn't asked for one. Only use custom navigation types when `ui.navigationSystem.mode == "custom"` is set in `figma-config.json`.
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## When to Invoke This Skill
|
|
31
|
+
|
|
32
|
+
Invoke when ANY of these are detected in the design or task:
|
|
33
|
+
|
|
34
|
+
1. **Phase 3D - navigation sub-component / affordance:** a nested instance of a navigation-capable design element:
|
|
35
|
+
- Tab bar / segmented section switcher that changes the top-level view
|
|
36
|
+
- Navigation bar / header with a **back button** or close affordance
|
|
37
|
+
- A row/card whose variant implies a **push/detail** affordance (chevron, "Detaylar", disclosure)
|
|
38
|
+
- A link/button whose label implies leaving the screen ("Devam", "Tümünü gör")
|
|
39
|
+
- A deep-link entry point (the design is a landing surface reached by URL)
|
|
40
|
+
2. **Phase 4B - View:** the view renders a back button, tab strip, or a control that should navigate.
|
|
41
|
+
3. **Ad-hoc:** wiring a screen's push/pop/tab/deep-link flow outside the full pipeline.
|
|
42
|
+
|
|
43
|
+
**Not navigation (false-positive guards):**
|
|
44
|
+
- A segmented control that filters in-place (no screen change) → plain `Binding<Enum>`, not navigation.
|
|
45
|
+
- A button that submits a form → that's `/figma-form-integration`; the *result* may navigate, but the control is a form action.
|
|
46
|
+
- A modal card of data / a toast / a loading HUD → `/figma-overlays`. A bottom sheet surface → `/figma-bottom-sheets`.
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## Native-first architecture (default)
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
Parent / Scene (caller)
|
|
54
|
+
| owns NavigationStack(path:) or tab Binding, maps Output → edge
|
|
55
|
+
v
|
|
56
|
+
ComponentView
|
|
57
|
+
| init(arrivalData…, output: @escaping (Output) -> Void)
|
|
58
|
+
| emits .selected(id) / .continue / .back - never routes
|
|
59
|
+
v
|
|
60
|
+
(caller) path.append(route) | selectedTab = .x | present(.sheet)
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
- **Push/pop within a flow:** caller owns `NavigationStack(path:)`; component emits `Output`; caller appends a `Hashable` route; `navigationDestination(for:)` renders it.
|
|
64
|
+
- **Back button:** prefer the system back button. A custom header back affordance emits `.back` (or calls `dismiss`); it renders only when there is something to pop (depth > 0 / `isPresented`).
|
|
65
|
+
- **Tabs:** `TabView(selection:)` bound to an observable selection the caller owns; the component reads/writes the binding, never a global.
|
|
66
|
+
- **Deep links:** caller parses URL → a `Hashable` route / tab selection and applies it to the same path/selection state the UI already uses. Component only exposes the entry state.
|
|
67
|
+
- **Modal presentation of another screen:** `.sheet(item:)` / `.fullScreenCover(item:)` driven by caller state (data-driven, no `AnyView`). A *bottom sheet surface* is `/figma-bottom-sheets`; an *alert/data modal card* is `/figma-overlays`.
|
|
68
|
+
|
|
69
|
+
Design tokens (spacing, colors, typography) follow `reference/ui` + the project's figma-config token patterns - never raw literals.
|
|
70
|
+
|
|
71
|
+
## Config hook - project-supplied navigation system
|
|
72
|
+
|
|
73
|
+
When `figma-config.json` declares:
|
|
74
|
+
```jsonc
|
|
75
|
+
"ui": { "navigationSystem": {
|
|
76
|
+
"mode": "custom",
|
|
77
|
+
"router": "AppRouter", // type that dispatches routes
|
|
78
|
+
"routeEnum": "AppRoute", // exhaustive cross-surface route enum
|
|
79
|
+
"sceneType": "SceneContent", // headless screen container (optional)
|
|
80
|
+
"navigatorProtocol": "NavigatorProtocol" // DI'd selection/present API (optional)
|
|
81
|
+
}}
|
|
82
|
+
```
|
|
83
|
+
then apply the SAME rules, but express them in the project's vocabulary resolved from config (do NOT hardcode the names below - read them from `ui.navigationSystem`):
|
|
84
|
+
|
|
85
|
+
- Wrap screen content in `{sceneType}` (headless container) instead of a bare `NavigationStack` child, when provided.
|
|
86
|
+
- Screens still emit a typed `Output`; the domain coordinator maps outputs to the project's route type (`{routeEnum}`) and the `{router}` dispatches.
|
|
87
|
+
- Cross-surface jumps go through the DI'd `{navigatorProtocol}` (`selectTab`, `present`), never by importing a sibling domain.
|
|
88
|
+
- Deep links build the project's `{routeEnum}` case and hand it to the router's deep-link entry.
|
|
89
|
+
|
|
90
|
+
If `mode` is absent or `native`, use the native-first section above. Resolve types via `tools/resource-utility` / repo grep only when the config points at real files.
|
|
91
|
+
|
|
92
|
+
## Android (Jetpack Compose)
|
|
93
|
+
|
|
94
|
+
Same rules, Compose vocabulary. Default is stock **Navigation Compose**; a project system is used only when `ui.navigationSystem.mode == "custom"`.
|
|
95
|
+
|
|
96
|
+
- **Push/pop within a flow:** caller owns `NavHostController` + `NavHost`; the composable emits events via lambdas (`onSelect: (Id) -> Unit`), never calls `navController.navigate(...)` itself. Prefer type-safe routes (`@Serializable` route objects, `navigation-compose` 2.8+).
|
|
97
|
+
- **Back:** system back / `navController.popBackStack()`; intercept only with `BackHandler { }` when the screen owns the gesture. A custom top-bar back emits `onBack` / calls the caller's pop.
|
|
98
|
+
- **Tabs:** state-hoisted selection (`var selected by rememberSaveable`) bound to a `NavigationBar`; the composable reads/writes the hoisted state, never a global.
|
|
99
|
+
- **Deep links:** `navDeepLink { uriPattern = ... }` on the destination, or the caller parses the URI → route and applies it to the same `NavController`. The composable never parses URIs.
|
|
100
|
+
- **Present another screen modally:** caller-owned `var showX by remember` → `Dialog`/`ModalBottomSheet` (see `/figma-bottom-sheets`).
|
|
101
|
+
|
|
102
|
+
**Config hook:** `ui.navigationSystem.mode == "custom"` → use the project's `{router}` / `{routeEnum}` (and a screen container if `{sceneType}` is set) resolved from config; cross-graph jumps go through the DI'd `{navigatorProtocol}`.
|
|
103
|
+
|
|
104
|
+
Tokens/theme via `MaterialTheme` + the project's figma-config token patterns.
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## Integration with Phase 3D
|
|
109
|
+
|
|
110
|
+
When Phase 3D detects a navigation affordance, record it in `04b_component_architecture.md` and classify it as an **Output**, not a route:
|
|
111
|
+
|
|
112
|
+
```markdown
|
|
113
|
+
## Navigation
|
|
114
|
+
| Affordance | Emits | Caller maps to |
|
|
115
|
+
|---|---|---|
|
|
116
|
+
| Back button (header) | `.back` closure / dismiss | pop current stack |
|
|
117
|
+
| Row chevron ("Detaylar") | `.selected(id)` | push detail route |
|
|
118
|
+
| Tab strip | `selection: Binding<Tab>` | caller-owned tab state |
|
|
119
|
+
| Deep-link landing | entry `route`/`tab` param | parser sets caller state |
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Property classification addition:
|
|
123
|
+
|
|
124
|
+
| Classification | When | Swift Pattern |
|
|
125
|
+
|---|---|---|
|
|
126
|
+
| Navigation output | Component has a push/select/back affordance | `output: (Output) -> Void` / `onSelect: (ID) -> Void` closure on the View |
|
|
127
|
+
| Tab/selection binding | Component switches top-level content | `selection: Binding<SomeTab>` |
|
|
128
|
+
| Deep-link entry | Component is a URL landing surface | plain entry `route`/`tab` param on the View/Configuration |
|
|
129
|
+
|
|
130
|
+
The affordance's **label/icon copy is static Configuration content** (localized), exactly like other copy - only the *intent* is an Output closure.
|
|
131
|
+
|
|
132
|
+
## Integration with Phase 4B
|
|
133
|
+
|
|
134
|
+
- Never hardcode `NavigationLink(destination: ConcreteScreen())` inside a reusable component - emit the Output.
|
|
135
|
+
- Prefer the system back button; a custom back affordance calls `@Environment(\.dismiss)` or emits `.back`.
|
|
136
|
+
- Bind tab/selection via the caller-owned binding; never a global.
|
|
137
|
+
- If `ui.navigationSystem.mode == custom`, wrap in `{sceneType}` and route via `{router}`/`{routeEnum}` per config.
|
|
138
|
+
- Verify by building + running on the simulator (`reference/build-and-test` / `figma-to-component` Phase 5): the transition happens, the back button appears only after a push, deep links land on the right surface.
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## When NOT to Use Navigation Integration
|
|
143
|
+
- In-place filters/segmented state that never changes screen → `Binding<Enum>`.
|
|
144
|
+
- Form submission → `/figma-form-integration` (the *result* may navigate; the control is a form action).
|
|
145
|
+
- Toast / loading / alert / data modal → `/figma-overlays`. Bottom-sheet surface → `/figma-bottom-sheets`.
|
|
146
|
+
|
|
147
|
+
## Checklist - Before Leaving the Skill
|
|
148
|
+
```
|
|
149
|
+
[ ] Component emits a typed Output for every navigation affordance - no hardcoded routes
|
|
150
|
+
[ ] Back uses system back / dismiss / .back output; renders only when poppable
|
|
151
|
+
[ ] Tab/selection is a caller-owned binding, not a global
|
|
152
|
+
[ ] Deep-link entry is plain state the caller's parser can set (component never parses URLs)
|
|
153
|
+
[ ] Default is native SwiftUI (NavigationStack/navigationDestination/TabView/.sheet(item:))
|
|
154
|
+
[ ] Custom system used ONLY when figma-config ui.navigationSystem.mode == custom (names from config)
|
|
155
|
+
[ ] Labels/icons are localized Configuration copy; only intent is a closure
|
|
156
|
+
```
|