yadflow 2.11.1 → 2.13.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 +3 -3
- package/cli/docs.mjs +7 -5
- package/package.json +1 -1
- package/skills/yad-checks/references/check-gates.md +11 -4
- package/skills/yad-checks/templates/checks/spec-link.sh +38 -13
- package/skills/yad-checks/templates/gitlab/yad-checks.gitlab-ci.yml +9 -3
- package/skills/yad-docs/templates/app/src/App.tsx +69 -21
- package/skills/yad-docs/templates/app/src/components/Canvas/FlowCanvas.tsx +47 -0
- package/skills/yad-docs/templates/app/src/components/DetailPanel/RightPanel.tsx +9 -1
- package/skills/yad-docs/templates/app/src/components/Sidebar/StepList.tsx +0 -9
- package/skills/yad-docs/templates/app/src/components/shared/CommandPalette.tsx +6 -3
- package/skills/yad-docs/templates/app/src/hooks/useAnimationQueue.ts +12 -3
- package/skills/yad-docs/templates/app/src/hooks/usePlayback.ts +3 -3
- package/skills/yad-docs/templates/app/src/store/useFlowStore.ts +8 -0
- package/skills/yad-docs-overview/SKILL.md +20 -13
package/CHANGELOG.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
|
|
1
|
+
# [2.13.0](https://github.com/abdelrahmannasr/yadflow/compare/v2.12.0...v2.13.0) (2026-06-16)
|
|
2
2
|
|
|
3
3
|
|
|
4
|
-
###
|
|
4
|
+
### Features
|
|
5
5
|
|
|
6
|
-
* **
|
|
6
|
+
* **docs:** make the report the main documentation, mount the SPA under /app/ ([#71](https://github.com/abdelrahmannasr/yadflow/issues/71)) ([1993e4d](https://github.com/abdelrahmannasr/yadflow/commit/1993e4dc282df281474ca1923acd52dbd1262dcb))
|
|
7
7
|
|
|
8
8
|
# [2.2.0](https://github.com/abdelrahmannasr/yadflow/compare/v2.1.0...v2.2.0) (2026-06-14)
|
|
9
9
|
|
package/cli/docs.mjs
CHANGED
|
@@ -43,11 +43,12 @@ export function deployTargetFromHub(hub = {}) {
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
// The Vite `base` for one site: join the project base path (from docs.json) with the per-site
|
|
46
|
-
// subpath.
|
|
46
|
+
// subpath. The overview SPA mounts under `app/` so the hand-maintained report.html can own the
|
|
47
|
+
// Pages root (`<base>/`) as the main documentation; per-epic sites nest under epics/<id>/. Always a
|
|
47
48
|
// leading+trailing slash so it works as a Vite base and a router basename.
|
|
48
49
|
export function siteBasePath(docs = {}, { epic, overview } = {}) {
|
|
49
50
|
const root = (docs.basePath || '/').replace(/\/+$/, '') || '';
|
|
50
|
-
const sub = overview ? '' : epic ? `/epics/${epic}` : '';
|
|
51
|
+
const sub = overview ? '/app' : epic ? `/epics/${epic}` : '';
|
|
51
52
|
const joined = `${root}${sub}`.replace(/\/+/g, '/');
|
|
52
53
|
return joined ? `${joined.replace(/\/$/, '')}/` : '/';
|
|
53
54
|
}
|
|
@@ -113,12 +114,13 @@ export function docsStale(manifest, { artifactHash, repoHeads = {}, templateVers
|
|
|
113
114
|
|
|
114
115
|
// ---- pure: the Pages CI workflow (committed by `yad docs sync --wire`) ---------------------------
|
|
115
116
|
// GitHub Actions deploy-pages, or a GitLab `pages` job. Both assemble a single `public/` tree: the
|
|
116
|
-
//
|
|
117
|
-
//
|
|
117
|
+
// hand-maintained report.html owns the root (`<base>/` — the main documentation), the overview SPA
|
|
118
|
+
// mounts under `app/`, and every per-epic site nests under `epics/<id>/` — matching siteBasePath
|
|
119
|
+
// (overview at `<base>/app/`, epics at `<base>/epics/EP-<slug>/`). A concurrency group prevents the
|
|
118
120
|
// deploy from retriggering. The shared shell script keeps the two platforms byte-for-byte aligned.
|
|
119
121
|
const BUILD_PUBLIC = [
|
|
120
122
|
'mkdir -p public',
|
|
121
|
-
'if [ -d docs/sdlc-site ]; then (cd docs/sdlc-site && npm ci && npm run build) && cp -r docs/sdlc-site/dist/. public
|
|
123
|
+
'if [ -d docs/sdlc-site ]; then (cd docs/sdlc-site && npm ci && npm run build) && mkdir -p public/app && cp -r docs/sdlc-site/dist/. public/app/ && cp docs/sdlc-site/public/report.html public/index.html && cp docs/sdlc-site/public/report.html public/report.html; fi',
|
|
122
124
|
'for d in epics/*/docs-site; do [ -d "$d" ] || continue; id=$(basename "$(dirname "$d")"); (cd "$d" && npm ci && npm run build) && mkdir -p "public/epics/$id" && cp -r "$d/dist/." "public/epics/$id/"; done',
|
|
123
125
|
];
|
|
124
126
|
export function pagesWorkflow(platform) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "yadflow",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.13.0",
|
|
4
4
|
"description": "Yadflow — the gated, team, multi-repo SDLC: author → review → build with a PR-driven review gate and a zero-dependency `yad` CLI (setup, gate, commit, open-pr, ship, repo). A BMAD module + 30 yad-* skills.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"author": "AbdelRahman Nasr",
|
|
@@ -18,10 +18,17 @@ repo uses. Each reads conventions established by earlier steps — it invents no
|
|
|
18
18
|
|
|
19
19
|
## 1. spec-link (`templates/checks/spec-link.sh`)
|
|
20
20
|
|
|
21
|
-
-
|
|
22
|
-
|
|
23
|
-
-
|
|
24
|
-
`
|
|
21
|
+
- Checks every non-merge commit in `<base>..HEAD` **per commit** (not aggregated across the range),
|
|
22
|
+
so the report names each offending commit and one bad commit never masks the rest.
|
|
23
|
+
- Maintenance commits are **exempt**: a Conventional-Commits subject of type `ci`, `chore`, `build`,
|
|
24
|
+
or `test` (optional `(scope)` / breaking `!`) **PASSes** without a link — CI wiring, dependency
|
|
25
|
+
bumps, and test-infra changes legitimately link no story.
|
|
26
|
+
- For every other commit, requires a `Task: <story>-<task>` trailer. **FAIL** if absent.
|
|
27
|
+
- The trailer must be a well-formed `<story>-T<NN>` id. **FAIL** on a malformed trailer (e.g.
|
|
28
|
+
`EP-demo-S01` with no `-T<NN>`) rather than letting it slip through the suffix-strip.
|
|
29
|
+
- Strips the `-T<NN>` suffix from the task to get `<story>` and requires `specs/<story>/link.md` to
|
|
30
|
+
exist. **FAIL** if missing.
|
|
31
|
+
- An empty range (no non-merge commits) **PASSes**.
|
|
25
32
|
- Portable across bash 3.2 (macOS) and 4+ (no `mapfile`).
|
|
26
33
|
- **Fails closed** when `<base>` can't be resolved (so a shallow clone / wrong base never PASSes blind).
|
|
27
34
|
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# spec-link gate (Phase 3 build plan §C).
|
|
3
|
-
#
|
|
3
|
+
# Every NON-MAINTENANCE commit must link a real story/spec: it must carry a
|
|
4
4
|
# `Task: <story>-<task>` trailer whose <story> resolves to a specs/<story>/link.md.
|
|
5
|
-
#
|
|
5
|
+
# Maintenance commits (ci/chore/build/test) are EXEMPT — CI wiring, dependency bumps,
|
|
6
|
+
# and test-infra changes legitimately link no story. Checked per commit (not aggregated
|
|
7
|
+
# across the range), so the report names every offending commit.
|
|
6
8
|
set -euo pipefail
|
|
7
9
|
|
|
8
10
|
BASE="${1:-${SDLC_BASE:-origin/main}}"
|
|
@@ -14,25 +16,48 @@ if ! git rev-parse --verify --quiet "${BASE}^{commit}" >/dev/null; then
|
|
|
14
16
|
fi
|
|
15
17
|
RANGE="${BASE}..HEAD"
|
|
16
18
|
|
|
17
|
-
#
|
|
18
|
-
|
|
19
|
+
# Conventional-Commits types exempt from the spec-link requirement (optional (scope) and breaking !).
|
|
20
|
+
EXEMPT='ci|chore|build|test'
|
|
19
21
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
22
|
+
# Portable across bash 3.2 (macOS) and 4+ — no mapfile; feed the loop via heredoc (not a pipe) so
|
|
23
|
+
# the failure count survives the loop body.
|
|
24
|
+
commits="$(git rev-list --no-merges "$RANGE")"
|
|
25
|
+
if [ -z "$commits" ]; then
|
|
26
|
+
echo "PASS [spec-link]: no non-merge commits in ${RANGE}"
|
|
27
|
+
exit 0
|
|
23
28
|
fi
|
|
24
29
|
|
|
25
30
|
rc=0
|
|
26
|
-
while IFS= read -r
|
|
27
|
-
[ -z "$
|
|
28
|
-
|
|
31
|
+
while IFS= read -r sha; do
|
|
32
|
+
[ -z "$sha" ] && continue
|
|
33
|
+
short="$(git log -1 --format=%h "$sha")"
|
|
34
|
+
subject="$(git log -1 --format=%s "$sha")"
|
|
35
|
+
if printf '%s' "$subject" | grep -qE "^(${EXEMPT})(\([a-z0-9._-]+\))?!?: "; then
|
|
36
|
+
echo "PASS [spec-link]: ${short} '${subject}' — maintenance commit (exempt)"
|
|
37
|
+
continue
|
|
38
|
+
fi
|
|
39
|
+
task="$(git log -1 --format='%(trailers:key=Task,valueonly)' "$sha" | sed '/^$/d' | head -1)"
|
|
40
|
+
if [ -z "$task" ]; then
|
|
41
|
+
echo "FAIL [spec-link]: ${short} '${subject}' has no 'Task:' trailer"
|
|
42
|
+
rc=1
|
|
43
|
+
continue
|
|
44
|
+
fi
|
|
45
|
+
# The trailer must be a real <story>-T<NN> id. Without this guard a malformed trailer
|
|
46
|
+
# (e.g. 'EP-demo-S01' with no -T<NN>) would survive the suffix-strip unchanged and PASS
|
|
47
|
+
# whenever specs/<that>/link.md happens to exist.
|
|
48
|
+
if ! printf '%s' "$task" | grep -qE '.+-T[0-9]+$'; then
|
|
49
|
+
echo "FAIL [spec-link]: ${short} '${subject}' has a malformed Task trailer '${task}' (expected <story>-T<NN>)."
|
|
50
|
+
rc=1
|
|
51
|
+
continue
|
|
52
|
+
fi
|
|
53
|
+
story="$(printf '%s' "$task" | sed -E 's/-T[0-9]+$//')"
|
|
29
54
|
if [ -f "specs/${story}/link.md" ]; then
|
|
30
|
-
echo "PASS [spec-link]: ${
|
|
55
|
+
echo "PASS [spec-link]: ${short} ${task} -> specs/${story}/link.md"
|
|
31
56
|
else
|
|
32
|
-
echo "FAIL [spec-link]: ${
|
|
57
|
+
echo "FAIL [spec-link]: ${short} ${task} references specs/${story}/ but link.md is missing."
|
|
33
58
|
rc=1
|
|
34
59
|
fi
|
|
35
60
|
done <<EOF
|
|
36
|
-
$
|
|
61
|
+
$commits
|
|
37
62
|
EOF
|
|
38
63
|
exit "$rc"
|
|
@@ -9,7 +9,14 @@
|
|
|
9
9
|
# pipeline's default stage regardless of any `stages:` order the foreign root defines — a foreign
|
|
10
10
|
# `stages:` list can neither break them nor reorder them. Job names are yad-prefixed to avoid
|
|
11
11
|
# colliding with the host project's own job names.
|
|
12
|
-
|
|
12
|
+
#
|
|
13
|
+
# `image:`/`tags:` live on the .sdlc_mr_only anchor below — NOT a top-level `default:`. An included
|
|
14
|
+
# top-level `default:` merges into the HOST root pipeline; if the host already sets a top-level
|
|
15
|
+
# `image:`, GitLab rejects the whole config ("image is defined in top-level and 'default:' entry").
|
|
16
|
+
# Putting them on the anchor scopes them to the gate jobs (which all `extends: .sdlc_mr_only`) and
|
|
17
|
+
# never touches the host root.
|
|
18
|
+
|
|
19
|
+
.sdlc_mr_only:
|
|
13
20
|
image: node:20
|
|
14
21
|
# Runner selection. These jobs need a docker/dind executor; on instances whose runners are all
|
|
15
22
|
# tag-locked (run_untagged: false) an untagged image job never starts. Set the YAD_RUNNER_TAGS
|
|
@@ -19,8 +26,6 @@ default:
|
|
|
19
26
|
# instead strand it `pending` — if so, set the variable to a real tag. Single value only:
|
|
20
27
|
# `tags: [$VAR]` is one tag equal to the whole variable, not a comma-split.
|
|
21
28
|
tags: [$YAD_RUNNER_TAGS]
|
|
22
|
-
|
|
23
|
-
.sdlc_mr_only:
|
|
24
29
|
rules:
|
|
25
30
|
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
|
26
31
|
|
|
@@ -45,6 +50,7 @@ yad-build-test-lint:
|
|
|
45
50
|
variables:
|
|
46
51
|
YAD_TEST_MAX_WORKERS: "2" # cap jest/vitest test workers in CI; ignored by other runners
|
|
47
52
|
script:
|
|
53
|
+
- npm ci
|
|
48
54
|
- bash checks/build-test-lint.sh
|
|
49
55
|
|
|
50
56
|
# Pattern gates: commit subject + MR title + MR body all follow the convention (profile: code).
|
|
@@ -14,34 +14,69 @@ import { RoleSelectPage } from './pages/RoleSelectPage';
|
|
|
14
14
|
import { StakeholderDocPage } from './pages/StakeholderDocPage';
|
|
15
15
|
import { usePlayback } from './hooks/usePlayback';
|
|
16
16
|
import { useFlowStore } from './store/useFlowStore';
|
|
17
|
+
import { Icon } from './components/shared/Icon';
|
|
18
|
+
|
|
19
|
+
const railStyle = {
|
|
20
|
+
borderColor: 'var(--color-border-default)',
|
|
21
|
+
background: 'var(--color-bg-primary)',
|
|
22
|
+
};
|
|
17
23
|
|
|
18
24
|
function Dashboard() {
|
|
19
25
|
usePlayback();
|
|
20
26
|
const isLogsPanelOpen = useFlowStore((s) => s.isLogsPanelOpen);
|
|
27
|
+
const isLeftPanelOpen = useFlowStore((s) => s.isLeftPanelOpen);
|
|
28
|
+
const isRightPanelOpen = useFlowStore((s) => s.isRightPanelOpen);
|
|
29
|
+
const toggleLeftPanel = useFlowStore((s) => s.toggleLeftPanel);
|
|
30
|
+
const toggleRightPanel = useFlowStore((s) => s.toggleRightPanel);
|
|
21
31
|
|
|
22
32
|
return (
|
|
23
33
|
<div className="flex flex-1 overflow-hidden">
|
|
24
|
-
{/* Left Sidebar */}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
{/* Left Sidebar (collapsible — collapse to widen the canvas) */}
|
|
35
|
+
{isLeftPanelOpen ? (
|
|
36
|
+
<aside
|
|
37
|
+
className="w-80 flex-none flex flex-col border-r z-10"
|
|
38
|
+
style={railStyle}
|
|
39
|
+
>
|
|
40
|
+
{/* Collapse header */}
|
|
41
|
+
<div className="flex items-center justify-between px-4 pt-3 pb-1">
|
|
42
|
+
<span className="text-[10px] font-bold uppercase tracking-wider" style={{ color: 'var(--color-text-muted)' }}>
|
|
43
|
+
Navigator
|
|
44
|
+
</span>
|
|
45
|
+
<button
|
|
46
|
+
onClick={toggleLeftPanel}
|
|
47
|
+
title="Collapse panel"
|
|
48
|
+
aria-label="Collapse navigator panel"
|
|
49
|
+
className="flex h-7 w-7 items-center justify-center rounded-md text-slate-400 transition-colors hover:bg-white/5 hover:text-white"
|
|
50
|
+
>
|
|
51
|
+
<Icon name="left_panel_close" size={18} />
|
|
52
|
+
</button>
|
|
53
|
+
</div>
|
|
36
54
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
55
|
+
{/* Path Selection */}
|
|
56
|
+
<div className="p-4 border-b overflow-y-auto" style={{ borderColor: 'var(--color-border-default)' }}>
|
|
57
|
+
<PathSelector />
|
|
58
|
+
</div>
|
|
41
59
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
60
|
+
{/* Step Timeline */}
|
|
61
|
+
<div className="flex-1 overflow-y-auto p-4">
|
|
62
|
+
<StepList />
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
{/* Footer */}
|
|
66
|
+
<SidebarFooter />
|
|
67
|
+
</aside>
|
|
68
|
+
) : (
|
|
69
|
+
<div className="w-10 flex-none flex flex-col items-center border-r z-10 pt-3" style={railStyle}>
|
|
70
|
+
<button
|
|
71
|
+
onClick={toggleLeftPanel}
|
|
72
|
+
title="Expand navigator"
|
|
73
|
+
aria-label="Expand navigator panel"
|
|
74
|
+
className="flex h-8 w-8 items-center justify-center rounded-md text-slate-400 transition-colors hover:bg-white/5 hover:text-white"
|
|
75
|
+
>
|
|
76
|
+
<Icon name="left_panel_open" size={18} />
|
|
77
|
+
</button>
|
|
78
|
+
</div>
|
|
79
|
+
)}
|
|
45
80
|
|
|
46
81
|
{/* Main Canvas Area */}
|
|
47
82
|
<main className="flex-1 flex flex-col overflow-hidden relative flow-grid"
|
|
@@ -65,8 +100,21 @@ function Dashboard() {
|
|
|
65
100
|
<PlaybackBar />
|
|
66
101
|
</main>
|
|
67
102
|
|
|
68
|
-
{/* Right Detail Panel */}
|
|
69
|
-
|
|
103
|
+
{/* Right Detail Panel (collapsible — collapse to widen the canvas) */}
|
|
104
|
+
{isRightPanelOpen ? (
|
|
105
|
+
<RightPanel />
|
|
106
|
+
) : (
|
|
107
|
+
<div className="w-10 flex-none flex flex-col items-center border-l z-10 pt-3" style={railStyle}>
|
|
108
|
+
<button
|
|
109
|
+
onClick={toggleRightPanel}
|
|
110
|
+
title="Expand details"
|
|
111
|
+
aria-label="Expand step-details panel"
|
|
112
|
+
className="flex h-8 w-8 items-center justify-center rounded-md text-slate-400 transition-colors hover:bg-white/5 hover:text-white"
|
|
113
|
+
>
|
|
114
|
+
<Icon name="right_panel_open" size={18} />
|
|
115
|
+
</button>
|
|
116
|
+
</div>
|
|
117
|
+
)}
|
|
70
118
|
</div>
|
|
71
119
|
);
|
|
72
120
|
}
|
|
@@ -75,6 +75,26 @@ export const FlowCanvas: React.FC = () => {
|
|
|
75
75
|
|
|
76
76
|
const messages = currentStep?.messages || [];
|
|
77
77
|
|
|
78
|
+
// Optional loop-back arc, swept below the canvas to read as "the pipeline repeats".
|
|
79
|
+
// Rendered only when both endpoint nodes exist (e.g. the SDLC-overview's
|
|
80
|
+
// trust-log → product-hub cycle); a no-op for diagrams without them.
|
|
81
|
+
const loopBack = useMemo(() => {
|
|
82
|
+
if (dims.width === 0) return null;
|
|
83
|
+
const a = COMPONENTS.find((c) => c.id === 'trust-log');
|
|
84
|
+
const b = COMPONENTS.find((c) => c.id === 'product-hub');
|
|
85
|
+
if (!a || !b) return null;
|
|
86
|
+
const x1 = (a.position.x / 100) * dims.width;
|
|
87
|
+
const y1 = (a.position.y / 100) * dims.height;
|
|
88
|
+
const x2 = (b.position.x / 100) * dims.width;
|
|
89
|
+
const y2 = (b.position.y / 100) * dims.height;
|
|
90
|
+
const yb = dims.height * 0.985;
|
|
91
|
+
return {
|
|
92
|
+
d: `M ${x1} ${y1} C ${x1} ${yb}, ${x2} ${yb}, ${x2} ${y2}`,
|
|
93
|
+
labelX: (x1 + x2) / 2,
|
|
94
|
+
labelY: dims.height * 0.92,
|
|
95
|
+
};
|
|
96
|
+
}, [dims]);
|
|
97
|
+
|
|
78
98
|
return (
|
|
79
99
|
<div ref={containerRef} className="relative h-full w-full overflow-hidden">
|
|
80
100
|
{/* Canvas Header Overlay */}
|
|
@@ -159,6 +179,33 @@ export const FlowCanvas: React.FC = () => {
|
|
|
159
179
|
containerHeight={dims.height}
|
|
160
180
|
/>
|
|
161
181
|
))}
|
|
182
|
+
|
|
183
|
+
{/* Loop-back: the pipeline repeats per epic */}
|
|
184
|
+
{loopBack && (
|
|
185
|
+
<g>
|
|
186
|
+
<path
|
|
187
|
+
d={loopBack.d}
|
|
188
|
+
fill="none"
|
|
189
|
+
stroke="#ff6490"
|
|
190
|
+
strokeWidth={1.5}
|
|
191
|
+
strokeDasharray="6 5"
|
|
192
|
+
opacity={0.55}
|
|
193
|
+
markerEnd="url(#arrowhead)"
|
|
194
|
+
/>
|
|
195
|
+
<text
|
|
196
|
+
x={loopBack.labelX}
|
|
197
|
+
y={loopBack.labelY}
|
|
198
|
+
textAnchor="middle"
|
|
199
|
+
fontSize={12}
|
|
200
|
+
fontWeight={700}
|
|
201
|
+
letterSpacing={0.6}
|
|
202
|
+
fill="#ff8db0"
|
|
203
|
+
opacity={0.85}
|
|
204
|
+
>
|
|
205
|
+
↺ pipeline repeats per epic
|
|
206
|
+
</text>
|
|
207
|
+
</g>
|
|
208
|
+
)}
|
|
162
209
|
</svg>
|
|
163
210
|
)}
|
|
164
211
|
|
|
@@ -12,6 +12,7 @@ export function RightPanel() {
|
|
|
12
12
|
const getCurrentStep = useFlowStore((s) => s.getCurrentStep);
|
|
13
13
|
const activeStepIndex = useFlowStore((s) => s.activeStepIndex);
|
|
14
14
|
const selectedPath = useFlowStore((s) => s.selectedPath);
|
|
15
|
+
const toggleRightPanel = useFlowStore((s) => s.toggleRightPanel);
|
|
15
16
|
const navigate = useNavigate();
|
|
16
17
|
const step = getCurrentStep();
|
|
17
18
|
|
|
@@ -29,7 +30,14 @@ export function RightPanel() {
|
|
|
29
30
|
<h3 className="text-slate-100 text-sm font-bold font-display uppercase tracking-wider">
|
|
30
31
|
Step Details
|
|
31
32
|
</h3>
|
|
32
|
-
<
|
|
33
|
+
<button
|
|
34
|
+
onClick={toggleRightPanel}
|
|
35
|
+
title="Collapse panel"
|
|
36
|
+
aria-label="Collapse step-details panel"
|
|
37
|
+
className="flex h-7 w-7 items-center justify-center rounded-md text-slate-400 transition-colors hover:bg-white/5 hover:text-white"
|
|
38
|
+
>
|
|
39
|
+
<Icon name="right_panel_close" size={18} />
|
|
40
|
+
</button>
|
|
33
41
|
</div>
|
|
34
42
|
|
|
35
43
|
{!step ? (
|
|
@@ -43,15 +43,6 @@ export const StepList = () => {
|
|
|
43
43
|
}
|
|
44
44
|
};
|
|
45
45
|
|
|
46
|
-
// Reset expanded state when active step changes externally (playback)
|
|
47
|
-
const prevActiveRef = { current: activeStepIndex };
|
|
48
|
-
if (prevActiveRef.current !== activeStepIndex) {
|
|
49
|
-
prevActiveRef.current = activeStepIndex;
|
|
50
|
-
if (expandedIndex !== null && expandedIndex !== activeStepIndex) {
|
|
51
|
-
// Auto-expand the new active step during playback
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
46
|
return (
|
|
56
47
|
<div className="flex flex-col gap-2">
|
|
57
48
|
<div className="flex items-center justify-between mb-2">
|
|
@@ -28,13 +28,16 @@ export function CommandPalette() {
|
|
|
28
28
|
return () => window.removeEventListener('keydown', handler);
|
|
29
29
|
}, [isOpen, toggle]);
|
|
30
30
|
|
|
31
|
-
// Reset on open
|
|
32
|
-
|
|
31
|
+
// Reset on open — adjust state during render (not inside an effect) so it does
|
|
32
|
+
// not trigger an extra cascading render (react-hooks/set-state-in-effect).
|
|
33
|
+
const [wasOpen, setWasOpen] = useState(isOpen);
|
|
34
|
+
if (isOpen !== wasOpen) {
|
|
35
|
+
setWasOpen(isOpen);
|
|
33
36
|
if (isOpen) {
|
|
34
37
|
setQuery('');
|
|
35
38
|
setSelectedIndex(0);
|
|
36
39
|
}
|
|
37
|
-
}
|
|
40
|
+
}
|
|
38
41
|
|
|
39
42
|
const results = useMemo(() => {
|
|
40
43
|
if (!query.trim()) {
|
|
@@ -9,13 +9,22 @@ export function useAnimationQueue() {
|
|
|
9
9
|
const [animKey, setAnimKey] = useState(0);
|
|
10
10
|
const timersRef = useRef<ReturnType<typeof setTimeout>[]>([]);
|
|
11
11
|
|
|
12
|
-
//
|
|
12
|
+
// Reset the pulse state the instant the step (or speed) changes — done during
|
|
13
|
+
// render via the "adjust state on change" pattern rather than inside the effect,
|
|
14
|
+
// so it does not trigger an extra cascading render (react-hooks/set-state-in-effect).
|
|
15
|
+
const stepKey = `${activeStepIndex}-${speed}`;
|
|
16
|
+
const [resetKey, setResetKey] = useState(stepKey);
|
|
17
|
+
if (stepKey !== resetKey) {
|
|
18
|
+
setResetKey(stepKey);
|
|
19
|
+
setCompletedTargets(new Set());
|
|
20
|
+
setAnimKey((k) => k + 1);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// When step changes, schedule the target-pulse completion events.
|
|
13
24
|
useEffect(() => {
|
|
14
25
|
// Clear previous timers
|
|
15
26
|
timersRef.current.forEach(clearTimeout);
|
|
16
27
|
timersRef.current = [];
|
|
17
|
-
setCompletedTargets(new Set());
|
|
18
|
-
setAnimKey((k) => k + 1);
|
|
19
28
|
|
|
20
29
|
// Get messages from the current step
|
|
21
30
|
const step = useFlowStore.getState().getCurrentStep();
|
|
@@ -30,7 +30,7 @@ export function usePlayback() {
|
|
|
30
30
|
return () => {
|
|
31
31
|
if (elapsedRef.current) clearInterval(elapsedRef.current);
|
|
32
32
|
};
|
|
33
|
-
}, [playbackState]);
|
|
33
|
+
}, [playbackState, tickElapsed, resetElapsed]);
|
|
34
34
|
|
|
35
35
|
// Generate logs on step change
|
|
36
36
|
useEffect(() => {
|
|
@@ -56,7 +56,7 @@ export function usePlayback() {
|
|
|
56
56
|
});
|
|
57
57
|
}, msg.delay / speed);
|
|
58
58
|
}
|
|
59
|
-
}, [activeStepIndex, speed]);
|
|
59
|
+
}, [activeStepIndex, speed, addLog, getCurrentSteps]);
|
|
60
60
|
|
|
61
61
|
useEffect(() => {
|
|
62
62
|
if (playbackState !== "playing") {
|
|
@@ -96,5 +96,5 @@ export function usePlayback() {
|
|
|
96
96
|
return () => {
|
|
97
97
|
if (timerRef.current) clearTimeout(timerRef.current);
|
|
98
98
|
};
|
|
99
|
-
}, [playbackState, activeStepIndex, speed, animatingMessages]);
|
|
99
|
+
}, [playbackState, activeStepIndex, speed, animatingMessages, getCurrentSteps, nextStep, setAnimatingMessages]);
|
|
100
100
|
}
|
|
@@ -26,6 +26,8 @@ interface FlowStore {
|
|
|
26
26
|
isReferencePanelOpen: boolean;
|
|
27
27
|
isCommandPaletteOpen: boolean;
|
|
28
28
|
isLogsPanelOpen: boolean;
|
|
29
|
+
isLeftPanelOpen: boolean;
|
|
30
|
+
isRightPanelOpen: boolean;
|
|
29
31
|
|
|
30
32
|
// Logs
|
|
31
33
|
logs: LogEntry[];
|
|
@@ -57,6 +59,8 @@ interface FlowStore {
|
|
|
57
59
|
toggleReferencePanel: () => void;
|
|
58
60
|
toggleCommandPalette: () => void;
|
|
59
61
|
toggleLogsPanel: () => void;
|
|
62
|
+
toggleLeftPanel: () => void;
|
|
63
|
+
toggleRightPanel: () => void;
|
|
60
64
|
|
|
61
65
|
// Logs
|
|
62
66
|
addLog: (entry: Omit<LogEntry, 'id' | 'timestamp'>) => void;
|
|
@@ -90,6 +94,8 @@ export const useFlowStore = create<FlowStore>((set, get) => ({
|
|
|
90
94
|
isReferencePanelOpen: false,
|
|
91
95
|
isCommandPaletteOpen: false,
|
|
92
96
|
isLogsPanelOpen: false,
|
|
97
|
+
isLeftPanelOpen: true,
|
|
98
|
+
isRightPanelOpen: true,
|
|
93
99
|
logs: [],
|
|
94
100
|
elapsedTime: 0,
|
|
95
101
|
|
|
@@ -157,6 +163,8 @@ export const useFlowStore = create<FlowStore>((set, get) => ({
|
|
|
157
163
|
toggleReferencePanel: () => set((s) => ({ isReferencePanelOpen: !s.isReferencePanelOpen })),
|
|
158
164
|
toggleCommandPalette: () => set((s) => ({ isCommandPaletteOpen: !s.isCommandPaletteOpen })),
|
|
159
165
|
toggleLogsPanel: () => set((s) => ({ isLogsPanelOpen: !s.isLogsPanelOpen })),
|
|
166
|
+
toggleLeftPanel: () => set((s) => ({ isLeftPanelOpen: !s.isLeftPanelOpen })),
|
|
167
|
+
toggleRightPanel: () => set((s) => ({ isRightPanelOpen: !s.isRightPanelOpen })),
|
|
160
168
|
|
|
161
169
|
// Logs
|
|
162
170
|
addLog: (entry) => {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: yad-docs-overview
|
|
3
|
-
description: 'Generates the project-level SDLC-overview interactive site — the same React/Vite/Tailwind shell as the per-epic docs — showing every yadflow stage from setup → ship: the pipeline as a flow canvas, each skill/gate as a flow step, the durable .sdlc state objects as system components, and the lenses as stakeholder roles. Themed with yadflow''s own brand palette for continuity, built from config.yaml + module-help.csv + the overview diagram.
|
|
3
|
+
description: 'Generates the project-level SDLC-overview interactive site — the same React/Vite/Tailwind shell as the per-epic docs — showing every yadflow stage from setup → ship: the pipeline as a flow canvas, each skill/gate as a flow step, the durable .sdlc state objects as system components, and the lenses as stakeholder roles. Themed with yadflow''s own brand palette for continuity, built from config.yaml + module-help.csv + the overview diagram. The hand-maintained report is the MAIN documentation at the Pages root (report.html, also served as index.html); the interactive SPA mounts under `app/` and is reached from it (and links back). Deploys via `yad docs deploy --overview`. This is project documentation, not a gated state — it never touches any epic''s state or approvals. Use when the user says "generate the overview site", "build the SDLC overview docs", or after the pipeline (module-help.csv / config.yaml / skill count) changes.'
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# SDLC — Author the Overview Site (project-level, the pipeline as a living map)
|
|
@@ -8,8 +8,10 @@ description: 'Generates the project-level SDLC-overview interactive site — the
|
|
|
8
8
|
**Goal:** Render the **whole yadflow pipeline** — every stage from setup → ship — as an interactive site,
|
|
9
9
|
reusing the same shell as the per-epic docs (`skills/yad-docs/templates/app/`). Where `yad-docs`
|
|
10
10
|
animates one epic's flows, this animates the **workflow itself**: the front gates, the build half, the
|
|
11
|
-
automation dial, the setup connectors.
|
|
12
|
-
|
|
11
|
+
automation dial, the setup connectors. The hand-maintained overview report stays the **main
|
|
12
|
+
documentation at the Pages root** (`<base>/`, served from `public/report.html` and `public/index.html`);
|
|
13
|
+
this interactive SPA mounts under `<base>/app/` and is reached from the report — the report links
|
|
14
|
+
forward to `app/`, the app links back to the report root.
|
|
13
15
|
|
|
14
16
|
This is **project documentation, not a gated state** — there is no epic, no `state.json`, no approvals.
|
|
15
17
|
It only reads the pipeline definition and writes a project-level site. When a docs target is connected
|
|
@@ -70,7 +72,8 @@ determinism rules as `yad-docs`: stable-ID sort by skill pipeline order / phase,
|
|
|
70
72
|
timestamps in the data files), theme the `:root` of `index.css` from **yadflow's brand palette** — the
|
|
71
73
|
the legacy report's `:root`: `--accent: #2471a3` and the node colors (`--artifact-*`, `--gate-*`,
|
|
72
74
|
`--earns-*`, `--locked-*`, `--sentinel-*`) — and substitute the Vite base from `.sdlc/docs.json`
|
|
73
|
-
`basePath` (the
|
|
75
|
+
`basePath` **with `app/` appended** (the SPA mounts under `<base>/app/`, e.g. `/<repo>/app/`, so the
|
|
76
|
+
report can own the root). `siteBasePath(docs, { overview: true })` in `cli/docs.mjs` computes this.
|
|
74
77
|
|
|
75
78
|
### Step 4 — Write the overview build manifest (the staleness baseline)
|
|
76
79
|
Write `docs/sdlc-site/.docs-build.json` — `yad-docs-sync` compares against it:
|
|
@@ -91,13 +94,16 @@ doc-shell upgrade triggers a rebuild). `skillCount` rides along in the manifest
|
|
|
91
94
|
— it is **not** a separate hash input, since `module-help.csv` already moves whenever the skill set does.
|
|
92
95
|
Not per-epic artifacts/repo heads.
|
|
93
96
|
|
|
94
|
-
### Step 5 —
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
97
|
+
### Step 5 — The report is the main documentation; the SPA is reached from it
|
|
98
|
+
The hand-maintained static report lives at `docs/sdlc-site/public/report.html` and is the **primary
|
|
99
|
+
documentation at the Pages root**. The deploy (`BUILD_PUBLIC` in `cli/docs.mjs`, mirrored by the Pages CI
|
|
100
|
+
workflow) copies it to **both** `public/index.html` (the landing `<base>/`) and `public/report.html`
|
|
101
|
+
(so the `<base>/report.html` URL keeps working), and copies the built SPA into `public/app/`. Wire the
|
|
102
|
+
two cross-links: the report links **forward** to the interactive map with a relative `app/` href (its
|
|
103
|
+
hero CTA); the app's `TopNavBar` "Full report" link points **back** to the report root
|
|
104
|
+
(`import.meta.env.BASE_URL` with the trailing `app/` stripped). No orphaned `docs/index.html` at the repo
|
|
105
|
+
root. This generalizes the standing rule that feature work hand-updates the report: the overview SPA now
|
|
106
|
+
**regenerates** instead, while the report stays the front door.
|
|
101
107
|
|
|
102
108
|
### Step 6 — Build / deploy (`action`)
|
|
103
109
|
- `action: generate` (default) — generate source + manifest; stop.
|
|
@@ -106,8 +112,9 @@ report: the overview site now **regenerates** instead.
|
|
|
106
112
|
|
|
107
113
|
### Step 7 — Stop. Report (no gate, no epic)
|
|
108
114
|
Report: the site path (`docs/sdlc-site/`), the data files produced, that the theme is the yadflow brand
|
|
109
|
-
palette, the deploy URL or "build-only", the staleness baseline, and that the
|
|
110
|
-
at `public/
|
|
115
|
+
palette, the deploy URL or "build-only", the staleness baseline, and that the report is the main
|
|
116
|
+
documentation at `<base>/` (`public/index.html` + `public/report.html`) with the interactive SPA mounted
|
|
117
|
+
under `<base>/app/` and cross-linked. Never touches any epic state.
|
|
111
118
|
|
|
112
119
|
## Hard rules
|
|
113
120
|
|