@donartcha/openlag 0.1.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.
Files changed (99) hide show
  1. package/LICENSE +373 -0
  2. package/README.md +82 -0
  3. package/bin/openlag.js +2 -0
  4. package/dist/assets/arc-4YUHkXo3.js +1 -0
  5. package/dist/assets/architectureDiagram-3BPJPVTR-WeGmL7HM.js +36 -0
  6. package/dist/assets/blockDiagram-GPEHLZMM-CtV7ubAx.js +132 -0
  7. package/dist/assets/c4Diagram-AAUBKEIU-DqYDW5c3.js +10 -0
  8. package/dist/assets/channel-Tsel3-MK.js +1 -0
  9. package/dist/assets/chunk-2J33WTMH-BE8P9tjh.js +1 -0
  10. package/dist/assets/chunk-4BX2VUAB-Bi7oLGF5.js +1 -0
  11. package/dist/assets/chunk-55IACEB6-D9Xhxp_r.js +1 -0
  12. package/dist/assets/chunk-727SXJPM-Dz8jKE60.js +206 -0
  13. package/dist/assets/chunk-AQP2D5EJ-BzmM0IeH.js +231 -0
  14. package/dist/assets/chunk-FMBD7UC4-Cvl5dpcx.js +15 -0
  15. package/dist/assets/chunk-ND2GUHAM-Dz2efqnq.js +1 -0
  16. package/dist/assets/chunk-QZHKN3VN-CwblgSnQ.js +1 -0
  17. package/dist/assets/classDiagram-4FO5ZUOK-Bgm-_cW8.js +1 -0
  18. package/dist/assets/classDiagram-v2-Q7XG4LA2-Bgm-_cW8.js +1 -0
  19. package/dist/assets/cose-bilkent-S5V4N54A-h_A3nZUx.js +1 -0
  20. package/dist/assets/cytoscape.esm-D_LviqZs.js +331 -0
  21. package/dist/assets/dagre-BM42HDAG-CN_B2Doz.js +4 -0
  22. package/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
  23. package/dist/assets/diagram-2AECGRRQ-C9TAFwjG.js +43 -0
  24. package/dist/assets/diagram-5GNKFQAL-BThljQLo.js +10 -0
  25. package/dist/assets/diagram-KO2AKTUF-bRPq25Se.js +3 -0
  26. package/dist/assets/diagram-LMA3HP47-BubLCIus.js +24 -0
  27. package/dist/assets/diagram-OG6HWLK6-CJpfhIsS.js +24 -0
  28. package/dist/assets/erDiagram-TEJ5UH35-6Xkza9wL.js +85 -0
  29. package/dist/assets/flowDiagram-I6XJVG4X-Bq_to3hX.js +162 -0
  30. package/dist/assets/ganttDiagram-6RSMTGT7-C3CmvYl7.js +292 -0
  31. package/dist/assets/gitGraphDiagram-PVQCEYII-C93LTfrl.js +106 -0
  32. package/dist/assets/graph-CAnANduQ.js +1 -0
  33. package/dist/assets/index-0RMQQ34p.css +1 -0
  34. package/dist/assets/index-ByxguSZe.js +729 -0
  35. package/dist/assets/infoDiagram-5YYISTIA-CMfuwygl.js +2 -0
  36. package/dist/assets/init-Gi6I4Gst.js +1 -0
  37. package/dist/assets/ishikawaDiagram-YF4QCWOH-CbJ5ojDF.js +70 -0
  38. package/dist/assets/journeyDiagram-JHISSGLW-C_Xz8YyT.js +139 -0
  39. package/dist/assets/kanban-definition-UN3LZRKU-GVv_iRMq.js +89 -0
  40. package/dist/assets/katex-DkKDou_j.js +257 -0
  41. package/dist/assets/layout-DGIYPm2g.js +1 -0
  42. package/dist/assets/linear-BNEtUH2J.js +1 -0
  43. package/dist/assets/mindmap-definition-RKZ34NQL-DIsL0XSF.js +96 -0
  44. package/dist/assets/ordinal-Cboi1Yqb.js +1 -0
  45. package/dist/assets/pieDiagram-4H26LBE5-CSCTSOjk.js +30 -0
  46. package/dist/assets/quadrantDiagram-W4KKPZXB-CQQ9OaFY.js +7 -0
  47. package/dist/assets/requirementDiagram-4Y6WPE33-Cjn3la_S.js +84 -0
  48. package/dist/assets/sankeyDiagram-5OEKKPKP-DoVspvVc.js +40 -0
  49. package/dist/assets/sequenceDiagram-3UESZ5HK-UsoGmL4w.js +162 -0
  50. package/dist/assets/stateDiagram-AJRCARHV-DLmf7Dc8.js +1 -0
  51. package/dist/assets/stateDiagram-v2-BHNVJYJU-jkiDZ_3u.js +1 -0
  52. package/dist/assets/timeline-definition-PNZ67QCA-HfyRxZ8p.js +120 -0
  53. package/dist/assets/vennDiagram-CIIHVFJN-B6pM3L33.js +34 -0
  54. package/dist/assets/wardley-L42UT6IY-B-LdKtrI.js +173 -0
  55. package/dist/assets/wardleyDiagram-YWT4CUSO-BD45zhOu.js +78 -0
  56. package/dist/assets/xychartDiagram-2RQKCTM6-zsDMbUiS.js +7 -0
  57. package/dist/cli/openlag.js +1793 -0
  58. package/dist/index.html +14 -0
  59. package/index.html +13 -0
  60. package/package.json +84 -0
  61. package/scripts/cli/build.ts +34 -0
  62. package/scripts/cli/dev.ts +35 -0
  63. package/scripts/cli/generate.ts +92 -0
  64. package/scripts/cli/init.ts +427 -0
  65. package/scripts/cli/lint.ts +29 -0
  66. package/scripts/cli/openlag.ts +110 -0
  67. package/scripts/cli/vite-bin.ts +8 -0
  68. package/scripts/core/parser/diagnostic.ts +34 -0
  69. package/scripts/core/parser/normalizer.ts +27 -0
  70. package/scripts/core/parser/scanner.ts +30 -0
  71. package/scripts/core/parser/schemas.ts +23 -0
  72. package/scripts/core/parser/types.ts +30 -0
  73. package/scripts/core/parser.ts +127 -0
  74. package/scripts/generate-relations.ts +53 -0
  75. package/scripts/lint/lint-engine.ts +85 -0
  76. package/scripts/lint/lint-profiles.ts +49 -0
  77. package/scripts/lint/lint-rules.ts +174 -0
  78. package/scripts/lint/lint-types.ts +43 -0
  79. package/src/App.tsx +164 -0
  80. package/src/components/DocumentationView.tsx +905 -0
  81. package/src/components/GraphView.tsx +529 -0
  82. package/src/components/GuideView.tsx +535 -0
  83. package/src/components/ImpactView.tsx +365 -0
  84. package/src/components/MarkdownRenderer.tsx +120 -0
  85. package/src/components/OrphansView.tsx +360 -0
  86. package/src/components/SettingsView.tsx +146 -0
  87. package/src/core/generated/relation-definitions.ts +622 -0
  88. package/src/core/graph/GraphQueryLayer.ts +194 -0
  89. package/src/core/registry/ArtifactRegistry.ts +19 -0
  90. package/src/core/registry/RelationRegistry.ts +27 -0
  91. package/src/core/semantic/artifact-layers.ts +43 -0
  92. package/src/core/semantic/ownership-rules.ts +13 -0
  93. package/src/core/semantic/types.ts +11 -0
  94. package/src/index.css +121 -0
  95. package/src/lib/reportUtils.ts +59 -0
  96. package/src/main.tsx +10 -0
  97. package/src/store.ts +146 -0
  98. package/src/types.ts +77 -0
  99. package/vite.config.ts +31 -0
