@work-graph/cli 0.2.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/README.md +31 -0
- package/bin/work-graph.mjs +238 -0
- package/package.json +38 -0
- package/vendor/packages/design-tokens/generated/gripe-dark-default.css +67 -0
- package/vendor/packages/design-tokens/generated/marketplace-default.css +67 -0
- package/vendor/packages/design-tokens/generated/workgraph-dark.css +67 -0
- package/vendor/packages/workgraph-mcp/README.md +28 -0
- package/vendor/packages/workgraph-mcp/bin/workgraph-mcp.mjs +21 -0
- package/vendor/packages/workgraph-mcp/package.json +37 -0
- package/vendor/packages/workgraph-mcp/src/handlers.mjs +761 -0
- package/vendor/packages/workgraph-mcp/src/index.mjs +638 -0
- package/vendor/packages/workgraph-mcp/src/prompts.mjs +162 -0
- package/vendor/public/assets/workgraph-logo.svg +11 -0
- package/vendor/public/fonts/GraphikLCG/GraphikLCG-Medium.woff2 +0 -0
- package/vendor/public/fonts/GraphikLCG/GraphikLCG-Regular.woff2 +0 -0
- package/vendor/public/fonts/GraphikLCG/GraphikLCG-Semibold.woff2 +0 -0
- package/vendor/public/fonts/GraphikLCG/stylesheet.css +25 -0
- package/vendor/public/graph-canvas-lit-flow.css +154 -0
- package/vendor/public/graph-canvas-lit-flow.css.map +7 -0
- package/vendor/public/graph-canvas-lit-flow.js +8530 -0
- package/vendor/public/graph-canvas-lit-flow.js.map +7 -0
- package/vendor/src/agentBehaviorRulesAudit.mjs +168 -0
- package/vendor/src/agentBehaviorRulesBundle.mjs +144 -0
- package/vendor/src/agentRunApi.mjs +136 -0
- package/vendor/src/agentToolLoopGuard.mjs +88 -0
- package/vendor/src/agentWorkerClaudeProvider.mjs +288 -0
- package/vendor/src/agentWorkerCursorSdkProvider.mjs +156 -0
- package/vendor/src/agentWorkerLiveLoop.mjs +455 -0
- package/vendor/src/agentWorkerLocalCliProvider.mjs +217 -0
- package/vendor/src/agentWorkerLocalRunner.mjs +246 -0
- package/vendor/src/agentWorkerOpenAiProvider.mjs +459 -0
- package/vendor/src/analyticsPanelProjection.mjs +212 -0
- package/vendor/src/analyticsRecordStore.mjs +165 -0
- package/vendor/src/analyticsRecordWorkItems.mjs +104 -0
- package/vendor/src/architectureL1Canon.mjs +419 -0
- package/vendor/src/architectureLayout.mjs +229 -0
- package/vendor/src/architectureSnapshot.mjs +490 -0
- package/vendor/src/architectureViewsProjection.mjs +116 -0
- package/vendor/src/atomInspector.mjs +253 -0
- package/vendor/src/atomInspectorApi.mjs +130 -0
- package/vendor/src/auditGapMatrixRefresh.mjs +121 -0
- package/vendor/src/backlogSchemaLint.mjs +176 -0
- package/vendor/src/blockedOnebaseGoPreflightEval.mjs +100 -0
- package/vendor/src/bracketIrTraceSignal.mjs +93 -0
- package/vendor/src/bvcAtomParser.mjs +210 -0
- package/vendor/src/bvcDialectRegistry.mjs +86 -0
- package/vendor/src/bvcFileFormat.mjs +218 -0
- package/vendor/src/bvcFormatCli.mjs +55 -0
- package/vendor/src/bvcLintCli.mjs +48 -0
- package/vendor/src/bvcNewWritePolicy.mjs +70 -0
- package/vendor/src/charterPreflightPromoteGate.mjs +194 -0
- package/vendor/src/claimNoEligibleEval.mjs +205 -0
- package/vendor/src/closingAnalysisSuggest.mjs +59 -0
- package/vendor/src/codeGapAnalyzer.mjs +308 -0
- package/vendor/src/codeGapBacklogFeeder.mjs +82 -0
- package/vendor/src/codeGapDraftIntakeApi.mjs +307 -0
- package/vendor/src/codeGapOperatorProjection.mjs +60 -0
- package/vendor/src/codeSyntaxHighlight.mjs +123 -0
- package/vendor/src/codegenEvidence.mjs +187 -0
- package/vendor/src/compilerRoundTripCli.mjs +164 -0
- package/vendor/src/dagreGraphLayout.mjs +78 -0
- package/vendor/src/draftIntakePromotionRules.mjs +205 -0
- package/vendor/src/epicWorkScope.mjs +85 -0
- package/vendor/src/evalLiveLlmEnv.mjs +63 -0
- package/vendor/src/evidenceReadModel.mjs +167 -0
- package/vendor/src/gfsOverlayProjectPassport.mjs +235 -0
- package/vendor/src/globalStepPathToBvcReferences.mjs +196 -0
- package/vendor/src/goldenPath.mjs +69 -0
- package/vendor/src/graphCanvasLayout.mjs +464 -0
- package/vendor/src/graphCanvasLitFlow/client/graphCanvasMinimap.ts +261 -0
- package/vendor/src/graphCanvasLitFlow/client/graphCanvasSvgEdges.ts +259 -0
- package/vendor/src/graphCanvasLitFlow/client/graphCanvasTheme.css +152 -0
- package/vendor/src/graphCanvasLitFlow/client/graphCardNode.ts +328 -0
- package/vendor/src/graphCanvasLitFlow/client/mountGraphCanvasLitFlow.ts +322 -0
- package/vendor/src/graphCanvasLitFlow/graphCanvasEdgeLabels.mjs +58 -0
- package/vendor/src/graphCanvasLitFlow/graphCanvasEdgeRouter.mjs +142 -0
- package/vendor/src/graphCanvasLitFlow/graphCanvasLayoutProfile.mjs +32 -0
- package/vendor/src/graphCanvasLitFlow/graphCanvasNodeMetrics.mjs +45 -0
- package/vendor/src/graphCanvasLitFlow/graphCanvasProjection.mjs +115 -0
- package/vendor/src/graphCanvasLitFlow/graphCanvasProjectionToFlow.mjs +133 -0
- package/vendor/src/graphCanvasLitFlow/graphCanvasTraversal.mjs +77 -0
- package/vendor/src/graphCanvasLitFlow/layoutIntentRoadmapWorkStack.mjs +73 -0
- package/vendor/src/graphCanvasLitFlow/resolveGraphCanvasOverlaps.mjs +77 -0
- package/vendor/src/graphRagContextSlice.mjs +461 -0
- package/vendor/src/gvmVerifyWorkerGate.mjs +95 -0
- package/vendor/src/homeSnapshotApi.mjs +131 -0
- package/vendor/src/homeSnapshotProjection.mjs +275 -0
- package/vendor/src/inboxEventStream.mjs +140 -0
- package/vendor/src/intentComposerApi.mjs +245 -0
- package/vendor/src/intentGraphGbcSliceBoundary.mjs +258 -0
- package/vendor/src/intentGraphProjection.mjs +208 -0
- package/vendor/src/intentHierarchy.mjs +241 -0
- package/vendor/src/intentNodeLint.mjs +107 -0
- package/vendor/src/intentNodeRuntime.mjs +185 -0
- package/vendor/src/intentRoadmapCanvas.mjs +393 -0
- package/vendor/src/intentRoadmapEpicProjection.mjs +122 -0
- package/vendor/src/intentRoadmapMermaid.mjs +165 -0
- package/vendor/src/intentRoadmapProjection.mjs +85 -0
- package/vendor/src/intentTreeLint.mjs +114 -0
- package/vendor/src/intentTreeMigration.mjs +150 -0
- package/vendor/src/intentTreeWorkItems.mjs +227 -0
- package/vendor/src/kanbanBoardProjection.mjs +58 -0
- package/vendor/src/languageAdapterRegistry.mjs +180 -0
- package/vendor/src/languageAdapters/goAdapter.mjs +62 -0
- package/vendor/src/languageAdapters/jsTsAdapter.mjs +60 -0
- package/vendor/src/languageAdapters/jsonYamlAdapter.mjs +103 -0
- package/vendor/src/languageAdapters/onebaseOsAdapter.mjs +55 -0
- package/vendor/src/languageAdapters/plaintextAdapter.mjs +36 -0
- package/vendor/src/languageAdapters/shared.mjs +68 -0
- package/vendor/src/languageAdapters/stepAdapter.mjs +81 -0
- package/vendor/src/lintPlanWorkAlignment.mjs +136 -0
- package/vendor/src/loopHintRepeatToolEval.mjs +153 -0
- package/vendor/src/lowcodeScaffoldCli.mjs +386 -0
- package/vendor/src/markdownDocumentRender.mjs +208 -0
- package/vendor/src/memoryPanelProjection.mjs +116 -0
- package/vendor/src/memoryRecordWriter.mjs +243 -0
- package/vendor/src/memoryWorkerSlice.mjs +238 -0
- package/vendor/src/migrateStepToBvc.mjs +133 -0
- package/vendor/src/missionControlServerHandlers.mjs +195 -0
- package/vendor/src/missionControlUiClient.mjs +278 -0
- package/vendor/src/onebaseCliCapabilityProbe.mjs +107 -0
- package/vendor/src/onebaseCliRunner.mjs +145 -0
- package/vendor/src/onebaseGrossProfitStaticVerify.mjs +98 -0
- package/vendor/src/onebaseParityEvidenceSync.mjs +88 -0
- package/vendor/src/onebasePvrgGraphNodes.mjs +257 -0
- package/vendor/src/onebaseRestEvidenceAdapter.mjs +216 -0
- package/vendor/src/onebaseVectorDslCodegenReadiness.mjs +137 -0
- package/vendor/src/onebaseWorkItemTemplate.mjs +154 -0
- package/vendor/src/onebaseWorkerTools.mjs +586 -0
- package/vendor/src/operatorShellProjection.mjs +102 -0
- package/vendor/src/pipelineProseRender.mjs +180 -0
- package/vendor/src/pipelineStageLint.mjs +118 -0
- package/vendor/src/promptRulesEditorApi.mjs +174 -0
- package/vendor/src/promptRulesProjection.mjs +134 -0
- package/vendor/src/pvrg/bladeAdapter.mjs +40 -0
- package/vendor/src/pvrgTaskScope.mjs +152 -0
- package/vendor/src/releaseGateMatrix.mjs +188 -0
- package/vendor/src/schematicView.mjs +305 -0
- package/vendor/src/seedAnalyticsRecord.mjs +217 -0
- package/vendor/src/semanticSearchBm25.mjs +103 -0
- package/vendor/src/semanticSearchExcerpts.mjs +68 -0
- package/vendor/src/semanticSearchTfidfVector.mjs +86 -0
- package/vendor/src/semanticSearchWorkflow.mjs +366 -0
- package/vendor/src/stepAtomFormatter.mjs +413 -0
- package/vendor/src/stepGraphSlice.mjs +318 -0
- package/vendor/src/ui/atoms/badge.mjs +40 -0
- package/vendor/src/ui/atoms/badgeClient.mjs +32 -0
- package/vendor/src/ui/atoms/button.mjs +114 -0
- package/vendor/src/ui/atoms/buttonClient.mjs +49 -0
- package/vendor/src/ui/atoms/icon.mjs +23 -0
- package/vendor/src/ui/atoms/input.mjs +38 -0
- package/vendor/src/ui/atoms/modal.mjs +44 -0
- package/vendor/src/ui/atoms/select.mjs +98 -0
- package/vendor/src/ui/backlogShellButtons.mjs +238 -0
- package/vendor/src/ui/htmlEscape.mjs +11 -0
- package/vendor/src/ui/molecules/rating.mjs +48 -0
- package/vendor/src/ui/molecules/tabs.mjs +70 -0
- package/vendor/src/ui/organisms/modal.mjs +1 -0
- package/vendor/src/ui/pages/uiKitPage.mjs +147 -0
- package/vendor/src/ui/workItemStatusTone.mjs +36 -0
- package/vendor/src/unifiedLinkageProjection.mjs +264 -0
- package/vendor/src/verificationLoop.mjs +206 -0
- package/vendor/src/workGraphBacklogPersist.mjs +234 -0
- package/vendor/src/workGraphBacklogUiServer.mjs +9192 -0
- package/vendor/src/workGraphBoundedTargetFileRead.mjs +178 -0
- package/vendor/src/workGraphCycleSlice.mjs +184 -0
- package/vendor/src/workGraphDaemonTick.mjs +307 -0
- package/vendor/src/workGraphDaemonWatch.mjs +157 -0
- package/vendor/src/workGraphEngineRoot.mjs +136 -0
- package/vendor/src/workGraphInstallLayout.mjs +65 -0
- package/vendor/src/workGraphLlmUsefulnessEval.mjs +611 -0
- package/vendor/src/workGraphPhasePromoteReadyQueue.mjs +159 -0
- package/vendor/src/workGraphProjectHost.mjs +149 -0
- package/vendor/src/workGraphProjectInit.mjs +392 -0
- package/vendor/src/workGraphPromoteReadyApi.mjs +115 -0
- package/vendor/src/workGraphRecoveryPolicy.mjs +124 -0
- package/vendor/src/workGraphRunnerQueueProjection.mjs +187 -0
- package/vendor/src/workGraphRuntime.mjs +1008 -0
- package/vendor/src/workGraphToolSurfaceAudit.mjs +372 -0
- package/vendor/src/workGraphToolTransportRuntime.mjs +195 -0
- package/vendor/src/workGraphWorkerProvider.mjs +600 -0
- package/vendor/src/workItemBvcQuality.mjs +262 -0
- package/vendor/src/workItemCreateAnalysis.mjs +157 -0
- package/vendor/src/workItemDecisionPipeline.mjs +278 -0
- package/vendor/src/workItemEpicCascade.mjs +176 -0
- package/vendor/src/workItemExecutionGate.mjs +78 -0
- package/vendor/src/workItemHierarchy.mjs +226 -0
- package/vendor/src/workItemProseLint.mjs +133 -0
- package/vendor/src/workItemTextRusify.mjs +794 -0
- package/vendor/src/workItemTraceEnvelope.mjs +158 -0
- package/vendor/src/workItemUiReferences.mjs +272 -0
- package/vendor/src/workflowEpicGrouping.mjs +67 -0
- package/vendor/src/workflowTreeProjection.mjs +53 -0
- package/vendor/src/workspaceRegistry.mjs +150 -0
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
import { readdir, readFile } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
export const STEP_GRAPH_SLICE_SCHEMA = 'step-graph.slice.v1';
|
|
5
|
+
export const STEP_GRAPH_PROJECTION_SCHEMA = 'step-graph.projection.v1';
|
|
6
|
+
export const STEP_GRAPH_NODE_SEP = '\u001f';
|
|
7
|
+
|
|
8
|
+
const STEP_BLOCK_PATTERN = /^#([^\n<]+)<\[\n([\s\S]*?)\n\]>/gmu;
|
|
9
|
+
const STEP_REF_PATTERN = /#([A-Za-zА-Яа-яЁё0-9_]+)/gu;
|
|
10
|
+
|
|
11
|
+
const DEFAULT_SCAN_ROOTS = [
|
|
12
|
+
'charter',
|
|
13
|
+
'protocols',
|
|
14
|
+
'plans',
|
|
15
|
+
'intent',
|
|
16
|
+
'ui',
|
|
17
|
+
'rules',
|
|
18
|
+
'skills',
|
|
19
|
+
'work',
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
const DEFAULT_MAX_NODES = 32;
|
|
23
|
+
const DEFAULT_MAX_DEPTH = 2;
|
|
24
|
+
|
|
25
|
+
const compareText = (left, right) => String(left).localeCompare(String(right), 'en', { sensitivity: 'variant' });
|
|
26
|
+
|
|
27
|
+
function normalizeLogicalPath(path) {
|
|
28
|
+
return String(path ?? '').replace(/\\/g, '/').trim();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function globalNodeId(logicalPath, stepName) {
|
|
32
|
+
return `${normalizeLogicalPath(logicalPath)}${STEP_GRAPH_NODE_SEP}${stepName}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function previewText(body, maxLen = 120) {
|
|
36
|
+
const lines = String(body ?? '')
|
|
37
|
+
.split(/\r?\n/u)
|
|
38
|
+
.map((line) => line.trim())
|
|
39
|
+
.filter(Boolean)
|
|
40
|
+
.slice(0, 4);
|
|
41
|
+
const text = lines.join(' ').replace(/\s+/gu, ' ').trim();
|
|
42
|
+
if (text.length <= maxLen) {
|
|
43
|
+
return text;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return `${text.slice(0, maxLen)}…`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function extractStepRefTargets(text, selfName) {
|
|
50
|
+
const out = new Set();
|
|
51
|
+
for (const match of String(text ?? '').matchAll(STEP_REF_PATTERN)) {
|
|
52
|
+
const name = match[1];
|
|
53
|
+
if (!name || name === selfName) {
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (name.endsWith('[') || name.includes('<')) {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
out.add(name);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return [...out];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function parseStepBlocks(text) {
|
|
68
|
+
return [...String(text ?? '').matchAll(STEP_BLOCK_PATTERN)].map((match) => ({
|
|
69
|
+
name: match[1].trim(),
|
|
70
|
+
body: match[2],
|
|
71
|
+
}));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function walkStepFiles(absoluteRoot, relativeDir, output) {
|
|
75
|
+
const current = join(absoluteRoot, relativeDir);
|
|
76
|
+
let entries = [];
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
entries = await readdir(current, { withFileTypes: true });
|
|
80
|
+
} catch {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
for (const entry of entries) {
|
|
85
|
+
const rel = relativeDir ? `${relativeDir}/${entry.name}` : entry.name;
|
|
86
|
+
if (entry.isDirectory()) {
|
|
87
|
+
if (entry.name === 'node_modules' || entry.name === '.git') {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
await walkStepFiles(absoluteRoot, rel, output);
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (entry.isFile() && (entry.name.endsWith('.bvc') || entry.name.endsWith('.bvc'))) {
|
|
96
|
+
output.add(normalizeLogicalPath(rel));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export async function collectStepFilePaths(options = {}) {
|
|
102
|
+
const cwd = options.cwd ?? process.cwd();
|
|
103
|
+
const roots = options.roots ?? DEFAULT_SCAN_ROOTS;
|
|
104
|
+
const paths = new Set();
|
|
105
|
+
|
|
106
|
+
for (const root of roots) {
|
|
107
|
+
await walkStepFiles(cwd, root, paths);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return [...paths].sort(compareText);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export async function readProjectStepFiles(options = {}) {
|
|
114
|
+
const cwd = options.cwd ?? process.cwd();
|
|
115
|
+
const paths = options.paths ?? await collectStepFilePaths(options);
|
|
116
|
+
const files = [];
|
|
117
|
+
|
|
118
|
+
for (const logicalPath of paths) {
|
|
119
|
+
const text = await readFile(join(cwd, logicalPath), 'utf8');
|
|
120
|
+
files.push({ logicalPath, text });
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return files.sort((left, right) => compareText(left.logicalPath, right.logicalPath));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function buildStepGraphProjectionV1(files, options = {}) {
|
|
127
|
+
if (!Array.isArray(files)) {
|
|
128
|
+
throw new TypeError('files must be an array');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const nameToGlobalIds = new Map();
|
|
132
|
+
const nodes = [];
|
|
133
|
+
const edges = [];
|
|
134
|
+
let edgeIndex = 0;
|
|
135
|
+
|
|
136
|
+
for (const { logicalPath, text } of files) {
|
|
137
|
+
const blocks = parseStepBlocks(text);
|
|
138
|
+
const blockNames = new Set(blocks.map((block) => block.name));
|
|
139
|
+
|
|
140
|
+
for (const block of blocks) {
|
|
141
|
+
const nodeId = globalNodeId(logicalPath, block.name);
|
|
142
|
+
nodes.push({
|
|
143
|
+
id: nodeId,
|
|
144
|
+
stepName: block.name,
|
|
145
|
+
logicalPath,
|
|
146
|
+
preview: previewText(block.body),
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
const bucket = nameToGlobalIds.get(block.name) ?? [];
|
|
150
|
+
bucket.push(nodeId);
|
|
151
|
+
nameToGlobalIds.set(block.name, bucket);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
for (const block of blocks) {
|
|
155
|
+
const sourceId = globalNodeId(logicalPath, block.name);
|
|
156
|
+
const refs = extractStepRefTargets(block.body, block.name);
|
|
157
|
+
|
|
158
|
+
for (const refName of refs) {
|
|
159
|
+
if (blockNames.has(refName)) {
|
|
160
|
+
edges.push({
|
|
161
|
+
id: `step-edge-${edgeIndex += 1}`,
|
|
162
|
+
from: sourceId,
|
|
163
|
+
to: globalNodeId(logicalPath, refName),
|
|
164
|
+
relation: 'step_ref',
|
|
165
|
+
scope: 'intra_file',
|
|
166
|
+
});
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const candidates = [...(nameToGlobalIds.get(refName) ?? [])].sort(compareText);
|
|
171
|
+
if (candidates.length === 0) {
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
edges.push({
|
|
176
|
+
id: `step-edge-${edgeIndex += 1}`,
|
|
177
|
+
from: sourceId,
|
|
178
|
+
to: candidates[0],
|
|
179
|
+
relation: 'step_ref',
|
|
180
|
+
scope: candidates.length > 1 ? 'cross_file_ambiguous' : 'cross_file',
|
|
181
|
+
candidateCount: candidates.length,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const maxNodes = Number.isInteger(options.maxNodes) && options.maxNodes > 0
|
|
188
|
+
? options.maxNodes
|
|
189
|
+
: null;
|
|
190
|
+
|
|
191
|
+
const sortedNodes = nodes.sort((left, right) => compareText(left.id, right.id));
|
|
192
|
+
const sortedEdges = edges.sort((left, right) => compareText(left.id, right.id));
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
schema: STEP_GRAPH_PROJECTION_SCHEMA,
|
|
196
|
+
fileCount: files.length,
|
|
197
|
+
nodeCount: sortedNodes.length,
|
|
198
|
+
edgeCount: sortedEdges.length,
|
|
199
|
+
truncated: maxNodes !== null && sortedNodes.length > maxNodes,
|
|
200
|
+
nodes: maxNodes === null ? sortedNodes : sortedNodes.slice(0, maxNodes),
|
|
201
|
+
edges: sortedEdges,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function resolveSeedNodeId(projection, options = {}) {
|
|
206
|
+
const seedNodeId = normalizeLogicalPath(options.seedNodeId ?? options.nodeId ?? '');
|
|
207
|
+
if (seedNodeId.includes(STEP_GRAPH_NODE_SEP)) {
|
|
208
|
+
return seedNodeId;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const seedStepName = String(options.seedStepName ?? options.stepName ?? '').trim();
|
|
212
|
+
const seedPath = normalizeLogicalPath(options.seedPath ?? options.logicalPath ?? '');
|
|
213
|
+
|
|
214
|
+
if (seedStepName && seedPath) {
|
|
215
|
+
return globalNodeId(seedPath, seedStepName);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (seedStepName) {
|
|
219
|
+
const matches = projection.nodes.filter((node) => node.stepName === seedStepName);
|
|
220
|
+
if (matches.length === 1) {
|
|
221
|
+
return matches[0].id;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (matches.length > 1) {
|
|
225
|
+
throw new Error(`ambiguous seedStepName: ${seedStepName} (${matches.length} nodes)`);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
throw new Error(`unknown seedStepName: ${seedStepName}`);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (seedPath) {
|
|
232
|
+
const matches = projection.nodes.filter((node) => node.logicalPath === seedPath);
|
|
233
|
+
if (matches.length >= 1) {
|
|
234
|
+
return matches[0].id;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
throw new Error(`no step blocks in seedPath: ${seedPath}`);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
throw new Error('seedStepName, seedPath or seedNodeId is required');
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export function buildStepGraphSliceV1(projection, options = {}) {
|
|
244
|
+
const seedId = resolveSeedNodeId(projection, options);
|
|
245
|
+
const nodeById = new Map(projection.nodes.map((node) => [node.id, node]));
|
|
246
|
+
if (!nodeById.has(seedId)) {
|
|
247
|
+
throw new Error(`seed node not found in projection: ${seedId}`);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const maxNodes = Number.isInteger(options.maxNodes) && options.maxNodes > 0
|
|
251
|
+
? options.maxNodes
|
|
252
|
+
: DEFAULT_MAX_NODES;
|
|
253
|
+
const maxDepth = Number.isInteger(options.maxDepth) && options.maxDepth >= 0
|
|
254
|
+
? options.maxDepth
|
|
255
|
+
: DEFAULT_MAX_DEPTH;
|
|
256
|
+
|
|
257
|
+
const adjacency = new Map();
|
|
258
|
+
for (const edge of projection.edges) {
|
|
259
|
+
const bucket = adjacency.get(edge.from) ?? [];
|
|
260
|
+
bucket.push(edge);
|
|
261
|
+
adjacency.set(edge.from, bucket);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const nodes = new Map([[seedId, nodeById.get(seedId)]]);
|
|
265
|
+
const edges = [];
|
|
266
|
+
const queue = [{ id: seedId, depth: 0 }];
|
|
267
|
+
const visited = new Set([seedId]);
|
|
268
|
+
|
|
269
|
+
while (queue.length > 0 && nodes.size < maxNodes) {
|
|
270
|
+
const current = queue.shift();
|
|
271
|
+
for (const edge of adjacency.get(current.id) ?? []) {
|
|
272
|
+
edges.push(edge);
|
|
273
|
+
const next = nodeById.get(edge.to);
|
|
274
|
+
if (!next) {
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
nodes.set(next.id, next);
|
|
279
|
+
if (current.depth < maxDepth && !visited.has(next.id) && nodes.size < maxNodes) {
|
|
280
|
+
visited.add(next.id);
|
|
281
|
+
queue.push({ id: next.id, depth: current.depth + 1 });
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return {
|
|
287
|
+
schema: STEP_GRAPH_SLICE_SCHEMA,
|
|
288
|
+
seedNodeId: seedId,
|
|
289
|
+
maxNodes,
|
|
290
|
+
maxDepth,
|
|
291
|
+
truncated: nodes.size >= maxNodes,
|
|
292
|
+
nodeCount: nodes.size,
|
|
293
|
+
edgeCount: edges.length,
|
|
294
|
+
nodes: [...nodes.values()].sort((left, right) => compareText(left.id, right.id)),
|
|
295
|
+
edges: [...edges].sort((left, right) => compareText(left.id, right.id)),
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
export async function buildStepGraphProjectionFromRepo(options = {}) {
|
|
300
|
+
const files = await readProjectStepFiles(options);
|
|
301
|
+
return buildStepGraphProjectionV1(files, options);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
export async function buildStepGraphSliceFromRepo(options = {}) {
|
|
305
|
+
const { maxNodes, maxDepth, seedStepName, seedPath, seedNodeId, nodeId, stepName, logicalPath, ...projectionOptions } =
|
|
306
|
+
options;
|
|
307
|
+
const projection = await buildStepGraphProjectionFromRepo(projectionOptions);
|
|
308
|
+
return buildStepGraphSliceV1(projection, {
|
|
309
|
+
...(maxNodes !== undefined ? { maxNodes } : {}),
|
|
310
|
+
...(maxDepth !== undefined ? { maxDepth } : {}),
|
|
311
|
+
...(seedStepName !== undefined ? { seedStepName } : {}),
|
|
312
|
+
...(seedPath !== undefined ? { seedPath } : {}),
|
|
313
|
+
...(seedNodeId !== undefined ? { seedNodeId } : {}),
|
|
314
|
+
...(nodeId !== undefined ? { nodeId } : {}),
|
|
315
|
+
...(stepName !== undefined ? { stepName } : {}),
|
|
316
|
+
...(logicalPath !== undefined ? { logicalPath } : {}),
|
|
317
|
+
});
|
|
318
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { escapeHtml, escapeHtmlAttr } from '../htmlEscape.mjs';
|
|
2
|
+
|
|
3
|
+
const TONE_CLASS = {
|
|
4
|
+
default: 'wg-badge--default',
|
|
5
|
+
accent: 'wg-badge--accent',
|
|
6
|
+
muted: 'wg-badge--muted',
|
|
7
|
+
danger: 'wg-badge--danger',
|
|
8
|
+
ok: 'wg-badge--ok',
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @param {{ label?: string, tone?: string, testId?: string }} props
|
|
13
|
+
*/
|
|
14
|
+
export function renderUiBadge(props = {}) {
|
|
15
|
+
const label = props.label ?? '';
|
|
16
|
+
const tone = TONE_CLASS[props.tone] ? props.tone : 'default';
|
|
17
|
+
const attrs = [
|
|
18
|
+
'class="wg-badge ' + TONE_CLASS[tone] + '"',
|
|
19
|
+
props.testId ? `data-testid="${escapeHtmlAttr(props.testId)}"` : '',
|
|
20
|
+
].filter(Boolean).join(' ');
|
|
21
|
+
return `<span ${attrs}>${escapeHtml(label)}</span>`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const UI_BADGE_CSS = `
|
|
25
|
+
.wg-badge {
|
|
26
|
+
display: inline-flex;
|
|
27
|
+
align-items: center;
|
|
28
|
+
border-radius: 999px;
|
|
29
|
+
padding: 2px 8px;
|
|
30
|
+
font-size: 11px;
|
|
31
|
+
font-weight: 600;
|
|
32
|
+
letter-spacing: 0.02em;
|
|
33
|
+
background: var(--panel-2);
|
|
34
|
+
color: var(--text);
|
|
35
|
+
}
|
|
36
|
+
.wg-badge--accent { background: var(--accent-soft); color: var(--accent); }
|
|
37
|
+
.wg-badge--muted { background: var(--panel-2); color: var(--muted); }
|
|
38
|
+
.wg-badge--danger { background: rgba(var(--ui-danger-rgb, 241 76 76), 0.18); color: rgb(var(--ui-danger-rgb, 241 76 76)); }
|
|
39
|
+
.wg-badge--ok { background: rgba(106, 153, 85, 0.2); color: var(--ok); }
|
|
40
|
+
`;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser-inline badge renderer (no imports).
|
|
3
|
+
* @param {{ label?: string, tone?: string, testId?: string, title?: string }} props
|
|
4
|
+
*/
|
|
5
|
+
export function renderClientUiBadge(props = {}) {
|
|
6
|
+
const escapeAttr = (value) => String(value ?? '')
|
|
7
|
+
.replace(/&/g, '&')
|
|
8
|
+
.replace(/"/g, '"')
|
|
9
|
+
.replace(/</g, '<');
|
|
10
|
+
|
|
11
|
+
const escapeText = (value) => String(value ?? '')
|
|
12
|
+
.replace(/&/g, '&')
|
|
13
|
+
.replace(/</g, '<')
|
|
14
|
+
.replace(/>/g, '>');
|
|
15
|
+
|
|
16
|
+
const TONE_CLASS = {
|
|
17
|
+
default: 'wg-badge--default',
|
|
18
|
+
accent: 'wg-badge--accent',
|
|
19
|
+
muted: 'wg-badge--muted',
|
|
20
|
+
danger: 'wg-badge--danger',
|
|
21
|
+
ok: 'wg-badge--ok',
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const tone = TONE_CLASS[props.tone] ? props.tone : 'default';
|
|
25
|
+
const attrs = [
|
|
26
|
+
'class="wg-badge ' + TONE_CLASS[tone] + '"',
|
|
27
|
+
props.testId ? 'data-testid="' + escapeAttr(props.testId) + '"' : '',
|
|
28
|
+
props.title ? 'title="' + escapeAttr(props.title) + '"' : '',
|
|
29
|
+
].filter(Boolean);
|
|
30
|
+
|
|
31
|
+
return '<span ' + attrs.join(' ') + '>' + escapeText(props.label ?? '') + '</span>';
|
|
32
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { escapeHtml, escapeHtmlAttr } from '../htmlEscape.mjs';
|
|
2
|
+
|
|
3
|
+
const VARIANT_CLASS = {
|
|
4
|
+
primary: 'wg-btn--primary',
|
|
5
|
+
secondary: 'wg-btn--secondary',
|
|
6
|
+
flat: 'wg-btn--flat',
|
|
7
|
+
soft: 'wg-btn--soft',
|
|
8
|
+
black: 'wg-btn--black',
|
|
9
|
+
inverse: 'wg-btn--inverse',
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const SIZE_CLASS = {
|
|
13
|
+
lg: 'wg-btn--lg',
|
|
14
|
+
md: 'wg-btn--md',
|
|
15
|
+
sm: 'wg-btn--sm',
|
|
16
|
+
xs: 'wg-btn--xs',
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @param {{
|
|
21
|
+
* label?: string,
|
|
22
|
+
* labelHtml?: string,
|
|
23
|
+
* variant?: string,
|
|
24
|
+
* size?: string,
|
|
25
|
+
* type?: string,
|
|
26
|
+
* disabled?: boolean,
|
|
27
|
+
* hidden?: boolean,
|
|
28
|
+
* testId?: string,
|
|
29
|
+
* id?: string,
|
|
30
|
+
* className?: string,
|
|
31
|
+
* href?: string | null,
|
|
32
|
+
* unstyled?: boolean,
|
|
33
|
+
* ariaLabel?: string,
|
|
34
|
+
* ariaPressed?: boolean | string,
|
|
35
|
+
* ariaSelected?: boolean | string,
|
|
36
|
+
* role?: string,
|
|
37
|
+
* attrs?: Record<string, string | number | boolean | null | undefined>,
|
|
38
|
+
* }} props
|
|
39
|
+
*/
|
|
40
|
+
export function renderUiButton(props = {}) {
|
|
41
|
+
const label = props.label ?? '';
|
|
42
|
+
const variant = VARIANT_CLASS[props.variant] ? props.variant : 'primary';
|
|
43
|
+
const size = SIZE_CLASS[props.size] ? props.size : 'md';
|
|
44
|
+
const type = props.type ?? 'button';
|
|
45
|
+
const classes = props.unstyled
|
|
46
|
+
? (props.className ?? '')
|
|
47
|
+
: [
|
|
48
|
+
'wg-btn',
|
|
49
|
+
VARIANT_CLASS[variant],
|
|
50
|
+
SIZE_CLASS[size],
|
|
51
|
+
props.className ?? '',
|
|
52
|
+
].filter(Boolean).join(' ');
|
|
53
|
+
const attrs = [
|
|
54
|
+
`class="${escapeHtmlAttr(classes)}"`,
|
|
55
|
+
props.id ? `id="${escapeHtmlAttr(props.id)}"` : '',
|
|
56
|
+
props.testId ? `data-testid="${escapeHtmlAttr(props.testId)}"` : '',
|
|
57
|
+
props.role ? `role="${escapeHtmlAttr(props.role)}"` : '',
|
|
58
|
+
props.ariaLabel ? `aria-label="${escapeHtmlAttr(props.ariaLabel)}"` : '',
|
|
59
|
+
props.ariaPressed != null
|
|
60
|
+
? `aria-pressed="${props.ariaPressed === true || props.ariaPressed === 'true' ? 'true' : 'false'}"`
|
|
61
|
+
: '',
|
|
62
|
+
props.ariaSelected != null
|
|
63
|
+
? `aria-selected="${props.ariaSelected === true || props.ariaSelected === 'true' ? 'true' : 'false'}"`
|
|
64
|
+
: '',
|
|
65
|
+
props.hidden ? 'hidden' : '',
|
|
66
|
+
props.disabled ? 'disabled' : '',
|
|
67
|
+
].filter(Boolean);
|
|
68
|
+
|
|
69
|
+
if (props.attrs) {
|
|
70
|
+
for (const [key, value] of Object.entries(props.attrs)) {
|
|
71
|
+
if (value == null || value === false) continue;
|
|
72
|
+
if (value === true) attrs.push(key);
|
|
73
|
+
else attrs.push(`${key}="${escapeHtmlAttr(String(value))}"`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const inner = props.labelHtml ?? escapeHtml(label);
|
|
78
|
+
|
|
79
|
+
if (props.href) {
|
|
80
|
+
return `<a href="${escapeHtmlAttr(props.href)}" ${attrs.join(' ')}>${inner}</a>`;
|
|
81
|
+
}
|
|
82
|
+
return `<button type="${escapeHtmlAttr(type)}" ${attrs.join(' ')}>${inner}</button>`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export const UI_BUTTON_CSS = `
|
|
86
|
+
.wg-btn {
|
|
87
|
+
display: inline-flex;
|
|
88
|
+
align-items: center;
|
|
89
|
+
justify-content: center;
|
|
90
|
+
gap: 6px;
|
|
91
|
+
border: 1px solid transparent;
|
|
92
|
+
border-radius: var(--ui-radius-control, 0.375rem);
|
|
93
|
+
font: inherit;
|
|
94
|
+
cursor: pointer;
|
|
95
|
+
text-decoration: none;
|
|
96
|
+
color: rgb(var(--ui-accent-foreground-rgb, 255 255 255));
|
|
97
|
+
background: rgb(var(--ui-accent-rgb, 0 102 255));
|
|
98
|
+
padding: 8px 14px;
|
|
99
|
+
}
|
|
100
|
+
.wg-btn--primary:hover:not(:disabled),
|
|
101
|
+
.wg-btn:not([class*="wg-btn--"]):hover:not(:disabled) { background: rgb(var(--ui-accent-hover-rgb, 0 90 230)); }
|
|
102
|
+
.wg-btn:disabled { opacity: 0.55; cursor: not-allowed; }
|
|
103
|
+
.wg-btn--secondary { background: rgb(var(--ui-control-bg-rgb, 45 45 48)); color: rgb(var(--ui-text-rgb, 212 212 212)); border-color: rgb(var(--brand-border-rgb, 60 60 60)); }
|
|
104
|
+
.wg-btn--secondary:hover:not(:disabled) { background: rgb(var(--ui-control-bg-hover-rgb, 60 60 60)); color: rgb(var(--ui-text-rgb, 212 212 212)); }
|
|
105
|
+
.wg-btn--flat { background: transparent; color: rgb(var(--ui-link-rgb, 0 102 255)); }
|
|
106
|
+
.wg-btn--flat:hover:not(:disabled) { background: rgba(var(--ui-accent-rgb, 0 102 255), 0.12); color: rgb(var(--ui-link-hover-rgb, 0 90 230)); }
|
|
107
|
+
.wg-btn--soft { background: rgba(var(--ui-accent-rgb, 0 102 255), 0.16); color: rgb(var(--ui-accent-rgb, 0 102 255)); }
|
|
108
|
+
.wg-btn--soft:hover:not(:disabled) { background: rgba(var(--ui-accent-rgb, 0 102 255), 0.24); color: rgb(var(--ui-accent-rgb, 0 102 255)); }
|
|
109
|
+
.wg-btn--black { background: rgb(var(--ui-cta-rgb, 0 0 0)); color: rgb(var(--ui-cta-foreground-rgb, 255 255 255)); }
|
|
110
|
+
.wg-btn--inverse { background: rgb(var(--ui-surface-rgb, 37 37 38)); color: rgb(var(--ui-text-rgb, 212 212 212)); border-color: rgb(var(--brand-border-rgb, 60 60 60)); }
|
|
111
|
+
.wg-btn--sm { padding: 4px 10px; font-size: 13px; }
|
|
112
|
+
.wg-btn--xs { padding: 2px 8px; font-size: 12px; }
|
|
113
|
+
.wg-btn--lg { padding: 10px 18px; font-size: 15px; }
|
|
114
|
+
`;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser-inline button renderer (no imports). Injected into backlog UI client script.
|
|
3
|
+
* @param {Record<string, unknown>} props
|
|
4
|
+
*/
|
|
5
|
+
export function renderClientUiButton(props = {}) {
|
|
6
|
+
const escapeAttr = (value) => String(value ?? '')
|
|
7
|
+
.replace(/&/g, '&')
|
|
8
|
+
.replace(/"/g, '"')
|
|
9
|
+
.replace(/</g, '<');
|
|
10
|
+
|
|
11
|
+
const escapeText = (value) => String(value ?? '')
|
|
12
|
+
.replace(/&/g, '&')
|
|
13
|
+
.replace(/</g, '<')
|
|
14
|
+
.replace(/>/g, '>');
|
|
15
|
+
|
|
16
|
+
const VARIANT_CLASS = {
|
|
17
|
+
primary: 'wg-btn--primary',
|
|
18
|
+
secondary: 'wg-btn--secondary',
|
|
19
|
+
flat: 'wg-btn--flat',
|
|
20
|
+
soft: 'wg-btn--soft',
|
|
21
|
+
};
|
|
22
|
+
const SIZE_CLASS = {
|
|
23
|
+
sm: 'wg-btn--sm',
|
|
24
|
+
xs: 'wg-btn--xs',
|
|
25
|
+
md: 'wg-btn--md',
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const label = props.label ?? '';
|
|
29
|
+
const variant = VARIANT_CLASS[props.variant] ? props.variant : 'secondary';
|
|
30
|
+
const size = SIZE_CLASS[props.size] ? props.size : 'sm';
|
|
31
|
+
const classes = props.unstyled
|
|
32
|
+
? (props.className ?? '')
|
|
33
|
+
: ['wg-btn', VARIANT_CLASS[variant], SIZE_CLASS[size], props.className ?? ''].filter(Boolean).join(' ');
|
|
34
|
+
|
|
35
|
+
const attrs = [`class="${escapeAttr(classes)}"`, 'type="button"'];
|
|
36
|
+
if (props.id) attrs.push(`id="${escapeAttr(props.id)}"`);
|
|
37
|
+
if (props.testId) attrs.push(`data-testid="${escapeAttr(props.testId)}"`);
|
|
38
|
+
if (props.disabled) attrs.push('disabled');
|
|
39
|
+
if (props.attrs && typeof props.attrs === 'object') {
|
|
40
|
+
for (const [key, value] of Object.entries(props.attrs)) {
|
|
41
|
+
if (value == null || value === false) continue;
|
|
42
|
+
if (value === true) attrs.push(key);
|
|
43
|
+
else attrs.push(`${key}="${escapeAttr(String(value))}"`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const inner = props.labelHtml ?? escapeText(label);
|
|
48
|
+
return `<button ${attrs.join(' ')}>${inner}</button>`;
|
|
49
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { escapeHtml, escapeHtmlAttr } from '../htmlEscape.mjs';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @param {{ name?: string, size?: number, testId?: string, title?: string }} props
|
|
5
|
+
*/
|
|
6
|
+
export function renderUiIcon(props = {}) {
|
|
7
|
+
const name = props.name ?? 'dot';
|
|
8
|
+
const size = Number(props.size) > 0 ? Number(props.size) : 16;
|
|
9
|
+
const title = props.title ?? name;
|
|
10
|
+
const attrs = [
|
|
11
|
+
'class="wg-icon"',
|
|
12
|
+
`data-icon="${escapeHtmlAttr(name)}"`,
|
|
13
|
+
`width="${size}"`,
|
|
14
|
+
`height="${size}"`,
|
|
15
|
+
`aria-hidden="true"`,
|
|
16
|
+
props.testId ? `data-testid="${escapeHtmlAttr(props.testId)}"` : '',
|
|
17
|
+
].filter(Boolean).join(' ');
|
|
18
|
+
return `<svg ${attrs} viewBox="0 0 16 16" role="img"><title>${escapeHtml(title)}</title><circle cx="8" cy="8" r="4" fill="currentColor"/></svg>`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const UI_ICON_CSS = `
|
|
22
|
+
.wg-icon { display: inline-block; vertical-align: middle; color: rgb(var(--ui-accent-rgb, 0 102 255)); flex-shrink: 0; }
|
|
23
|
+
`;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { escapeHtml, escapeHtmlAttr } from '../htmlEscape.mjs';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @param {{ name?: string, value?: string, placeholder?: string, type?: string, disabled?: boolean, testId?: string }} props
|
|
5
|
+
*/
|
|
6
|
+
export function renderUiTextInput(props = {}) {
|
|
7
|
+
const name = props.name ?? '';
|
|
8
|
+
const value = props.value ?? '';
|
|
9
|
+
const placeholder = props.placeholder ?? '';
|
|
10
|
+
const type = props.type ?? 'text';
|
|
11
|
+
const attrs = [
|
|
12
|
+
'class="wg-input"',
|
|
13
|
+
name ? `name="${escapeHtmlAttr(name)}"` : '',
|
|
14
|
+
`type="${escapeHtmlAttr(type)}"`,
|
|
15
|
+
placeholder ? `placeholder="${escapeHtmlAttr(placeholder)}"` : '',
|
|
16
|
+
value ? `value="${escapeHtmlAttr(value)}"` : '',
|
|
17
|
+
props.disabled ? 'disabled' : '',
|
|
18
|
+
props.testId ? `data-testid="${escapeHtmlAttr(props.testId)}"` : '',
|
|
19
|
+
].filter(Boolean).join(' ');
|
|
20
|
+
return `<input ${attrs}>`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const UI_INPUT_CSS = `
|
|
24
|
+
.wg-input {
|
|
25
|
+
width: 100%;
|
|
26
|
+
box-sizing: border-box;
|
|
27
|
+
border: 1px solid rgb(var(--brand-border-rgb, 60 60 60));
|
|
28
|
+
border-radius: var(--ui-radius-control-sm, 0.25rem);
|
|
29
|
+
background: rgb(var(--ui-control-bg-rgb, 45 45 48));
|
|
30
|
+
color: rgb(var(--ui-text-rgb, 212 212 212));
|
|
31
|
+
padding: 8px 10px;
|
|
32
|
+
font: inherit;
|
|
33
|
+
}
|
|
34
|
+
.wg-input:focus {
|
|
35
|
+
outline: 2px solid rgb(var(--ui-focus-ring-rgb, 0 102 255));
|
|
36
|
+
outline-offset: 1px;
|
|
37
|
+
}
|
|
38
|
+
`;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { escapeHtml, escapeHtmlAttr } from '../htmlEscape.mjs';
|
|
2
|
+
import { renderUiButton } from './button.mjs';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @param {{ title?: string, bodyHtml?: string, open?: boolean, testId?: string }} props
|
|
6
|
+
*/
|
|
7
|
+
export function renderUiModal(props = {}) {
|
|
8
|
+
const title = props.title ?? 'Modal';
|
|
9
|
+
const bodyHtml = props.bodyHtml ?? '';
|
|
10
|
+
const open = props.open !== false;
|
|
11
|
+
const hidden = open ? '' : ' hidden';
|
|
12
|
+
const testId = props.testId ?? 'ui-modal';
|
|
13
|
+
return (
|
|
14
|
+
'<div class="wg-modal' + (open ? ' is-open' : '') + '"' + hidden +
|
|
15
|
+
' data-testid="' + escapeHtmlAttr(testId) + '" role="dialog" aria-modal="true" aria-label="' + escapeHtmlAttr(title) + '">' +
|
|
16
|
+
'<div class="wg-modal__backdrop"></div>' +
|
|
17
|
+
'<div class="wg-modal__panel">' +
|
|
18
|
+
'<header class="wg-modal__header"><h3>' + escapeHtml(title) + '</h3>' +
|
|
19
|
+
renderUiButton({ label: '×', variant: 'flat', size: 'xs', testId: 'ui-modal-close', className: 'wg-modal__close' }) +
|
|
20
|
+
'</header>' +
|
|
21
|
+
'<div class="wg-modal__body">' + bodyHtml + '</div>' +
|
|
22
|
+
'</div></div>'
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const UI_MODAL_CSS = `
|
|
27
|
+
.wg-modal { position: fixed; inset: 0; z-index: 100; display: none; }
|
|
28
|
+
.wg-modal.is-open { display: block; }
|
|
29
|
+
.wg-modal__backdrop { position: absolute; inset: 0; background: rgba(0,0,0,0.55); }
|
|
30
|
+
.wg-modal__panel {
|
|
31
|
+
position: relative;
|
|
32
|
+
margin: 10vh auto;
|
|
33
|
+
width: min(480px, 92vw);
|
|
34
|
+
background: rgb(var(--ui-surface-rgb, 37 37 38));
|
|
35
|
+
border: 1px solid rgb(var(--brand-border-rgb, 60 60 60));
|
|
36
|
+
border-radius: var(--ui-radius-modal, 0.5rem);
|
|
37
|
+
color: rgb(var(--ui-text-rgb, 212 212 212));
|
|
38
|
+
box-shadow: var(--shadow-card, 0 8px 24px rgba(0,0,0,0.35));
|
|
39
|
+
}
|
|
40
|
+
.wg-modal__header { display: flex; justify-content: space-between; align-items: center; padding: 12px 14px; border-bottom: 1px solid rgb(var(--brand-border-rgb, 60 60 60)); }
|
|
41
|
+
.wg-modal__header h3 { margin: 0; font-size: 15px; }
|
|
42
|
+
.wg-modal__body { padding: 14px; }
|
|
43
|
+
.wg-modal__close { min-width: 28px; }
|
|
44
|
+
`;
|