@jamie-tam/forge 6.0.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/LICENSE +21 -0
- package/README.md +389 -0
- package/agents/architect.md +92 -0
- package/agents/builder.md +122 -0
- package/agents/code-reviewer.md +107 -0
- package/agents/concept-designer.md +207 -0
- package/agents/craft-reviewer.md +132 -0
- package/agents/critic.md +130 -0
- package/agents/doc-writer.md +85 -0
- package/agents/dreamer.md +129 -0
- package/agents/e2e-runner.md +89 -0
- package/agents/gotcha-hunter.md +127 -0
- package/agents/prototype-builder.md +193 -0
- package/agents/prototype-codifier.md +204 -0
- package/agents/prototype-reviewer.md +163 -0
- package/agents/security-reviewer.md +108 -0
- package/agents/spec-reviewer.md +94 -0
- package/agents/tracer.md +98 -0
- package/agents/wireframer.md +109 -0
- package/commands/abort.md +25 -0
- package/commands/bugfix.md +151 -0
- package/commands/evolve.md +118 -0
- package/commands/feature.md +236 -0
- package/commands/forge.md +100 -0
- package/commands/greenfield.md +185 -0
- package/commands/hotfix.md +98 -0
- package/commands/refactor.md +147 -0
- package/commands/resume.md +25 -0
- package/commands/setup.md +201 -0
- package/commands/status.md +27 -0
- package/commands/task-force.md +110 -0
- package/commands/validate.md +12 -0
- package/dist/__tests__/active-manifest.test.js +272 -0
- package/dist/__tests__/copy.test.js +96 -0
- package/dist/__tests__/gate-check.test.js +384 -0
- package/dist/__tests__/wiki.test.js +472 -0
- package/dist/__tests__/work-manifest.test.js +304 -0
- package/dist/active-manifest.js +229 -0
- package/dist/cli.js +158 -0
- package/dist/copy.js +124 -0
- package/dist/gate-check.js +326 -0
- package/dist/hooks.js +60 -0
- package/dist/init.js +140 -0
- package/dist/manifest.js +90 -0
- package/dist/merge.js +77 -0
- package/dist/paths.js +36 -0
- package/dist/uninstall.js +216 -0
- package/dist/update.js +158 -0
- package/dist/verify-manifest.js +65 -0
- package/dist/verify.js +98 -0
- package/dist/wiki-ui.js +310 -0
- package/dist/wiki.js +364 -0
- package/dist/work-manifest.js +798 -0
- package/hooks/config/gate-requirements.json +79 -0
- package/hooks/hooks.json +143 -0
- package/hooks/scripts/analyze-telemetry.sh +114 -0
- package/hooks/scripts/gate-enforcer.sh +164 -0
- package/hooks/scripts/pre-compact.sh +90 -0
- package/hooks/scripts/session-start.sh +81 -0
- package/hooks/scripts/telemetry.sh +41 -0
- package/hooks/scripts/wiki-lint.sh +87 -0
- package/hooks/templates/AGENTS.md.template +48 -0
- package/hooks/templates/CLAUDE.md.template +45 -0
- package/package.json +55 -0
- package/protocols/README.md +40 -0
- package/protocols/codex.md +151 -0
- package/protocols/graphify.md +156 -0
- package/references/common/agent-coordination.md +65 -0
- package/references/common/coding-standards.md +54 -0
- package/references/common/feature-tracking.md +21 -0
- package/references/common/io-protocol.md +36 -0
- package/references/common/phases.md +57 -0
- package/references/common/quality-gates.md +130 -0
- package/references/common/skill-authoring.md +154 -0
- package/references/common/skill-compliance.md +30 -0
- package/references/python/standards.md +44 -0
- package/references/react/standards.md +61 -0
- package/references/typescript/standards.md +42 -0
- package/rules/common/forge-system.md +59 -0
- package/rules/common/git-workflow.md +40 -0
- package/rules/common/guardrails.md +37 -0
- package/rules/common/quality-gates.md +18 -0
- package/rules/common/security.md +50 -0
- package/rules/common/skill-selection.md +78 -0
- package/rules/common/testing.md +58 -0
- package/rules/common/verification.md +39 -0
- package/skills/build-pr-workflow/SKILL.md +301 -0
- package/skills/build-pr-workflow/references/pr-template.md +62 -0
- package/skills/build-pr-workflow/references/subagent-merge.md +47 -0
- package/skills/build-pr-workflow/references/worktree-setup.md +125 -0
- package/skills/build-prototype/SKILL.md +264 -0
- package/skills/build-scaffold/SKILL.md +340 -0
- package/skills/build-tdd/SKILL.md +89 -0
- package/skills/build-wireframe/SKILL.md +110 -0
- package/skills/build-wireframe/assets/baseline-template.html +486 -0
- package/skills/build-wireframe/references/demo-walkthroughs.md +170 -0
- package/skills/build-wireframe/references/gotchas.md +188 -0
- package/skills/build-wireframe/references/legend-lines.md +141 -0
- package/skills/concept-slides/SKILL.md +192 -0
- package/skills/deliver-db-migration/SKILL.md +466 -0
- package/skills/deliver-deploy/SKILL.md +407 -0
- package/skills/deliver-onboarding/SKILL.md +198 -0
- package/skills/deliver-onboarding/references/document-templates.md +393 -0
- package/skills/deliver-onboarding/templates/getting-started.md +122 -0
- package/skills/discover-codebase-analysis/SKILL.md +448 -0
- package/skills/discover-requirements/SKILL.md +418 -0
- package/skills/discover-requirements/templates/prd.md +99 -0
- package/skills/discover-requirements/templates/technical-spec.md +123 -0
- package/skills/discover-requirements/templates/user-stories.md +76 -0
- package/skills/harden/SKILL.md +214 -0
- package/skills/iterate-prototype/SKILL.md +241 -0
- package/skills/plan-architecture/SKILL.md +457 -0
- package/skills/plan-architecture/templates/adr-template.md +52 -0
- package/skills/plan-architecture/templates/api-contract.md +99 -0
- package/skills/plan-architecture/templates/db-schema.md +81 -0
- package/skills/plan-architecture/templates/system-design.md +111 -0
- package/skills/plan-brainstorm/SKILL.md +433 -0
- package/skills/plan-design-system/SKILL.md +279 -0
- package/skills/plan-task-decompose/SKILL.md +454 -0
- package/skills/quality-code-review/SKILL.md +286 -0
- package/skills/quality-security-audit/SKILL.md +292 -0
- package/skills/quality-security-audit/references/audit-report-template.md +89 -0
- package/skills/quality-security-audit/references/owasp-checks.md +178 -0
- package/skills/quality-test-execution/SKILL.md +435 -0
- package/skills/quality-test-plan/SKILL.md +297 -0
- package/skills/quality-test-plan/references/test-type-guide.md +263 -0
- package/skills/quality-test-plan/templates/e2e-test-plan.md +72 -0
- package/skills/quality-test-plan/templates/integration-test-plan.md +74 -0
- package/skills/quality-test-plan/templates/load-test-plan.md +111 -0
- package/skills/quality-test-plan/templates/smoke-test-plan.md +68 -0
- package/skills/quality-test-plan/templates/unit-test-plan.md +56 -0
- package/skills/quality-uiux/SKILL.md +481 -0
- package/skills/support-debug/SKILL.md +464 -0
- package/skills/support-dream/SKILL.md +213 -0
- package/skills/support-gotcha/SKILL.md +249 -0
- package/skills/support-runtime-reachability/SKILL.md +190 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-01-passes-app-use/src/app.ts +7 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-01-passes-app-use/src/handlers/cases.ts +7 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-02-orphan-no-app-use/src/app.ts +8 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-02-orphan-no-app-use/src/handlers/cases.ts +7 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-03-orphan-import-only/src/App.tsx +5 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-03-orphan-import-only/src/components/RingingBanner.tsx +7 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-03-orphan-import-only/src/hooks/useTwilio.ts +6 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-04-jsx-component-rendered/src/App.tsx +5 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-04-jsx-component-rendered/src/components/MyComp.tsx +3 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-05-jsx-component-not-rendered/src/App.tsx +3 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-05-jsx-component-not-rendered/src/components/Orphan.tsx +3 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-06-class-instantiated/src/lib/Service.ts +6 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-06-class-instantiated/src/main.ts +4 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-07-class-not-instantiated/src/lib/Lonely.ts +5 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-07-class-not-instantiated/src/main.ts +2 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-08-default-export-imported-and-called/src/handler.ts +3 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-08-default-export-imported-and-called/src/main.ts +3 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-09-default-export-orphan/src/handler.ts +3 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-09-default-export-orphan/src/main.ts +2 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-10-aliased-named-export/src/lib.ts +5 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-10-aliased-named-export/src/main.ts +3 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-11-re-export-chain/src/lib/index.ts +1 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-11-re-export-chain/src/lib/internal.ts +3 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-11-re-export-chain/src/main.ts +3 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-12-test-only-caller/src/util.test.ts +5 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-12-test-only-caller/src/util.ts +3 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-13-gated-pending-annotation/src/future.ts +4 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-14-untraceable-annotation/src/decorated.ts +4 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-15-untraceable-empty/src/lazy.ts +4 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-16-python-module/src/lib.py +15 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-16-python-module/src/main.py +5 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-17-router-use/src/parent.ts +5 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-17-router-use/src/routes/cases.ts +5 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-18-shadowed-name-fp/src/lib/foo.ts +3 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-18-shadowed-name-fp/src/other.ts +8 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-19-same-name-different-module/src/handlers/cases.ts +4 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-19-same-name-different-module/src/handlers/users.ts +4 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-19-same-name-different-module/src/main.ts +5 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-20-aliased-import-usage/src/handlers/cases.ts +3 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-20-aliased-import-usage/src/main.ts +4 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-21-mixed-default-and-named/src/lib.ts +5 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-21-mixed-default-and-named/src/main.ts +5 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-22-dynamic-import-then-caller/src/lib.ts +3 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-22-dynamic-import-then-caller/src/main.ts +8 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-23-dynamic-import-with-space/src/lib.ts +3 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-23-dynamic-import-with-space/src/main.ts +7 -0
- package/skills/support-runtime-reachability/scripts/check.mjs +638 -0
- package/skills/support-runtime-reachability/scripts/check.test.mjs +244 -0
- package/skills/support-skill-validator/SKILL.md +194 -0
- package/skills/support-skill-validator/references/false-positives.md +59 -0
- package/skills/support-skill-validator/references/validation-checks.md +280 -0
- package/skills/support-system-guide/SKILL.md +311 -0
- package/skills/support-task-force/SKILL.md +265 -0
- package/skills/support-task-force/references/dispatch-pattern.md +178 -0
- package/skills/support-task-force/references/synthesis-template.md +126 -0
- package/skills/support-wiki-bootstrap/SKILL.md +37 -0
- package/skills/support-wiki-lint/SKILL.md +196 -0
- package/skills/support-wiki-lint/scripts/lint.mjs +488 -0
- package/skills/support-wiki-lint/scripts/lint.test.mjs +196 -0
- package/templates/README.md +23 -0
- package/templates/aiwiki/CLAUDE.md.template +78 -0
- package/templates/aiwiki/schemas/architecture.md +118 -0
- package/templates/aiwiki/schemas/convention.md +112 -0
- package/templates/aiwiki/schemas/decision.md +144 -0
- package/templates/aiwiki/schemas/gotcha.md +118 -0
- package/templates/aiwiki/schemas/oracle.md +105 -0
- package/templates/aiwiki/schemas/session.md +125 -0
- package/templates/manifests/bugfix.yaml +41 -0
- package/templates/manifests/feature.yaml +69 -0
- package/templates/manifests/greenfield.yaml +61 -0
- package/templates/manifests/hotfix.yaml +45 -0
- package/templates/manifests/refactor.yaml +44 -0
- package/templates/manifests/v5/SCHEMA.md +327 -0
- package/templates/manifests/v5/feature.yaml +77 -0
- package/templates/manifests/v6/SCHEMA.md +199 -0
- package/templates/wiki-html/dream-detail.html +378 -0
- package/templates/wiki-html/dreams-list.html +155 -0
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<title>Wireframe — annotated</title>
|
|
6
|
+
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
|
7
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
8
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
9
|
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
|
10
|
+
|
|
11
|
+
<script src="https://cdn.tailwindcss.com/3.4.13"></script>
|
|
12
|
+
<script src="https://unpkg.com/lucide@0.453.0/dist/umd/lucide.js"></script>
|
|
13
|
+
<script src="https://unpkg.com/react@18.3.1/umd/react.development.js" crossorigin="anonymous"></script>
|
|
14
|
+
<script src="https://unpkg.com/react-dom@18.3.1/umd/react-dom.development.js" crossorigin="anonymous"></script>
|
|
15
|
+
<script src="https://unpkg.com/@babel/standalone@7.29.0/babel.min.js" crossorigin="anonymous"></script>
|
|
16
|
+
|
|
17
|
+
<style>
|
|
18
|
+
:root {
|
|
19
|
+
--background: 0 0% 100%;
|
|
20
|
+
--foreground: 240 10% 3.9%;
|
|
21
|
+
--card: 0 0% 100%;
|
|
22
|
+
--muted: 240 4.8% 95.9%;
|
|
23
|
+
--muted-foreground: 240 3.8% 46.1%;
|
|
24
|
+
--border: 240 5.9% 90%;
|
|
25
|
+
}
|
|
26
|
+
html, body {
|
|
27
|
+
background: hsl(var(--background));
|
|
28
|
+
color: hsl(var(--foreground));
|
|
29
|
+
font-family: 'Inter', -apple-system, system-ui, sans-serif;
|
|
30
|
+
font-feature-settings: "cv01", "ss03";
|
|
31
|
+
-webkit-font-smoothing: antialiased;
|
|
32
|
+
}
|
|
33
|
+
.bg-muted\/40 { background: hsl(var(--muted) / 0.4); }
|
|
34
|
+
.text-muted-foreground { color: hsl(var(--muted-foreground)); }
|
|
35
|
+
.border-border { border-color: hsl(var(--border)); }
|
|
36
|
+
</style>
|
|
37
|
+
</head>
|
|
38
|
+
<body>
|
|
39
|
+
<div id="root"></div>
|
|
40
|
+
|
|
41
|
+
<script type="text/babel" data-presets="env,react">
|
|
42
|
+
|
|
43
|
+
// ─── primitives ──────────────────────────────────────────────────────────
|
|
44
|
+
const I = ({ name, className="w-4 h-4", strokeWidth=1.75 }) => {
|
|
45
|
+
const ref = React.useRef(null);
|
|
46
|
+
React.useEffect(() => {
|
|
47
|
+
if (ref.current && window.lucide) {
|
|
48
|
+
ref.current.innerHTML = '';
|
|
49
|
+
const el = document.createElement('i');
|
|
50
|
+
el.setAttribute('data-lucide', name);
|
|
51
|
+
el.setAttribute('stroke-width', String(strokeWidth));
|
|
52
|
+
ref.current.appendChild(el);
|
|
53
|
+
window.lucide.createIcons({ attrs: { class: className }});
|
|
54
|
+
}
|
|
55
|
+
}, [name, className, strokeWidth]);
|
|
56
|
+
return <span ref={ref} className={"inline-flex "+className} />;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const Card = ({className="", children, ...rest}) => (
|
|
60
|
+
<div className={"rounded-lg border border-border bg-white "+className} {...rest}>{children}</div>
|
|
61
|
+
);
|
|
62
|
+
const Separator = ({className=""}) => <div className={"h-px w-full bg-border "+className} />;
|
|
63
|
+
const Badge = ({variant="outline", className="", children}) => {
|
|
64
|
+
const v = {
|
|
65
|
+
outline: "border border-border text-zinc-700",
|
|
66
|
+
secondary: "bg-zinc-100 text-zinc-700 border border-transparent",
|
|
67
|
+
default: "bg-zinc-900 text-white border border-transparent",
|
|
68
|
+
}[variant];
|
|
69
|
+
return <span className={`inline-flex items-center gap-1 rounded-md px-1.5 py-0.5 text-[11px] font-medium leading-none ${v} ${className}`}>{children}</span>;
|
|
70
|
+
};
|
|
71
|
+
const Button = ({variant="default", size="sm", className="", children, ...p}) => {
|
|
72
|
+
const v = {
|
|
73
|
+
default: "bg-zinc-900 text-white hover:bg-zinc-800 border border-zinc-900",
|
|
74
|
+
secondary: "bg-zinc-100 text-zinc-900 hover:bg-zinc-200 border border-transparent",
|
|
75
|
+
ghost: "text-zinc-700 hover:bg-zinc-100 border border-transparent",
|
|
76
|
+
outline: "bg-white text-zinc-900 hover:bg-zinc-50 border border-border",
|
|
77
|
+
accent: "bg-amber-500 text-white hover:bg-amber-600 border border-amber-500",
|
|
78
|
+
success: "bg-emerald-500 text-white hover:bg-emerald-600 border border-emerald-500",
|
|
79
|
+
danger: "bg-rose-500 text-white hover:bg-rose-600 border border-rose-500",
|
|
80
|
+
}[variant];
|
|
81
|
+
const s = { sm: "h-9 px-4 text-[13px]", xs: "h-7 px-2.5 text-[12px]", md: "h-10 px-5 text-[14px]" }[size];
|
|
82
|
+
return <button className={`inline-flex items-center justify-center gap-1.5 rounded-md font-medium transition ${v} ${s} ${className}`} {...p}>{children}</button>;
|
|
83
|
+
};
|
|
84
|
+
const Avatar = ({initials, className=""}) => (
|
|
85
|
+
<span className={`inline-flex items-center justify-center rounded-full bg-zinc-100 text-zinc-600 text-[11px] font-semibold ${className}`}>{initials}</span>
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
// ─── shared shell ────────────────────────────────────────────────────────
|
|
89
|
+
const Topbar = ({mode="ready", livePill=null, anchorId=null, livePillAnchorId=null}) => {
|
|
90
|
+
const presence = {
|
|
91
|
+
ready: {cls: "border-emerald-200 bg-emerald-50 text-emerald-800", dot: "bg-emerald-500", label: "Ready"},
|
|
92
|
+
busy: {cls: "border-amber-200 bg-amber-50 text-amber-800", dot: "bg-amber-500", label: "Busy"},
|
|
93
|
+
oncall: {cls: "border-amber-200 bg-amber-50 text-amber-800", dot: "bg-amber-500", label: "On call"},
|
|
94
|
+
}[mode];
|
|
95
|
+
return (
|
|
96
|
+
<div className="relative flex items-center gap-5 h-16 px-7 border-b border-border bg-white">
|
|
97
|
+
<div className="flex items-center gap-2.5">
|
|
98
|
+
<div className="w-7 h-7 rounded bg-zinc-900 grid place-items-center text-white text-[13px] font-bold">⊛</div>
|
|
99
|
+
<span className="text-[17px] font-semibold tracking-tight">brand</span>
|
|
100
|
+
</div>
|
|
101
|
+
<div className="ml-3 flex items-center gap-2.5 h-10 px-3.5 rounded-md border border-border bg-zinc-50 w-[340px]">
|
|
102
|
+
<I name="search" className="w-4 h-4 text-zinc-400" />
|
|
103
|
+
<span className="text-[14px] text-zinc-400">⌘K Search · jump · run command</span>
|
|
104
|
+
</div>
|
|
105
|
+
<div className="flex-1" />
|
|
106
|
+
{livePill && (
|
|
107
|
+
<span className="relative inline-flex items-center gap-2 h-9 px-3 rounded-md border border-amber-300 bg-amber-50 text-amber-900 text-[13.5px] font-semibold">
|
|
108
|
+
{livePillAnchorId && <span id={livePillAnchorId} className="absolute left-1/2 bottom-0" data-callout-anchor="" />}
|
|
109
|
+
<I name="phone" className="w-4 h-4" />
|
|
110
|
+
{livePill}
|
|
111
|
+
</span>
|
|
112
|
+
)}
|
|
113
|
+
<span className={`relative inline-flex items-center gap-2 h-9 px-3 rounded-md border ${presence.cls} text-[13.5px] font-medium`}>
|
|
114
|
+
<span className={`w-2 h-2 rounded-full ${presence.dot}`} /> {presence.label}
|
|
115
|
+
<I name="chevron-down" className="w-3.5 h-3.5 opacity-60" />
|
|
116
|
+
</span>
|
|
117
|
+
<div className="relative">
|
|
118
|
+
<button className="h-9 w-9 rounded-md grid place-items-center hover:bg-zinc-100 border border-border">
|
|
119
|
+
<I name="bell" className="w-[18px] h-[18px] text-zinc-700" />
|
|
120
|
+
</button>
|
|
121
|
+
<span className="absolute -top-1 -right-1 inline-flex items-center justify-center min-w-[18px] h-[18px] px-1 rounded-full bg-rose-500 text-white text-[11px] font-bold">2</span>
|
|
122
|
+
</div>
|
|
123
|
+
<div className="flex items-center gap-2 pl-1">
|
|
124
|
+
<Avatar initials="PN" className="w-8 h-8 text-[12px]" />
|
|
125
|
+
<span className="text-[13.5px] text-zinc-700">Priya</span>
|
|
126
|
+
<I name="chevron-down" className="w-3.5 h-3.5 text-zinc-400" />
|
|
127
|
+
</div>
|
|
128
|
+
{anchorId && <span id={anchorId} className="absolute left-1/2 bottom-0" data-callout-anchor="" />}
|
|
129
|
+
</div>
|
|
130
|
+
);
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const Sidebar = ({active="home", anchor=null}) => {
|
|
134
|
+
const items = [
|
|
135
|
+
{key:"home", icon:"home", label:"Home"},
|
|
136
|
+
{key:"cases", icon:"search", label:"Cases"},
|
|
137
|
+
{key:"customers", icon:"users", label:"Customers"},
|
|
138
|
+
{key:"reports", icon:"bar-chart-3", label:"Reports"},
|
|
139
|
+
{key:"library", icon:"book-open", label:"Knowledge"},
|
|
140
|
+
];
|
|
141
|
+
return (
|
|
142
|
+
<div className="w-14 shrink-0 border-r border-border bg-white flex flex-col items-center py-4 gap-2 relative">
|
|
143
|
+
{items.map((it) => (
|
|
144
|
+
<div key={it.key} className="relative">
|
|
145
|
+
<button title={it.label}
|
|
146
|
+
className={`w-10 h-10 rounded-md grid place-items-center transition ${it.key===active?"bg-amber-50 text-amber-700 ring-1 ring-amber-200":"text-zinc-500 hover:bg-zinc-100"}`}>
|
|
147
|
+
<I name={it.icon} className="w-[18px] h-[18px]" />
|
|
148
|
+
</button>
|
|
149
|
+
{anchor && it.key === anchor.key && <span id={anchor.id} className="absolute top-1/2 left-0" data-callout-anchor="" />}
|
|
150
|
+
</div>
|
|
151
|
+
))}
|
|
152
|
+
<div className="flex-1" />
|
|
153
|
+
<button title="Settings" className="w-10 h-10 rounded-md grid place-items-center text-zinc-500 hover:bg-zinc-100">
|
|
154
|
+
<I name="settings" className="w-[18px] h-[18px]" />
|
|
155
|
+
</button>
|
|
156
|
+
</div>
|
|
157
|
+
);
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
const InboxRow = ({channel, id, name, sub, active=false, accent}) => {
|
|
161
|
+
const ch = { phone:"phone", email:"mail", chat:"message-square", whatsapp:"message-circle", calendar:"calendar-clock" }[channel];
|
|
162
|
+
const accentBg = active ? (accent==="emerald" ? "bg-emerald-50 border-l-emerald-500" : "bg-amber-50 border-l-amber-500") : "";
|
|
163
|
+
return (
|
|
164
|
+
<div className={`flex items-center gap-2 px-2.5 py-2 rounded-md cursor-pointer ${active ? `border-l-2 ${accentBg}` : "hover:bg-zinc-50"}`}>
|
|
165
|
+
<I name={ch} className="w-4 h-4 text-zinc-500 shrink-0" />
|
|
166
|
+
<span className="font-mono text-[12.5px] text-zinc-500 shrink-0">{id}</span>
|
|
167
|
+
<span className="text-[13.5px] text-zinc-800 truncate flex-1">{name}</span>
|
|
168
|
+
{sub && <span className="text-[12px] text-zinc-500 truncate">{sub}</span>}
|
|
169
|
+
</div>
|
|
170
|
+
);
|
|
171
|
+
};
|
|
172
|
+
const SectionHead = ({label, count, expanded=true}) => (
|
|
173
|
+
<div className="flex items-center gap-2 px-2.5 py-2.5">
|
|
174
|
+
<I name={expanded?"chevron-down":"chevron-right"} className="w-4 h-4 text-zinc-400" />
|
|
175
|
+
<span className="text-[13px] font-semibold text-zinc-800">{label}</span>
|
|
176
|
+
<span className="text-[12px] text-zinc-400 font-mono">· {count}</span>
|
|
177
|
+
</div>
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
const LeftCaseList = ({activeCaseId=null, anchorId=null, injectActive=null}) => (
|
|
181
|
+
<aside className="w-[300px] shrink-0 border-r border-border bg-white flex flex-col relative">
|
|
182
|
+
<div className="p-4 border-b border-border">
|
|
183
|
+
<Button variant="accent" className="w-full justify-between h-11 px-4 text-[14px]" size="md">
|
|
184
|
+
<span className="inline-flex items-center gap-2">
|
|
185
|
+
<I name="sparkles" className="w-4 h-4" />
|
|
186
|
+
Pick Next
|
|
187
|
+
</span>
|
|
188
|
+
<I name="arrow-right" className="w-4 h-4" />
|
|
189
|
+
</Button>
|
|
190
|
+
</div>
|
|
191
|
+
<div className="flex-1 overflow-hidden px-1.5 py-1">
|
|
192
|
+
<SectionHead label="Open" count={injectActive ? "5" : "4"} />
|
|
193
|
+
<div className="space-y-0.5">
|
|
194
|
+
{injectActive && <InboxRow channel={injectActive.channel} id={injectActive.id} name={injectActive.name} active={activeCaseId===injectActive.id} accent={injectActive.accent || "amber"} />}
|
|
195
|
+
<InboxRow channel="phone" id="CS-101" name="Sample · phone case" active={activeCaseId==="CS-101"} accent="amber" />
|
|
196
|
+
<InboxRow channel="email" id="CS-102" name="Sample · email case" active={activeCaseId==="CS-102"} accent="emerald" />
|
|
197
|
+
<InboxRow channel="chat" id="CS-103" name="Sample · chat case" />
|
|
198
|
+
</div>
|
|
199
|
+
<SectionHead label="Later" count="0" expanded={false} />
|
|
200
|
+
<SectionHead label="Done" count="0" expanded={false} />
|
|
201
|
+
</div>
|
|
202
|
+
{anchorId && <span id={anchorId} className="absolute left-0 top-1/2" data-callout-anchor="" />}
|
|
203
|
+
</aside>
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
const RightRail = ({customer="Customer Name", tier="Tier · NPS", status="Active", anchorId=null}) => (
|
|
207
|
+
<aside className="w-[300px] shrink-0 border-l border-border bg-white flex flex-col relative">
|
|
208
|
+
{anchorId && <span id={anchorId} className="absolute left-0 top-1/2" data-callout-anchor="" />}
|
|
209
|
+
<div className="border-b border-border px-4 py-3 flex gap-4 text-[13px] font-semibold">
|
|
210
|
+
<span className="text-amber-700 border-b-2 border-amber-500 pb-2 -mb-3">Details</span>
|
|
211
|
+
<span className="text-zinc-400">AI Copilot</span>
|
|
212
|
+
<span className="text-zinc-400">Apps</span>
|
|
213
|
+
</div>
|
|
214
|
+
<div className="px-4 py-4 text-[12.5px] space-y-4 overflow-y-auto">
|
|
215
|
+
<div>
|
|
216
|
+
<div className="text-[11px] uppercase tracking-[0.08em] text-zinc-500 font-semibold mb-1.5">Status</div>
|
|
217
|
+
<div className="text-zinc-800">{status}</div>
|
|
218
|
+
</div>
|
|
219
|
+
<Separator />
|
|
220
|
+
<div>
|
|
221
|
+
<div className="text-[11px] uppercase tracking-[0.08em] text-zinc-500 font-semibold mb-1.5">Customer</div>
|
|
222
|
+
<div className="text-[14px] font-semibold text-zinc-900">{customer}</div>
|
|
223
|
+
<div className="text-[12px] text-zinc-500">{tier}</div>
|
|
224
|
+
</div>
|
|
225
|
+
</div>
|
|
226
|
+
</aside>
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
// ─── callout system ──────────────────────────────────────────────────────
|
|
230
|
+
const Callouts = ({hostRef, scale, callouts}) => {
|
|
231
|
+
const [paths, setPaths] = React.useState([]);
|
|
232
|
+
const [box, setBox] = React.useState({w:0, h:0});
|
|
233
|
+
|
|
234
|
+
React.useLayoutEffect(() => { setPaths([]); }, [callouts]);
|
|
235
|
+
|
|
236
|
+
const recompute = React.useCallback(() => {
|
|
237
|
+
const host = hostRef.current;
|
|
238
|
+
if (!host) return;
|
|
239
|
+
const hostRect = host.getBoundingClientRect();
|
|
240
|
+
setBox({w: hostRect.width, h: hostRect.height});
|
|
241
|
+
const next = (callouts||[]).map((c) => {
|
|
242
|
+
const el = host.querySelector(`#${c.id}`);
|
|
243
|
+
if (!el) return null;
|
|
244
|
+
const r = el.getBoundingClientRect();
|
|
245
|
+
const ax = r.left - hostRect.left + (c.anchor==="left" ? 0 : c.anchor==="right" ? r.width : r.width/2);
|
|
246
|
+
const ay = r.top - hostRect.top + (c.anchor==="top" ? 0 : c.anchor==="bottom" ? r.height : r.height/2);
|
|
247
|
+
const margin = 60;
|
|
248
|
+
let lx, ly, side = c.side;
|
|
249
|
+
if (side === "left") { lx = -margin; ly = ay; }
|
|
250
|
+
else if (side === "right") { lx = hostRect.width + margin; ly = ay; }
|
|
251
|
+
else if (side === "top") { lx = ax; ly = -margin; }
|
|
252
|
+
else { lx = ax; ly = hostRect.height + margin; }
|
|
253
|
+
return { ...c, ax, ay, lx, ly, side };
|
|
254
|
+
}).filter(Boolean);
|
|
255
|
+
|
|
256
|
+
// collision avoidance — push later labels right (top/bottom) or down (left/right)
|
|
257
|
+
const labelW = 230, labelH = 36, gap = 12;
|
|
258
|
+
for (const sideKey of ["top", "bottom"]) {
|
|
259
|
+
const group = next.filter(p => p.side === sideKey).sort((a,b) => a.lx - b.lx);
|
|
260
|
+
for (let i = 1; i < group.length; i++) {
|
|
261
|
+
const minDelta = labelW + gap;
|
|
262
|
+
if (group[i].lx - group[i-1].lx < minDelta) group[i].lx = group[i-1].lx + minDelta;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
for (const sideKey of ["left", "right"]) {
|
|
266
|
+
const group = next.filter(p => p.side === sideKey).sort((a,b) => a.ay - b.ay);
|
|
267
|
+
for (let i = 1; i < group.length; i++) {
|
|
268
|
+
const minDelta = labelH + gap;
|
|
269
|
+
if (group[i].ly - group[i-1].ly < minDelta) group[i].ly = group[i-1].ly + minDelta;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
setPaths(next);
|
|
273
|
+
}, [hostRef, callouts]);
|
|
274
|
+
|
|
275
|
+
React.useEffect(() => {
|
|
276
|
+
recompute();
|
|
277
|
+
const ro = new ResizeObserver(recompute);
|
|
278
|
+
if (hostRef.current) ro.observe(hostRef.current);
|
|
279
|
+
window.addEventListener('resize', recompute);
|
|
280
|
+
const t1 = setTimeout(recompute, 80);
|
|
281
|
+
const t2 = setTimeout(recompute, 240);
|
|
282
|
+
return () => { ro.disconnect(); window.removeEventListener('resize', recompute); clearTimeout(t1); clearTimeout(t2); };
|
|
283
|
+
}, [recompute, scale, callouts]);
|
|
284
|
+
|
|
285
|
+
const labelW = 230, labelH = 32;
|
|
286
|
+
|
|
287
|
+
return (
|
|
288
|
+
<div className="absolute inset-0 pointer-events-none" style={{zIndex: 5}}>
|
|
289
|
+
<svg className="absolute inset-0" width={box.w} height={box.h} style={{overflow:"visible"}}>
|
|
290
|
+
{paths.map((p) => {
|
|
291
|
+
let d;
|
|
292
|
+
if (p.side === "left" || p.side === "right") {
|
|
293
|
+
if (Math.abs(p.ly - p.ay) < 2) {
|
|
294
|
+
d = `M ${p.lx} ${p.ly} L ${p.ax} ${p.ay}`;
|
|
295
|
+
} else {
|
|
296
|
+
const elbowX = p.side === "right" ? p.lx - 16 : p.lx + 16;
|
|
297
|
+
d = `M ${p.lx} ${p.ly} L ${elbowX} ${p.ly} L ${elbowX} ${p.ay} L ${p.ax} ${p.ay}`;
|
|
298
|
+
}
|
|
299
|
+
} else {
|
|
300
|
+
const elbowY = p.side === "bottom" ? p.ly - 16 : p.ly + 16;
|
|
301
|
+
d = `M ${p.lx} ${p.ly} L ${p.lx} ${elbowY} L ${p.ax} ${elbowY} L ${p.ax} ${p.ay}`;
|
|
302
|
+
}
|
|
303
|
+
return (
|
|
304
|
+
<g key={p.id}>
|
|
305
|
+
<path d={d} fill="none" stroke="rgb(161 161 170)" strokeWidth="1" strokeDasharray="3 3" />
|
|
306
|
+
<circle cx={p.ax} cy={p.ay} r="3" fill="rgb(82 82 91)" />
|
|
307
|
+
</g>
|
|
308
|
+
);
|
|
309
|
+
})}
|
|
310
|
+
</svg>
|
|
311
|
+
{paths.map((p) => {
|
|
312
|
+
const style = { position:"absolute", width: labelW };
|
|
313
|
+
if (p.side === "left") { style.right = box.w - p.lx; style.top = p.ly - labelH/2; }
|
|
314
|
+
else if (p.side === "right") { style.left = p.lx; style.top = p.ly - labelH/2; }
|
|
315
|
+
else if (p.side === "top") { style.left = p.lx - labelW/2; style.bottom = box.h - p.ly + labelH/2; }
|
|
316
|
+
else { style.left = p.lx - labelW/2; style.top = p.ly - labelH/2; }
|
|
317
|
+
return (
|
|
318
|
+
<div key={p.id} style={style}
|
|
319
|
+
className="text-[12px] leading-[1.3] text-zinc-700 bg-amber-50 border border-amber-200 rounded px-2.5 py-1.5 shadow-sm text-center">
|
|
320
|
+
{p.label}
|
|
321
|
+
</div>
|
|
322
|
+
);
|
|
323
|
+
})}
|
|
324
|
+
</div>
|
|
325
|
+
);
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
// ─── stage shell ─────────────────────────────────────────────────────────
|
|
329
|
+
const stageStyle = {width: 1920, height: 1080};
|
|
330
|
+
const StateShell = ({children}) => (
|
|
331
|
+
<div className="absolute top-0 left-0 origin-top-left rounded-md border border-border overflow-hidden bg-white" style={stageStyle}>
|
|
332
|
+
{children}
|
|
333
|
+
</div>
|
|
334
|
+
);
|
|
335
|
+
|
|
336
|
+
// ─── EXAMPLE STATE — replace with your own ───────────────────────────────
|
|
337
|
+
const Example = () => (
|
|
338
|
+
<StateShell>
|
|
339
|
+
<Topbar mode="ready" anchorId="x-topbar" />
|
|
340
|
+
<div className="flex" style={{height: 1080-64}}>
|
|
341
|
+
<Sidebar active="home" anchor={{key:"home", id:"x-sidebar"}} />
|
|
342
|
+
<LeftCaseList anchorId="x-caselist" />
|
|
343
|
+
<main className="flex-1 min-w-0 overflow-hidden flex flex-col">
|
|
344
|
+
<div className="flex-1 px-12 py-10 space-y-6">
|
|
345
|
+
<div className="relative">
|
|
346
|
+
<span id="x-greeting" className="absolute left-1/2 bottom-0" data-callout-anchor="" />
|
|
347
|
+
<div className="text-[40px] font-semibold tracking-tight leading-none">Hello, agent</div>
|
|
348
|
+
<div className="text-[16px] text-muted-foreground mt-3">This is the example state. Replace it.</div>
|
|
349
|
+
</div>
|
|
350
|
+
<div className="relative">
|
|
351
|
+
<span id="x-content" className="absolute right-0 top-1/2" data-callout-anchor="" />
|
|
352
|
+
<Card className="p-6">
|
|
353
|
+
<div className="text-[11px] uppercase tracking-[0.08em] text-zinc-500 font-semibold mb-2">Section</div>
|
|
354
|
+
<div className="text-[18px] text-zinc-900">Replace this card with your real centre content.</div>
|
|
355
|
+
</Card>
|
|
356
|
+
</div>
|
|
357
|
+
</div>
|
|
358
|
+
<div className="relative px-12 py-5 border-t border-border bg-zinc-50/60 flex items-center gap-8">
|
|
359
|
+
<span id="x-footer" className="absolute left-1/2 bottom-0" data-callout-anchor="" />
|
|
360
|
+
<span className="text-[12px] uppercase tracking-[0.08em] text-zinc-500 font-semibold">Today</span>
|
|
361
|
+
<span className="text-[14px] text-zinc-700">23 handled · CSAT 4.6 · FCR 87%</span>
|
|
362
|
+
</div>
|
|
363
|
+
</main>
|
|
364
|
+
</div>
|
|
365
|
+
</StateShell>
|
|
366
|
+
);
|
|
367
|
+
|
|
368
|
+
const Example_CALLOUTS = [
|
|
369
|
+
{ id: "x-topbar", side: "top", anchor: "bottom", label: "Topbar — brand · search · presence · bell · user" },
|
|
370
|
+
{ id: "x-sidebar", side: "left", anchor: "left", label: "Sidebar — navigation column" },
|
|
371
|
+
{ id: "x-caselist", side: "left", anchor: "left", label: "Inbox — case list grouped by Open / Later / Done" },
|
|
372
|
+
{ id: "x-greeting", side: "top", anchor: "bottom", label: "Greeting — day-orientation glance" },
|
|
373
|
+
{ id: "x-content", side: "right", anchor: "right", label: "Section — replace with your real annotation" },
|
|
374
|
+
{ id: "x-footer", side: "bottom", anchor: "bottom", label: "Today's KPI strip" },
|
|
375
|
+
];
|
|
376
|
+
|
|
377
|
+
// ─── view registry ───────────────────────────────────────────────────────
|
|
378
|
+
const VIEWS = {
|
|
379
|
+
"example": {label: "Example", group: "agent", render: Example, callouts: Example_CALLOUTS},
|
|
380
|
+
// Add more states here. Pattern: {label, group: "agent"|"sup"|"demo", render, callouts}
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
// ─── tabs ────────────────────────────────────────────────────────────────
|
|
384
|
+
const Tabs = ({active, onSelect}) => {
|
|
385
|
+
const groupKeys = (g) => Object.keys(VIEWS).filter(k => VIEWS[k].group === g);
|
|
386
|
+
const agentKeys = groupKeys("agent");
|
|
387
|
+
const demoKeys = groupKeys("demo");
|
|
388
|
+
const supKeys = groupKeys("sup");
|
|
389
|
+
const TabBtn = ({k}) => {
|
|
390
|
+
const v = VIEWS[k];
|
|
391
|
+
const isActive = active === k;
|
|
392
|
+
return (
|
|
393
|
+
<button onClick={() => onSelect(k)}
|
|
394
|
+
className={`h-8 px-3 rounded-md text-[13px] font-semibold transition border ${isActive?"bg-amber-50 text-amber-900 ring-1 ring-amber-200 border-amber-200":"bg-white text-zinc-700 border-transparent hover:bg-zinc-100"}`}>
|
|
395
|
+
{v.label}
|
|
396
|
+
</button>
|
|
397
|
+
);
|
|
398
|
+
};
|
|
399
|
+
return (
|
|
400
|
+
<div className="flex flex-wrap items-center gap-x-4 gap-y-2 px-8 py-3 bg-white border border-border rounded-md mx-auto mb-4 overflow-x-auto" style={{maxWidth: 2400}}>
|
|
401
|
+
{agentKeys.length > 0 && (
|
|
402
|
+
<div className="flex items-center gap-2">
|
|
403
|
+
<span className="text-[10.5px] uppercase tracking-[0.12em] text-zinc-500 font-bold">Agent</span>
|
|
404
|
+
<div className="flex items-center gap-1 ml-1">{agentKeys.map(k => <TabBtn key={k} k={k} />)}</div>
|
|
405
|
+
</div>
|
|
406
|
+
)}
|
|
407
|
+
{demoKeys.length > 0 && <>
|
|
408
|
+
<span className="border-l border-border h-6 mx-2" />
|
|
409
|
+
<div className="flex items-center gap-2">
|
|
410
|
+
<span className="text-[10.5px] uppercase tracking-[0.12em] text-cyan-600 font-bold">Demo</span>
|
|
411
|
+
<div className="flex items-center gap-1 ml-1">{demoKeys.map(k => <TabBtn key={k} k={k} />)}</div>
|
|
412
|
+
</div>
|
|
413
|
+
</>}
|
|
414
|
+
{supKeys.length > 0 && <>
|
|
415
|
+
<span className="border-l border-border h-6 mx-2" />
|
|
416
|
+
<div className="flex items-center gap-2">
|
|
417
|
+
<span className="text-[10.5px] uppercase tracking-[0.12em] text-violet-600 font-bold">Supervisor</span>
|
|
418
|
+
<div className="flex items-center gap-1 ml-1">{supKeys.map(k => <TabBtn key={k} k={k} />)}</div>
|
|
419
|
+
</div>
|
|
420
|
+
</>}
|
|
421
|
+
<div className="ml-auto text-[11px] text-zinc-400 font-mono shrink-0">#{active}</div>
|
|
422
|
+
</div>
|
|
423
|
+
);
|
|
424
|
+
};
|
|
425
|
+
|
|
426
|
+
// ─── App ─────────────────────────────────────────────────────────────────
|
|
427
|
+
const App = () => {
|
|
428
|
+
const initial = (window.location.hash || "").replace("#","");
|
|
429
|
+
const [active, setActive] = React.useState(VIEWS[initial] ? initial : Object.keys(VIEWS)[0]);
|
|
430
|
+
// demo views push their per-step callouts here; null = use static view.callouts
|
|
431
|
+
const [dynamicCallouts, setDynamicCallouts] = React.useState(null);
|
|
432
|
+
|
|
433
|
+
// NO reset-on-active effect — demo cleanup-on-unmount handles it.
|
|
434
|
+
|
|
435
|
+
React.useEffect(() => {
|
|
436
|
+
const onHash = () => {
|
|
437
|
+
const h = window.location.hash.replace("#","");
|
|
438
|
+
if (VIEWS[h]) setActive(h);
|
|
439
|
+
};
|
|
440
|
+
window.addEventListener("hashchange", onHash);
|
|
441
|
+
return () => window.removeEventListener("hashchange", onHash);
|
|
442
|
+
}, []);
|
|
443
|
+
|
|
444
|
+
const onSelect = (k) => { window.location.hash = "#"+k; setActive(k); };
|
|
445
|
+
|
|
446
|
+
const view = VIEWS[active];
|
|
447
|
+
const Render = view.render;
|
|
448
|
+
const effectiveCallouts = dynamicCallouts !== null ? dynamicCallouts : view.callouts;
|
|
449
|
+
|
|
450
|
+
const stageRef = React.useRef(null);
|
|
451
|
+
const [scale, setScale] = React.useState(1);
|
|
452
|
+
React.useEffect(() => {
|
|
453
|
+
const onResize = () => {
|
|
454
|
+
if (!stageRef.current) return;
|
|
455
|
+
const w = stageRef.current.clientWidth;
|
|
456
|
+
setScale(Math.min(1, w / 1920));
|
|
457
|
+
};
|
|
458
|
+
onResize();
|
|
459
|
+
window.addEventListener('resize', onResize);
|
|
460
|
+
return () => window.removeEventListener('resize', onResize);
|
|
461
|
+
}, [active]);
|
|
462
|
+
|
|
463
|
+
return (
|
|
464
|
+
<div className="bg-background text-foreground min-h-screen">
|
|
465
|
+
<Tabs active={active} onSelect={onSelect} />
|
|
466
|
+
<div className="mx-auto pb-12" style={{maxWidth: 2400}}>
|
|
467
|
+
<div ref={stageRef} className="relative overflow-visible mt-32" style={{aspectRatio: "16 / 9", marginLeft: 320, marginRight: 320, width: "calc(100% - 640px)", maxWidth: 1920}}>
|
|
468
|
+
<div className="absolute inset-0" style={{transform: `scale(${scale})`, transformOrigin: "top left"}}>
|
|
469
|
+
<Render setCallouts={setDynamicCallouts} />
|
|
470
|
+
</div>
|
|
471
|
+
<Callouts hostRef={stageRef} scale={scale} callouts={effectiveCallouts} />
|
|
472
|
+
</div>
|
|
473
|
+
<div className="px-8 mt-6 mx-auto text-[12px] text-zinc-500" style={{maxWidth: 2400}}>
|
|
474
|
+
<span className="uppercase tracking-[0.08em] font-semibold text-zinc-700">{VIEWS[active].label}</span>
|
|
475
|
+
<span className="mx-2">·</span>
|
|
476
|
+
<span>1920 × 1080 · scaled to fit · annotated callouts in the side gutters</span>
|
|
477
|
+
</div>
|
|
478
|
+
</div>
|
|
479
|
+
</div>
|
|
480
|
+
);
|
|
481
|
+
};
|
|
482
|
+
|
|
483
|
+
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
|
|
484
|
+
</script>
|
|
485
|
+
</body>
|
|
486
|
+
</html>
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# Demo walkthroughs — interactive click-through state
|
|
2
|
+
|
|
3
|
+
Demo views are wireframe states with internal `step` state — the user clicks a button (Accept / End / Save / Edit / etc.) and the view advances to the next step. Each step has its own callouts, swapped dynamically when the step changes.
|
|
4
|
+
|
|
5
|
+
This pattern is what makes the artifact feel like a "click-through prototype" instead of static slides.
|
|
6
|
+
|
|
7
|
+
## Why it's tricky
|
|
8
|
+
|
|
9
|
+
The tab navigation pattern uses a static `view.callouts` array per view. For demos, the callouts depend on internal state, which the App doesn't know about. The naive approach — let the demo component define `_CALLOUTS` and switch between them — doesn't work because the Callouts overlay is rendered by App, not the demo.
|
|
10
|
+
|
|
11
|
+
There are two failure modes you must avoid:
|
|
12
|
+
|
|
13
|
+
1. **No callouts ever render.** If the demo doesn't push its callouts to App, App reads `view.callouts: []` and renders nothing.
|
|
14
|
+
2. **Callouts disappear immediately on mount.** If App has a useEffect that resets dynamic callouts on view change (e.g. `useEffect(() => setDynamic(null), [active])`), it overrides the demo's mount-time push because parent useEffects run AFTER child useEffects. The demo sets the array, then App immediately nulls it. Result: zero callouts on screen.
|
|
15
|
+
|
|
16
|
+
Both happened during iteration. The fix below avoids both.
|
|
17
|
+
|
|
18
|
+
## The pattern (correct version)
|
|
19
|
+
|
|
20
|
+
### App side
|
|
21
|
+
|
|
22
|
+
```jsx
|
|
23
|
+
const App = () => {
|
|
24
|
+
const [active, setActive] = React.useState(...);
|
|
25
|
+
// demos push their per-step callouts here; null = use static view.callouts
|
|
26
|
+
const [dynamicCallouts, setDynamicCallouts] = React.useState(null);
|
|
27
|
+
// NO reset effect on [active] — demo's cleanup-on-unmount handles it
|
|
28
|
+
|
|
29
|
+
const view = VIEWS[active];
|
|
30
|
+
const Render = view.render;
|
|
31
|
+
const effectiveCallouts = dynamicCallouts !== null ? dynamicCallouts : view.callouts;
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<div>
|
|
35
|
+
<Tabs active={active} onSelect={setActive} />
|
|
36
|
+
<div ref={stageRef} ...>
|
|
37
|
+
<div className="absolute inset-0" style={{transform: `scale(${scale})`, transformOrigin: "top left"}}>
|
|
38
|
+
{/* Static views ignore the prop; demos consume it */}
|
|
39
|
+
<Render setCallouts={setDynamicCallouts} />
|
|
40
|
+
</div>
|
|
41
|
+
<Callouts hostRef={stageRef} scale={scale} callouts={effectiveCallouts} />
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
);
|
|
45
|
+
};
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Demo side
|
|
49
|
+
|
|
50
|
+
```jsx
|
|
51
|
+
// Top-level array per demo — clear, easy to scan, matches step indices
|
|
52
|
+
const D1_STEP_CALLOUTS = [
|
|
53
|
+
// step 0
|
|
54
|
+
[
|
|
55
|
+
{ id: "d1s0-banner", side: "top", anchor: "top",
|
|
56
|
+
label: "Inbound banner · 8s auto-accept countdown" },
|
|
57
|
+
],
|
|
58
|
+
// step 1
|
|
59
|
+
[
|
|
60
|
+
{ id: "d1s1-callpanel", side: "right", anchor: "right",
|
|
61
|
+
label: "Live call panel · transcript + controls" },
|
|
62
|
+
{ id: "d1s1-end", side: "bottom", anchor: "bottom",
|
|
63
|
+
label: "End → opens the wrap-up modal" },
|
|
64
|
+
],
|
|
65
|
+
// step 2 — wrap-up modal
|
|
66
|
+
[
|
|
67
|
+
{ id: "a-wrapmodal", side: "right", anchor: "right",
|
|
68
|
+
label: "Wrap-up · same modal as the canonical S7 view" },
|
|
69
|
+
{ id: "d1s2-save", side: "bottom", anchor: "bottom",
|
|
70
|
+
label: "Save & next loops back to home" },
|
|
71
|
+
],
|
|
72
|
+
// step 3 — toast
|
|
73
|
+
[
|
|
74
|
+
{ id: "d1s3-toast", side: "top", anchor: "top",
|
|
75
|
+
label: "Demo complete · ↻ Restart to replay" },
|
|
76
|
+
],
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
const DemoInbound = ({setCallouts}) => {
|
|
80
|
+
const [step, setStep] = React.useState(0);
|
|
81
|
+
const restart = () => setStep(0);
|
|
82
|
+
|
|
83
|
+
// Push the active step's callouts up. Cleanup-on-unmount nulls them so
|
|
84
|
+
// switching to another view doesn't leak stale callouts.
|
|
85
|
+
React.useEffect(() => {
|
|
86
|
+
if (setCallouts) setCallouts(D1_STEP_CALLOUTS[step] || []);
|
|
87
|
+
return () => { if (setCallouts) setCallouts(null); };
|
|
88
|
+
}, [step, setCallouts]);
|
|
89
|
+
|
|
90
|
+
if (step === 0) return <DemoStage step={0} total={4} label="Banner · awaiting accept" restart={restart}>
|
|
91
|
+
<InboundBanner anchorId="d1s0-banner" onAccept={() => setStep(1)} />
|
|
92
|
+
{/* dimmed shell underneath */}
|
|
93
|
+
</DemoStage>;
|
|
94
|
+
|
|
95
|
+
if (step === 1) return <DemoStage step={1} total={4} label="Active call · 02:14" restart={restart}>
|
|
96
|
+
{/* call panel with anchorId="d1s1-callpanel"; End button with anchorId="d1s1-end" */}
|
|
97
|
+
</DemoStage>;
|
|
98
|
+
|
|
99
|
+
// ...
|
|
100
|
+
};
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
The cleanup function `return () => setCallouts(null)` is what makes view switches clean. Order on view change:
|
|
104
|
+
1. Old demo unmounts → cleanup runs → `setDynamicCallouts(null)`
|
|
105
|
+
2. New view mounts
|
|
106
|
+
3. If new view is another demo: its useEffect fires → pushes its step-0 callouts
|
|
107
|
+
4. If new view is static: nothing pushes; `dynamicCallouts` stays null; App falls back to `view.callouts`
|
|
108
|
+
|
|
109
|
+
No race, no stale callouts.
|
|
110
|
+
|
|
111
|
+
## DemoStage wrapper
|
|
112
|
+
|
|
113
|
+
The demo's outer wrapper that adds the step-indicator strip at the top of the canvas:
|
|
114
|
+
|
|
115
|
+
```jsx
|
|
116
|
+
const DemoStage = ({step, total, label, restart, children}) => (
|
|
117
|
+
<div className="absolute top-0 left-0 origin-top-left rounded-md border border-border overflow-hidden bg-white" style={{width: 1920, height: 1080}}>
|
|
118
|
+
{/* step indicator strip — 32px tall, sits above the wireframe content */}
|
|
119
|
+
<div className="flex items-center gap-3 h-8 px-5 bg-amber-50/60 border-b border-amber-200 text-[12px]">
|
|
120
|
+
<span className="font-semibold text-amber-800 uppercase tracking-[0.06em]">Step {step + 1} of {total}</span>
|
|
121
|
+
<span className="text-zinc-500">·</span>
|
|
122
|
+
<span className="text-zinc-700">{label}</span>
|
|
123
|
+
<button onClick={restart} className="ml-auto inline-flex items-center gap-1 text-amber-700 hover:underline">
|
|
124
|
+
<I name="rotate-ccw" className="w-3 h-3" /> Restart
|
|
125
|
+
</button>
|
|
126
|
+
</div>
|
|
127
|
+
{/* content gets 1080 - 32 = 1048 px of height */}
|
|
128
|
+
{children}
|
|
129
|
+
</div>
|
|
130
|
+
);
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
When the children include a Topbar (h-16 = 64px) and a flex shell, give the inner flex `style={{height: 1080 - 32 - 64}}` to account for both the step strip and the topbar.
|
|
134
|
+
|
|
135
|
+
## Reusable demo sub-components
|
|
136
|
+
|
|
137
|
+
Don't refactor static state components to accept a million props. Keep the static path simple. For demos, build small `Demo*` variants that accept the action callbacks and anchor IDs:
|
|
138
|
+
|
|
139
|
+
- `DemoInboundBanner({onAccept, anchorId})` — green slide-down with a clickable Accept
|
|
140
|
+
- `DemoVoiceCenter({onEnd, direction, customerName, caseId, title, elapsed, anchorIds})` — call panel with End button wired up
|
|
141
|
+
- `DemoOutboundRingingCenter({onConnect, customerName, caseId, anchorId})` — pulsing ringing card
|
|
142
|
+
- `DemoRightRailWithCall({onCall, customer, phone, anchorIds})` — the right rail with a Call button
|
|
143
|
+
- `DemoCompleteToast({restart, anchorId})` — top-right toast with restart link
|
|
144
|
+
|
|
145
|
+
For the wrap-up modal: **don't fork**. The S7 `WrapUpModal` should accept `onSave`, `onHome`, `headline`, `subline`, `saveAnchorId` props (all optional). S7 calls it with no props (existing behavior); demos pass their own. One source of truth — what supervisors see in S7 is what agents see at the end of every demo.
|
|
146
|
+
|
|
147
|
+
## Anchor ID convention
|
|
148
|
+
|
|
149
|
+
For demo anchors, use `d{N}s{step}-{element}` — e.g. `d1s2-save`, `d2s0-call`. This makes IDs unique across all 14+ views and easy to grep.
|
|
150
|
+
|
|
151
|
+
For the wrap-up modal, demos REUSE the canonical S7 anchors (`a-wrapmodal`, `a-summary`, `a-axes`, `a-merge`, `a-status`) plus their own save-button anchor. The modal renders only one at a time; no ID conflict.
|
|
152
|
+
|
|
153
|
+
## Inject-active-row pattern
|
|
154
|
+
|
|
155
|
+
When a demo accepts a new inbound (call, chat, email), the inbox should reflect the new case. The shared `LeftCaseList` accepts an `injectActive` prop:
|
|
156
|
+
|
|
157
|
+
```jsx
|
|
158
|
+
<LeftCaseList
|
|
159
|
+
activeCaseId="CS-4861"
|
|
160
|
+
injectActive={{id: "CS-4861", name: "Chan · live call", channel: "phone"}}
|
|
161
|
+
/>
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
The injected row prepends to the Open section, the Open count bumps by 1, and the row gets the `active` highlight when `activeCaseId` matches. The existing rows below stay (modeling that the customer might have OTHER cases too — e.g. a separate email).
|
|
165
|
+
|
|
166
|
+
This avoids the trap of "the highlighted case in the inbox is the wrong channel" — accepting a phone call but highlighting an existing email row.
|
|
167
|
+
|
|
168
|
+
## Step indicator UX
|
|
169
|
+
|
|
170
|
+
A small but important touch: the step indicator strip sits at the very top of the canvas (above the topbar), in a faint amber band, with the format `STEP N OF M · {label} · ↻ Restart`. It says enough to orient a viewer who jumped in mid-flow without reading the surrounding context. Don't let `{label}` exceed ~30 characters or it pushes Restart off-screen at smaller scales.
|