@@ -0,0 +1,360 @@
1
+ import React, { useMemo, useState } from 'react';
2
+ import { useStore } from '../store';
3
+ import { ArtifactType } from '../types';
4
+ import { AlertCircle, Search, Trash2, ChevronRight, FileText, ExternalLink, ShieldAlert, AlertTriangle, Info, Download } from 'lucide-react';
5
+ import { generateGapsReport, downloadTextFile } from '../lib/reportUtils';
6
+
7
+ export const OrphansView: React.FC = () => {
8
+ const { fullGraph: graph, setView, selectedArtifactId, setSelectedArtifact, globalFilters, setGlobalFilter } = useStore();
9
+ const [searchQuery, setSearchQuery] = useState('');
10
+ const [typeFilter, setTypeFilter] = useState<string | 'ALL'>('ALL');
11
+ const [violationFilter, setViolationFilter] = useState<string | 'ALL'>('ALL');
12
+
13
+ const filterLayer = globalFilters.layer;
14
+ const filterOwner = globalFilters.owner;
15
+ const filterTeam = globalFilters.team;
16
+
17
+ const setFilterLayer = (val: string) => setGlobalFilter('layer', val);
18
+ const setFilterOwner = (val: string) => setGlobalFilter('owner', val);
19
+ const setFilterTeam = (val: string) => setGlobalFilter('team', val);
20
+
21
+ const filterOptions = useMemo(() => {
22
+ if (!graph || !graph.artifacts) return { layers: [], owners: [], teams: [] };
23
+
24
+ const layers = new Set<string>();
25
+ const owners = new Set<string>();
26
+ const teams = new Set<string>();
27
+
28
+ graph.artifacts.forEach(a => {
29
+ if (a.layer) layers.add(a.layer);
30
+ if (a.ownership?.owner) owners.add(a.ownership.owner);
31
+ if (a.ownership?.team) teams.add(a.ownership.team);
32
+ });
33
+
34
+ return {
35
+ layers: Array.from(layers).sort(),
36
+ owners: Array.from(owners).sort(),
37
+ teams: Array.from(teams).sort(),
38
+ };
39
+ }, [graph]);
40
+
41
+ const gaps = useMemo(() => {
42
+ if (!graph) return [];
43
+ const linkedIds = new Set<string>();
44
+ graph.relations.forEach(rel => {
45
+ linkedIds.add(rel.from);
46
+ linkedIds.add(rel.to);
47
+ });
48
+
49
+ const violations: { artifact: any, type: string, message: string, severity: 'HIGH' | 'MEDIUM' | 'LOW' }[] = [];
50
+
51
+ graph.artifacts.forEach(art => {
52
+ if (!linkedIds.has(art.id)) {
53
+ violations.push({ artifact: art, type: 'ORPHAN', message: 'No relationships defined', severity: 'HIGH' });
54
+ }
55
+ if (!art.layer) {
56
+ violations.push({ artifact: art, type: 'NO_LAYER', message: 'Missing layer classification', severity: 'MEDIUM' });
57
+ }
58
+ if (!art.ownership?.owner) {
59
+ violations.push({ artifact: art, type: 'NO_OWNER', message: 'Missing ownership (owner)', severity: 'HIGH' });
60
+ }
61
+ if (!art.ownership?.team) {
62
+ violations.push({ artifact: art, type: 'NO_TEAM', message: 'Missing organizational team', severity: 'LOW' });
63
+ }
64
+ if (!art.description || art.description.length < 5) {
65
+ violations.push({ artifact: art, type: 'NO_DESCRIPTION', message: 'Description is too short or missing', severity: 'MEDIUM' });
66
+ }
67
+ });
68
+
69
+ return violations;
70
+ }, [graph]);
71
+
72
+ const filteredGaps = useMemo(() => {
73
+ return gaps.filter(gap => {
74
+ const art = gap.artifact;
75
+
76
+ if (filterLayer !== 'ALL' && art.layer !== filterLayer) return false;
77
+ if (filterOwner !== 'ALL' && art.ownership?.owner !== filterOwner) return false;
78
+ if (filterTeam !== 'ALL' && art.ownership?.team !== filterTeam) return false;
79
+
80
+ if (selectedArtifactId && art.id !== selectedArtifactId) return false;
81
+
82
+ const matchesSearch =
83
+ art.id.toLowerCase().includes(searchQuery.toLowerCase()) ||
84
+ art.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
85
+ (art.description || '').toLowerCase().includes(searchQuery.toLowerCase());
86
+
87
+ const matchesType = typeFilter === 'ALL' || art.type === typeFilter;
88
+ const matchesViolation = violationFilter === 'ALL' || gap.type === violationFilter;
89
+
90
+ return matchesSearch && matchesType && matchesViolation;
91
+ });
92
+ }, [gaps, searchQuery, typeFilter, violationFilter, filterLayer, filterOwner, filterTeam, selectedArtifactId]);
93
+
94
+ const artifactTypes = useMemo(() => {
95
+ // Match the grouped keys in DocumentationView
96
+ return [
97
+ 'REQUIREMENT', 'USE_CASE', 'DESIGN', 'COMPONENT',
98
+ 'CODE_ENTITY', 'TEST', 'DOCUMENTATION', 'INCIDENT',
99
+ 'INFRASTRUCTURE', 'DEPLOYMENT', 'MONITORING', 'MAINTENANCE'
100
+ ];
101
+ }, []);
102
+
103
+ const violationTypes = useMemo(() => {
104
+ return Array.from(new Set(gaps.map(g => g.type)));
105
+ }, [gaps]);
106
+
107
+ const goToDocs = (id: string) => {
108
+ setSelectedArtifact(id);
109
+ setView('docs');
110
+ };
111
+
112
+ const hasActiveFilters = searchQuery !== '' || typeFilter !== 'ALL' || violationFilter !== 'ALL' || filterLayer !== 'ALL' || filterOwner !== 'ALL' || filterTeam !== 'ALL' || selectedArtifactId !== null;
113
+
114
+ const handleGenerateReport = () => {
115
+ const reportContent = generateGapsReport(filteredGaps, hasActiveFilters);
116
+ downloadTextFile('openlag-gaps-report.md', reportContent);
117
+ };
118
+
119
+ if (!graph) return <div className="p-8 text-white/50">Loading...</div>;
120
+
121
+ const SeverityIcon = ({ severity }: { severity: string }) => {
122
+ switch(severity) {
123
+ case 'HIGH': return <ShieldAlert size={14} className="text-red-500/80" />;
124
+ case 'MEDIUM': return <AlertTriangle size={14} className="text-amber-500/80" />;
125
+ case 'LOW': return <Info size={14} className="text-blue-500/80" />;
126
+ default: return <AlertCircle size={14} className="text-white/40" />;
127
+ }
128
+ };
129
+
130
+ const severityColor = (severity: string) => {
131
+ switch(severity) {
132
+ case 'HIGH': return 'border-red-500/20 bg-red-500/5 hover:border-red-500/40 text-red-400';
133
+ case 'MEDIUM': return 'border-amber-500/20 bg-amber-500/5 hover:border-amber-500/40 text-amber-400';
134
+ case 'LOW': return 'border-blue-500/20 bg-blue-500/5 hover:border-blue-500/40 text-blue-400';
135
+ default: return 'border-white/10 hover:border-white/30 text-white/60';
136
+ }
137
+ };
138
+
139
+ return (
140
+ <div className="h-full w-full bg-[#0a0a0a] flex flex-col overflow-hidden">
141
+ <div className="p-8 lg:px-16 pt-12 shrink-0 border-b border-white/5 bg-[#0c0c0c]/30">
142
+ <div className="flex flex-col md:flex-row md:items-start justify-between gap-6 mb-8">
143
+ <div className="flex-1">
144
+ <div className="flex items-center gap-3 mb-2">
145
+ <h1 className="text-3xl font-serif italic tracking-tight">Traceability GAPs</h1>
146
+ <div className="bg-red-500/20 border border-red-500/30 text-red-400 text-[10px] font-mono px-2 py-0.5 rounded uppercase tracking-wider flex items-center gap-1.5 shadow-[0_0_15px_rgba(239,68,68,0.1)]">
147
+ <AlertCircle size={12} />
148
+ {filteredGaps.length} Violations Found
149
+ </div>
150
+ </div>
151
+ <p className="text-xs text-white/40 max-w-2xl leading-relaxed">
152
+ These artifacts have been flagged for structural or documentation violations such as missing links, absent ownership, or incomplete descriptions. Clean these up to improve the system's traceability.
153
+ </p>
154
+ </div>
155
+
156
+ <div className="flex flex-col items-end gap-2 shrink-0">
157
+ {/* View Specific Filters */}
158
+ <div className="flex gap-2 p-0.5 bg-white/5 border border-white/10 rounded-md w-full">
159
+ <div className="relative flex items-center flex-1">
160
+ <Search className="absolute left-3 text-white/20" size={12} />
161
+ <input
162
+ type="text"
163
+ placeholder="Search..."
164
+ value={searchQuery}
165
+ onChange={(e) => setSearchQuery(e.target.value)}
166
+ className="bg-transparent border-none py-1.5 pl-8 pr-3 text-[10px] text-white focus:outline-none w-full placeholder:text-white/10"
167
+ />
168
+ </div>
169
+ <div className="w-px h-4 bg-white/10 self-center" />
170
+ <select
171
+ value={violationFilter}
172
+ onChange={(e) => setViolationFilter(e.target.value)}
173
+ className={`bg-transparent py-1.5 px-3 text-[10px] outline-none cursor-pointer uppercase tracking-widest transition-all border rounded-sm flex-1 ${
174
+ violationFilter !== 'ALL'
175
+ ? 'bg-blue-500/20 border-blue-500/40 text-blue-400 font-bold'
176
+ : 'border-transparent border-l-white/10 text-white/50 hover:border-white/20'
177
+ }`}
178
+ >
179
+ <option value="ALL" className="bg-[#0c0c0c] text-white font-normal">All Violations</option>
180
+ {violationTypes.map(type => (
181
+ <option key={type} value={type} className="bg-[#0c0c0c] text-white font-normal">{type.replace('_', ' ')}</option>
182
+ ))}
183
+ </select>
184
+ <select
185
+ value={typeFilter}
186
+ onChange={(e) => setTypeFilter(e.target.value)}
187
+ className={`bg-transparent py-1.5 px-3 text-[10px] outline-none cursor-pointer uppercase tracking-widest transition-all border rounded-sm flex-1 ${
188
+ typeFilter !== 'ALL'
189
+ ? 'bg-blue-500/20 border-blue-500/40 text-blue-400 font-bold'
190
+ : 'border-transparent border-l-white/10 text-white/50 hover:border-white/20'
191
+ }`}
192
+ >
193
+ <option value="ALL" className="bg-[#0c0c0c] text-white font-normal">All Types</option>
194
+ {artifactTypes.map(type => (
195
+ <option key={type} value={type} className="bg-[#0c0c0c] text-white font-normal">{type.replace('_', ' ')}</option>
196
+ ))}
197
+ </select>
198
+ <select
199
+ value={selectedArtifactId || ''}
200
+ onChange={(e) => setSelectedArtifact(e.target.value || null)}
201
+ className={`bg-transparent py-1.5 px-3 text-[10px] outline-none cursor-pointer tracking-widest transition-all border rounded-sm flex-1 font-mono max-w-[150px] ${
202
+ selectedArtifactId
203
+ ? 'bg-emerald-500/20 border-emerald-500/40 text-emerald-400 font-bold'
204
+ : 'border-transparent border-l-white/10 text-white/50 hover:border-white/20'
205
+ }`}
206
+ >
207
+ <option value="" className="bg-[#0c0c0c] text-white font-sans uppercase">Artifact...</option>
208
+ {(graph?.artifacts || [])
209
+ .filter(a => typeFilter === 'ALL' || a.type === typeFilter)
210
+ .sort((a,b) => a.id.localeCompare(b.id))
211
+ .map(a => (
212
+ <option key={a.id} value={a.id} className="bg-[#0c0c0c] text-white">
213
+ {a.id}
214
+ </option>
215
+ ))}
216
+ </select>
217
+ </div>
218
+
219
+ {/* Global Filters */}
220
+ <div className="flex gap-1 w-full lg:w-auto items-center">
221
+ <select
222
+ value={filterLayer}
223
+ onChange={(e) => setFilterLayer(e.target.value)}
224
+ className={`bg-transparent py-1 px-1 text-[9px] focus:outline-none cursor-pointer uppercase tracking-wider flex-1 text-center transition-all border rounded-sm ${
225
+ filterLayer !== 'ALL'
226
+ ? 'bg-blue-500/20 border-blue-500/40 text-blue-400 font-bold'
227
+ : 'border-white/10 text-white/50 hover:border-white/20'
228
+ }`}
229
+ >
230
+ <option value="ALL" className="bg-[#0c0c0c] text-white font-normal">LAYER</option>
231
+ {filterOptions.layers.map(layer => (
232
+ <option key={layer} value={layer} className="bg-[#0c0c0c] text-white font-normal">{layer}</option>
233
+ ))}
234
+ </select>
235
+ <select
236
+ value={filterOwner}
237
+ onChange={(e) => setFilterOwner(e.target.value)}
238
+ className={`bg-transparent py-1 px-1 text-[9px] focus:outline-none cursor-pointer uppercase tracking-wider flex-1 text-center transition-all border rounded-sm ${
239
+ filterOwner !== 'ALL'
240
+ ? 'bg-blue-500/20 border-blue-500/40 text-blue-400 font-bold'
241
+ : 'border-white/10 text-white/50 hover:border-white/20'
242
+ }`}
243
+ >
244
+ <option value="ALL" className="bg-[#0c0c0c] text-white font-normal">OWNER</option>
245
+ {filterOptions.owners.map(owner => (
246
+ <option key={owner} value={owner} className="bg-[#0c0c0c] text-white font-normal">{owner}</option>
247
+ ))}
248
+ </select>
249
+ <select
250
+ value={filterTeam}
251
+ onChange={(e) => setFilterTeam(e.target.value)}
252
+ className={`bg-transparent py-1 px-1 text-[9px] focus:outline-none cursor-pointer uppercase tracking-wider flex-1 text-center transition-all border rounded-sm ${
253
+ filterTeam !== 'ALL'
254
+ ? 'bg-blue-500/20 border-blue-500/40 text-blue-400 font-bold'
255
+ : 'border-white/10 text-white/50 hover:border-white/20'
256
+ }`}
257
+ >
258
+ <option value="ALL" className="bg-[#0c0c0c] text-white font-normal">TEAM</option>
259
+ {filterOptions.teams.map(team => (
260
+ <option key={team} value={team} className="bg-[#0c0c0c] text-white font-normal">{team}</option>
261
+ ))}
262
+ </select>
263
+
264
+ <button
265
+ onClick={handleGenerateReport}
266
+ className="bg-emerald-500/20 text-emerald-400 hover:bg-emerald-500/30 border border-emerald-500/40 font-mono text-[9px] uppercase tracking-widest px-3 py-1 flex items-center gap-1.5 rounded-sm transition-all h-full"
267
+ title="Download GAPs Report"
268
+ >
269
+ <Download size={12} />
270
+ Generar reporte
271
+ </button>
272
+ </div>
273
+ </div>
274
+ </div>
275
+ </div>
276
+
277
+ <div className="flex-1 overflow-y-auto p-8 lg:px-16 pb-12 custom-scrollbar">
278
+ {filteredGaps.length === 0 ? (
279
+ <div className="h-64 flex flex-col items-center justify-center border border-dashed border-white/10 rounded-lg">
280
+ <Trash2 className="text-white/10 mb-4" size={48} strokeWidth={1} />
281
+ <p className="text-white/30 text-sm italic">No violations found matching filters.</p>
282
+ </div>
283
+ ) : (
284
+ <div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6">
285
+ {filteredGaps.map((gap, idx) => {
286
+ const art = gap.artifact;
287
+ return (
288
+ <div key={`${art.id}-${gap.type}-${idx}`} className={`border p-5 rounded-sm transition-all group relative overflow-hidden flex flex-col ${severityColor(gap.severity)}`}>
289
+ <div className="absolute top-0 right-0 p-3 opacity-0 group-hover:opacity-100 transition-opacity">
290
+ <button
291
+ onClick={() => goToDocs(art.id)}
292
+ className="p-2 bg-white/5 hover:bg-emerald-500/20 text-white/40 hover:text-emerald-400 rounded transition-colors"
293
+ title="View in Documentation"
294
+ >
295
+ <ExternalLink size={14} />
296
+ </button>
297
+ </div>
298
+
299
+ <div className="flex items-center gap-2 mb-4">
300
+ <div className={`p-2 rounded border bg-black/40 ${severityColor(gap.severity)}`}>
301
+ <SeverityIcon severity={gap.severity} />
302
+ </div>
303
+ <div className="flex flex-col gap-1">
304
+ <div className="flex items-center gap-2">
305
+ <span className="text-[10px] font-bold uppercase tracking-widest leading-none">{gap.type}</span>
306
+ {art.layer && <span className="text-[8px] bg-white/5 text-white/50 px-1 rounded-sm uppercase tracking-widest">{art.layer}</span>}
307
+ </div>
308
+ <span className="text-[9px] font-mono text-white/20 mt-1 uppercase tracking-tighter italic leading-none">{art.id}</span>
309
+ </div>
310
+ </div>
311
+
312
+ <div className="mb-4">
313
+ <div className="text-[10px] font-mono text-white/60 mb-1 opacity-60">Message</div>
314
+ <div className="font-serif text-sm leading-tight">{gap.message}</div>
315
+ </div>
316
+
317
+ <hr className="border-white/5 my-4" />
318
+
319
+ <h3 className="font-serif text-lg text-white mb-2 leading-tight">{art.title}</h3>
320
+ {(art.ownership?.owner || art.ownership?.team) && (
321
+ <div className="flex gap-2 mb-2">
322
+ {art.ownership.owner && <span className="text-[9px] text-blue-400 border border-blue-400/20 bg-blue-400/5 px-1 rounded-sm font-mono">OWNER: {art.ownership.owner}</span>}
323
+ {art.ownership.team && <span className="text-[9px] text-emerald-400 border border-emerald-400/20 bg-emerald-400/5 px-1 rounded-sm font-mono">TEAM: {art.ownership.team}</span>}
324
+ </div>
325
+ )}
326
+ <p className="text-xs text-white/40 leading-relaxed line-clamp-2 mb-6 flex-1 min-h-[36px]">
327
+ {art.description}
328
+ </p>
329
+
330
+ <div className="flex items-center justify-between border-t border-white/5 pt-4 mt-auto">
331
+ <div className="flex items-center gap-4">
332
+ <div className="flex flex-col">
333
+ <span className="text-[8px] uppercase tracking-tighter text-white/20">Artifact Type</span>
334
+ <span className="text-[10px] text-white/60 font-mono">{art.type}</span>
335
+ </div>
336
+ {art.subType && (
337
+ <div className="flex flex-col border-l border-white/10 pl-4">
338
+ <span className="text-[8px] uppercase tracking-tighter text-white/20">Subtype</span>
339
+ <span className="text-[10px] text-white/60 font-mono">{art.subType}</span>
340
+ </div>
341
+ )}
342
+ </div>
343
+ <button
344
+ onClick={() => goToDocs(art.id)}
345
+ className="text-[10px] text-emerald-400/60 hover:text-emerald-400 font-bold uppercase tracking-widest flex items-center gap-1.5 transition-colors"
346
+ >
347
+ Open Docs
348
+ <ChevronRight size={12} />
349
+ </button>
350
+ </div>
351
+ </div>
352
+ );
353
+ })}
354
+ </div>
355
+ )}
356
+ </div>
357
+ </div>
358
+ );
359
+ };
360
+
@@ -0,0 +1,146 @@
1
+ import React from 'react';
2
+ import { useStore } from '../store';
3
+ import { Settings, Save, AlertCircle } from 'lucide-react';
4
+
5
+ export const SettingsView: React.FC = () => {
6
+ const { settings, updateSettings } = useStore();
7
+
8
+ const handleGraphFocusChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
9
+ updateSettings({ graphFocusDepth: parseInt(e.target.value) });
10
+ };
11
+
12
+ const handleDocsFocusChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
13
+ updateSettings({ docsFocusDepth: parseInt(e.target.value) });
14
+ };
15
+
16
+ return (
17
+ <div className="h-full w-full bg-[#0a0a0a] text-white p-8 overflow-y-auto">
18
+ <div className="max-w-3xl mx-auto">
19
+ <div className="flex items-center gap-3 mb-8 border-b border-white/10 pb-6">
20
+ <div className="p-3 bg-indigo-500/10 border border-indigo-500/20 rounded-md">
21
+ <Settings className="text-indigo-400" size={24} />
22
+ </div>
23
+ <div>
24
+ <h1 className="text-2xl font-serif">Settings</h1>
25
+ <p className="text-white/40 text-sm mt-1">Configure workspace preferences and focus behaviors.</p>
26
+ </div>
27
+ </div>
28
+
29
+ <div className="space-y-8">
30
+ <section className="bg-[#111] border border-white/10 p-6 rounded-md">
31
+ <h2 className="text-lg font-bold mb-4 flex items-center gap-2">
32
+ Global Preferences
33
+ </h2>
34
+ <div className="space-y-4">
35
+ <div className="flex justify-between items-start border-t border-white/5 pt-4">
36
+ <div>
37
+ <h3 className="font-medium text-sm">Language</h3>
38
+ <p className="text-xs text-white/50 mt-1 max-w-sm">
39
+ Set the preferred language for guides and documentation interfaces where supported.
40
+ </p>
41
+ </div>
42
+ <select
43
+ value={settings.language || 'EN'}
44
+ onChange={(e) => updateSettings({ language: e.target.value as 'EN' | 'ES' })}
45
+ className="bg-black text-white border border-white/20 rounded px-3 py-1.5 text-sm outline-none focus:border-indigo-500 transition-colors"
46
+ >
47
+ <option value="EN" className="bg-black text-white">English</option>
48
+ <option value="ES" className="bg-black text-white">Español</option>
49
+ </select>
50
+ </div>
51
+ </div>
52
+ </section>
53
+
54
+ <section className="bg-[#111] border border-white/10 p-6 rounded-md">
55
+ <h2 className="text-lg font-bold mb-4 flex items-center gap-2">
56
+ Graph View Configuration
57
+ </h2>
58
+ <div className="space-y-4">
59
+ <div className="flex justify-between items-start border-t border-white/5 pt-4">
60
+ <div>
61
+ <h3 className="font-medium text-sm">Selection Focus Depth (Subgraph Exploration)</h3>
62
+ <p className="text-xs text-white/50 mt-1 max-w-sm">
63
+ How many levels of relationships to project into the subgraph when an artifact is focused.
64
+ </p>
65
+ </div>
66
+ <select
67
+ value={settings.graphFocusDepth}
68
+ onChange={handleGraphFocusChange}
69
+ className="bg-black border border-white/20 rounded px-3 py-1.5 text-sm outline-none focus:border-indigo-500 transition-colors"
70
+ >
71
+ <option value={1}>1 Level (Direct)</option>
72
+ <option value={2}>2 Levels</option>
73
+ <option value={3}>3 Levels</option>
74
+ </select>
75
+ </div>
76
+
77
+ <div className="flex justify-between items-start border-t border-white/5 pt-4">
78
+ <div>
79
+ <h3 className="font-medium text-sm">Show Weak Relations</h3>
80
+ <p className="text-xs text-white/50 mt-1 max-w-sm">
81
+ Display semantic, generic, or weakly coupled relations (e.g., RELATES_TO, DOCUMENTS). Turn off to reduce noise and hubs.
82
+ </p>
83
+ </div>
84
+ <label className="relative inline-flex items-center cursor-pointer">
85
+ <input
86
+ type="checkbox"
87
+ value=""
88
+ className="sr-only peer"
89
+ checked={settings.showWeakRelations}
90
+ onChange={(e) => updateSettings({ showWeakRelations: e.target.checked })}
91
+ />
92
+ <div className="w-11 h-6 bg-white/10 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-indigo-500"></div>
93
+ </label>
94
+ </div>
95
+ </div>
96
+ </section>
97
+
98
+ <section className="bg-[#111] border border-white/10 p-6 rounded-md">
99
+ <h2 className="text-lg font-bold mb-4 flex items-center gap-2">
100
+ Documentation View Configuration
101
+ </h2>
102
+ <div className="space-y-4">
103
+ <div className="flex justify-between items-start border-t border-white/5 pt-4">
104
+ <div>
105
+ <h3 className="font-medium text-sm">Default Focus Mode</h3>
106
+ <p className="text-xs text-white/50 mt-1 max-w-sm">
107
+ Whether the impact focus filter should be ON or OFF by default when opening Documentation view.
108
+ </p>
109
+ </div>
110
+ <label className="relative inline-flex items-center cursor-pointer">
111
+ <input
112
+ type="checkbox"
113
+ value=""
114
+ className="sr-only peer"
115
+ checked={settings.defaultDocsFocusMode}
116
+ onChange={(e) => updateSettings({ defaultDocsFocusMode: e.target.checked })}
117
+ />
118
+ <div className="w-11 h-6 bg-white/10 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-emerald-500/50"></div>
119
+ </label>
120
+ </div>
121
+
122
+ <div className="flex justify-between items-start border-t border-white/5 pt-4">
123
+ <div>
124
+ <h3 className="font-medium text-sm">Documentation Focus Depth</h3>
125
+ <p className="text-xs text-white/50 mt-1 max-w-sm">
126
+ How many levels of relationships to include in the documentation when Focus Mode is ON.
127
+ </p>
128
+ </div>
129
+ <select
130
+ value={settings.docsFocusDepth}
131
+ onChange={handleDocsFocusChange}
132
+ className="bg-black border border-white/20 rounded px-3 py-1.5 text-sm outline-none focus:border-indigo-500 transition-colors"
133
+ >
134
+ <option value={1}>1 Level (Direct Dependencies)</option>
135
+ <option value={2}>2 Levels</option>
136
+ <option value={3}>3 Levels</option>
137
+ <option value={0}>Infinite (Full Impact Graph)</option>
138
+ </select>
139
+ </div>
140
+ </div>
141
+ </section>
142
+ </div>
143
+ </div>
144
+ </div>
145
+ );
146
+ };