@principal-ai/file-city-react 0.3.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 (51) hide show
  1. package/dist/builder/cityDataUtils.d.ts +15 -0
  2. package/dist/builder/cityDataUtils.d.ts.map +1 -0
  3. package/dist/builder/cityDataUtils.js +348 -0
  4. package/dist/components/ArchitectureMapHighlightLayers.d.ts +63 -0
  5. package/dist/components/ArchitectureMapHighlightLayers.d.ts.map +1 -0
  6. package/dist/components/ArchitectureMapHighlightLayers.js +1040 -0
  7. package/dist/components/CityViewWithReactFlow.d.ts +14 -0
  8. package/dist/components/CityViewWithReactFlow.d.ts.map +1 -0
  9. package/dist/components/CityViewWithReactFlow.js +266 -0
  10. package/dist/config/files.json +996 -0
  11. package/dist/hooks/useCodeCityData.d.ts +21 -0
  12. package/dist/hooks/useCodeCityData.d.ts.map +1 -0
  13. package/dist/hooks/useCodeCityData.js +57 -0
  14. package/dist/index.d.ts +14 -0
  15. package/dist/index.d.ts.map +1 -0
  16. package/dist/index.js +29 -0
  17. package/dist/render/client/drawLayeredBuildings.d.ts +51 -0
  18. package/dist/render/client/drawLayeredBuildings.d.ts.map +1 -0
  19. package/dist/render/client/drawLayeredBuildings.js +650 -0
  20. package/dist/stories/ArchitectureMapGridLayout.stories.d.ts +73 -0
  21. package/dist/stories/ArchitectureMapGridLayout.stories.d.ts.map +1 -0
  22. package/dist/stories/ArchitectureMapGridLayout.stories.js +345 -0
  23. package/dist/stories/ArchitectureMapHighlightLayers.stories.d.ts +78 -0
  24. package/dist/stories/ArchitectureMapHighlightLayers.stories.d.ts.map +1 -0
  25. package/dist/stories/ArchitectureMapHighlightLayers.stories.js +270 -0
  26. package/dist/stories/CityViewWithReactFlow.stories.d.ts +24 -0
  27. package/dist/stories/CityViewWithReactFlow.stories.d.ts.map +1 -0
  28. package/dist/stories/CityViewWithReactFlow.stories.js +778 -0
  29. package/dist/stories/sample-data.d.ts +4 -0
  30. package/dist/stories/sample-data.d.ts.map +1 -0
  31. package/dist/stories/sample-data.js +268 -0
  32. package/dist/types/react-types.d.ts +17 -0
  33. package/dist/types/react-types.d.ts.map +1 -0
  34. package/dist/types/react-types.js +4 -0
  35. package/dist/utils/fileColorHighlightLayers.d.ts +86 -0
  36. package/dist/utils/fileColorHighlightLayers.d.ts.map +1 -0
  37. package/dist/utils/fileColorHighlightLayers.js +283 -0
  38. package/package.json +49 -0
  39. package/src/builder/cityDataUtils.ts +430 -0
  40. package/src/components/ArchitectureMapHighlightLayers.tsx +1518 -0
  41. package/src/components/CityViewWithReactFlow.tsx +365 -0
  42. package/src/config/files.json +996 -0
  43. package/src/hooks/useCodeCityData.ts +82 -0
  44. package/src/index.ts +64 -0
  45. package/src/render/client/drawLayeredBuildings.ts +946 -0
  46. package/src/stories/ArchitectureMapGridLayout.stories.tsx +410 -0
  47. package/src/stories/ArchitectureMapHighlightLayers.stories.tsx +312 -0
  48. package/src/stories/CityViewWithReactFlow.stories.tsx +787 -0
  49. package/src/stories/sample-data.ts +301 -0
  50. package/src/types/react-types.ts +18 -0
  51. package/src/utils/fileColorHighlightLayers.ts +378 -0
