@polymorphism-tech/morph-spec 8.38.0 → 8.41.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/CLAUDE.md +20 -9
- package/README.md +2 -2
- package/bin/morph-spec.js +60 -43
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/CLAUDE.md +4 -3
- package/framework/MORPH.md +10 -9
- package/framework/agents.json +43 -26
- package/framework/docs/CLI.md +7 -5
- package/framework/evals/rubrics/code-quality.md +4 -4
- package/framework/evals/rubrics/condensed.md +2 -2
- package/framework/hooks/README.md +1 -1
- package/framework/hooks/claude-code/core/telemetry-log.js +39 -8
- package/framework/hooks/claude-code/core/worktree-create.js +22 -6
- package/framework/hooks/shared/git-paths.js +38 -5
- package/framework/mcp/registry.json +58 -66
- package/framework/rules/infrastructure-standards.md +0 -1
- package/framework/rules/morph-workflow.md +9 -4
- package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
- package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
- package/framework/skills/core/morph-apply/SKILL.md +59 -25
- package/framework/skills/core/morph-brainstorming/SKILL.md +1 -1
- package/framework/skills/core/morph-eval/SKILL.md +1 -1
- package/framework/skills/core/morph-hotfix/SKILL.md +2 -2
- package/framework/skills/core/morph-implement/SKILL.md +48 -24
- package/framework/skills/core/morph-media/SKILL.md +2 -2
- package/framework/skills/core/morph-plan/SKILL.md +10 -3
- package/framework/skills/core/morph-proposal/SKILL.md +1 -1
- package/framework/skills/core/morph-review/SKILL.md +1 -1
- package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
- package/framework/skills/core/morph-standards/SKILL.md +1 -1
- package/framework/skills/core/morph-uiux/SKILL.md +7 -6
- package/framework/standards/PROVENANCE.json +4 -4
- package/framework/standards/STANDARDS.json +9 -9
- package/framework/standards/ai-agents/context-providers.md +16 -26
- package/framework/standards/ai-agents/durable-workflows-hitl.md +15 -21
- package/framework/standards/ai-agents/modalities-image-gen.md +21 -25
- package/framework/standards/ai-agents/modalities-speech-to-text.md +4 -15
- package/framework/standards/ai-agents/modalities-text-to-speech.md +2 -11
- package/framework/standards/ai-agents/modalities-vision.md +1 -18
- package/framework/standards/ai-agents/prompt-sources.md +0 -4
- package/framework/standards/architecture/code-comments.md +52 -16
- package/framework/standards/backend/dotnet/testing.md +4 -2
- package/framework/standards/backend/integrations/neon-auth/neon-auth.md +63 -50
- package/framework/standards/frontend/design-system/ai-image-generation.md +42 -28
- package/framework/standards/frontend/design-system/ai-video-generation.md +24 -9
- package/framework/standards/frontend/nextjs/testing.md +4 -2
- package/framework/standards/integration/mcp/mcp-tools.md +64 -126
- package/framework/templates/code/dotnet/abstractions/result.cs +0 -2
- package/framework/templates/code/dotnet/ai-agents/AgentBootstrap.cs.template +26 -39
- package/framework/templates/code/dotnet/ai-agents/EmbeddingService.cs.template +28 -68
- package/framework/templates/code/dotnet/ai-agents/ModelRegistry.cs.template +99 -117
- package/framework/templates/code/dotnet/ai-agents/PromptRepository.cs.template +2 -44
- package/framework/templates/code/dotnet/ai-agents/ResilientToolMiddleware.cs.template +40 -61
- package/framework/templates/code/dotnet/ai-agents/VectorSearchTool.cs.template +54 -107
- package/framework/templates/code/dotnet/ai-agents/model-registry.json.template +53 -58
- package/framework/templates/code/dotnet/contracts/contracts-vsa.cs +192 -281
- package/framework/templates/code/dotnet/database/migration.cs +49 -83
- package/framework/templates/code/dotnet/database/vector-entity.cs +2 -46
- package/framework/templates/code/dotnet/extensions/result-extensions.cs +0 -2
- package/framework/templates/code/dotnet/feature/validator.cs.hbs +11 -15
- package/framework/templates/code/dotnet/jobs/agent.cs +3 -51
- package/framework/templates/code/dotnet/jobs/job.cs +2 -97
- package/framework/templates/code/dotnet/test.cs +2 -87
- package/framework/templates/code/typescript/contracts.ts +140 -167
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentApi.cs +1 -48
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentFactory.cs +3 -79
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentFactoryOptions.cs +1 -70
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentSpec.cs +1 -47
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentSpecOptions.cs +2 -99
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/AiKitAssemblyMarker.cs +0 -6
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Conversation/ConversationStore.cs +3 -88
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Conversation/IConversationStore.cs +1 -41
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Conversation/InMemoryConversationStore.cs +0 -21
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/CountingChatClient.cs +1 -25
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalAssertion.cs +2 -63
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalCacheMissException.cs +0 -30
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalCacheOptions.cs +1 -52
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalCacheStamp.cs +3 -78
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalFixture.cs +4 -109
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalMode.cs +0 -22
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalQualityEvaluators.cs +1 -36
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalScenario.cs +2 -57
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalSummary.cs +0 -75
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/OfflineChatClient.cs +2 -59
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/ShortPathResponseCache.cs +4 -111
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/IMediaGenerator.cs +0 -34
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/IMediaProvider.cs +1 -57
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaArtifact.cs +0 -19
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaDisclosure.cs +2 -74
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaGenerator.cs +2 -33
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaIdempotencyKey.cs +1 -43
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaJob.cs +1 -96
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaJobId.cs +0 -13
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaJobState.cs +0 -27
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaPricing.cs +0 -33
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaProvenance.cs +0 -31
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaRequest.cs +3 -75
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaResult.cs +0 -42
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaReview.cs +0 -39
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaSeams.cs +1 -55
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaStore.cs +3 -59
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MorphMedia.cs +0 -46
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/Providers/GoogleImageProvider.cs +3 -28
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/Providers/OpenAIImageProvider.cs +7 -61
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/Providers/VeoVideoProvider.cs +2 -37
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Middleware/IUsageCostSink.cs +1 -23
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Middleware/UsageCostMiddleware.cs +2 -82
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Middleware/UsageCostRecord.cs +1 -68
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Observability/MorphOpenTelemetryExtensions.cs +1 -62
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Observability/MorphTelemetryOptions.cs +2 -62
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/IProviderClientFactory.cs +1 -25
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/ModelAlias.cs +0 -124
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/ModelRegistry.cs +1 -73
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Rag/DeltaIngest.cs +1 -66
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Rag/DeltaPlan.cs +0 -41
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/AgentFactoryTests.cs +2 -112
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/AgentSpecTests.cs +2 -43
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/ConversationStoreTests.cs +5 -110
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/DeltaIngestTests.cs +7 -103
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalAssertionTests.cs +1 -42
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalCacheMissTests.cs +1 -30
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalCacheStampTests.cs +1 -19
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalFailuresTests.cs +0 -48
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalFixtureTests.cs +1 -29
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalRefreshTests.cs +2 -38
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalWorkspace.cs +1 -14
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/KitEvals.cs +4 -69
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/OfflineChatClientTests.cs +1 -10
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeChatClient.cs +1 -57
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeChatClientTests.cs +0 -26
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeClock.cs +1 -19
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeMediaProvider.cs +0 -32
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeProviderClientFactory.cs +0 -14
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeUsageCostSink.cs +0 -24
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/InMemoryMediaStore.cs +0 -32
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaAccessibilityTests.cs +0 -37
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaAliasIsNotAnAgentTests.cs +0 -24
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaDisclosureTests.cs +0 -78
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaGeneratorTests.cs +0 -53
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaIdempotencyKeyTests.cs +0 -44
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaJobStateTests.cs +0 -44
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaReviewTests.cs +0 -19
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaStoreTests.cs +0 -24
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/ModelRegistryTests.cs +6 -48
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/SkeletonSmokeTests.cs +1 -13
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/TelemetryTests.cs +2 -71
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/UsageCostMiddlewareTests.cs +5 -100
- package/framework/templates/frontend/nextjs/Dockerfile.nextjs.hbs +39 -43
- package/framework/templates/frontend/nextjs/env.mjs.hbs +29 -32
- package/framework/templates/frontend/nextjs/page.tsx.hbs +21 -22
- package/framework/templates/frontend/nextjs/use-feature.ts.hbs +42 -54
- package/framework/templates/integrations/asaas-client.cs +332 -387
- package/framework/templates/integrations/asaas-webhook.cs +290 -351
- package/framework/templates/integrations/neon-auth-config.cs +9 -105
- package/framework/templates/project/validate.js +97 -120
- package/package.json +2 -2
- package/src/commands/dashboard/dashboard.js +80 -53
- package/src/commands/mcp/mcp-setup.js +37 -9
- package/src/commands/project/doctor.js +12 -6
- package/src/commands/project/init.js +6 -3
- package/src/commands/project/install-plugin-cmd.js +1 -1
- package/src/commands/project/update.js +5 -0
- package/src/commands/project/worktree.js +60 -4
- package/src/commands/state/gate-check.js +5 -0
- package/src/commands/tasks/dag.js +27 -4
- package/src/commands/validation/verify.js +22 -38
- package/src/dashboard/app/App.jsx +80 -16
- package/src/dashboard/app/components/ActivityFeed.jsx +172 -0
- package/src/dashboard/app/components/CommandPalette.jsx +155 -0
- package/src/dashboard/app/components/DetailPanel.jsx +47 -11
- package/src/dashboard/app/components/EntityLink.jsx +55 -0
- package/src/dashboard/app/components/ErrorBoundary.jsx +54 -0
- package/src/dashboard/app/components/FlowGraph.jsx +19 -17
- package/src/dashboard/app/components/KnowledgeMarkdown.jsx +53 -0
- package/src/dashboard/app/components/Markdown.jsx +47 -0
- package/src/dashboard/app/components/PhaseRail.jsx +111 -0
- package/src/dashboard/app/components/SearchFilter.jsx +40 -22
- package/src/dashboard/app/components/Sidebar.jsx +146 -37
- package/src/dashboard/app/hooks/useMorphQuery.js +54 -0
- package/src/dashboard/app/hooks/usePref.js +38 -0
- package/src/dashboard/app/lib/format.js +69 -0
- package/src/dashboard/app/lib/phases.js +56 -0
- package/src/dashboard/app/lib/project.js +50 -0
- package/src/dashboard/app/pages/Activity.jsx +24 -0
- package/src/dashboard/app/pages/AgentMap.jsx +371 -370
- package/src/dashboard/app/pages/FeatureDetail.jsx +604 -0
- package/src/dashboard/app/pages/Features.jsx +400 -0
- package/src/dashboard/app/pages/Guide.jsx +146 -0
- package/src/dashboard/app/pages/HookInspector.jsx +357 -322
- package/src/dashboard/app/pages/PhasePipeline.jsx +277 -408
- package/src/dashboard/app/pages/Portfolio.jsx +156 -0
- package/src/dashboard/app/pages/Rubrics.jsx +101 -0
- package/src/dashboard/app/pages/RulesMap.jsx +300 -273
- package/src/dashboard/app/pages/SkillsLibrary.jsx +483 -366
- package/src/dashboard/app/pages/Standards.jsx +407 -332
- package/src/dashboard/app/styles/index.css +187 -47
- package/src/dashboard/index.html +14 -12
- package/src/dashboard/package.json +32 -24
- package/src/dashboard/scanner/activity-scanner.js +351 -0
- package/src/dashboard/scanner/agents-scanner.js +59 -1
- package/src/dashboard/scanner/docs-scanner.js +59 -0
- package/src/dashboard/scanner/feature-status.js +10 -0
- package/src/dashboard/scanner/features-scanner.js +360 -0
- package/src/dashboard/scanner/index.js +14 -7
- package/src/dashboard/scanner/phases-scanner.js +14 -15
- package/src/dashboard/scanner/portfolio.js +153 -0
- package/src/dashboard/scanner/project-api.js +195 -0
- package/src/dashboard/scanner/rules-scanner.js +42 -37
- package/src/dashboard/scanner/skills-scanner.js +105 -43
- package/src/dashboard/scanner/standards-scanner.js +2 -0
- package/src/dashboard/vite-plugin-morph-data.js +134 -17
- package/src/lib/detectors/claude-config-detector.js +2 -7
- package/src/lib/detectors/index.js +5 -1
- package/src/lib/evaluator-report.js +22 -0
- package/src/lib/generators/settings-generator.js +208 -209
- package/src/lib/installers/mcp-installer.js +724 -719
- package/src/lib/installers/memory-installer.js +1 -2
- package/src/lib/projects/registry.js +154 -0
- package/src/lib/risk-detector.js +13 -0
- package/src/lib/tasks/tasks-json-parser.js +21 -0
- package/src/lib/validators/code-budget/code-budget-validator.js +32 -4
- package/src/lib/worktree/env-allocator.js +9 -3
- package/src/scripts/install-plugin.js +1 -1
- package/src/scripts/setup-infra.js +2 -4
- package/src/utils/agents-installer.js +108 -13
- package/src/utils/banner.js +1 -1
- package/src/commands/index.js +0 -13
- package/src/commands/project/checkpoint.js +0 -209
- package/src/core/state/phase-state-machine.js +0 -197
- package/src/dashboard/app/hooks/useDetailPanel.js +0 -15
- package/src/dashboard/app/pages/LiveState.jsx +0 -206
- package/src/dashboard/scanner/squads-scanner.js +0 -13
- package/src/dashboard/scanner/state-scanner.js +0 -85
- package/src/lib/generators/index.js +0 -10
- package/src/lib/hooks/hook-executor.js +0 -167
- package/src/lib/validators/checkpoint-persistence.js +0 -39
- package/src/lib/validators/shared/emit-validator-dispatch.js +0 -14
- package/src/lib/validators/shared/index.js +0 -12
- package/src/lib/validators/shared/issue-counter.js +0 -14
- package/src/lib/validators/shared/result-formatter.js +0 -124
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { Link, useParams } from 'react-router-dom';
|
|
3
|
+
import {
|
|
4
|
+
CheckCircle, XCircle, CircleDashed, GitBranch, Archive, HouseSimple, CaretRight, MagnifyingGlass, WarningCircle,
|
|
5
|
+
Rows, Kanban, PauseCircle,
|
|
6
|
+
} from '@phosphor-icons/react';
|
|
7
|
+
import PhaseRail from '../components/PhaseRail.jsx';
|
|
8
|
+
import { useMorphQuery } from '../hooks/useMorphQuery.js';
|
|
9
|
+
import { usePref, useNow } from '../hooks/usePref.js';
|
|
10
|
+
import { PHASES, phaseMeta, LOCATION_LABEL } from '../lib/phases.js';
|
|
11
|
+
import { timeAgo } from '../lib/format.js';
|
|
12
|
+
import { apiBase, featureHref } from '../lib/project.js';
|
|
13
|
+
import { isFinished } from '../../scanner/feature-status.js';
|
|
14
|
+
|
|
15
|
+
const COMPLETED_PAGE = 30;
|
|
16
|
+
const BOARD_DONE_LIMIT = 8;
|
|
17
|
+
/** An event this recent means an agent is working on the feature right now. */
|
|
18
|
+
export const WORKING_MS = 3 * 60 * 1000;
|
|
19
|
+
|
|
20
|
+
const LOCATION_ICON = { worktree: GitBranch, archive: Archive, root: HouseSimple };
|
|
21
|
+
const SORTS = {
|
|
22
|
+
updated: { label: 'Mais recentes', cmp: (a, b) => (lastTouch(b) || '').localeCompare(lastTouch(a) || '') },
|
|
23
|
+
name: { label: 'Nome', cmp: (a, b) => a.name.localeCompare(b.name) },
|
|
24
|
+
score: { label: 'Score', cmp: (a, b) => (b.score ?? b.legacyScore ?? -1) - (a.score ?? a.legacyScore ?? -1) },
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
function lastTouch(f) {
|
|
28
|
+
const a = f.lastActivity?.ts || '';
|
|
29
|
+
const b = f.updatedAt || '';
|
|
30
|
+
return a > b ? a : b;
|
|
31
|
+
}
|
|
32
|
+
export function isWorking(f, now = Date.now()) {
|
|
33
|
+
const t = Date.parse(f.lastActivity?.ts || '');
|
|
34
|
+
return !isFinished(f) && !Number.isNaN(t) && now - t < WORKING_MS;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function VerifyMark({ verification }) {
|
|
38
|
+
if (!verification?.status) return <CircleDashed weight="light" className="text-[var(--color-faint)]" size={16} aria-label="sem verify" />;
|
|
39
|
+
return verification.status === 'pass'
|
|
40
|
+
? <CheckCircle weight="light" className="text-[var(--color-ok)]" size={16} aria-label="último verify passou" />
|
|
41
|
+
: <XCircle weight="light" className="text-[var(--color-bad)]" size={16} aria-label="último verify falhou" />;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function TaskMeter({ tasks, wide = false }) {
|
|
45
|
+
if (!tasks?.total) return <span className="text-xs text-[var(--color-faint)]">sem tasks</span>;
|
|
46
|
+
const pct = Math.round((tasks.done / tasks.total) * 100);
|
|
47
|
+
return (
|
|
48
|
+
<div className="flex items-center gap-2" title={`${tasks.done} de ${tasks.total} tasks concluídas`}>
|
|
49
|
+
<div className={`h-1 overflow-hidden rounded-full bg-white/[0.07] ${wide ? 'flex-1' : 'w-12'}`}>
|
|
50
|
+
<div className="h-full rounded-full bg-[var(--color-fg)]/70" style={{ width: `${pct}%` }} />
|
|
51
|
+
</div>
|
|
52
|
+
<span className="font-mono text-xs tabular-nums text-[var(--color-dim)]">{tasks.done}/{tasks.total}</span>
|
|
53
|
+
</div>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function WorkingDot() {
|
|
58
|
+
return (
|
|
59
|
+
<span className="relative inline-flex h-2 w-2 shrink-0" title="o agente está trabalhando agora" aria-label="trabalhando agora">
|
|
60
|
+
<span className="absolute inset-0 animate-ping rounded-full bg-[var(--color-phase-implement)] opacity-60 motion-reduce:hidden" />
|
|
61
|
+
<span className="relative h-2 w-2 rounded-full bg-[var(--color-phase-implement)]" />
|
|
62
|
+
</span>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function stageText(f) {
|
|
67
|
+
if (f.status === 'completed') return 'Concluída';
|
|
68
|
+
if (f.status === 'abandoned') return 'Abandonada';
|
|
69
|
+
if (f.location === 'archive') return 'Arquivada';
|
|
70
|
+
const paused = f.gates.find((g) => g.paused);
|
|
71
|
+
if (paused) return `Esperando você no gate de ${phaseMeta(paused.gate).label.toLowerCase()}`;
|
|
72
|
+
if (f.inEvaluation) return 'Em avaliação independente';
|
|
73
|
+
return phaseMeta(f.phase).label;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** The second line of a feature: what just happened while it is live, else where it stands. */
|
|
77
|
+
function subline(f, now) {
|
|
78
|
+
if (!isFinished(f) && f.lastActivity?.title) {
|
|
79
|
+
return <>{isWorking(f, now) && <WorkingDot />}<span className="truncate">{f.lastActivity.title}, {timeAgo(f.lastActivity.titleTs, now)}</span></>;
|
|
80
|
+
}
|
|
81
|
+
return <span className="truncate">{stageText(f)}{f.description ? `. ${f.description}` : ''}</span>;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function FeatureRow({ f, now, project, projectName }) {
|
|
85
|
+
const LocIcon = LOCATION_ICON[f.location] || HouseSimple;
|
|
86
|
+
const score = f.score ?? f.legacyScore;
|
|
87
|
+
return (
|
|
88
|
+
<li>
|
|
89
|
+
<Link
|
|
90
|
+
to={featureHref(project, f.name)}
|
|
91
|
+
className="group fluid grid grid-cols-1 gap-3 px-5 py-4 transition-colors hover:bg-white/[0.025] md:grid-cols-[minmax(0,1fr)_14rem_6.5rem_1.5rem_2.5rem_6.5rem_1.75rem] md:items-center md:gap-5"
|
|
92
|
+
>
|
|
93
|
+
<div className="min-w-0">
|
|
94
|
+
<div className="flex items-center gap-2">
|
|
95
|
+
<span className="truncate font-medium text-[var(--color-fg)]">{f.name}</span>
|
|
96
|
+
{projectName && <span className="shrink-0 text-xs text-[var(--color-faint)]">{projectName}</span>}
|
|
97
|
+
{f.workType && f.workType !== 'feature' && (
|
|
98
|
+
<span className="shrink-0 rounded-full px-2 py-px text-[11px] text-[var(--color-dim)] shadow-[inset_0_0_0_1px_var(--color-hair-strong)]">{f.workType}</span>
|
|
99
|
+
)}
|
|
100
|
+
{f.rootStale && (
|
|
101
|
+
<WarningCircle weight="light" size={15} className="shrink-0 text-[var(--color-warn)]" aria-label="cópia da raiz possivelmente desatualizada" />
|
|
102
|
+
)}
|
|
103
|
+
</div>
|
|
104
|
+
<p className="mt-0.5 flex items-center gap-2 text-sm text-[var(--color-dim)]">{subline(f, now)}</p>
|
|
105
|
+
</div>
|
|
106
|
+
<PhaseRail feature={f} />
|
|
107
|
+
{/* One line on a phone; on desktop the wrapper dissolves into the row's grid. */}
|
|
108
|
+
<div className="flex items-center gap-5 md:contents">
|
|
109
|
+
<TaskMeter tasks={f.tasks} />
|
|
110
|
+
<VerifyMark verification={f.verification} />
|
|
111
|
+
<span className="font-mono text-sm tabular-nums text-[var(--color-dim)]" title={score != null ? 'score do avaliador' : 'sem score'}>
|
|
112
|
+
{score != null ? score : '—'}
|
|
113
|
+
</span>
|
|
114
|
+
<span className="flex items-center gap-1.5 text-xs text-[var(--color-faint)]" title={LOCATION_LABEL[f.location]}>
|
|
115
|
+
<LocIcon weight="light" size={14} />
|
|
116
|
+
{timeAgo(lastTouch(f), now)}
|
|
117
|
+
</span>
|
|
118
|
+
</div>
|
|
119
|
+
<span className="fluid hidden h-7 w-7 items-center justify-center rounded-full bg-white/[0.04] text-[var(--color-dim)] transition-transform group-hover:translate-x-0.5 group-hover:bg-white/[0.08] group-hover:text-[var(--color-fg)] md:flex">
|
|
120
|
+
<CaretRight weight="light" size={14} />
|
|
121
|
+
</span>
|
|
122
|
+
</Link>
|
|
123
|
+
</li>
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function Section({ title, hint, features, now, project }) {
|
|
128
|
+
return (
|
|
129
|
+
<section>
|
|
130
|
+
<div className="mb-3 flex items-baseline justify-between px-1">
|
|
131
|
+
<h2 className="text-sm font-medium text-[var(--color-fg)]">{title}</h2>
|
|
132
|
+
{hint && <span className="text-xs text-[var(--color-faint)]">{hint}</span>}
|
|
133
|
+
</div>
|
|
134
|
+
<div className="bezel">
|
|
135
|
+
<ul className="bezel-core divide-y divide-[var(--color-hair)] overflow-hidden">
|
|
136
|
+
{features.map((f) => <FeatureRow key={f.name} f={f} now={now} project={project} />)}
|
|
137
|
+
</ul>
|
|
138
|
+
</div>
|
|
139
|
+
</section>
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function BoardCard({ f, now, project }) {
|
|
144
|
+
const score = f.score ?? f.legacyScore;
|
|
145
|
+
const paused = f.gates.find((g) => g.paused);
|
|
146
|
+
return (
|
|
147
|
+
<li>
|
|
148
|
+
<Link
|
|
149
|
+
to={featureHref(project, f.name)}
|
|
150
|
+
className={[
|
|
151
|
+
'fluid block rounded-xl bg-[var(--color-raised)] px-3.5 py-3 transition-[transform,box-shadow] hover:-translate-y-px active:scale-[0.99]',
|
|
152
|
+
paused ? 'shadow-[inset_0_0_0_1px_var(--color-warn)]' : 'shadow-[inset_0_0_0_1px_var(--color-hair),inset_0_1px_0_rgb(255_255_255_/_0.05)] hover:shadow-[inset_0_0_0_1px_var(--color-hair-strong)]',
|
|
153
|
+
].join(' ')}
|
|
154
|
+
>
|
|
155
|
+
<div className="flex items-start justify-between gap-2">
|
|
156
|
+
<span className="min-w-0 break-words text-sm font-medium leading-snug text-[var(--color-fg)]">{f.name}</span>
|
|
157
|
+
{isWorking(f, now) ? <span className="mt-1.5"><WorkingDot /></span> : null}
|
|
158
|
+
</div>
|
|
159
|
+
{paused && (
|
|
160
|
+
<p className="mt-1.5 flex items-center gap-1.5 text-xs text-[var(--color-warn)]">
|
|
161
|
+
<PauseCircle weight="light" size={14} /> esperando você
|
|
162
|
+
</p>
|
|
163
|
+
)}
|
|
164
|
+
{!paused && f.inEvaluation && <p className="mt-1.5 text-xs text-[var(--color-phase-review)]">em avaliação</p>}
|
|
165
|
+
{f.lastActivity?.title && !isFinished(f) && (
|
|
166
|
+
<p className="mt-1.5 line-clamp-2 text-xs text-[var(--color-dim)]">{f.lastActivity.title}</p>
|
|
167
|
+
)}
|
|
168
|
+
<div className="mt-3"><TaskMeter tasks={f.tasks} wide /></div>
|
|
169
|
+
<div className="mt-2.5 flex items-center justify-between text-xs text-[var(--color-faint)]">
|
|
170
|
+
<span className="flex items-center gap-2">
|
|
171
|
+
<VerifyMark verification={f.verification} />
|
|
172
|
+
{score != null && <span className="font-mono tabular-nums text-[var(--color-dim)]">{score}</span>}
|
|
173
|
+
{f.workType && f.workType !== 'feature' && <span>{f.workType}</span>}
|
|
174
|
+
</span>
|
|
175
|
+
<span>{timeAgo(lastTouch(f), now)}</span>
|
|
176
|
+
</div>
|
|
177
|
+
</Link>
|
|
178
|
+
</li>
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function Board({ features, showDone, now, project }) {
|
|
183
|
+
const active = features.filter((f) => !isFinished(f));
|
|
184
|
+
const done = features.filter(isFinished).slice(0, BOARD_DONE_LIMIT);
|
|
185
|
+
const columns = PHASES.map((p) => ({ ...p, items: active.filter((f) => f.phase === p.id) }));
|
|
186
|
+
if (showDone) columns.push({ id: 'done', label: 'Concluídas', color: 'var(--color-ok)', items: done });
|
|
187
|
+
return (
|
|
188
|
+
<div className="-mx-4 overflow-x-auto px-4 pb-4 md:mx-0 md:px-0">
|
|
189
|
+
<div className="grid auto-cols-[16.5rem] grid-flow-col gap-3 md:auto-cols-[minmax(11rem,1fr)]">
|
|
190
|
+
{columns.map((c) => (
|
|
191
|
+
<section key={c.id} className="bezel min-w-0" aria-label={`${c.label}: ${c.items.length}`}>
|
|
192
|
+
<div className="bezel-core flex h-full min-h-[14rem] flex-col p-2.5">
|
|
193
|
+
<h2 className="flex items-center justify-between px-1.5 pb-2.5 pt-1 text-sm">
|
|
194
|
+
<span className="flex items-center gap-2 font-medium"><span className="h-2 w-2 rounded-full" style={{ background: c.color }} />{c.label}</span>
|
|
195
|
+
<span className="font-mono text-xs text-[var(--color-faint)]">{c.items.length}</span>
|
|
196
|
+
</h2>
|
|
197
|
+
{c.items.length === 0
|
|
198
|
+
? <p className="px-1.5 text-xs text-[var(--color-faint)]">{c.id === 'done' ? 'Nada concluído ainda.' : 'Nenhuma feature aqui.'}</p>
|
|
199
|
+
: <ul className="space-y-2">{c.items.map((f) => <BoardCard key={f.name} f={f} now={now} project={project} />)}</ul>}
|
|
200
|
+
</div>
|
|
201
|
+
</section>
|
|
202
|
+
))}
|
|
203
|
+
</div>
|
|
204
|
+
</div>
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function Empty({ title, children }) {
|
|
209
|
+
return (
|
|
210
|
+
<div className="bezel mx-auto mt-16 max-w-lg">
|
|
211
|
+
<div className="bezel-core px-8 py-10 text-center">
|
|
212
|
+
<h2 className="text-lg font-medium">{title}</h2>
|
|
213
|
+
<div className="mt-2 text-sm leading-relaxed text-[var(--color-dim)]">{children}</div>
|
|
214
|
+
</div>
|
|
215
|
+
</div>
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export function Segmented({ value, onChange, options, label }) {
|
|
220
|
+
return (
|
|
221
|
+
<div role="radiogroup" aria-label={label} className="inline-flex rounded-full bg-white/[0.03] p-1 shadow-[inset_0_0_0_1px_var(--color-hair)]">
|
|
222
|
+
{options.map((o) => (
|
|
223
|
+
<button
|
|
224
|
+
key={o.id}
|
|
225
|
+
type="button"
|
|
226
|
+
role="radio"
|
|
227
|
+
aria-checked={value === o.id}
|
|
228
|
+
onClick={() => onChange(o.id)}
|
|
229
|
+
className={[
|
|
230
|
+
'fluid flex items-center gap-1.5 rounded-full px-3 py-1.5 text-sm transition-colors active:scale-[0.98]',
|
|
231
|
+
value === o.id ? 'bg-white/[0.1] text-[var(--color-fg)]' : 'text-[var(--color-dim)] hover:text-[var(--color-fg)]',
|
|
232
|
+
].join(' ')}
|
|
233
|
+
>
|
|
234
|
+
{o.icon && <o.icon weight="light" size={15} />}{o.label}
|
|
235
|
+
</button>
|
|
236
|
+
))}
|
|
237
|
+
</div>
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const chipClass = (on) => [
|
|
242
|
+
'fluid rounded-full px-3 py-1 text-xs transition-colors active:scale-[0.98]',
|
|
243
|
+
on ? 'bg-white/[0.09] text-[var(--color-fg)] shadow-[inset_0_0_0_1px_var(--color-hair-strong)]' : 'text-[var(--color-faint)] shadow-[inset_0_0_0_1px_var(--color-hair)] hover:text-[var(--color-dim)]',
|
|
244
|
+
].join(' ');
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* The feature home. All state comes in as props so the view renders the same
|
|
248
|
+
* with the live API and with the smoke test's fixture.
|
|
249
|
+
*/
|
|
250
|
+
export function FeaturesView({
|
|
251
|
+
data, query, onQuery, completedShown, onShowMore,
|
|
252
|
+
view = 'list', onView = () => {}, types = [], onTypes = () => {}, sort = 'updated', onSort = () => {},
|
|
253
|
+
showDone = true, onShowDone = () => {}, now = Date.now(), searchRef, project,
|
|
254
|
+
}) {
|
|
255
|
+
const { project: info, features } = data;
|
|
256
|
+
const q = query.trim().toLowerCase();
|
|
257
|
+
const presentTypes = [...new Set(features.map((f) => f.workType || 'feature'))].sort();
|
|
258
|
+
const visible = features
|
|
259
|
+
.filter((f) => !q || f.name.toLowerCase().includes(q) || (f.description || '').toLowerCase().includes(q))
|
|
260
|
+
.filter((f) => types.length === 0 || types.includes(f.workType || 'feature'))
|
|
261
|
+
.sort(SORTS[sort]?.cmp || SORTS.updated.cmp);
|
|
262
|
+
const awaiting = visible.filter((f) => f.awaiting);
|
|
263
|
+
const active = visible.filter((f) => !f.awaiting && !isFinished(f));
|
|
264
|
+
const done = visible.filter(isFinished);
|
|
265
|
+
const working = features.filter((f) => isWorking(f, now)).length;
|
|
266
|
+
|
|
267
|
+
return (
|
|
268
|
+
<div className="mx-auto max-w-7xl px-4 pb-24 pt-10 md:px-10 md:pt-14">
|
|
269
|
+
<header className="rise mb-8">
|
|
270
|
+
<p className="text-sm text-[var(--color-dim)]">Projeto</p>
|
|
271
|
+
<h1 className="mt-1 break-words text-4xl font-semibold tracking-tight md:text-5xl">{info.name}</h1>
|
|
272
|
+
<p className="mt-3 flex flex-wrap items-center gap-x-2 text-sm text-[var(--color-dim)]">
|
|
273
|
+
{info.stack && <span>stack <span className="font-mono text-[var(--color-fg)]">{info.stack}</span>,</span>}
|
|
274
|
+
<span>trust <span className="font-mono text-[var(--color-fg)]">{info.trust || 'auto'}</span>,</span>
|
|
275
|
+
<span>{features.filter((f) => !isFinished(f)).length} ativas de {features.length}</span>
|
|
276
|
+
{working > 0 && <span className="flex items-center gap-1.5 text-[var(--color-phase-implement)]"><WorkingDot /> {working} com agente trabalhando agora</span>}
|
|
277
|
+
</p>
|
|
278
|
+
</header>
|
|
279
|
+
|
|
280
|
+
{features.length > 0 && (
|
|
281
|
+
<div className="rise-2 mb-8 flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
|
|
282
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
283
|
+
<Segmented label="Visualização" value={view} onChange={onView} options={[{ id: 'list', label: 'Lista', icon: Rows }, { id: 'board', label: 'Quadro', icon: Kanban }]} />
|
|
284
|
+
{presentTypes.length > 1 && presentTypes.map((t) => (
|
|
285
|
+
<button key={t} type="button" aria-pressed={types.includes(t)} className={chipClass(types.includes(t))}
|
|
286
|
+
onClick={() => onTypes(types.includes(t) ? types.filter((x) => x !== t) : [...types, t])}>{t}</button>
|
|
287
|
+
))}
|
|
288
|
+
{view === 'board' && (
|
|
289
|
+
<button type="button" aria-pressed={showDone} className={chipClass(showDone)} onClick={() => onShowDone(!showDone)}>concluídas recentes</button>
|
|
290
|
+
)}
|
|
291
|
+
</div>
|
|
292
|
+
<div className="flex items-center gap-2">
|
|
293
|
+
{view === 'list' && (
|
|
294
|
+
<label className="text-sm text-[var(--color-dim)]">
|
|
295
|
+
<span className="sr-only">Ordenar por</span>
|
|
296
|
+
<select value={sort} onChange={(e) => onSort(e.target.value)}
|
|
297
|
+
className="fluid rounded-full bg-white/[0.04] px-3 py-2 text-sm text-[var(--color-fg)] shadow-[inset_0_0_0_1px_var(--color-hair)] outline-none">
|
|
298
|
+
{Object.entries(SORTS).map(([id, s]) => <option key={id} value={id}>{s.label}</option>)}
|
|
299
|
+
</select>
|
|
300
|
+
</label>
|
|
301
|
+
)}
|
|
302
|
+
<label className="relative block w-full md:w-72">
|
|
303
|
+
<span className="sr-only">Buscar feature</span>
|
|
304
|
+
<MagnifyingGlass weight="light" size={16} className="pointer-events-none absolute left-3.5 top-1/2 -translate-y-1/2 text-[var(--color-faint)]" />
|
|
305
|
+
<input
|
|
306
|
+
ref={searchRef}
|
|
307
|
+
value={query}
|
|
308
|
+
onChange={(e) => onQuery(e.target.value)}
|
|
309
|
+
onKeyDown={(e) => { if (e.key === 'Escape') { onQuery(''); e.currentTarget.blur(); } }}
|
|
310
|
+
placeholder="Buscar feature"
|
|
311
|
+
className="fluid w-full rounded-full bg-white/[0.04] py-2 pl-10 pr-10 text-sm text-[var(--color-fg)] shadow-[inset_0_0_0_1px_var(--color-hair)] outline-none transition-shadow placeholder:text-[var(--color-faint)] focus:shadow-[inset_0_0_0_1px_var(--color-hair-strong)]"
|
|
312
|
+
/>
|
|
313
|
+
<kbd className="pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 rounded-md px-1.5 font-mono text-[11px] text-[var(--color-faint)] shadow-[inset_0_0_0_1px_var(--color-hair)]">/</kbd>
|
|
314
|
+
</label>
|
|
315
|
+
</div>
|
|
316
|
+
</div>
|
|
317
|
+
)}
|
|
318
|
+
|
|
319
|
+
{!info.initialized && (
|
|
320
|
+
<Empty title="Este diretório não usa morph-spec">
|
|
321
|
+
Rode <code className="font-mono text-[var(--color-fg)]">morph-spec init</code> na raiz do projeto e abra o dashboard de novo.
|
|
322
|
+
</Empty>
|
|
323
|
+
)}
|
|
324
|
+
{info.legacyStateOnly && (
|
|
325
|
+
<Empty title="Projeto no modelo de estado antigo">
|
|
326
|
+
As features deste projeto vivem só no <code className="font-mono">state.json</code>, sem <code className="font-mono">feature.json</code> por feature.
|
|
327
|
+
Rode <code className="font-mono text-[var(--color-fg)]">morph-spec update</code> para migrar.
|
|
328
|
+
</Empty>
|
|
329
|
+
)}
|
|
330
|
+
{info.initialized && !info.legacyStateOnly && features.length === 0 && (
|
|
331
|
+
<Empty title="Nenhuma feature ainda">
|
|
332
|
+
No Claude Code, comece uma com <code className="font-mono text-[var(--color-fg)]">/morph-proposal</code>. Ela aparece aqui assim que o <code className="font-mono">feature.json</code> nascer.
|
|
333
|
+
</Empty>
|
|
334
|
+
)}
|
|
335
|
+
{features.length > 0 && visible.length === 0 && (
|
|
336
|
+
<p className="px-1 text-sm text-[var(--color-dim)]">Nenhuma feature com esses filtros.</p>
|
|
337
|
+
)}
|
|
338
|
+
|
|
339
|
+
{view === 'board' && visible.length > 0 && <div className="rise-3"><Board features={visible} showDone={showDone} now={now} project={project} /></div>}
|
|
340
|
+
|
|
341
|
+
{view === 'list' && (
|
|
342
|
+
<div className="rise-3 space-y-10">
|
|
343
|
+
{awaiting.length > 0 && <Section title="Esperando você" hint="gate pausado" features={awaiting} now={now} project={project} />}
|
|
344
|
+
{active.length > 0 && <Section title="Em andamento" hint={`${active.length}`} features={active} now={now} project={project} />}
|
|
345
|
+
{done.length > 0 && (
|
|
346
|
+
<div>
|
|
347
|
+
<Section title="Concluídas" hint={`${done.length}`} features={done.slice(0, completedShown)} now={now} project={project} />
|
|
348
|
+
{done.length > completedShown && (
|
|
349
|
+
<button
|
|
350
|
+
type="button"
|
|
351
|
+
onClick={onShowMore}
|
|
352
|
+
className="fluid mt-4 rounded-full px-4 py-2 text-sm text-[var(--color-dim)] shadow-[inset_0_0_0_1px_var(--color-hair)] transition-colors hover:text-[var(--color-fg)] active:scale-[0.98]"
|
|
353
|
+
>
|
|
354
|
+
Mostrar mais {Math.min(COMPLETED_PAGE, done.length - completedShown)}
|
|
355
|
+
</button>
|
|
356
|
+
)}
|
|
357
|
+
</div>
|
|
358
|
+
)}
|
|
359
|
+
</div>
|
|
360
|
+
)}
|
|
361
|
+
</div>
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
export default function Features() {
|
|
366
|
+
const { project } = useParams();
|
|
367
|
+
const { data, error, loading } = useMorphQuery(`${apiBase(project)}/features`, { scope: ['features', 'activity'], project });
|
|
368
|
+
const [query, setQuery] = useState('');
|
|
369
|
+
const [completedShown, setCompletedShown] = useState(COMPLETED_PAGE);
|
|
370
|
+
const [view, setView] = usePref('features.view', 'list');
|
|
371
|
+
const [types, setTypes] = usePref('features.types', []);
|
|
372
|
+
const [sort, setSort] = usePref('features.sort', 'updated');
|
|
373
|
+
const [showDone, setShowDone] = usePref('features.board.showDone', true);
|
|
374
|
+
const now = useNow();
|
|
375
|
+
const searchRef = useRef(null);
|
|
376
|
+
|
|
377
|
+
// `/` jumps to the search, like most dev tools.
|
|
378
|
+
useEffect(() => {
|
|
379
|
+
const onKey = (e) => {
|
|
380
|
+
if (e.key !== '/' || e.ctrlKey || e.metaKey || e.altKey) return;
|
|
381
|
+
const tag = document.activeElement?.tagName;
|
|
382
|
+
if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return;
|
|
383
|
+
e.preventDefault();
|
|
384
|
+
searchRef.current?.focus();
|
|
385
|
+
};
|
|
386
|
+
window.addEventListener('keydown', onKey);
|
|
387
|
+
return () => window.removeEventListener('keydown', onKey);
|
|
388
|
+
}, []);
|
|
389
|
+
|
|
390
|
+
if (error) return <Empty title="Não consegui ler as features">{error}</Empty>;
|
|
391
|
+
if (loading || !data) return <p className="px-10 pt-16 text-sm text-[var(--color-faint)]">Lendo o projeto…</p>;
|
|
392
|
+
return (
|
|
393
|
+
<FeaturesView
|
|
394
|
+
data={data} query={query} onQuery={setQuery} completedShown={completedShown}
|
|
395
|
+
onShowMore={() => setCompletedShown((n) => n + COMPLETED_PAGE)}
|
|
396
|
+
view={view} onView={setView} types={types} onTypes={setTypes} sort={sort} onSort={setSort}
|
|
397
|
+
showDone={showDone} onShowDone={setShowDone} now={now} searchRef={searchRef} project={project}
|
|
398
|
+
/>
|
|
399
|
+
);
|
|
400
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { useMemo, useState } from 'react';
|
|
2
|
+
import { Link, useParams } from 'react-router-dom';
|
|
3
|
+
import GithubSlugger from 'github-slugger';
|
|
4
|
+
import morphData from 'virtual:morph-data';
|
|
5
|
+
import KnowledgeMarkdown from '../components/KnowledgeMarkdown.jsx';
|
|
6
|
+
import EntityLink from '../components/EntityLink.jsx';
|
|
7
|
+
import { PHASES, GATE_LABEL } from '../lib/phases.js';
|
|
8
|
+
|
|
9
|
+
const GUIDE = morphData.docs?.guide || [];
|
|
10
|
+
|
|
11
|
+
/** Same slugs rehype-slug gives the rendered headings, in document order. */
|
|
12
|
+
export function tocOf(doc) {
|
|
13
|
+
const slugger = new GithubSlugger();
|
|
14
|
+
return (doc?.headings || []).map((h) => ({ ...h, slug: slugger.slug(h.text) }));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function personasIn(phaseId) {
|
|
18
|
+
return (morphData.agents?.list || []).filter((a) => a.activePhases?.includes(phaseId));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** The whole flow on one screen: phase, the skill that runs it, who works in it, the gate that closes it. */
|
|
22
|
+
export function Flow() {
|
|
23
|
+
return (
|
|
24
|
+
<section aria-label="O fluxo do morph-spec" className="bezel">
|
|
25
|
+
<div className="bezel-core px-5 py-6 md:px-8">
|
|
26
|
+
<h2 className="text-lg font-semibold tracking-tight">Como uma feature anda</h2>
|
|
27
|
+
<p className="mt-1 max-w-3xl text-sm leading-relaxed text-[var(--color-dim)]">
|
|
28
|
+
Você descreve o que quer; o Claude conduz cada fase com uma skill, despacha personas especialistas como sub-agents e para nos gates.
|
|
29
|
+
Com trust <span className="font-mono text-[var(--color-fg)]">auto</span>, um gate só pausa para você quando aparece um sinal de risco.
|
|
30
|
+
No fim, um avaliador independente dá a nota.
|
|
31
|
+
</p>
|
|
32
|
+
<ol className="mt-6 grid gap-3 md:grid-cols-5">
|
|
33
|
+
{PHASES.map((p) => {
|
|
34
|
+
const people = personasIn(p.id);
|
|
35
|
+
return (
|
|
36
|
+
<li key={p.id} className="relative rounded-xl bg-[var(--color-raised)] p-4 shadow-[inset_0_0_0_1px_var(--color-hair)]">
|
|
37
|
+
<span className="absolute inset-x-4 top-0 h-0.5 rounded-full" style={{ background: p.color }} />
|
|
38
|
+
<p className="flex items-baseline justify-between gap-2">
|
|
39
|
+
<EntityLink kind="phase" id={p.id} plain className="font-medium text-[var(--color-fg)] underline-offset-2 hover:underline">{p.label}</EntityLink>
|
|
40
|
+
{p.optional && <span className="text-xs text-[var(--color-faint)]">opcional</span>}
|
|
41
|
+
</p>
|
|
42
|
+
{p.skill && <p className="mt-2 text-xs text-[var(--color-dim)]">skill <EntityLink kind="skill" id={p.skill} className="font-mono" /></p>}
|
|
43
|
+
{people.length > 0 && (
|
|
44
|
+
<p className="mt-2 flex flex-wrap gap-1.5">
|
|
45
|
+
{people.map((a) => <EntityLink key={a.id} kind="persona" id={a.id} chip>{a.id}</EntityLink>)}
|
|
46
|
+
</p>
|
|
47
|
+
)}
|
|
48
|
+
{GATE_LABEL[p.id] && (
|
|
49
|
+
<p className="mt-3 flex items-center gap-1.5 text-xs text-[var(--color-dim)]">
|
|
50
|
+
<span className="h-2 w-2 rounded-full ring-1 ring-[var(--color-ok)]" /> fecha no {GATE_LABEL[p.id]}
|
|
51
|
+
</p>
|
|
52
|
+
)}
|
|
53
|
+
</li>
|
|
54
|
+
);
|
|
55
|
+
})}
|
|
56
|
+
</ol>
|
|
57
|
+
</div>
|
|
58
|
+
</section>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function GuideView({ docId }) {
|
|
63
|
+
const doc = GUIDE.find((d) => d.id === docId) || GUIDE[0];
|
|
64
|
+
const toc = useMemo(() => tocOf(doc).filter((h) => h.depth >= 2), [doc]);
|
|
65
|
+
const [filter, setFilter] = useState('');
|
|
66
|
+
const q = filter.trim().toLowerCase();
|
|
67
|
+
const shown = q ? toc.filter((h) => h.text.toLowerCase().includes(q)) : toc;
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<div className="mx-auto max-w-7xl px-4 pb-24 pt-10 md:px-10 md:pt-14">
|
|
71
|
+
<header className="rise mb-8">
|
|
72
|
+
<h1 className="text-4xl font-semibold tracking-tight md:text-5xl">Guia</h1>
|
|
73
|
+
<p className="mt-3 max-w-2xl text-sm leading-relaxed text-[var(--color-dim)]">
|
|
74
|
+
O que o morph-spec instala no seu projeto e como o Claude o usa. Os nomes de skills, personas e standards citados no texto levam às páginas deles.
|
|
75
|
+
</p>
|
|
76
|
+
</header>
|
|
77
|
+
|
|
78
|
+
<div className="rise-2 mb-10"><Flow /></div>
|
|
79
|
+
|
|
80
|
+
{!doc && <p className="text-sm text-[var(--color-dim)]">Nenhum documento do framework encontrado.</p>}
|
|
81
|
+
{doc && (
|
|
82
|
+
<>
|
|
83
|
+
<nav aria-label="Documentos" className="rise-3 mb-6 grid gap-2 md:grid-cols-3">
|
|
84
|
+
{GUIDE.map((d) => {
|
|
85
|
+
const active = d.id === doc.id;
|
|
86
|
+
return (
|
|
87
|
+
<Link
|
|
88
|
+
key={d.id}
|
|
89
|
+
to={`/guide/${d.id}`}
|
|
90
|
+
aria-current={active ? 'page' : undefined}
|
|
91
|
+
className={[
|
|
92
|
+
'fluid rounded-xl px-4 py-3 transition-colors',
|
|
93
|
+
active ? 'bg-white/[0.07] shadow-[inset_0_0_0_1px_var(--color-hair-strong)]' : 'shadow-[inset_0_0_0_1px_var(--color-hair)] hover:bg-white/[0.03]',
|
|
94
|
+
].join(' ')}
|
|
95
|
+
>
|
|
96
|
+
<p className="text-sm font-medium text-[var(--color-fg)]">{d.title}</p>
|
|
97
|
+
<p className="mt-0.5 text-xs leading-relaxed text-[var(--color-dim)]">{d.summary}</p>
|
|
98
|
+
</Link>
|
|
99
|
+
);
|
|
100
|
+
})}
|
|
101
|
+
</nav>
|
|
102
|
+
|
|
103
|
+
<div className="grid gap-6 lg:grid-cols-[15rem_minmax(0,1fr)]">
|
|
104
|
+
<aside className="lg:sticky lg:top-4 lg:max-h-[calc(100dvh-2rem)] lg:self-start lg:overflow-y-auto">
|
|
105
|
+
<label className="mb-3 block">
|
|
106
|
+
<span className="sr-only">Filtrar seções</span>
|
|
107
|
+
<input
|
|
108
|
+
value={filter}
|
|
109
|
+
onChange={(e) => setFilter(e.target.value)}
|
|
110
|
+
placeholder="Filtrar seções"
|
|
111
|
+
className="fluid w-full rounded-full bg-white/[0.04] px-3.5 py-1.5 text-sm text-[var(--color-fg)] shadow-[inset_0_0_0_1px_var(--color-hair)] outline-none placeholder:text-[var(--color-faint)]"
|
|
112
|
+
/>
|
|
113
|
+
</label>
|
|
114
|
+
<ul className="space-y-0.5 text-sm">
|
|
115
|
+
{shown.map((h) => (
|
|
116
|
+
<li key={h.slug}>
|
|
117
|
+
<a
|
|
118
|
+
href={`#${h.slug}`}
|
|
119
|
+
className={[
|
|
120
|
+
'fluid block truncate rounded-md py-1 text-[var(--color-dim)] transition-colors hover:bg-white/[0.04] hover:text-[var(--color-fg)]',
|
|
121
|
+
h.depth === 3 ? 'pl-5 text-xs' : 'pl-2',
|
|
122
|
+
].join(' ')}
|
|
123
|
+
title={h.text}
|
|
124
|
+
>
|
|
125
|
+
{h.text}
|
|
126
|
+
</a>
|
|
127
|
+
</li>
|
|
128
|
+
))}
|
|
129
|
+
</ul>
|
|
130
|
+
</aside>
|
|
131
|
+
<div className="bezel min-w-0">
|
|
132
|
+
<div className="bezel-core min-w-0 px-6 py-8 md:px-12 md:py-10">
|
|
133
|
+
<KnowledgeMarkdown text={doc.content} />
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
</div>
|
|
137
|
+
</>
|
|
138
|
+
)}
|
|
139
|
+
</div>
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export default function Guide() {
|
|
144
|
+
const { doc } = useParams();
|
|
145
|
+
return <GuideView docId={doc} />;
|
|
146
|
+
}
|