archrip 0.2.2 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/templates/slash-commands/claude/archrip-scan.md +2 -2
- package/dist/templates/slash-commands/codex/archrip-scan.md +2 -2
- package/dist/templates/slash-commands/gemini/archrip-scan.md +2 -2
- package/dist/utils/layout.js +3 -2
- package/dist/utils/layout.js.map +1 -1
- package/dist/viewer-template/src/App.tsx +93 -5
- package/dist/viewer-template/src/components/CommandPalette.tsx +174 -0
- package/dist/viewer-template/src/components/Legend.tsx +35 -5
- package/dist/viewer-template/src/components/UseCaseFilter.tsx +62 -3
- package/dist/viewer-template/src/hooks/useCategoryFilter.ts +36 -0
- package/dist/viewer-template/src/hooks/useCommandPalette.ts +91 -0
- package/dist/viewer-template/src/hooks/useFlowAnimation.ts +82 -0
- package/dist/viewer-template/src/hooks/useKeyboardShortcuts.ts +39 -0
- package/dist/viewer-template/src/hooks/useUseCaseFilter.ts +166 -21
- package/dist/viewer-template/src/index.css +19 -0
- package/dist/viewer-template/src/types.ts +3 -2
- package/dist/viewer-template/src/utils/layout.ts +3 -2
- package/package.json +1 -1
|
@@ -129,9 +129,9 @@ After writing the file:
|
|
|
129
129
|
- UseCase: `id`, `name`, `nodeIds` — all required
|
|
130
130
|
|
|
131
131
|
### Node Rules
|
|
132
|
-
- `id`: kebab-case, prefixed by category abbreviation (ctrl-, svc-, port-, adpt-, model-, ext-, job-, dto-)
|
|
132
|
+
- `id`: kebab-case, prefixed by category abbreviation (ctrl-, svc-, port-, adpt-, model-, db-, ext-, job-, dto-)
|
|
133
133
|
- `layer`: non-negative integer. **Higher = closer to domain core / more stable. Lower = closer to external world / more volatile.** Dagre (TB) places higher layers lower on screen; concentric places them at center. Use as many layers as the architecture requires (typically 3-6). Example for DDD: 0=external, 1=adapters, 2=controllers, 3=app services, 4=ports, 5=domain entities. Example for MVC: 0=external, 1=controllers, 2=services, 3=models.
|
|
134
|
-
- `category`: one of controller, service, port, adapter, model, external, job, dto (or custom)
|
|
134
|
+
- `category`: one of controller, service, port, adapter, model, database, external, job, dto (or custom). Use `model` for domain entities/value objects (core business logic). Use `database` for DB tables, migrations, ORMs, and infrastructure persistence.
|
|
135
135
|
- `label`: display name for the node
|
|
136
136
|
- `filePath`: relative from project root
|
|
137
137
|
- `depth` (optional): 0=overview, 1=structure, 2=detail. Auto-inferred from `layer` if omitted: with 3+ unique layers, lowest → 0, middle → 1, highest → 2. With 1-2 layers, all nodes get depth 0 (always visible).
|
|
@@ -129,9 +129,9 @@ After writing the file:
|
|
|
129
129
|
- UseCase: `id`, `name`, `nodeIds` — all required
|
|
130
130
|
|
|
131
131
|
### Node Rules
|
|
132
|
-
- `id`: kebab-case, prefixed by category abbreviation (ctrl-, svc-, port-, adpt-, model-, ext-, job-, dto-)
|
|
132
|
+
- `id`: kebab-case, prefixed by category abbreviation (ctrl-, svc-, port-, adpt-, model-, db-, ext-, job-, dto-)
|
|
133
133
|
- `layer`: non-negative integer. **Higher = closer to domain core / more stable. Lower = closer to external world / more volatile.** Dagre (TB) places higher layers lower on screen; concentric places them at center. Use as many layers as the architecture requires (typically 3-6). Example for DDD: 0=external, 1=adapters, 2=controllers, 3=app services, 4=ports, 5=domain entities. Example for MVC: 0=external, 1=controllers, 2=services, 3=models.
|
|
134
|
-
- `category`: one of controller, service, port, adapter, model, external, job, dto (or custom)
|
|
134
|
+
- `category`: one of controller, service, port, adapter, model, database, external, job, dto (or custom). Use `model` for domain entities/value objects (core business logic). Use `database` for DB tables, migrations, ORMs, and infrastructure persistence.
|
|
135
135
|
- `label`: display name for the node
|
|
136
136
|
- `filePath`: relative from project root
|
|
137
137
|
- `depth` (optional): 0=overview, 1=structure, 2=detail. Auto-inferred from `layer` if omitted: with 3+ unique layers, lowest → 0, middle → 1, highest → 2. With 1-2 layers, all nodes get depth 0 (always visible).
|
|
@@ -129,9 +129,9 @@ After writing the file:
|
|
|
129
129
|
- UseCase: `id`, `name`, `nodeIds` — all required
|
|
130
130
|
|
|
131
131
|
### Node Rules
|
|
132
|
-
- `id`: kebab-case, prefixed by category abbreviation (ctrl-, svc-, port-, adpt-, model-, ext-, job-, dto-)
|
|
132
|
+
- `id`: kebab-case, prefixed by category abbreviation (ctrl-, svc-, port-, adpt-, model-, db-, ext-, job-, dto-)
|
|
133
133
|
- `layer`: non-negative integer. **Higher = closer to domain core / more stable. Lower = closer to external world / more volatile.** Dagre (TB) places higher layers lower on screen; concentric places them at center. Use as many layers as the architecture requires (typically 3-6). Example for DDD: 0=external, 1=adapters, 2=controllers, 3=app services, 4=ports, 5=domain entities. Example for MVC: 0=external, 1=controllers, 2=services, 3=models.
|
|
134
|
-
- `category`: one of controller, service, port, adapter, model, external, job, dto (or custom)
|
|
134
|
+
- `category`: one of controller, service, port, adapter, model, database, external, job, dto (or custom). Use `model` for domain entities/value objects (core business logic). Use `database` for DB tables, migrations, ORMs, and infrastructure persistence.
|
|
135
135
|
- `label`: display name for the node
|
|
136
136
|
- `filePath`: relative from project root
|
|
137
137
|
- `depth` (optional): 0=overview, 1=structure, 2=detail. Auto-inferred from `layer` if omitted: with 3+ unique layers, lowest → 0, middle → 1, highest → 2. With 1-2 layers, all nodes get depth 0 (always visible).
|
package/dist/utils/layout.js
CHANGED
|
@@ -105,8 +105,9 @@ const CATEGORY_RING_PRIORITY = {
|
|
|
105
105
|
dto: 3,
|
|
106
106
|
controller: 4,
|
|
107
107
|
adapter: 5,
|
|
108
|
-
|
|
109
|
-
|
|
108
|
+
database: 6,
|
|
109
|
+
job: 7,
|
|
110
|
+
external: 8,
|
|
110
111
|
};
|
|
111
112
|
const DEFAULT_RING_PRIORITY = 4; // unknown categories treated as controller-level
|
|
112
113
|
/**
|
package/dist/utils/layout.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"layout.js","sourceRoot":"","sources":["../../src/utils/layout.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,gBAAgB,CAAC;AAenC,MAAM,UAAU,GAAG,GAAG,CAAC;AACvB,MAAM,WAAW,GAAG,EAAE,CAAC;AACvB,MAAM,QAAQ,GAAG,GAAG,CAAC;AACrB,MAAM,QAAQ,GAAG,EAAE,CAAC;AAEpB,8BAA8B;AAC9B,MAAM,YAAY,GAAG,GAAG,CAAC;AACzB,MAAM,eAAe,GAAG,GAAG,CAAC;AAE5B;;;GAGG;AACH,MAAM,UAAU,aAAa,CAAC,IAAsB;IAClD,IAAI,IAAI,CAAC,OAAO,CAAC,MAAM,KAAK,YAAY,EAAE,CAAC;QACzC,OAAO,uBAAuB,CAAC,IAAI,CAAC,CAAC;IACvC,CAAC;IACD,OAAO,kBAAkB,CAAC,IAAI,CAAC,CAAC;AAClC,CAAC;AAED;;;GAGG;AACH,SAAS,kBAAkB,CAAC,IAAsB;IAChD,MAAM,CAAC,GAAG,IAAI,KAAK,CAAC,QAAQ,CAAC,KAAK,EAAE,CAAC;IACrC,CAAC,CAAC,QAAQ,CAAC;QACT,OAAO,EAAE,IAAI;QACb,OAAO,EAAE,QAAQ;QACjB,OAAO,EAAE,QAAQ;QACjB,OAAO,EAAE,EAAE;QACX,OAAO,EAAE,EAAE;KACZ,CAAC,CAAC;IACH,CAAC,CAAC,mBAAmB,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IAElC,+BAA+B;IAC/B,KAAK,MAAM,IAAI,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;QAC9B,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE;YACjB,KAAK,EAAE,UAAU;YACjB,MAAM,EAAE,WAAW;YACnB,IAAI,EAAE,IAAI,CAAC,KAAK;SACjB,CAAC,CAAC;IACL,CAAC;IAED,YAAY;IACZ,KAAK,MAAM,IAAI,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;QAC9B,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC;IACtC,CAAC;IAED,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;IAEhB,MAAM,KAAK,GAAiB,EAAE,CAAC;IAC/B,KAAK,MAAM,MAAM,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC;QAC/B,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QACzB,IAAI,CAAC,EAAE,CAAC;YACN,KAAK,CAAC,IAAI,CAAC;gBACT,EAAE,EAAE,MAAM;gBACV,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,UAAU,GAAG,CAAC;gBACvB,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,WAAW,GAAG,CAAC;gBACxB,KAAK,EAAE,UAAU;gBACjB,MAAM,EAAE,WAAW;aACpB,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IAED,OAAO,EAAE,KAAK,EAAE,CAAC;AACnB,CAAC;AAED;;;GAGG;AACH,SAAS,iBAAiB,CACxB,KAAgC;IAEhC,MAAM,GAAG,GAAG,IAAI,GAAG,EAAuB,CAAC;IAC3C,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,IAAI,MAAM,GAAG,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QAClC,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,MAAM,GAAG,IAAI,GAAG,EAAE,CAAC;YACnB,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QAC/B,CAAC;QACD,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QAExB,IAAI,MAAM,GAAG,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QAClC,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,MAAM,GAAG,IAAI,GAAG,EAAE,CAAC;YACnB,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QAC/B,CAAC;QACD,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAC1B,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED;;;GAGG;AACH,SAAS,YAAY,CAAC,MAAgB;IACpC,IAAI,MAAM,GAAG,CAAC,CAAC;IACf,IAAI,MAAM,GAAG,CAAC,CAAC;IACf,KAAK,MAAM,CAAC,IAAI,MAAM,EAAE,CAAC;QACvB,MAAM,IAAI,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;QACtB,MAAM,IAAI,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;IACxB,CAAC;IACD,OAAO,IAAI,CAAC,KAAK,CAAC,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC;AACpE,CAAC;AAED;;;;GAIG;AACH,MAAM,sBAAsB,GAA2B;IACrD,KAAK,EAAE,CAAC;IACR,IAAI,EAAE,CAAC;IACP,OAAO,EAAE,CAAC;IACV,GAAG,EAAE,CAAC;IACN,UAAU,EAAE,CAAC;IACb,OAAO,EAAE,CAAC;IACV,GAAG,EAAE,CAAC;IACN,QAAQ,EAAE,CAAC;CACZ,CAAC;AAEF,MAAM,qBAAqB,GAAG,CAAC,CAAC,CAAC,iDAAiD;AAElF;;;;GAIG;AACH,SAAS,cAAc,CAAC,QAAgB,EAAE,KAAa;IACrD,MAAM,QAAQ,GAAG,sBAAsB,CAAC,QAAQ,CAAC,IAAI,qBAAqB,CAAC;IAC3E,yGAAyG;IACzG,OAAO,QAAQ,GAAG,IAAI,GAAG,KAAK,CAAC;AACjC,CAAC;AAED;;;;;;;;;;GAUG;AACH,SAAS,uBAAuB,CAAC,IAAsB;IACrD,IAAI,IAAI,CAAC,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC5B,OAAO,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC;IACvB,CAAC;IAED,2CAA2C;IAC3C,MAAM,UAAU,GAAG,IAAI,GAAG,EAAoB,CAAC;IAC/C,MAAM,YAAY,GAAG,IAAI,GAAG,EAAkB,CAAC;IAC/C,KAAK,MAAM,IAAI,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;QAC9B,MAAM,OAAO,GAAG,cAAc,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC;QAC1D,YAAY,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,EAAE,OAAO,CAAC,CAAC;QACnC,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QACtC,IAAI,KAAK,EAAE,CAAC;YACV,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACtB,CAAC;aAAM,CAAC;YACN,UAAU,CAAC,GAAG,CAAC,OAAO,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC;QACrC,CAAC;IACH,CAAC;IAED,yDAAyD;IACzD,MAAM,cAAc,GAAG,CAAC,GAAG,UAAU,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;IAEpE,8CAA8C;IAC9C,MAAM,SAAS,GAAG,iBAAiB,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAEhD,MAAM,KAAK,GAAiB,EAAE,CAAC;IAC/B,MAAM,YAAY,GAAG,IAAI,GAAG,EAAkB,CAAC;IAE/C,KAAK,IAAI,SAAS,GAAG,CAAC,EAAE,SAAS,GAAG,cAAc,CAAC,MAAM,EAAE,SAAS,EAAE,EAAE,CAAC;QACvE,MAAM,OAAO,GAAG,cAAc,CAAC,SAAS,CAAE,CAAC;QAC3C,MAAM,OAAO,GAAG,UAAU,CAAC,GAAG,CAAC,OAAO,CAAE,CAAC;QACzC,MAAM,KAAK,GAAG,OAAO,CAAC,MAAM,CAAC;QAE7B,IAAI,SAAS,KAAK,CAAC,IAAI,KAAK,KAAK,CAAC,EAAE,CAAC;YACnC,+BAA+B;YAC/B,YAAY,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAE,EAAE,CAAC,CAAC,CAAC;YACjC,KAAK,CAAC,IAAI,CAAC;gBACT,EAAE,EAAE,OAAO,CAAC,CAAC,CAAE;gBACf,CAAC,EAAE,CAAC,UAAU,GAAG,CAAC;gBAClB,CAAC,EAAE,CAAC,WAAW,GAAG,CAAC;gBACnB,KAAK,EAAE,UAAU;gBACjB,MAAM,EAAE,WAAW;aACpB,CAAC,CAAC;YACH,SAAS;QACX,CAAC;QAED,+DAA+D;QAC/D,MAAM,SAAS,GAA0C,EAAE,CAAC;QAC5D,MAAM,YAAY,GAAqB,EAAE,CAAC;QAE1C,KAAK,MAAM,EAAE,IAAI,OAAO,EAAE,CAAC;YACzB,MAAM,SAAS,GAAG,SAAS,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;YACpC,IAAI,CAAC,SAAS,EAAE,CAAC;gBACf,YAAY,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC;gBAC1B,SAAS;YACX,CAAC;YAED,MAAM,oBAAoB,GAAa,EAAE,CAAC;YAC1C,KAAK,MAAM,CAAC,IAAI,SAAS,EAAE,CAAC;gBAC1B,MAAM,KAAK,GAAG,YAAY,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;gBAClC,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;oBACxB,oBAAoB,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;gBACnC,CAAC;YACH,CAAC;YAED,IAAI,oBAAoB,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;gBACtC,YAAY,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC;YAC5B,CAAC;iBAAM,CAAC;gBACN,SAAS,CAAC,IAAI,CAAC;oBACb,EAAE;oBACF,WAAW,EAAE,YAAY,CAAC,oBAAoB,CAAC;iBAChD,CAAC,CAAC;YACL,CAAC;QACH,CAAC;QAED,8DAA8D;QAC9D,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,WAAW,GAAG,CAAC,CAAC,WAAW,CAAC,CAAC;QACxD,MAAM,MAAM,GAAG;YACb,GAAG,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YAC7B,GAAG,YAAY,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SACjC,CAAC;QAEF,6CAA6C;QAC7C,qEAAqE;QACrE,MAAM,UAAU,GAAG,IAAI,CAAC,GAAG,CAAC,SAAS,GAAG,YAAY,EAAE,YAAY,GAAG,CAAC,CAAC,CAAC;QACxE,MAAM,aAAa,GAAG,KAAK,GAAG,eAAe,CAAC;QAC9C,MAAM,SAAS,GAAG,aAAa,GAAG,CAAC,CAAC,GAAG,IAAI,CAAC,EAAE,CAAC,CAAC;QAChD,MAAM,UAAU,GAAG,IAAI,CAAC,GAAG,CAAC,SAAS,EAAE,UAAU,CAAC,CAAC;QAEnD,MAAM,SAAS,GAAG,CAAC,CAAC,GAAG,IAAI,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC;QACxC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,EAAE,CAAC,EAAE,EAAE,CAAC;YAC/B,+CAA+C;YAC/C,MAAM,KAAK,GAAG,CAAC,GAAG,SAAS,GAAG,IAAI,CAAC,EAAE,GAAG,CAAC,CAAC;YAC1C,MAAM,EAAE,GAAG,MAAM,CAAC,CAAC,CAAE,CAAC;YACtB,YAAY,CAAC,GAAG,CAAC,EAAE,EAAE,KAAK,CAAC,CAAC;YAE5B,MAAM,CAAC,GAAG,UAAU,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;YACvC,MAAM,CAAC,GAAG,UAAU,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;YAEvC,KAAK,CAAC,IAAI,CAAC;gBACT,EAAE;gBACF,CAAC,EAAE,CAAC,GAAG,UAAU,GAAG,CAAC;gBACrB,CAAC,EAAE,CAAC,GAAG,WAAW,GAAG,CAAC;gBACtB,KAAK,EAAE,UAAU;gBACjB,MAAM,EAAE,WAAW;aACpB,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IAED,OAAO,EAAE,KAAK,EAAE,CAAC;AACnB,CAAC"}
|
|
1
|
+
{"version":3,"file":"layout.js","sourceRoot":"","sources":["../../src/utils/layout.ts"],"names":[],"mappings":"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"}
|
|
@@ -16,13 +16,17 @@ import type { ArchFlowNode, ArchNodeData } from './types.ts';
|
|
|
16
16
|
import { getCategoryColors } from './types.ts';
|
|
17
17
|
import { ArchNode } from './components/nodes/ArchNode.tsx';
|
|
18
18
|
import { GroupNode } from './components/nodes/GroupNode.tsx';
|
|
19
|
+
import { CommandPalette } from './components/CommandPalette.tsx';
|
|
19
20
|
import { DetailPanel } from './components/DetailPanel.tsx';
|
|
20
21
|
import { UseCaseFilter } from './components/UseCaseFilter.tsx';
|
|
21
22
|
import { DepthFilter } from './components/DepthFilter.tsx';
|
|
22
23
|
import { Legend } from './components/Legend.tsx';
|
|
23
24
|
import { ThemeToggle } from './components/ThemeToggle.tsx';
|
|
24
25
|
import { useArchitecture } from './hooks/useArchitecture.ts';
|
|
26
|
+
import { useCategoryFilter } from './hooks/useCategoryFilter.ts';
|
|
27
|
+
import { useCommandPalette } from './hooks/useCommandPalette.ts';
|
|
25
28
|
import { useDepthFilter } from './hooks/useDepthFilter.ts';
|
|
29
|
+
import { useKeyboardShortcuts } from './hooks/useKeyboardShortcuts.ts';
|
|
26
30
|
import { useUseCaseFilter } from './hooks/useUseCaseFilter.ts';
|
|
27
31
|
import { useTheme } from './hooks/useTheme.ts';
|
|
28
32
|
|
|
@@ -31,10 +35,11 @@ const nodeTypes = { archNode: ArchNode, groupNode: GroupNode };
|
|
|
31
35
|
function AppInner() {
|
|
32
36
|
const { nodes, edges, useCases, projectName, layoutType, loading, error, onNodesChange, onEdgesChange } = useArchitecture();
|
|
33
37
|
const { depthLevel, setDepthLevel, visibleNodes, visibleEdges } = useDepthFilter(nodes, edges, layoutType);
|
|
34
|
-
const { selectedUseCase, setSelectedUseCase, categories, filteredNodes, filteredEdges } = useUseCaseFilter(visibleNodes, visibleEdges, useCases);
|
|
38
|
+
const { selectedUseCase, setSelectedUseCase, categories, filteredNodes, filteredEdges, flowInfo } = useUseCaseFilter(visibleNodes, visibleEdges, useCases);
|
|
39
|
+
const categoryFilter = useCategoryFilter();
|
|
35
40
|
const [selectedNodeId, setSelectedNodeId] = useQueryState('node', parseAsString.withOptions({ history: 'replace' }));
|
|
36
41
|
const { theme, toggleTheme } = useTheme();
|
|
37
|
-
const { fitView } = useReactFlow();
|
|
42
|
+
const { fitView, setCenter, getNodes } = useReactFlow();
|
|
38
43
|
|
|
39
44
|
const selectedNodeData: ArchNodeData | null = useMemo(() => {
|
|
40
45
|
if (!selectedNodeId) return null;
|
|
@@ -66,6 +71,70 @@ function AppInner() {
|
|
|
66
71
|
return () => cancelAnimationFrame(id);
|
|
67
72
|
}, [depthLevel, selectedUseCase, fitView]);
|
|
68
73
|
|
|
74
|
+
// Auto-center viewport on active flow step
|
|
75
|
+
useEffect(() => {
|
|
76
|
+
if (!flowInfo.flowNodeIds || flowInfo.activeStep < 0) return;
|
|
77
|
+
const activeNodeId = flowInfo.flowNodeIds[flowInfo.activeStep];
|
|
78
|
+
if (!activeNodeId) return;
|
|
79
|
+
const rfNodes = getNodes();
|
|
80
|
+
const target = rfNodes.find((n) => n.id === activeNodeId);
|
|
81
|
+
if (!target) return;
|
|
82
|
+
const x = target.position.x + (target.measured?.width ?? 180) / 2;
|
|
83
|
+
const y = target.position.y + (target.measured?.height ?? 80) / 2;
|
|
84
|
+
void setCenter(x, y, { zoom: 1.2, duration: 400 });
|
|
85
|
+
}, [flowInfo.activeStep, flowInfo.flowNodeIds, getNodes, setCenter]);
|
|
86
|
+
|
|
87
|
+
// Category filter: dim hidden categories
|
|
88
|
+
const displayNodes = useMemo(() => {
|
|
89
|
+
if (categoryFilter.hiddenCategories.size === 0) return filteredNodes;
|
|
90
|
+
return filteredNodes.map((node) => {
|
|
91
|
+
if (categoryFilter.hiddenCategories.has(node.data.category)) {
|
|
92
|
+
return { ...node, style: { ...node.style, opacity: 0.08, transition: 'opacity 0.3s' } };
|
|
93
|
+
}
|
|
94
|
+
return node;
|
|
95
|
+
});
|
|
96
|
+
}, [filteredNodes, categoryFilter.hiddenCategories]);
|
|
97
|
+
|
|
98
|
+
const displayEdges = useMemo(() => {
|
|
99
|
+
if (categoryFilter.hiddenCategories.size === 0) return filteredEdges;
|
|
100
|
+
const hiddenNodeIds = new Set<string>();
|
|
101
|
+
for (const node of filteredNodes) {
|
|
102
|
+
if (categoryFilter.hiddenCategories.has(node.data.category)) {
|
|
103
|
+
hiddenNodeIds.add(node.id);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return filteredEdges.map((edge) => {
|
|
107
|
+
if (hiddenNodeIds.has(edge.source) || hiddenNodeIds.has(edge.target)) {
|
|
108
|
+
return {
|
|
109
|
+
...edge,
|
|
110
|
+
style: { ...edge.style, opacity: 0.08, transition: 'opacity 0.3s' },
|
|
111
|
+
labelStyle: { ...((edge.labelStyle as Record<string, unknown>) ?? {}), opacity: 0 },
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
return edge;
|
|
115
|
+
});
|
|
116
|
+
}, [filteredEdges, filteredNodes, categoryFilter.hiddenCategories]);
|
|
117
|
+
|
|
118
|
+
// Command palette
|
|
119
|
+
const palette = useCommandPalette(displayNodes, categoryFilter.hiddenCategories);
|
|
120
|
+
|
|
121
|
+
const handlePaletteSelect = useCallback((nodeId: string) => {
|
|
122
|
+
palette.close();
|
|
123
|
+
void setSelectedNodeId(nodeId);
|
|
124
|
+
}, [palette, setSelectedNodeId]);
|
|
125
|
+
|
|
126
|
+
const handleEscape = useCallback(() => {
|
|
127
|
+
if (palette.isOpen) { palette.close(); return; }
|
|
128
|
+
if (selectedNodeId) { void setSelectedNodeId(null); return; }
|
|
129
|
+
if (selectedUseCase) { void setSelectedUseCase(null); return; }
|
|
130
|
+
}, [palette, selectedNodeId, setSelectedNodeId, selectedUseCase, setSelectedUseCase]);
|
|
131
|
+
|
|
132
|
+
useKeyboardShortcuts({
|
|
133
|
+
onTogglePalette: palette.toggle,
|
|
134
|
+
onEscape: handleEscape,
|
|
135
|
+
isPaletteOpen: palette.isOpen,
|
|
136
|
+
});
|
|
137
|
+
|
|
69
138
|
if (loading) {
|
|
70
139
|
return (
|
|
71
140
|
<div className="w-full h-screen flex items-center justify-center" style={{ color: 'var(--color-content-tertiary)', background: 'var(--color-surface-canvas)' }}>
|
|
@@ -88,8 +157,8 @@ function AppInner() {
|
|
|
88
157
|
return (
|
|
89
158
|
<div className="w-full h-screen relative">
|
|
90
159
|
<ReactFlow
|
|
91
|
-
nodes={
|
|
92
|
-
edges={
|
|
160
|
+
nodes={displayNodes}
|
|
161
|
+
edges={displayEdges}
|
|
93
162
|
onNodesChange={onNodesChange}
|
|
94
163
|
onEdgesChange={onEdgesChange}
|
|
95
164
|
onNodeClick={onNodeClick}
|
|
@@ -120,11 +189,17 @@ function AppInner() {
|
|
|
120
189
|
useCases={useCases}
|
|
121
190
|
selectedUseCase={selectedUseCase}
|
|
122
191
|
onSelect={handleUseCaseSelect}
|
|
192
|
+
flowInfo={flowInfo}
|
|
123
193
|
/>
|
|
124
194
|
)}
|
|
125
195
|
<DepthFilter depthLevel={depthLevel} onSelect={setDepthLevel} />
|
|
126
196
|
</div>
|
|
127
|
-
<Legend
|
|
197
|
+
<Legend
|
|
198
|
+
categories={categories}
|
|
199
|
+
hiddenCategories={categoryFilter.hiddenCategories}
|
|
200
|
+
onToggleCategory={categoryFilter.toggleCategory}
|
|
201
|
+
onShowAll={categoryFilter.showAll}
|
|
202
|
+
/>
|
|
128
203
|
|
|
129
204
|
{/* Title + Theme Toggle */}
|
|
130
205
|
<div className="absolute top-3 right-3 z-10 flex items-center gap-2">
|
|
@@ -157,6 +232,19 @@ function AppInner() {
|
|
|
157
232
|
/>
|
|
158
233
|
</>
|
|
159
234
|
)}
|
|
235
|
+
|
|
236
|
+
{palette.isOpen && (
|
|
237
|
+
<CommandPalette
|
|
238
|
+
query={palette.query}
|
|
239
|
+
onQueryChange={palette.setQuery}
|
|
240
|
+
results={palette.results}
|
|
241
|
+
activeIndex={palette.activeIndex}
|
|
242
|
+
onMoveUp={palette.moveUp}
|
|
243
|
+
onMoveDown={palette.moveDown}
|
|
244
|
+
onSelect={handlePaletteSelect}
|
|
245
|
+
onClose={palette.close}
|
|
246
|
+
/>
|
|
247
|
+
)}
|
|
160
248
|
</div>
|
|
161
249
|
);
|
|
162
250
|
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react';
|
|
2
|
+
|
|
3
|
+
import { getCategoryColors, getCategoryLabel } from '../types.ts';
|
|
4
|
+
import type { CommandPaletteResult } from '../hooks/useCommandPalette.ts';
|
|
5
|
+
|
|
6
|
+
interface CommandPaletteProps {
|
|
7
|
+
query: string;
|
|
8
|
+
onQueryChange: (q: string) => void;
|
|
9
|
+
results: CommandPaletteResult[];
|
|
10
|
+
activeIndex: number;
|
|
11
|
+
onMoveUp: () => void;
|
|
12
|
+
onMoveDown: () => void;
|
|
13
|
+
onSelect: (id: string) => void;
|
|
14
|
+
onClose: () => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function CommandPalette({
|
|
18
|
+
query,
|
|
19
|
+
onQueryChange,
|
|
20
|
+
results,
|
|
21
|
+
activeIndex,
|
|
22
|
+
onMoveUp,
|
|
23
|
+
onMoveDown,
|
|
24
|
+
onSelect,
|
|
25
|
+
onClose,
|
|
26
|
+
}: CommandPaletteProps) {
|
|
27
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
28
|
+
const listRef = useRef<HTMLDivElement>(null);
|
|
29
|
+
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
inputRef.current?.focus();
|
|
32
|
+
}, []);
|
|
33
|
+
|
|
34
|
+
// Scroll active item into view
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
const list = listRef.current;
|
|
37
|
+
if (!list) return;
|
|
38
|
+
const item = list.children[activeIndex] as HTMLElement | undefined;
|
|
39
|
+
item?.scrollIntoView({ block: 'nearest' });
|
|
40
|
+
}, [activeIndex]);
|
|
41
|
+
|
|
42
|
+
function handleKeyDown(e: React.KeyboardEvent) {
|
|
43
|
+
if (e.key === 'ArrowUp') {
|
|
44
|
+
e.preventDefault();
|
|
45
|
+
onMoveUp();
|
|
46
|
+
} else if (e.key === 'ArrowDown') {
|
|
47
|
+
e.preventDefault();
|
|
48
|
+
onMoveDown();
|
|
49
|
+
} else if (e.key === 'Enter') {
|
|
50
|
+
e.preventDefault();
|
|
51
|
+
const item = results[activeIndex];
|
|
52
|
+
if (item) onSelect(item.id);
|
|
53
|
+
} else if (e.key === 'Escape') {
|
|
54
|
+
e.preventDefault();
|
|
55
|
+
onClose();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<>
|
|
61
|
+
{/* Overlay */}
|
|
62
|
+
<div
|
|
63
|
+
className="fixed inset-0 z-60"
|
|
64
|
+
style={{ background: 'rgba(0, 0, 0, 0.3)' }}
|
|
65
|
+
onClick={onClose}
|
|
66
|
+
/>
|
|
67
|
+
|
|
68
|
+
{/* Palette card */}
|
|
69
|
+
<div
|
|
70
|
+
className="fixed z-60 rounded-xl border overflow-hidden"
|
|
71
|
+
style={{
|
|
72
|
+
top: '20%',
|
|
73
|
+
left: '50%',
|
|
74
|
+
transform: 'translateX(-50%)',
|
|
75
|
+
width: 480,
|
|
76
|
+
maxWidth: 'calc(100vw - 32px)',
|
|
77
|
+
background: 'var(--color-surface-primary)',
|
|
78
|
+
borderColor: 'var(--color-border-primary)',
|
|
79
|
+
boxShadow: '0 25px 50px -12px rgba(0, 0, 0, 0.25)',
|
|
80
|
+
}}
|
|
81
|
+
>
|
|
82
|
+
{/* Search input */}
|
|
83
|
+
<div
|
|
84
|
+
className="border-b"
|
|
85
|
+
style={{ borderColor: 'var(--color-border-primary)' }}
|
|
86
|
+
>
|
|
87
|
+
<input
|
|
88
|
+
ref={inputRef}
|
|
89
|
+
type="text"
|
|
90
|
+
value={query}
|
|
91
|
+
onChange={(e) => onQueryChange(e.target.value)}
|
|
92
|
+
onKeyDown={handleKeyDown}
|
|
93
|
+
placeholder="Search nodes..."
|
|
94
|
+
className="w-full px-4 py-3 text-sm bg-transparent outline-none"
|
|
95
|
+
style={{ color: 'var(--color-content-primary)' }}
|
|
96
|
+
/>
|
|
97
|
+
</div>
|
|
98
|
+
|
|
99
|
+
{/* Results list */}
|
|
100
|
+
<div
|
|
101
|
+
ref={listRef}
|
|
102
|
+
className="overflow-y-auto"
|
|
103
|
+
style={{ maxHeight: 320 }}
|
|
104
|
+
>
|
|
105
|
+
{results.length === 0 && (
|
|
106
|
+
<div
|
|
107
|
+
className="px-4 py-6 text-center text-sm"
|
|
108
|
+
style={{ color: 'var(--color-content-tertiary)' }}
|
|
109
|
+
>
|
|
110
|
+
No nodes found
|
|
111
|
+
</div>
|
|
112
|
+
)}
|
|
113
|
+
{results.map((item, index) => {
|
|
114
|
+
const colors = getCategoryColors(item.category);
|
|
115
|
+
const isActive = index === activeIndex;
|
|
116
|
+
return (
|
|
117
|
+
<button
|
|
118
|
+
key={item.id}
|
|
119
|
+
onClick={() => onSelect(item.id)}
|
|
120
|
+
className="w-full px-4 py-2.5 flex items-start gap-3 text-left cursor-pointer border-none"
|
|
121
|
+
style={{
|
|
122
|
+
background: isActive ? 'var(--color-surface-tertiary)' : 'transparent',
|
|
123
|
+
}}
|
|
124
|
+
>
|
|
125
|
+
<div
|
|
126
|
+
className="w-3 h-3 rounded-sm border mt-0.5 shrink-0"
|
|
127
|
+
style={{ background: colors.bg, borderColor: colors.border }}
|
|
128
|
+
/>
|
|
129
|
+
<div className="min-w-0 flex-1">
|
|
130
|
+
<div className="flex items-center gap-2">
|
|
131
|
+
<span
|
|
132
|
+
className="text-sm font-medium truncate"
|
|
133
|
+
style={{ color: 'var(--color-content-primary)' }}
|
|
134
|
+
>
|
|
135
|
+
{item.label}
|
|
136
|
+
</span>
|
|
137
|
+
<span
|
|
138
|
+
className="text-xs shrink-0"
|
|
139
|
+
style={{ color: 'var(--color-content-tertiary)' }}
|
|
140
|
+
>
|
|
141
|
+
{getCategoryLabel(item.category)}
|
|
142
|
+
</span>
|
|
143
|
+
</div>
|
|
144
|
+
{item.description && (
|
|
145
|
+
<p
|
|
146
|
+
className="text-xs mt-0.5 truncate"
|
|
147
|
+
style={{ color: 'var(--color-content-secondary)' }}
|
|
148
|
+
>
|
|
149
|
+
{item.description}
|
|
150
|
+
</p>
|
|
151
|
+
)}
|
|
152
|
+
</div>
|
|
153
|
+
</button>
|
|
154
|
+
);
|
|
155
|
+
})}
|
|
156
|
+
</div>
|
|
157
|
+
|
|
158
|
+
{/* Footer */}
|
|
159
|
+
<div
|
|
160
|
+
className="px-4 py-2 border-t flex items-center gap-4 text-xs"
|
|
161
|
+
style={{
|
|
162
|
+
borderColor: 'var(--color-border-primary)',
|
|
163
|
+
color: 'var(--color-content-tertiary)',
|
|
164
|
+
background: 'var(--color-surface-secondary)',
|
|
165
|
+
}}
|
|
166
|
+
>
|
|
167
|
+
<span><kbd className="font-mono">{'\u2191\u2193'}</kbd> Navigate</span>
|
|
168
|
+
<span><kbd className="font-mono">{'\u21B5'}</kbd> Select</span>
|
|
169
|
+
<span><kbd className="font-mono">Esc</kbd> Close</span>
|
|
170
|
+
</div>
|
|
171
|
+
</div>
|
|
172
|
+
</>
|
|
173
|
+
);
|
|
174
|
+
}
|
|
@@ -2,9 +2,12 @@ import { getCategoryColors, getCategoryLabel } from '../types.ts';
|
|
|
2
2
|
|
|
3
3
|
interface LegendProps {
|
|
4
4
|
categories: string[];
|
|
5
|
+
hiddenCategories: Set<string>;
|
|
6
|
+
onToggleCategory: (category: string) => void;
|
|
7
|
+
onShowAll: () => void;
|
|
5
8
|
}
|
|
6
9
|
|
|
7
|
-
export function Legend({ categories }: LegendProps) {
|
|
10
|
+
export function Legend({ categories, hiddenCategories, onToggleCategory, onShowAll }: LegendProps) {
|
|
8
11
|
return (
|
|
9
12
|
<div
|
|
10
13
|
className="absolute bottom-3 right-3 z-10 rounded-lg p-3 border"
|
|
@@ -23,19 +26,46 @@ export function Legend({ categories }: LegendProps) {
|
|
|
23
26
|
<div className="grid grid-cols-2 gap-x-4 gap-y-1">
|
|
24
27
|
{categories.map((cat) => {
|
|
25
28
|
const colors = getCategoryColors(cat);
|
|
29
|
+
const hidden = hiddenCategories.has(cat);
|
|
26
30
|
return (
|
|
27
|
-
<
|
|
31
|
+
<button
|
|
32
|
+
key={cat}
|
|
33
|
+
onClick={() => onToggleCategory(cat)}
|
|
34
|
+
className="flex items-center gap-1.5 cursor-pointer bg-transparent border-none p-0 text-left"
|
|
35
|
+
>
|
|
28
36
|
<div
|
|
29
37
|
className="w-3 h-3 rounded-sm border"
|
|
30
|
-
style={{
|
|
38
|
+
style={{
|
|
39
|
+
background: colors.bg,
|
|
40
|
+
borderColor: colors.border,
|
|
41
|
+
opacity: hidden ? 0.3 : 1,
|
|
42
|
+
transition: 'opacity 0.2s',
|
|
43
|
+
}}
|
|
31
44
|
/>
|
|
32
|
-
<span
|
|
45
|
+
<span
|
|
46
|
+
className="text-xs"
|
|
47
|
+
style={{
|
|
48
|
+
color: 'var(--color-content-secondary)',
|
|
49
|
+
opacity: hidden ? 0.4 : 1,
|
|
50
|
+
textDecoration: hidden ? 'line-through' : 'none',
|
|
51
|
+
transition: 'opacity 0.2s',
|
|
52
|
+
}}
|
|
53
|
+
>
|
|
33
54
|
{getCategoryLabel(cat)}
|
|
34
55
|
</span>
|
|
35
|
-
</
|
|
56
|
+
</button>
|
|
36
57
|
);
|
|
37
58
|
})}
|
|
38
59
|
</div>
|
|
60
|
+
{hiddenCategories.size > 0 && (
|
|
61
|
+
<button
|
|
62
|
+
onClick={onShowAll}
|
|
63
|
+
className="mt-2 text-xs cursor-pointer hover:underline bg-transparent border-none p-0"
|
|
64
|
+
style={{ color: 'var(--color-interactive-primary)' }}
|
|
65
|
+
>
|
|
66
|
+
Show All
|
|
67
|
+
</button>
|
|
68
|
+
)}
|
|
39
69
|
</div>
|
|
40
70
|
);
|
|
41
71
|
}
|
|
@@ -1,12 +1,53 @@
|
|
|
1
1
|
import type { UseCase } from '../types.ts';
|
|
2
|
+
import type { FlowAnimationState } from '../hooks/useFlowAnimation.ts';
|
|
2
3
|
|
|
3
4
|
interface UseCaseFilterProps {
|
|
4
5
|
useCases: UseCase[];
|
|
5
6
|
selectedUseCase: string | null;
|
|
6
7
|
onSelect: (useCaseId: string | null) => void;
|
|
8
|
+
flowInfo: FlowAnimationState;
|
|
7
9
|
}
|
|
8
10
|
|
|
9
|
-
|
|
11
|
+
function FlowButton({ label, onClick }: { label: string; onClick: () => void }) {
|
|
12
|
+
return (
|
|
13
|
+
<button
|
|
14
|
+
onClick={onClick}
|
|
15
|
+
className="w-7 h-7 flex items-center justify-center rounded border text-xs cursor-pointer"
|
|
16
|
+
style={{
|
|
17
|
+
background: 'var(--color-surface-secondary)',
|
|
18
|
+
borderColor: 'var(--color-border-primary)',
|
|
19
|
+
color: 'var(--color-content-secondary)',
|
|
20
|
+
}}
|
|
21
|
+
aria-label={label}
|
|
22
|
+
>
|
|
23
|
+
{label === 'Previous' && (
|
|
24
|
+
<svg width="12" height="12" viewBox="0 0 12 12" fill="currentColor">
|
|
25
|
+
<path d="M8 1.5L3.5 6L8 10.5" stroke="currentColor" strokeWidth="1.5" fill="none" strokeLinecap="round" strokeLinejoin="round" />
|
|
26
|
+
</svg>
|
|
27
|
+
)}
|
|
28
|
+
{label === 'Play' && (
|
|
29
|
+
<svg width="12" height="12" viewBox="0 0 12 12" fill="currentColor">
|
|
30
|
+
<path d="M3 1.5L10 6L3 10.5V1.5Z" />
|
|
31
|
+
</svg>
|
|
32
|
+
)}
|
|
33
|
+
{label === 'Pause' && (
|
|
34
|
+
<svg width="12" height="12" viewBox="0 0 12 12" fill="currentColor">
|
|
35
|
+
<rect x="2.5" y="1.5" width="2.5" height="9" rx="0.5" />
|
|
36
|
+
<rect x="7" y="1.5" width="2.5" height="9" rx="0.5" />
|
|
37
|
+
</svg>
|
|
38
|
+
)}
|
|
39
|
+
{label === 'Next' && (
|
|
40
|
+
<svg width="12" height="12" viewBox="0 0 12 12" fill="currentColor">
|
|
41
|
+
<path d="M4 1.5L8.5 6L4 10.5" stroke="currentColor" strokeWidth="1.5" fill="none" strokeLinecap="round" strokeLinejoin="round" />
|
|
42
|
+
</svg>
|
|
43
|
+
)}
|
|
44
|
+
</button>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function UseCaseFilter({ useCases, selectedUseCase, onSelect, flowInfo }: UseCaseFilterProps) {
|
|
49
|
+
const selectedUc = selectedUseCase ? useCases.find((uc) => uc.id === selectedUseCase) : undefined;
|
|
50
|
+
|
|
10
51
|
return (
|
|
11
52
|
<div
|
|
12
53
|
className="rounded-lg p-3 w-64 border"
|
|
@@ -40,11 +81,29 @@ export function UseCaseFilter({ useCases, selectedUseCase, onSelect }: UseCaseFi
|
|
|
40
81
|
</option>
|
|
41
82
|
))}
|
|
42
83
|
</select>
|
|
43
|
-
{
|
|
84
|
+
{selectedUc && (
|
|
44
85
|
<div className="mt-2">
|
|
45
86
|
<p className="text-xs" style={{ color: 'var(--color-content-secondary)' }}>
|
|
46
|
-
{
|
|
87
|
+
{selectedUc.description}
|
|
47
88
|
</p>
|
|
89
|
+
{flowInfo.flowNodeIds && (
|
|
90
|
+
<div className="mt-1.5">
|
|
91
|
+
<p
|
|
92
|
+
className="text-xs font-mono mb-1.5"
|
|
93
|
+
style={{ color: 'var(--color-interactive-primary)' }}
|
|
94
|
+
>
|
|
95
|
+
Step {flowInfo.activeStep + 1}/{flowInfo.flowNodeIds.length}
|
|
96
|
+
</p>
|
|
97
|
+
<div className="flex items-center gap-1">
|
|
98
|
+
<FlowButton label="Previous" onClick={flowInfo.prev} />
|
|
99
|
+
<FlowButton
|
|
100
|
+
label={flowInfo.isPlaying ? 'Pause' : 'Play'}
|
|
101
|
+
onClick={flowInfo.isPlaying ? flowInfo.pause : flowInfo.play}
|
|
102
|
+
/>
|
|
103
|
+
<FlowButton label="Next" onClick={flowInfo.next} />
|
|
104
|
+
</div>
|
|
105
|
+
</div>
|
|
106
|
+
)}
|
|
48
107
|
<button
|
|
49
108
|
onClick={() => onSelect(null)}
|
|
50
109
|
className="mt-1.5 text-xs cursor-pointer hover:underline"
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { useCallback, useMemo } from 'react';
|
|
2
|
+
import { useQueryState, parseAsString } from 'nuqs';
|
|
3
|
+
|
|
4
|
+
interface CategoryFilterState {
|
|
5
|
+
hiddenCategories: Set<string>;
|
|
6
|
+
toggleCategory: (category: string) => void;
|
|
7
|
+
showAll: () => void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function useCategoryFilter(): CategoryFilterState {
|
|
11
|
+
const [hideParam, setHideParam] = useQueryState('hide', parseAsString.withOptions({ history: 'replace' }));
|
|
12
|
+
|
|
13
|
+
const hiddenCategories = useMemo(() => {
|
|
14
|
+
if (!hideParam) return new Set<string>();
|
|
15
|
+
return new Set(hideParam.split(',').filter(Boolean));
|
|
16
|
+
}, [hideParam]);
|
|
17
|
+
|
|
18
|
+
const toggleCategory = useCallback(
|
|
19
|
+
(category: string) => {
|
|
20
|
+
const next = new Set(hiddenCategories);
|
|
21
|
+
if (next.has(category)) {
|
|
22
|
+
next.delete(category);
|
|
23
|
+
} else {
|
|
24
|
+
next.add(category);
|
|
25
|
+
}
|
|
26
|
+
void setHideParam(next.size > 0 ? [...next].join(',') : null);
|
|
27
|
+
},
|
|
28
|
+
[hiddenCategories, setHideParam],
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
const showAll = useCallback(() => {
|
|
32
|
+
void setHideParam(null);
|
|
33
|
+
}, [setHideParam]);
|
|
34
|
+
|
|
35
|
+
return { hiddenCategories, toggleCategory, showAll };
|
|
36
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { useCallback, useMemo, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
import type { ArchFlowNode } from '../types.ts';
|
|
4
|
+
|
|
5
|
+
export interface CommandPaletteResult {
|
|
6
|
+
id: string;
|
|
7
|
+
label: string;
|
|
8
|
+
category: string;
|
|
9
|
+
description: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface CommandPaletteState {
|
|
13
|
+
isOpen: boolean;
|
|
14
|
+
toggle: () => void;
|
|
15
|
+
close: () => void;
|
|
16
|
+
query: string;
|
|
17
|
+
setQuery: (q: string) => void;
|
|
18
|
+
results: CommandPaletteResult[];
|
|
19
|
+
activeIndex: number;
|
|
20
|
+
moveUp: () => void;
|
|
21
|
+
moveDown: () => void;
|
|
22
|
+
activeItem: CommandPaletteResult | null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const MAX_RESULTS = 20;
|
|
26
|
+
|
|
27
|
+
export function useCommandPalette(
|
|
28
|
+
nodes: ArchFlowNode[],
|
|
29
|
+
hiddenCategories: Set<string>,
|
|
30
|
+
): CommandPaletteState {
|
|
31
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
32
|
+
const [query, setQueryRaw] = useState('');
|
|
33
|
+
const [activeIndex, setActiveIndex] = useState(0);
|
|
34
|
+
|
|
35
|
+
const toggle = useCallback(() => {
|
|
36
|
+
setIsOpen((prev) => {
|
|
37
|
+
if (!prev) {
|
|
38
|
+
setQueryRaw('');
|
|
39
|
+
setActiveIndex(0);
|
|
40
|
+
}
|
|
41
|
+
return !prev;
|
|
42
|
+
});
|
|
43
|
+
}, []);
|
|
44
|
+
|
|
45
|
+
const close = useCallback(() => {
|
|
46
|
+
setIsOpen(false);
|
|
47
|
+
}, []);
|
|
48
|
+
|
|
49
|
+
const setQuery = useCallback((q: string) => {
|
|
50
|
+
setQueryRaw(q);
|
|
51
|
+
setActiveIndex(0);
|
|
52
|
+
}, []);
|
|
53
|
+
|
|
54
|
+
const results = useMemo(() => {
|
|
55
|
+
const searchable = nodes.filter(
|
|
56
|
+
(n) => !n.hidden && !hiddenCategories.has(n.data.category),
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
if (!query.trim()) {
|
|
60
|
+
return searchable.slice(0, MAX_RESULTS).map((n) => ({
|
|
61
|
+
id: n.id,
|
|
62
|
+
label: n.data.label,
|
|
63
|
+
category: n.data.category,
|
|
64
|
+
description: n.data.description,
|
|
65
|
+
}));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const q = query.toLowerCase();
|
|
69
|
+
return searchable
|
|
70
|
+
.filter((n) => n.data.label.toLowerCase().includes(q))
|
|
71
|
+
.slice(0, MAX_RESULTS)
|
|
72
|
+
.map((n) => ({
|
|
73
|
+
id: n.id,
|
|
74
|
+
label: n.data.label,
|
|
75
|
+
category: n.data.category,
|
|
76
|
+
description: n.data.description,
|
|
77
|
+
}));
|
|
78
|
+
}, [nodes, hiddenCategories, query]);
|
|
79
|
+
|
|
80
|
+
const moveUp = useCallback(() => {
|
|
81
|
+
setActiveIndex((prev) => (prev <= 0 ? prev : prev - 1));
|
|
82
|
+
}, []);
|
|
83
|
+
|
|
84
|
+
const moveDown = useCallback(() => {
|
|
85
|
+
setActiveIndex((prev) => (prev >= results.length - 1 ? prev : prev + 1));
|
|
86
|
+
}, [results.length]);
|
|
87
|
+
|
|
88
|
+
const activeItem = results[activeIndex] ?? null;
|
|
89
|
+
|
|
90
|
+
return { isOpen, toggle, close, query, setQuery, results, activeIndex, moveUp, moveDown, activeItem };
|
|
91
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
import type { UseCase } from '../types.ts';
|
|
4
|
+
|
|
5
|
+
export interface FlowAnimationState {
|
|
6
|
+
activeStep: number;
|
|
7
|
+
flowNodeIds: string[] | null;
|
|
8
|
+
isPlaying: boolean;
|
|
9
|
+
play: () => void;
|
|
10
|
+
pause: () => void;
|
|
11
|
+
next: () => void;
|
|
12
|
+
prev: () => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const NOOP = () => {};
|
|
16
|
+
|
|
17
|
+
const IDLE_STATE: FlowAnimationState = {
|
|
18
|
+
activeStep: -1,
|
|
19
|
+
flowNodeIds: null,
|
|
20
|
+
isPlaying: false,
|
|
21
|
+
play: NOOP,
|
|
22
|
+
pause: NOOP,
|
|
23
|
+
next: NOOP,
|
|
24
|
+
prev: NOOP,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export function useFlowAnimation(
|
|
28
|
+
useCases: UseCase[],
|
|
29
|
+
selectedUseCase: string | null,
|
|
30
|
+
): FlowAnimationState {
|
|
31
|
+
const [activeStep, setActiveStep] = useState(0);
|
|
32
|
+
const [isPlaying, setIsPlaying] = useState(true);
|
|
33
|
+
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
34
|
+
|
|
35
|
+
const uc = selectedUseCase ? useCases.find((u) => u.id === selectedUseCase) : undefined;
|
|
36
|
+
const flow = uc?.flow;
|
|
37
|
+
const flowLength = flow?.length ?? 0;
|
|
38
|
+
|
|
39
|
+
// Reset on use case change
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
setActiveStep(0);
|
|
42
|
+
setIsPlaying(true);
|
|
43
|
+
}, [selectedUseCase]);
|
|
44
|
+
|
|
45
|
+
// Auto-advance timer
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
if (!flow || flowLength < 2 || !isPlaying) return;
|
|
48
|
+
|
|
49
|
+
timerRef.current = setTimeout(() => {
|
|
50
|
+
setActiveStep((prev) => {
|
|
51
|
+
const next = prev + 1;
|
|
52
|
+
return next >= flowLength ? 0 : next;
|
|
53
|
+
});
|
|
54
|
+
}, 800);
|
|
55
|
+
|
|
56
|
+
return () => {
|
|
57
|
+
if (timerRef.current !== null) {
|
|
58
|
+
clearTimeout(timerRef.current);
|
|
59
|
+
timerRef.current = null;
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
}, [flow, flowLength, isPlaying, activeStep]);
|
|
63
|
+
|
|
64
|
+
const play = useCallback(() => setIsPlaying(true), []);
|
|
65
|
+
const pause = useCallback(() => setIsPlaying(false), []);
|
|
66
|
+
|
|
67
|
+
const next = useCallback(() => {
|
|
68
|
+
setIsPlaying(false);
|
|
69
|
+
setActiveStep((prev) => (prev + 1 >= flowLength ? prev : prev + 1));
|
|
70
|
+
}, [flowLength]);
|
|
71
|
+
|
|
72
|
+
const prev = useCallback(() => {
|
|
73
|
+
setIsPlaying(false);
|
|
74
|
+
setActiveStep((prev) => (prev <= 0 ? 0 : prev - 1));
|
|
75
|
+
}, []);
|
|
76
|
+
|
|
77
|
+
if (!flow || flowLength < 2) {
|
|
78
|
+
return IDLE_STATE;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return { activeStep, flowNodeIds: flow, isPlaying, play, pause, next, prev };
|
|
82
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
|
|
3
|
+
interface KeyboardShortcutActions {
|
|
4
|
+
onTogglePalette: () => void;
|
|
5
|
+
onEscape: () => void;
|
|
6
|
+
isPaletteOpen: boolean;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function useKeyboardShortcuts(actions: KeyboardShortcutActions): void {
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
function handleKeyDown(e: KeyboardEvent) {
|
|
12
|
+
// Skip when focused on form elements
|
|
13
|
+
const tag = (e.target as HTMLElement).tagName;
|
|
14
|
+
if (tag === 'INPUT' || tag === 'SELECT' || tag === 'TEXTAREA') {
|
|
15
|
+
// Allow Escape even in input
|
|
16
|
+
if (e.key === 'Escape') {
|
|
17
|
+
actions.onEscape();
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Cmd/Ctrl+K: toggle palette
|
|
24
|
+
if (e.key === 'k' && (e.metaKey || e.ctrlKey)) {
|
|
25
|
+
e.preventDefault();
|
|
26
|
+
actions.onTogglePalette();
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (e.key === 'Escape') {
|
|
31
|
+
actions.onEscape();
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
window.addEventListener('keydown', handleKeyDown);
|
|
37
|
+
return () => window.removeEventListener('keydown', handleKeyDown);
|
|
38
|
+
}, [actions]);
|
|
39
|
+
}
|
|
@@ -3,6 +3,7 @@ import { useQueryState, parseAsString } from 'nuqs';
|
|
|
3
3
|
import type { Edge } from '@xyflow/react';
|
|
4
4
|
|
|
5
5
|
import type { ArchFlowNode, UseCase } from '../types.ts';
|
|
6
|
+
import { useFlowAnimation } from './useFlowAnimation.ts';
|
|
6
7
|
|
|
7
8
|
function isNodeActive(node: ArchFlowNode, activeIds: Set<string>): boolean {
|
|
8
9
|
if (node.data.isGroup && node.data.memberNodes) {
|
|
@@ -11,8 +12,20 @@ function isNodeActive(node: ArchFlowNode, activeIds: Set<string>): boolean {
|
|
|
11
12
|
return activeIds.has(node.id);
|
|
12
13
|
}
|
|
13
14
|
|
|
15
|
+
/** Resolve a node's effective ID for flow matching (group → first active member) */
|
|
16
|
+
function getFlowId(node: ArchFlowNode, flowSet: Set<string>): string | null {
|
|
17
|
+
if (flowSet.has(node.id)) return node.id;
|
|
18
|
+
if (node.data.isGroup && node.data.memberNodes) {
|
|
19
|
+
const member = node.data.memberNodes.find((m) => flowSet.has(m.id));
|
|
20
|
+
if (member) return member.id;
|
|
21
|
+
}
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
|
|
14
25
|
export function useUseCaseFilter(nodes: ArchFlowNode[], edges: Edge[], useCases: UseCase[]) {
|
|
15
26
|
const [selectedUseCase, setSelectedUseCase] = useQueryState('uc', parseAsString.withOptions({ history: 'replace' }));
|
|
27
|
+
const flowState = useFlowAnimation(useCases, selectedUseCase);
|
|
28
|
+
const { activeStep, flowNodeIds } = flowState;
|
|
16
29
|
|
|
17
30
|
const categories = useMemo(() => {
|
|
18
31
|
const set = new Set<string>();
|
|
@@ -27,19 +40,77 @@ export function useUseCaseFilter(nodes: ArchFlowNode[], edges: Edge[], useCases:
|
|
|
27
40
|
const uc = useCases.find((u) => u.id === selectedUseCase);
|
|
28
41
|
if (!uc) return nodes;
|
|
29
42
|
const activeIds = new Set(uc.nodeIds);
|
|
43
|
+
|
|
44
|
+
// No flow — static highlight (backward compat)
|
|
45
|
+
if (!flowNodeIds) {
|
|
46
|
+
return nodes.map((node) => {
|
|
47
|
+
if (node.hidden) return node;
|
|
48
|
+
const active = isNodeActive(node, activeIds);
|
|
49
|
+
return {
|
|
50
|
+
...node,
|
|
51
|
+
style: {
|
|
52
|
+
...node.style,
|
|
53
|
+
opacity: active ? 1 : 0.15,
|
|
54
|
+
transition: 'opacity 0.3s',
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Flow animation mode
|
|
61
|
+
const flowSet = new Set(flowNodeIds);
|
|
62
|
+
const activeFlowId = flowNodeIds[activeStep];
|
|
63
|
+
const passedIds = new Set(flowNodeIds.slice(0, activeStep));
|
|
64
|
+
|
|
30
65
|
return nodes.map((node) => {
|
|
31
66
|
if (node.hidden) return node;
|
|
32
67
|
const active = isNodeActive(node, activeIds);
|
|
68
|
+
if (!active) {
|
|
69
|
+
// Non-participating node
|
|
70
|
+
return {
|
|
71
|
+
...node,
|
|
72
|
+
style: { ...node.style, opacity: 0.08, transition: 'opacity 0.3s' },
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const flowId = getFlowId(node, flowSet);
|
|
77
|
+
if (!flowId) {
|
|
78
|
+
// In use case but not in flow — dim slightly
|
|
79
|
+
return {
|
|
80
|
+
...node,
|
|
81
|
+
style: { ...node.style, opacity: 0.3, transition: 'opacity 0.3s' },
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (flowId === activeFlowId) {
|
|
86
|
+
// Active step — glow + scale
|
|
87
|
+
return {
|
|
88
|
+
...node,
|
|
89
|
+
style: {
|
|
90
|
+
...node.style,
|
|
91
|
+
opacity: 1,
|
|
92
|
+
boxShadow: 'var(--shadow-node-flow-active)',
|
|
93
|
+
transform: 'scale(1.02)',
|
|
94
|
+
transition: 'opacity 0.3s, box-shadow 0.3s, transform 0.3s',
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (passedIds.has(flowId)) {
|
|
100
|
+
// Already passed
|
|
101
|
+
return {
|
|
102
|
+
...node,
|
|
103
|
+
style: { ...node.style, opacity: 1, transition: 'opacity 0.3s' },
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Not yet reached
|
|
33
108
|
return {
|
|
34
109
|
...node,
|
|
35
|
-
style: {
|
|
36
|
-
...node.style,
|
|
37
|
-
opacity: active ? 1 : 0.15,
|
|
38
|
-
transition: 'opacity 0.3s',
|
|
39
|
-
},
|
|
110
|
+
style: { ...node.style, opacity: 0.3, transition: 'opacity 0.3s' },
|
|
40
111
|
};
|
|
41
112
|
});
|
|
42
|
-
}, [nodes, selectedUseCase, useCases]);
|
|
113
|
+
}, [nodes, selectedUseCase, useCases, flowNodeIds, activeStep]);
|
|
43
114
|
|
|
44
115
|
const filteredEdges = useMemo(() => {
|
|
45
116
|
if (!selectedUseCase) return edges;
|
|
@@ -55,19 +126,93 @@ export function useUseCaseFilter(nodes: ArchFlowNode[], edges: Edge[], useCases:
|
|
|
55
126
|
}
|
|
56
127
|
}
|
|
57
128
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
...edge
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
129
|
+
// No flow — static highlight (backward compat)
|
|
130
|
+
if (!flowNodeIds) {
|
|
131
|
+
return edges.map((edge) => ({
|
|
132
|
+
...edge,
|
|
133
|
+
style: {
|
|
134
|
+
...edge.style,
|
|
135
|
+
opacity: activeNodeIds.has(edge.source) && activeNodeIds.has(edge.target) ? 1 : 0.08,
|
|
136
|
+
transition: 'opacity 0.3s',
|
|
137
|
+
},
|
|
138
|
+
labelStyle: {
|
|
139
|
+
...((edge.labelStyle as Record<string, unknown>) ?? {}),
|
|
140
|
+
opacity: activeNodeIds.has(edge.source) && activeNodeIds.has(edge.target) ? 1 : 0,
|
|
141
|
+
},
|
|
142
|
+
}));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Flow animation mode — build flow edge lookup
|
|
146
|
+
// A flow edge is one connecting flow[i] → flow[i+1]
|
|
147
|
+
const flowEdgeMap = new Map<string, 'active' | 'passed' | 'upcoming'>();
|
|
148
|
+
for (let i = 0; i < flowNodeIds.length - 1; i++) {
|
|
149
|
+
const src = flowNodeIds[i];
|
|
150
|
+
const tgt = flowNodeIds[i + 1];
|
|
151
|
+
// Check both edge ID conventions
|
|
152
|
+
const key1 = `${src}->${tgt}`;
|
|
153
|
+
const key2 = `${tgt}->${src}`;
|
|
154
|
+
let status: 'active' | 'passed' | 'upcoming';
|
|
155
|
+
if (i + 1 === activeStep) {
|
|
156
|
+
status = 'active';
|
|
157
|
+
} else if (i + 1 <= activeStep) {
|
|
158
|
+
status = 'passed';
|
|
159
|
+
} else {
|
|
160
|
+
status = 'upcoming';
|
|
161
|
+
}
|
|
162
|
+
flowEdgeMap.set(key1, status);
|
|
163
|
+
flowEdgeMap.set(key2, status);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return edges.map((edge) => {
|
|
167
|
+
const bothActive = activeNodeIds.has(edge.source) && activeNodeIds.has(edge.target);
|
|
168
|
+
if (!bothActive) {
|
|
169
|
+
return {
|
|
170
|
+
...edge,
|
|
171
|
+
style: { ...edge.style, opacity: 0.08, transition: 'opacity 0.3s' },
|
|
172
|
+
className: '',
|
|
173
|
+
labelStyle: {
|
|
174
|
+
...((edge.labelStyle as Record<string, unknown>) ?? {}),
|
|
175
|
+
opacity: 0,
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const flowStatus = flowEdgeMap.get(edge.id);
|
|
181
|
+
if (flowStatus === 'active') {
|
|
182
|
+
return {
|
|
183
|
+
...edge,
|
|
184
|
+
style: { ...edge.style, opacity: 1, stroke: undefined, strokeWidth: undefined },
|
|
185
|
+
className: 'flow-edge-active',
|
|
186
|
+
labelStyle: {
|
|
187
|
+
...((edge.labelStyle as Record<string, unknown>) ?? {}),
|
|
188
|
+
opacity: 1,
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
if (flowStatus === 'passed') {
|
|
193
|
+
return {
|
|
194
|
+
...edge,
|
|
195
|
+
style: { ...edge.style, opacity: 1, transition: 'opacity 0.3s' },
|
|
196
|
+
className: '',
|
|
197
|
+
labelStyle: {
|
|
198
|
+
...((edge.labelStyle as Record<string, unknown>) ?? {}),
|
|
199
|
+
opacity: 1,
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// In use case but not a flow edge, or upcoming flow edge
|
|
205
|
+
return {
|
|
206
|
+
...edge,
|
|
207
|
+
style: { ...edge.style, opacity: 0.3, transition: 'opacity 0.3s' },
|
|
208
|
+
className: '',
|
|
209
|
+
labelStyle: {
|
|
210
|
+
...((edge.labelStyle as Record<string, unknown>) ?? {}),
|
|
211
|
+
opacity: 0.3,
|
|
212
|
+
},
|
|
213
|
+
};
|
|
214
|
+
});
|
|
215
|
+
}, [edges, nodes, selectedUseCase, useCases, flowNodeIds, activeStep]);
|
|
216
|
+
|
|
217
|
+
return { selectedUseCase, setSelectedUseCase, categories, filteredNodes, filteredEdges, flowInfo: flowState };
|
|
73
218
|
}
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
--shadow-panel: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px -1px rgba(0, 0, 0, 0.1);
|
|
34
34
|
--shadow-node: 0 1px 3px rgba(0, 0, 0, 0.1);
|
|
35
35
|
--shadow-node-selected: 0 0 0 2px rgba(37, 99, 235, 0.25);
|
|
36
|
+
--shadow-node-flow-active: 0 0 12px 2px rgba(59, 130, 246, 0.5);
|
|
36
37
|
|
|
37
38
|
/* Category colors (light) */
|
|
38
39
|
--cat-controller-bg: #dbeafe;
|
|
@@ -50,6 +51,9 @@
|
|
|
50
51
|
--cat-model-bg: #fee2e2;
|
|
51
52
|
--cat-model-border: #ef4444;
|
|
52
53
|
--cat-model-text: #991b1b;
|
|
54
|
+
--cat-database-bg: #fef3c7;
|
|
55
|
+
--cat-database-border: #d97706;
|
|
56
|
+
--cat-database-text: #92400e;
|
|
53
57
|
--cat-external-bg: #f3f4f6;
|
|
54
58
|
--cat-external-border: #6b7280;
|
|
55
59
|
--cat-external-text: #374151;
|
|
@@ -88,6 +92,7 @@
|
|
|
88
92
|
--shadow-panel: 0 1px 3px 0 rgba(0, 0, 0, 0.4), 0 1px 2px -1px rgba(0, 0, 0, 0.4);
|
|
89
93
|
--shadow-node: 0 1px 3px rgba(0, 0, 0, 0.3);
|
|
90
94
|
--shadow-node-selected: 0 0 0 2px rgba(96, 165, 250, 0.3);
|
|
95
|
+
--shadow-node-flow-active: 0 0 12px 2px rgba(96, 165, 250, 0.4);
|
|
91
96
|
|
|
92
97
|
/* Category colors (dark) */
|
|
93
98
|
--cat-controller-bg: #1e293b;
|
|
@@ -105,6 +110,9 @@
|
|
|
105
110
|
--cat-model-bg: #2b1111;
|
|
106
111
|
--cat-model-border: #f87171;
|
|
107
112
|
--cat-model-text: #fca5a5;
|
|
113
|
+
--cat-database-bg: #2b1f06;
|
|
114
|
+
--cat-database-border: #fbbf24;
|
|
115
|
+
--cat-database-text: #fde68a;
|
|
108
116
|
--cat-external-bg: #1e2330;
|
|
109
117
|
--cat-external-border: #9ca3af;
|
|
110
118
|
--cat-external-text: #d1d5db;
|
|
@@ -119,6 +127,17 @@
|
|
|
119
127
|
--cat-fallback-text: #d6d3d1;
|
|
120
128
|
}
|
|
121
129
|
|
|
130
|
+
/* ─── Flow Animation ─── */
|
|
131
|
+
@keyframes dash-flow {
|
|
132
|
+
to { stroke-dashoffset: -20; }
|
|
133
|
+
}
|
|
134
|
+
.react-flow__edge.flow-edge-active .react-flow__edge-path {
|
|
135
|
+
stroke: var(--color-interactive-primary) !important;
|
|
136
|
+
stroke-width: 2.5 !important;
|
|
137
|
+
stroke-dasharray: 8 4;
|
|
138
|
+
animation: dash-flow 0.6s linear infinite;
|
|
139
|
+
}
|
|
140
|
+
|
|
122
141
|
html,
|
|
123
142
|
body,
|
|
124
143
|
#root {
|
|
@@ -62,7 +62,8 @@ const CATEGORY_META = {
|
|
|
62
62
|
service: { label: 'Service', icon: '\u{2699}\u{FE0F}', color: { bg: '#dcfce7', border: '#22c55e', text: '#166534' } },
|
|
63
63
|
port: { label: 'Port', icon: '\u{1F50C}', color: { bg: '#ede9fe', border: '#8b5cf6', text: '#5b21b6' } },
|
|
64
64
|
adapter: { label: 'Adapter', icon: '\u{1F527}', color: { bg: '#ffedd5', border: '#f97316', text: '#9a3412' } },
|
|
65
|
-
model: { label: '
|
|
65
|
+
model: { label: 'Entity', icon: '\u{1F4BE}', color: { bg: '#fee2e2', border: '#ef4444', text: '#991b1b' } },
|
|
66
|
+
database: { label: 'DB / Infra', icon: '\u{1F5C4}\u{FE0F}', color: { bg: '#fef3c7', border: '#d97706', text: '#92400e' } },
|
|
66
67
|
external: { label: 'External', icon: '\u{2601}\u{FE0F}', color: { bg: '#f3f4f6', border: '#6b7280', text: '#374151' } },
|
|
67
68
|
job: { label: 'Job', icon: '\u{23F0}', color: { bg: '#fef9c3', border: '#eab308', text: '#854d0e' } },
|
|
68
69
|
dto: { label: 'DTO', icon: '\u{1F4E6}', color: { bg: '#cffafe', border: '#06b6d4', text: '#155e75' } },
|
|
@@ -76,7 +77,7 @@ function getMeta(category: string): CategoryMeta {
|
|
|
76
77
|
return CATEGORY_META[category as StandardCategory] ?? FALLBACK_META;
|
|
77
78
|
}
|
|
78
79
|
|
|
79
|
-
const STANDARD_CATEGORIES = ['controller', 'service', 'port', 'adapter', 'model', 'external', 'job', 'dto'] as const;
|
|
80
|
+
const STANDARD_CATEGORIES = ['controller', 'service', 'port', 'adapter', 'model', 'database', 'external', 'job', 'dto'] as const;
|
|
80
81
|
|
|
81
82
|
export function getCategoryColors(category: string): CategoryStyle {
|
|
82
83
|
const key = (STANDARD_CATEGORIES as readonly string[]).includes(category) ? category : 'fallback';
|