@@ -0,0 +1,301 @@
1
+ import { CityData, CityBuilding, CityDistrict } from '@principal-ai/file-city-builder';
2
+
3
+ // Helper function to create sample city data for stories
4
+ export function createSampleCityData(): CityData {
5
+ const buildings: CityBuilding[] = [];
6
+ const districts: CityDistrict[] = [];
7
+
8
+ // Define file structure
9
+ const fileStructure = [
10
+ // Source files
11
+ { path: 'src/index.ts', size: 1500 },
12
+ { path: 'src/App.tsx', size: 3200 },
13
+ { path: 'src/components/Header.tsx', size: 1800 },
14
+ { path: 'src/components/Footer.tsx', size: 1200 },
15
+ { path: 'src/components/Sidebar.tsx', size: 2100 },
16
+ { path: 'src/components/Card.tsx', size: 900 },
17
+ { path: 'src/components/Button.tsx', size: 600 },
18
+ { path: 'src/utils/helpers.ts', size: 2500 },
19
+ { path: 'src/utils/api.ts', size: 3100 },
20
+ { path: 'src/utils/validators.ts', size: 1400 },
21
+ { path: 'src/hooks/useAuth.ts', size: 800 },
22
+ { path: 'src/hooks/useData.ts', size: 1100 },
23
+ { path: 'src/styles/main.css', size: 4500 },
24
+ { path: 'src/styles/components.css', size: 2800 },
25
+
26
+ // Test files
27
+ { path: 'tests/unit/app.test.ts', size: 2200 },
28
+ { path: 'tests/unit/header.test.ts', size: 1600 },
29
+ { path: 'tests/unit/footer.test.tsx', size: 1400 },
30
+ { path: 'tests/integration/api.test.ts', size: 3400 },
31
+ { path: '__tests__/components.test.tsx', size: 2900 },
32
+ { path: '__tests__/utils.test.ts', size: 1900 },
33
+
34
+ // Config files
35
+ { path: 'package.json', size: 1200 },
36
+ { path: 'tsconfig.json', size: 800 },
37
+ { path: 'webpack.config.js', size: 2100 },
38
+ { path: '.eslintrc.js', size: 600 },
39
+ { path: '.prettierrc', size: 200 },
40
+ { path: 'README.md', size: 3500 },
41
+
42
+ // Documentation
43
+ { path: 'docs/README.md', size: 4200 },
44
+ { path: 'docs/API.md', size: 5100 },
45
+ { path: 'docs/CONTRIBUTING.md', size: 2300 },
46
+
47
+ // Build files
48
+ { path: 'dist/bundle.js', size: 45000 },
49
+ { path: 'dist/index.html', size: 800 },
50
+ { path: 'dist/styles.css', size: 12000 },
51
+
52
+ // Node modules (sample)
53
+ { path: 'node_modules/react/index.js', size: 8000 },
54
+ { path: 'node_modules/react/package.json', size: 1500 },
55
+ { path: 'node_modules/typescript/lib/typescript.js', size: 65000 },
56
+ { path: 'node_modules/@types/react/index.d.ts', size: 3200 },
57
+
58
+ // Deprecated files
59
+ { path: 'src/deprecated/OldComponent.tsx', size: 2400 },
60
+ { path: 'src/deprecated/LegacyAPI.ts', size: 3100 },
61
+ ];
62
+
63
+ // Create a simple grid layout
64
+ let currentX = 0;
65
+ let currentZ = 0;
66
+ const spacing = 2;
67
+ const maxPerRow = 10;
68
+ let itemsInRow = 0;
69
+
70
+ // Group files by directory
71
+ const filesByDir = new Map<string, typeof fileStructure>();
72
+ fileStructure.forEach(file => {
73
+ const dir = file.path.includes('/') ? file.path.substring(0, file.path.lastIndexOf('/')) : '';
74
+ if (!filesByDir.has(dir)) {
75
+ filesByDir.set(dir, []);
76
+ }
77
+ const dirFiles = filesByDir.get(dir);
78
+ if (dirFiles) {
79
+ dirFiles.push(file);
80
+ }
81
+ });
82
+
83
+ // Track district bounds
84
+ const districtBounds = new Map<
85
+ string,
86
+ { minX: number; maxX: number; minZ: number; maxZ: number }
87
+ >();
88
+
89
+ // Process each directory group
90
+ const sortedDirs = Array.from(filesByDir.keys()).sort();
91
+ let districtStartX = 0;
92
+
93
+ sortedDirs.forEach(dir => {
94
+ const files = filesByDir.get(dir);
95
+ if (!files) return;
96
+ const districtMinX = currentX;
97
+ const districtMinZ = currentZ;
98
+
99
+ files.forEach(file => {
100
+ const extension = file.path.includes('.') ? '.' + (file.path.split('.').pop() || '') : '';
101
+
102
+ // Calculate building dimensions based on file size
103
+ const height = Math.log(file.size + 1) * 2;
104
+ const width = Math.sqrt(file.size) / 10;
105
+ const depth = width;
106
+
107
+ buildings.push({
108
+ path: file.path,
109
+ position: {
110
+ x: currentX + width / 2,
111
+ y: height / 2,
112
+ z: currentZ + depth / 2,
113
+ },
114
+ dimensions: [width, height, depth],
115
+ type: 'file',
116
+ fileExtension: extension,
117
+ size: file.size,
118
+ lastModified: new Date(),
119
+ });
120
+
121
+ // Update position for next building
122
+ currentX += width + spacing;
123
+ itemsInRow++;
124
+
125
+ if (itemsInRow >= maxPerRow) {
126
+ currentX = districtStartX;
127
+ currentZ += depth + spacing;
128
+ itemsInRow = 0;
129
+ }
130
+ });
131
+
132
+ // Create district bounds
133
+ if (dir) {
134
+ const districtMaxX = currentX > districtMinX ? currentX : districtMinX + 10;
135
+ const districtMaxZ = currentZ > districtMinZ ? currentZ + 5 : districtMinZ + 10;
136
+
137
+ districtBounds.set(dir, {
138
+ minX: districtMinX - 1,
139
+ maxX: districtMaxX + 1,
140
+ minZ: districtMinZ - 1,
141
+ maxZ: districtMaxZ + 1,
142
+ });
143
+
144
+ // Move to next district area
145
+ currentX = districtMaxX + spacing * 3;
146
+ if (currentX > 100) {
147
+ currentX = 0;
148
+ currentZ = districtMaxZ + spacing * 3;
149
+ }
150
+ districtStartX = currentX;
151
+ itemsInRow = 0;
152
+ }
153
+ });
154
+
155
+ // Create districts from bounds
156
+ const allPaths = new Set(districtBounds.keys());
157
+ const processedPaths = new Set<string>();
158
+
159
+ // Helper to create all parent paths
160
+ const getParentPaths = (path: string): string[] => {
161
+ const parts = path.split('/');
162
+ const parents: string[] = [];
163
+ for (let i = 1; i < parts.length; i++) {
164
+ parents.push(parts.slice(0, i).join('/'));
165
+ }
166
+ return parents;
167
+ };
168
+
169
+ // Add all parent paths to the set
170
+ allPaths.forEach(path => {
171
+ getParentPaths(path).forEach(parent => allPaths.add(parent));
172
+ });
173
+
174
+ // Create districts for all paths
175
+ allPaths.forEach(path => {
176
+ if (processedPaths.has(path)) return;
177
+
178
+ // Find all children of this path
179
+ const children = Array.from(districtBounds.keys()).filter(
180
+ p => p.startsWith(path + '/') && !p.slice(path.length + 1).includes('/'),
181
+ );
182
+
183
+ let bounds;
184
+ if (districtBounds.has(path)) {
185
+ const pathBounds = districtBounds.get(path);
186
+ if (!pathBounds) return;
187
+ bounds = pathBounds;
188
+ } else if (children.length > 0) {
189
+ // Calculate bounds from children
190
+ const childBounds = children
191
+ .map(c => districtBounds.get(c))
192
+ .filter((b): b is NonNullable<typeof b> => b !== undefined);
193
+ if (childBounds.length === 0) return;
194
+ bounds = {
195
+ minX: Math.min(...childBounds.map(c => c.minX)),
196
+ maxX: Math.max(...childBounds.map(c => c.maxX)),
197
+ minZ: Math.min(...childBounds.map(c => c.minZ)),
198
+ maxZ: Math.max(...childBounds.map(c => c.maxZ)),
199
+ };
200
+ } else {
201
+ return; // Skip if no bounds
202
+ }
203
+
204
+ const fileCount = buildings.filter(
205
+ b => b.path === path || b.path.startsWith(path + '/'),
206
+ ).length;
207
+
208
+ districts.push({
209
+ path,
210
+ worldBounds: bounds,
211
+ fileCount,
212
+ type: 'directory',
213
+ });
214
+
215
+ processedPaths.add(path);
216
+ });
217
+
218
+ // Calculate overall bounds
219
+ const allX = buildings.map(b => b.position.x);
220
+ const allZ = buildings.map(b => b.position.z);
221
+ const bounds = {
222
+ minX: Math.min(...allX) - 5,
223
+ maxX: Math.max(...allX) + 5,
224
+ minZ: Math.min(...allZ) - 5,
225
+ maxZ: Math.max(...allZ) + 5,
226
+ };
227
+
228
+ return {
229
+ buildings,
230
+ districts,
231
+ bounds,
232
+ metadata: {
233
+ totalFiles: buildings.length,
234
+ totalDirectories: districts.length,
235
+ analyzedAt: new Date(),
236
+ rootPath: '/',
237
+ layoutConfig: {
238
+ paddingTop: 2,
239
+ paddingBottom: 2,
240
+ paddingLeft: 2,
241
+ paddingRight: 2,
242
+ paddingInner: 1,
243
+ paddingOuter: 3,
244
+ },
245
+ },
246
+ };
247
+ }
248
+
249
+ // Create a smaller sample for performance testing
250
+ export function createSmallSampleCityData(): CityData {
251
+ const buildings: CityBuilding[] = [
252
+ {
253
+ path: 'index.ts',
254
+ position: { x: 5, y: 3, z: 5 },
255
+ dimensions: [4, 6, 4],
256
+ type: 'file',
257
+ fileExtension: '.ts',
258
+ size: 1500,
259
+ lastModified: new Date(),
260
+ },
261
+ {
262
+ path: 'App.tsx',
263
+ position: { x: 12, y: 4, z: 5 },
264
+ dimensions: [5, 8, 5],
265
+ type: 'file',
266
+ fileExtension: '.tsx',
267
+ size: 3200,
268
+ lastModified: new Date(),
269
+ },
270
+ {
271
+ path: 'utils/helpers.ts',
272
+ position: { x: 5, y: 2.5, z: 15 },
273
+ dimensions: [3, 5, 3],
274
+ type: 'file',
275
+ fileExtension: '.ts',
276
+ size: 800,
277
+ lastModified: new Date(),
278
+ },
279
+ ];
280
+
281
+ const districts: CityDistrict[] = [
282
+ {
283
+ path: 'utils',
284
+ worldBounds: { minX: 2, maxX: 10, minZ: 12, maxZ: 20 },
285
+ fileCount: 1,
286
+ type: 'directory',
287
+ },
288
+ ];
289
+
290
+ return {
291
+ buildings,
292
+ districts,
293
+ bounds: { minX: 0, maxX: 20, minZ: 0, maxZ: 25 },
294
+ metadata: {
295
+ totalFiles: buildings.length,
296
+ totalDirectories: districts.length,
297
+ analyzedAt: new Date(),
298
+ rootPath: '/',
299
+ },
300
+ };
301
+ }
@@ -0,0 +1,18 @@
1
+ // React-specific types for code city visualization
2
+ // These extend the core types from @principal-ai/file-city-builder
3
+
4
+ import { CityBuilding, CityDistrict } from '@principal-ai/file-city-builder';
5
+
6
+ export interface MapInteractionState {
7
+ hoveredDistrict: CityDistrict | null;
8
+ hoveredBuilding: CityBuilding | null;
9
+ mousePos: { x: number; y: number };
10
+ }
11
+
12
+ export interface MapDisplayOptions {
13
+ showGrid: boolean;
14
+ showConnections: boolean;
15
+ maxConnections: number;
16
+ gridSize: number;
17
+ padding: number;
18
+ }
@@ -0,0 +1,378 @@
1
+ import defaultConfig from '../config/files.json';
2
+ import {
3
+ HighlightLayer,
4
+ LayerItem,
5
+ LayerRenderStrategy,
6
+ } from '../render/client/drawLayeredBuildings';
7
+
8
+ // Type definitions for the color configuration
9
+ export interface ColorLayerConfig {
10
+ color: string;
11
+ renderStrategy: LayerRenderStrategy;
12
+ opacity?: number;
13
+ borderWidth?: number;
14
+ priority?: number;
15
+ coverOptions?: {
16
+ opacity?: number;
17
+ image?: string;
18
+ text?: string;
19
+ textSize?: number;
20
+ backgroundColor?: string;
21
+ borderRadius?: number;
22
+ icon?: string;
23
+ iconSize?: number;
24
+ };
25
+ customRender?: (
26
+ ctx: CanvasRenderingContext2D,
27
+ bounds: { x: number; y: number; width: number; height: number },
28
+ scale: number,
29
+ ) => void;
30
+ }
31
+
32
+ export interface FileSuffixConfig {
33
+ primary: ColorLayerConfig;
34
+ secondary?: ColorLayerConfig;
35
+ displayName?: string;
36
+ description?: string;
37
+ category?: string;
38
+ source?: string;
39
+ }
40
+
41
+ export interface FileSuffixColorConfig {
42
+ version: string;
43
+ description: string;
44
+ lastUpdated: string;
45
+ suffixConfigs: Record<string, FileSuffixConfig>;
46
+ defaultConfig?: FileSuffixConfig;
47
+ includeUnmatched?: boolean;
48
+ }
49
+
50
+ /**
51
+ * Creates highlight layers for files based on file extension configurations.
52
+ *
53
+ * @param files - Array of file objects with at least a path property
54
+ * @param config - Optional configuration object with suffix mappings. If not provided, uses default config from files.json
55
+ * @returns Array of HighlightLayer objects for the map visualization
56
+ *
57
+ * @example
58
+ * // Using default configuration
59
+ * const files = [{ path: 'src/index.ts' }, { path: 'src/App.tsx' }];
60
+ * const layers = createFileColorHighlightLayers(files);
61
+ *
62
+ * @example
63
+ * // Using custom configuration
64
+ * const customConfig: FileSuffixColorConfig = {
65
+ * version: "1.0.0",
66
+ * description: "Custom colors",
67
+ * lastUpdated: "2025-01-26",
68
+ * suffixConfigs: {
69
+ * ".ts": {
70
+ * primary: {
71
+ * color: "#ff0000",
72
+ * renderStrategy: "border"
73
+ * }
74
+ * }
75
+ * }
76
+ * };
77
+ * const layers = createFileColorHighlightLayers(files, customConfig);
78
+ */
79
+ export function createFileColorHighlightLayers(
80
+ files: Array<{ path: string }> | null | undefined,
81
+ config?: FileSuffixColorConfig,
82
+ ): HighlightLayer[] {
83
+ if (!files || files.length === 0) {
84
+ return [];
85
+ }
86
+
87
+ // Use provided config or fall back to default from files.json
88
+ const colorConfig = config || (defaultConfig as FileSuffixColorConfig);
89
+
90
+ const { suffixConfigs, defaultConfig: defaultFileConfig, includeUnmatched = true } = colorConfig;
91
+
92
+ // Validation
93
+ if (!suffixConfigs || typeof suffixConfigs !== 'object') {
94
+ console.error('[FileColorHighlightLayers] Invalid suffixConfigs structure');
95
+ return [];
96
+ }
97
+
98
+ // Group files by their extension
99
+ const filesBySuffix = new Map<string, string[]>();
100
+ const unmatchedFilesBySuffix = new Map<string, string[]>();
101
+ const noExtensionFiles: string[] = [];
102
+
103
+ files.forEach(file => {
104
+ const filePath = file.path;
105
+ const lastSlash = filePath.lastIndexOf('/');
106
+ const fileName = lastSlash === -1 ? filePath : filePath.substring(lastSlash + 1);
107
+ const lastDot = fileName.lastIndexOf('.');
108
+
109
+ // Check for exact filename match first (e.g., LICENSE, Makefile)
110
+ if (suffixConfigs[fileName]) {
111
+ if (!filesBySuffix.has(fileName)) {
112
+ filesBySuffix.set(fileName, []);
113
+ }
114
+ const fileNameFiles = filesBySuffix.get(fileName);
115
+ if (fileNameFiles) {
116
+ fileNameFiles.push(filePath);
117
+ }
118
+ return;
119
+ }
120
+
121
+ if (lastDot === -1 || lastDot === fileName.length - 1) {
122
+ // No extension or ends with dot
123
+ if (includeUnmatched) {
124
+ noExtensionFiles.push(filePath);
125
+ }
126
+ return;
127
+ }
128
+
129
+ const extension = fileName.substring(lastDot).toLowerCase();
130
+
131
+ if (suffixConfigs[extension]) {
132
+ if (!filesBySuffix.has(extension)) {
133
+ filesBySuffix.set(extension, []);
134
+ }
135
+ const extFiles = filesBySuffix.get(extension);
136
+ if (extFiles) {
137
+ extFiles.push(filePath);
138
+ }
139
+ } else if (includeUnmatched) {
140
+ // Group unmatched files by their extension for individual legend entries
141
+ if (!unmatchedFilesBySuffix.has(extension)) {
142
+ unmatchedFilesBySuffix.set(extension, []);
143
+ }
144
+ const unmatchedExtFiles = unmatchedFilesBySuffix.get(extension);
145
+ if (unmatchedExtFiles) {
146
+ unmatchedExtFiles.push(filePath);
147
+ }
148
+ }
149
+ });
150
+
151
+ // Create highlight layers
152
+ const layers: HighlightLayer[] = [];
153
+
154
+ // Sort by file count (more files first) for consistent ordering
155
+ const sortedSuffixes = Array.from(filesBySuffix.entries()).sort(
156
+ ([, filesA], [, filesB]) => filesB.length - filesA.length,
157
+ );
158
+
159
+ // Create layers for matched files
160
+ let basePriority = 1;
161
+ sortedSuffixes.forEach(([suffix, files]) => {
162
+ const suffixConfig = suffixConfigs[suffix];
163
+ // Remove leading dot for extensions, use as-is for exact filenames
164
+ const extensionName = suffix.startsWith('.') ? suffix.substring(1) : suffix;
165
+
166
+ // Create primary layer
167
+ const primaryLayer: HighlightLayer = {
168
+ id: `ext-${extensionName}-primary`,
169
+ name: suffixConfig.displayName || extensionName.toUpperCase(),
170
+ color: suffixConfig.primary.color,
171
+ enabled: true,
172
+ opacity: suffixConfig.primary.opacity ?? 1.0,
173
+ priority: suffixConfig.primary.priority ?? basePriority,
174
+ items: files.map(
175
+ (path): LayerItem => ({
176
+ path,
177
+ type: 'file' as const,
178
+ renderStrategy: suffixConfig.primary.renderStrategy,
179
+ ...(suffixConfig.primary.coverOptions && {
180
+ coverOptions: suffixConfig.primary.coverOptions,
181
+ }),
182
+ ...(suffixConfig.primary.customRender && {
183
+ customRender: suffixConfig.primary.customRender,
184
+ }),
185
+ }),
186
+ ),
187
+ };
188
+
189
+ if (suffixConfig.primary.borderWidth) {
190
+ primaryLayer.borderWidth = suffixConfig.primary.borderWidth;
191
+ }
192
+
193
+ layers.push(primaryLayer);
194
+
195
+ // Create secondary layer if configured
196
+ if (suffixConfig.secondary) {
197
+ const secondary = suffixConfig.secondary;
198
+ const secondaryLayer: HighlightLayer = {
199
+ id: `ext-${extensionName}-secondary`,
200
+ name: `${suffixConfig.displayName || extensionName.toUpperCase()} Secondary`,
201
+ color: secondary.color,
202
+ enabled: true,
203
+ opacity: secondary.opacity ?? 1.0,
204
+ priority: secondary.priority ?? basePriority + 100, // Higher priority by default
205
+ items: files.map(
206
+ (path): LayerItem => ({
207
+ path,
208
+ type: 'file' as const,
209
+ renderStrategy: secondary.renderStrategy,
210
+ ...(secondary.coverOptions && { coverOptions: secondary.coverOptions }),
211
+ ...(secondary.customRender && { customRender: secondary.customRender }),
212
+ }),
213
+ ),
214
+ };
215
+
216
+ if (secondary.borderWidth) {
217
+ secondaryLayer.borderWidth = secondary.borderWidth;
218
+ }
219
+
220
+ layers.push(secondaryLayer);
221
+ }
222
+
223
+ basePriority += 2; // Leave room for primary + secondary layers
224
+ });
225
+
226
+ // Add layers for unmatched file extensions (each extension gets its own legend entry)
227
+ if (includeUnmatched && defaultFileConfig) {
228
+ // Sort unmatched extensions by file count
229
+ const sortedUnmatchedSuffixes = Array.from(unmatchedFilesBySuffix.entries()).sort(
230
+ ([, filesA], [, filesB]) => filesB.length - filesA.length,
231
+ );
232
+
233
+ sortedUnmatchedSuffixes.forEach(([suffix, files]) => {
234
+ const extensionName = suffix.startsWith('.') ? suffix.substring(1) : suffix;
235
+
236
+ const unmatchedLayer: HighlightLayer = {
237
+ id: `ext-${extensionName}-primary`,
238
+ name: extensionName.toUpperCase(),
239
+ color: defaultFileConfig.primary.color,
240
+ enabled: true,
241
+ opacity: defaultFileConfig.primary.opacity ?? 1.0,
242
+ priority: defaultFileConfig.primary.priority ?? basePriority,
243
+ items: files.map(
244
+ (path): LayerItem => ({
245
+ path,
246
+ type: 'file' as const,
247
+ renderStrategy: defaultFileConfig.primary.renderStrategy,
248
+ ...(defaultFileConfig.primary.coverOptions && {
249
+ coverOptions: defaultFileConfig.primary.coverOptions,
250
+ }),
251
+ ...(defaultFileConfig.primary.customRender && {
252
+ customRender: defaultFileConfig.primary.customRender,
253
+ }),
254
+ }),
255
+ ),
256
+ };
257
+
258
+ if (defaultFileConfig.primary.borderWidth) {
259
+ unmatchedLayer.borderWidth = defaultFileConfig.primary.borderWidth;
260
+ }
261
+
262
+ layers.push(unmatchedLayer);
263
+
264
+ // Add secondary layer if configured
265
+ if (defaultFileConfig.secondary) {
266
+ const secondary = defaultFileConfig.secondary;
267
+ const unmatchedSecondaryLayer: HighlightLayer = {
268
+ id: `ext-${extensionName}-secondary`,
269
+ name: `${extensionName.toUpperCase()} Secondary`,
270
+ color: secondary.color,
271
+ enabled: true,
272
+ opacity: secondary.opacity ?? 1.0,
273
+ priority: secondary.priority ?? basePriority + 100,
274
+ items: files.map(
275
+ (path): LayerItem => ({
276
+ path,
277
+ type: 'file' as const,
278
+ renderStrategy: secondary.renderStrategy,
279
+ ...(secondary.coverOptions && { coverOptions: secondary.coverOptions }),
280
+ ...(secondary.customRender && { customRender: secondary.customRender }),
281
+ }),
282
+ ),
283
+ };
284
+
285
+ if (secondary.borderWidth) {
286
+ unmatchedSecondaryLayer.borderWidth = secondary.borderWidth;
287
+ }
288
+
289
+ layers.push(unmatchedSecondaryLayer);
290
+ }
291
+
292
+ basePriority += 2;
293
+ });
294
+
295
+ // Add layer for files with no extension
296
+ if (noExtensionFiles.length > 0) {
297
+ const noExtLayer: HighlightLayer = {
298
+ id: 'other-files-primary',
299
+ name: 'OTHER',
300
+ color: defaultFileConfig.primary.color,
301
+ enabled: true,
302
+ opacity: defaultFileConfig.primary.opacity ?? 1.0,
303
+ priority: defaultFileConfig.primary.priority ?? basePriority,
304
+ items: noExtensionFiles.map(
305
+ (path): LayerItem => ({
306
+ path,
307
+ type: 'file' as const,
308
+ renderStrategy: defaultFileConfig.primary.renderStrategy,
309
+ ...(defaultFileConfig.primary.coverOptions && {
310
+ coverOptions: defaultFileConfig.primary.coverOptions,
311
+ }),
312
+ ...(defaultFileConfig.primary.customRender && {
313
+ customRender: defaultFileConfig.primary.customRender,
314
+ }),
315
+ }),
316
+ ),
317
+ };
318
+
319
+ if (defaultFileConfig.primary.borderWidth) {
320
+ noExtLayer.borderWidth = defaultFileConfig.primary.borderWidth;
321
+ }
322
+
323
+ layers.push(noExtLayer);
324
+
325
+ if (defaultFileConfig.secondary) {
326
+ const secondary = defaultFileConfig.secondary;
327
+ const noExtSecondaryLayer: HighlightLayer = {
328
+ id: 'other-files-secondary',
329
+ name: 'OTHER Secondary',
330
+ color: secondary.color,
331
+ enabled: true,
332
+ opacity: secondary.opacity ?? 1.0,
333
+ priority: secondary.priority ?? basePriority + 100,
334
+ items: noExtensionFiles.map(
335
+ (path): LayerItem => ({
336
+ path,
337
+ type: 'file' as const,
338
+ renderStrategy: secondary.renderStrategy,
339
+ ...(secondary.coverOptions && { coverOptions: secondary.coverOptions }),
340
+ ...(secondary.customRender && { customRender: secondary.customRender }),
341
+ }),
342
+ ),
343
+ };
344
+
345
+ if (secondary.borderWidth) {
346
+ noExtSecondaryLayer.borderWidth = secondary.borderWidth;
347
+ }
348
+
349
+ layers.push(noExtSecondaryLayer);
350
+ }
351
+ }
352
+ }
353
+
354
+ return layers;
355
+ }
356
+
357
+ /**
358
+ * Get the default file color configuration.
359
+ * This returns the configuration loaded from files.json.
360
+ */
361
+ export function getDefaultFileColorConfig(): FileSuffixColorConfig {
362
+ return defaultConfig as FileSuffixColorConfig;
363
+ }
364
+
365
+ /**
366
+ * Get a simple color mapping from the configuration.
367
+ * This is useful for backwards compatibility or simpler use cases.
368
+ *
369
+ * @param config - Optional configuration. If not provided, uses default config.
370
+ * @returns Record mapping file extensions to hex color strings
371
+ */
372
+ export function getFileColorMapping(config?: FileSuffixColorConfig): Record<string, string> {
373
+ const colorConfig = config || (defaultConfig as FileSuffixColorConfig);
374
+ return Object.entries(colorConfig.suffixConfigs).reduce((acc, [extension, suffixConfig]) => {
375
+ acc[extension] = suffixConfig.primary.color;
376
+ return acc;
377
+ }, {} as Record<string, string>);
378
+ }