@principal-ai/file-city-react 0.5.8 → 0.5.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/FileCity3D/FileCity3D.d.ts +9 -1
- package/dist/components/FileCity3D/FileCity3D.d.ts.map +1 -1
- package/dist/components/FileCity3D/FileCity3D.js +318 -82
- package/package.json +1 -3
- package/src/components/FileCity3D/FileCity3D.tsx +474 -229
- package/src/stories/FileCity3D.stories.tsx +443 -20
|
@@ -1,18 +1,17 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import type { Meta, StoryObj } from '@storybook/react';
|
|
3
|
-
import {
|
|
4
|
-
|
|
3
|
+
import {
|
|
4
|
+
FileCity3D,
|
|
5
|
+
type CityData,
|
|
6
|
+
type CityBuilding,
|
|
7
|
+
type CityDistrict,
|
|
8
|
+
type HighlightLayer,
|
|
9
|
+
type IsolationMode,
|
|
10
|
+
} from '../components/FileCity3D';
|
|
5
11
|
|
|
6
12
|
const meta: Meta<typeof FileCity3D> = {
|
|
7
13
|
title: 'Components/FileCity3D',
|
|
8
14
|
component: FileCity3D,
|
|
9
|
-
decorators: [
|
|
10
|
-
(Story) => (
|
|
11
|
-
<ThemeProvider>
|
|
12
|
-
<Story />
|
|
13
|
-
</ThemeProvider>
|
|
14
|
-
),
|
|
15
|
-
],
|
|
16
15
|
parameters: {
|
|
17
16
|
layout: 'fullscreen',
|
|
18
17
|
},
|
|
@@ -40,7 +39,7 @@ function generateBuildings(
|
|
|
40
39
|
startX: number,
|
|
41
40
|
startZ: number,
|
|
42
41
|
areaWidth: number,
|
|
43
|
-
areaDepth: number
|
|
42
|
+
areaDepth: number,
|
|
44
43
|
): CityBuilding[] {
|
|
45
44
|
const buildings: CityBuilding[] = [];
|
|
46
45
|
const allExtensions = [...CODE_EXTENSIONS, ...NON_CODE_EXTENSIONS];
|
|
@@ -56,9 +55,7 @@ function generateBuildings(
|
|
|
56
55
|
const lineCount = isCode
|
|
57
56
|
? Math.floor(Math.exp(Math.random() * Math.log(3000 - 20) + Math.log(20)))
|
|
58
57
|
: undefined;
|
|
59
|
-
const size = isCode
|
|
60
|
-
? lineCount! * 40
|
|
61
|
-
: Math.floor(Math.random() * 200000) + 1000;
|
|
58
|
+
const size = isCode ? lineCount! * 40 : Math.floor(Math.random() * 200000) + 1000;
|
|
62
59
|
|
|
63
60
|
buildings.push({
|
|
64
61
|
path: `${basePath}/file${i}.${ext}`,
|
|
@@ -92,28 +89,44 @@ const sampleCityData: CityData = {
|
|
|
92
89
|
worldBounds: { minX: -2, maxX: 42, minZ: -2, maxZ: 42 },
|
|
93
90
|
fileCount: 12,
|
|
94
91
|
type: 'directory',
|
|
95
|
-
label: {
|
|
92
|
+
label: {
|
|
93
|
+
text: 'src',
|
|
94
|
+
bounds: { minX: -2, maxX: 42, minZ: 42, maxZ: 46 },
|
|
95
|
+
position: 'bottom',
|
|
96
|
+
},
|
|
96
97
|
},
|
|
97
98
|
{
|
|
98
99
|
path: 'src/components',
|
|
99
100
|
worldBounds: { minX: 48, maxX: 82, minZ: -2, maxZ: 32 },
|
|
100
101
|
fileCount: 8,
|
|
101
102
|
type: 'directory',
|
|
102
|
-
label: {
|
|
103
|
+
label: {
|
|
104
|
+
text: 'components',
|
|
105
|
+
bounds: { minX: 48, maxX: 82, minZ: 32, maxZ: 36 },
|
|
106
|
+
position: 'bottom',
|
|
107
|
+
},
|
|
103
108
|
},
|
|
104
109
|
{
|
|
105
110
|
path: 'src/utils',
|
|
106
111
|
worldBounds: { minX: 48, maxX: 77, minZ: 38, maxZ: 67 },
|
|
107
112
|
fileCount: 6,
|
|
108
113
|
type: 'directory',
|
|
109
|
-
label: {
|
|
114
|
+
label: {
|
|
115
|
+
text: 'utils',
|
|
116
|
+
bounds: { minX: 48, maxX: 77, minZ: 67, maxZ: 71 },
|
|
117
|
+
position: 'bottom',
|
|
118
|
+
},
|
|
110
119
|
},
|
|
111
120
|
{
|
|
112
121
|
path: 'tests',
|
|
113
122
|
worldBounds: { minX: -2, maxX: 32, minZ: 48, maxZ: 72 },
|
|
114
123
|
fileCount: 5,
|
|
115
124
|
type: 'directory',
|
|
116
|
-
label: {
|
|
125
|
+
label: {
|
|
126
|
+
text: 'tests',
|
|
127
|
+
bounds: { minX: -2, maxX: 32, minZ: 72, maxZ: 76 },
|
|
128
|
+
position: 'bottom',
|
|
129
|
+
},
|
|
117
130
|
},
|
|
118
131
|
],
|
|
119
132
|
bounds: { minX: -5, maxX: 85, minZ: -5, maxZ: 80 },
|
|
@@ -164,7 +177,11 @@ function generateLargeCityData(): CityData {
|
|
|
164
177
|
buildings,
|
|
165
178
|
districts,
|
|
166
179
|
bounds: { minX: -10, maxX: totalSize + 10, minZ: -10, maxZ: totalSize + 10 },
|
|
167
|
-
metadata: {
|
|
180
|
+
metadata: {
|
|
181
|
+
totalFiles: buildings.length,
|
|
182
|
+
totalDirectories: districts.length,
|
|
183
|
+
rootPath: '/large-project',
|
|
184
|
+
},
|
|
168
185
|
};
|
|
169
186
|
}
|
|
170
187
|
|
|
@@ -211,7 +228,11 @@ function generateMonorepoCityData(): CityData {
|
|
|
211
228
|
buildings,
|
|
212
229
|
districts,
|
|
213
230
|
bounds: { minX: -10, maxX: 175, minZ: -10, maxZ: 110 },
|
|
214
|
-
metadata: {
|
|
231
|
+
metadata: {
|
|
232
|
+
totalFiles: buildings.length,
|
|
233
|
+
totalDirectories: districts.length,
|
|
234
|
+
rootPath: '/monorepo',
|
|
235
|
+
},
|
|
215
236
|
};
|
|
216
237
|
}
|
|
217
238
|
|
|
@@ -345,7 +366,7 @@ export const WithClickHandler: Story = {
|
|
|
345
366
|
args: {
|
|
346
367
|
cityData: sampleCityData,
|
|
347
368
|
height: '100vh',
|
|
348
|
-
onBuildingClick:
|
|
369
|
+
onBuildingClick: building => {
|
|
349
370
|
console.log('Clicked building:', building.path);
|
|
350
371
|
alert(`Clicked: ${building.path}`);
|
|
351
372
|
},
|
|
@@ -484,6 +505,109 @@ import authServerCityData from '../../../../assets/auth-server-city-data.json';
|
|
|
484
505
|
import electronAppCityData from '../../../../assets/electron-app-city-data.json';
|
|
485
506
|
import thisRepoCityData from '../../../../assets/this-repo-city-data.json';
|
|
486
507
|
|
|
508
|
+
// Tour step definitions for auth-server
|
|
509
|
+
interface TourStep {
|
|
510
|
+
id: string;
|
|
511
|
+
title: string;
|
|
512
|
+
description: string;
|
|
513
|
+
highlightLayers: HighlightLayer[];
|
|
514
|
+
isolationMode: IsolationMode;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
const authServerTourSteps: TourStep[] = [
|
|
518
|
+
{
|
|
519
|
+
id: 'overview',
|
|
520
|
+
title: 'Welcome to Auth Server',
|
|
521
|
+
description:
|
|
522
|
+
"This is the authentication server for Principal ADE. Let's explore its architecture.",
|
|
523
|
+
highlightLayers: [],
|
|
524
|
+
isolationMode: 'none' as const,
|
|
525
|
+
},
|
|
526
|
+
{
|
|
527
|
+
id: 'workos-auth',
|
|
528
|
+
title: 'WorkOS Authentication',
|
|
529
|
+
description:
|
|
530
|
+
'The core authentication flow using WorkOS. Handles OAuth callbacks, token exchange, and verification.',
|
|
531
|
+
highlightLayers: [
|
|
532
|
+
{
|
|
533
|
+
id: 'workos',
|
|
534
|
+
name: 'WorkOS Auth',
|
|
535
|
+
enabled: true,
|
|
536
|
+
color: '#22c55e',
|
|
537
|
+
items: [{ path: 'auth-server/src/app/api/auth/workos', type: 'directory' as const }],
|
|
538
|
+
},
|
|
539
|
+
],
|
|
540
|
+
isolationMode: 'transparent' as const,
|
|
541
|
+
},
|
|
542
|
+
{
|
|
543
|
+
id: 'browser-cli-tokens',
|
|
544
|
+
title: 'Token Endpoints',
|
|
545
|
+
description: 'Separate token endpoints for browser clients and CLI tools.',
|
|
546
|
+
highlightLayers: [
|
|
547
|
+
{
|
|
548
|
+
id: 'browser',
|
|
549
|
+
name: 'Browser Tokens',
|
|
550
|
+
enabled: true,
|
|
551
|
+
color: '#3b82f6',
|
|
552
|
+
items: [{ path: 'auth-server/src/app/api/auth/browser', type: 'directory' as const }],
|
|
553
|
+
},
|
|
554
|
+
{
|
|
555
|
+
id: 'cli',
|
|
556
|
+
name: 'CLI Tokens',
|
|
557
|
+
enabled: true,
|
|
558
|
+
color: '#f59e0b',
|
|
559
|
+
items: [{ path: 'auth-server/src/app/api/auth/cli', type: 'directory' as const }],
|
|
560
|
+
},
|
|
561
|
+
],
|
|
562
|
+
isolationMode: 'transparent' as const,
|
|
563
|
+
},
|
|
564
|
+
{
|
|
565
|
+
id: 'lib-utilities',
|
|
566
|
+
title: 'Core Libraries',
|
|
567
|
+
description: 'Shared utilities including telemetry, token storage, and session management.',
|
|
568
|
+
highlightLayers: [
|
|
569
|
+
{
|
|
570
|
+
id: 'lib',
|
|
571
|
+
name: 'Libraries',
|
|
572
|
+
enabled: true,
|
|
573
|
+
color: '#8b5cf6',
|
|
574
|
+
items: [{ path: 'auth-server/src/lib', type: 'directory' as const }],
|
|
575
|
+
},
|
|
576
|
+
],
|
|
577
|
+
isolationMode: 'collapse' as const,
|
|
578
|
+
},
|
|
579
|
+
{
|
|
580
|
+
id: 'api-testing',
|
|
581
|
+
title: 'API Testing with Bruno',
|
|
582
|
+
description: 'Bruno collection for testing all authentication endpoints.',
|
|
583
|
+
highlightLayers: [
|
|
584
|
+
{
|
|
585
|
+
id: 'bruno',
|
|
586
|
+
name: 'Bruno Tests',
|
|
587
|
+
enabled: true,
|
|
588
|
+
color: '#ef4444',
|
|
589
|
+
items: [{ path: 'auth-server/bruno', type: 'directory' as const }],
|
|
590
|
+
},
|
|
591
|
+
],
|
|
592
|
+
isolationMode: 'hide' as const,
|
|
593
|
+
},
|
|
594
|
+
{
|
|
595
|
+
id: 'architecture-docs',
|
|
596
|
+
title: 'Architecture Documentation',
|
|
597
|
+
description: 'OTEL canvas files and workflow definitions documenting the auth flows.',
|
|
598
|
+
highlightLayers: [
|
|
599
|
+
{
|
|
600
|
+
id: 'views',
|
|
601
|
+
name: 'Principal Views',
|
|
602
|
+
enabled: true,
|
|
603
|
+
color: '#ec4899',
|
|
604
|
+
items: [{ path: 'auth-server/.principal-views', type: 'directory' as const }],
|
|
605
|
+
},
|
|
606
|
+
],
|
|
607
|
+
isolationMode: 'transparent' as const,
|
|
608
|
+
},
|
|
609
|
+
];
|
|
610
|
+
|
|
487
611
|
/**
|
|
488
612
|
* Auth Server - Real repository data
|
|
489
613
|
*/
|
|
@@ -540,3 +664,302 @@ export const ThisRepo: Story = {
|
|
|
540
664
|
},
|
|
541
665
|
},
|
|
542
666
|
};
|
|
667
|
+
|
|
668
|
+
/**
|
|
669
|
+
* Auth Server Tour Simulation - Demonstrates how tours work in 3D
|
|
670
|
+
*/
|
|
671
|
+
const AuthServerTourTemplate: React.FC = () => {
|
|
672
|
+
const [currentStep, setCurrentStep] = React.useState(0);
|
|
673
|
+
const step = authServerTourSteps[currentStep];
|
|
674
|
+
|
|
675
|
+
const goToStep = (index: number) => {
|
|
676
|
+
if (index >= 0 && index < authServerTourSteps.length) {
|
|
677
|
+
setCurrentStep(index);
|
|
678
|
+
}
|
|
679
|
+
};
|
|
680
|
+
|
|
681
|
+
return (
|
|
682
|
+
<div
|
|
683
|
+
style={{ height: '100vh', display: 'flex', flexDirection: 'column', position: 'relative' }}
|
|
684
|
+
>
|
|
685
|
+
{/* 3D City */}
|
|
686
|
+
<FileCity3D
|
|
687
|
+
cityData={authServerCityData as CityData}
|
|
688
|
+
height="100%"
|
|
689
|
+
heightScaling="linear"
|
|
690
|
+
linearScale={0.5}
|
|
691
|
+
highlightLayers={step.highlightLayers}
|
|
692
|
+
isolationMode={step.isolationMode}
|
|
693
|
+
dimOpacity={0.12}
|
|
694
|
+
animation={{
|
|
695
|
+
startFlat: true,
|
|
696
|
+
autoStartDelay: 600,
|
|
697
|
+
staggerDelay: 8,
|
|
698
|
+
tension: 140,
|
|
699
|
+
friction: 14,
|
|
700
|
+
}}
|
|
701
|
+
showControls={true}
|
|
702
|
+
/>
|
|
703
|
+
|
|
704
|
+
{/* Tour controls - bottom bar */}
|
|
705
|
+
<div
|
|
706
|
+
style={{
|
|
707
|
+
position: 'absolute',
|
|
708
|
+
bottom: 0,
|
|
709
|
+
left: 0,
|
|
710
|
+
right: 0,
|
|
711
|
+
zIndex: 100,
|
|
712
|
+
background: 'rgba(15, 23, 42, 0.95)',
|
|
713
|
+
borderTop: '1px solid #334155',
|
|
714
|
+
padding: '16px 24px',
|
|
715
|
+
color: '#e2e8f0',
|
|
716
|
+
fontFamily: 'system-ui, sans-serif',
|
|
717
|
+
display: 'flex',
|
|
718
|
+
alignItems: 'center',
|
|
719
|
+
gap: 24,
|
|
720
|
+
}}
|
|
721
|
+
>
|
|
722
|
+
{/* Previous button */}
|
|
723
|
+
<button
|
|
724
|
+
onClick={() => goToStep(currentStep - 1)}
|
|
725
|
+
disabled={currentStep === 0}
|
|
726
|
+
style={{
|
|
727
|
+
padding: '10px 20px',
|
|
728
|
+
background: currentStep === 0 ? '#1e293b' : '#334155',
|
|
729
|
+
border: '1px solid #475569',
|
|
730
|
+
borderRadius: 6,
|
|
731
|
+
color: currentStep === 0 ? '#475569' : '#e2e8f0',
|
|
732
|
+
cursor: currentStep === 0 ? 'not-allowed' : 'pointer',
|
|
733
|
+
fontSize: 14,
|
|
734
|
+
fontWeight: 500,
|
|
735
|
+
}}
|
|
736
|
+
>
|
|
737
|
+
← Previous
|
|
738
|
+
</button>
|
|
739
|
+
|
|
740
|
+
{/* Step content - center */}
|
|
741
|
+
<div
|
|
742
|
+
style={{
|
|
743
|
+
flex: 1,
|
|
744
|
+
display: 'flex',
|
|
745
|
+
flexDirection: 'column',
|
|
746
|
+
alignItems: 'center',
|
|
747
|
+
gap: 8,
|
|
748
|
+
}}
|
|
749
|
+
>
|
|
750
|
+
{/* Step indicators */}
|
|
751
|
+
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
|
|
752
|
+
{authServerTourSteps.map((s, i) => (
|
|
753
|
+
<button
|
|
754
|
+
key={s.id}
|
|
755
|
+
onClick={() => goToStep(i)}
|
|
756
|
+
style={{
|
|
757
|
+
width: i === currentStep ? 12 : 10,
|
|
758
|
+
height: i === currentStep ? 12 : 10,
|
|
759
|
+
borderRadius: '50%',
|
|
760
|
+
border: i === currentStep ? '2px solid #3b82f6' : 'none',
|
|
761
|
+
background:
|
|
762
|
+
i === currentStep ? '#3b82f6' : i < currentStep ? '#22c55e' : '#475569',
|
|
763
|
+
cursor: 'pointer',
|
|
764
|
+
padding: 0,
|
|
765
|
+
transition: 'all 0.2s',
|
|
766
|
+
}}
|
|
767
|
+
title={s.title}
|
|
768
|
+
/>
|
|
769
|
+
))}
|
|
770
|
+
</div>
|
|
771
|
+
|
|
772
|
+
{/* Step info */}
|
|
773
|
+
<div style={{ textAlign: 'center' }}>
|
|
774
|
+
<span style={{ fontSize: 11, color: '#64748b' }}>
|
|
775
|
+
Step {currentStep + 1} of {authServerTourSteps.length}
|
|
776
|
+
</span>
|
|
777
|
+
<span style={{ margin: '0 8px', color: '#334155' }}>•</span>
|
|
778
|
+
<span style={{ fontSize: 16, fontWeight: 600 }}>{step.title}</span>
|
|
779
|
+
</div>
|
|
780
|
+
|
|
781
|
+
{/* Description */}
|
|
782
|
+
<p
|
|
783
|
+
style={{
|
|
784
|
+
margin: 0,
|
|
785
|
+
fontSize: 13,
|
|
786
|
+
color: '#94a3b8',
|
|
787
|
+
textAlign: 'center',
|
|
788
|
+
maxWidth: 600,
|
|
789
|
+
}}
|
|
790
|
+
>
|
|
791
|
+
{step.description}
|
|
792
|
+
</p>
|
|
793
|
+
|
|
794
|
+
{/* Isolation mode indicator */}
|
|
795
|
+
<div style={{ fontSize: 11, color: '#64748b' }}>
|
|
796
|
+
Isolation:{' '}
|
|
797
|
+
<code
|
|
798
|
+
style={{
|
|
799
|
+
color: '#94a3b8',
|
|
800
|
+
background: '#1e293b',
|
|
801
|
+
padding: '2px 6px',
|
|
802
|
+
borderRadius: 4,
|
|
803
|
+
}}
|
|
804
|
+
>
|
|
805
|
+
{step.isolationMode}
|
|
806
|
+
</code>
|
|
807
|
+
{step.highlightLayers.length > 0 && (
|
|
808
|
+
<span style={{ marginLeft: 8 }}>
|
|
809
|
+
• {step.highlightLayers.length} layer{step.highlightLayers.length > 1 ? 's' : ''}{' '}
|
|
810
|
+
active
|
|
811
|
+
</span>
|
|
812
|
+
)}
|
|
813
|
+
</div>
|
|
814
|
+
</div>
|
|
815
|
+
|
|
816
|
+
{/* Next button */}
|
|
817
|
+
<button
|
|
818
|
+
onClick={() => goToStep(currentStep + 1)}
|
|
819
|
+
disabled={currentStep === authServerTourSteps.length - 1}
|
|
820
|
+
style={{
|
|
821
|
+
padding: '10px 20px',
|
|
822
|
+
background: currentStep === authServerTourSteps.length - 1 ? '#1e293b' : '#3b82f6',
|
|
823
|
+
border: '1px solid transparent',
|
|
824
|
+
borderRadius: 6,
|
|
825
|
+
color: currentStep === authServerTourSteps.length - 1 ? '#475569' : '#ffffff',
|
|
826
|
+
cursor: currentStep === authServerTourSteps.length - 1 ? 'not-allowed' : 'pointer',
|
|
827
|
+
fontSize: 14,
|
|
828
|
+
fontWeight: 500,
|
|
829
|
+
}}
|
|
830
|
+
>
|
|
831
|
+
Next →
|
|
832
|
+
</button>
|
|
833
|
+
</div>
|
|
834
|
+
</div>
|
|
835
|
+
);
|
|
836
|
+
};
|
|
837
|
+
|
|
838
|
+
export const AuthServerTour: Story = {
|
|
839
|
+
render: () => <AuthServerTourTemplate />,
|
|
840
|
+
};
|
|
841
|
+
|
|
842
|
+
/**
|
|
843
|
+
* Directory Selection - Click directories to focus and collapse others
|
|
844
|
+
*/
|
|
845
|
+
const DirectorySelectionTemplate: React.FC = () => {
|
|
846
|
+
const [focusDirectory, setFocusDirectory] = React.useState<string | null>(null);
|
|
847
|
+
|
|
848
|
+
// Extract unique top-level directories from the auth server data
|
|
849
|
+
const directories = React.useMemo(() => {
|
|
850
|
+
const dirSet = new Set<string>();
|
|
851
|
+
(authServerCityData as CityData).buildings.forEach(building => {
|
|
852
|
+
const parts = building.path.split('/');
|
|
853
|
+
if (parts.length >= 2) {
|
|
854
|
+
// Get first two levels for more interesting navigation
|
|
855
|
+
dirSet.add(parts.slice(0, 2).join('/'));
|
|
856
|
+
}
|
|
857
|
+
});
|
|
858
|
+
return Array.from(dirSet).sort();
|
|
859
|
+
}, []);
|
|
860
|
+
|
|
861
|
+
return (
|
|
862
|
+
<div
|
|
863
|
+
style={{ height: '100vh', display: 'flex', flexDirection: 'column', position: 'relative' }}
|
|
864
|
+
>
|
|
865
|
+
{/* 3D City */}
|
|
866
|
+
<FileCity3D
|
|
867
|
+
cityData={authServerCityData as CityData}
|
|
868
|
+
height="100%"
|
|
869
|
+
heightScaling="linear"
|
|
870
|
+
linearScale={0.5}
|
|
871
|
+
focusDirectory={focusDirectory}
|
|
872
|
+
animation={{
|
|
873
|
+
startFlat: true,
|
|
874
|
+
autoStartDelay: 600,
|
|
875
|
+
staggerDelay: 8,
|
|
876
|
+
tension: 140,
|
|
877
|
+
friction: 14,
|
|
878
|
+
}}
|
|
879
|
+
showControls={true}
|
|
880
|
+
onBuildingClick={building => {
|
|
881
|
+
// Extract directory from building path
|
|
882
|
+
const parts = building.path.split('/');
|
|
883
|
+
if (parts.length >= 2) {
|
|
884
|
+
const dir = parts.slice(0, 2).join('/');
|
|
885
|
+
setFocusDirectory(prev => (prev === dir ? null : dir));
|
|
886
|
+
}
|
|
887
|
+
}}
|
|
888
|
+
/>
|
|
889
|
+
|
|
890
|
+
{/* Directory selector - bottom bar */}
|
|
891
|
+
<div
|
|
892
|
+
style={{
|
|
893
|
+
position: 'absolute',
|
|
894
|
+
bottom: 0,
|
|
895
|
+
left: 0,
|
|
896
|
+
right: 0,
|
|
897
|
+
zIndex: 100,
|
|
898
|
+
background: 'rgba(15, 23, 42, 0.95)',
|
|
899
|
+
borderTop: '1px solid #334155',
|
|
900
|
+
padding: '16px 24px',
|
|
901
|
+
color: '#e2e8f0',
|
|
902
|
+
fontFamily: 'system-ui, sans-serif',
|
|
903
|
+
}}
|
|
904
|
+
>
|
|
905
|
+
<div style={{ marginBottom: 12, fontSize: 12, color: '#64748b' }}>
|
|
906
|
+
Click a directory to focus (collapse others). Click again or "Show All" to reset.
|
|
907
|
+
</div>
|
|
908
|
+
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 8 }}>
|
|
909
|
+
<button
|
|
910
|
+
onClick={() => setFocusDirectory(null)}
|
|
911
|
+
style={{
|
|
912
|
+
padding: '8px 16px',
|
|
913
|
+
background: focusDirectory === null ? '#3b82f6' : '#334155',
|
|
914
|
+
border: '1px solid #475569',
|
|
915
|
+
borderRadius: 6,
|
|
916
|
+
color: focusDirectory === null ? '#ffffff' : '#e2e8f0',
|
|
917
|
+
cursor: 'pointer',
|
|
918
|
+
fontSize: 13,
|
|
919
|
+
fontWeight: 500,
|
|
920
|
+
}}
|
|
921
|
+
>
|
|
922
|
+
Show All
|
|
923
|
+
</button>
|
|
924
|
+
{directories.map(dir => (
|
|
925
|
+
<button
|
|
926
|
+
key={dir}
|
|
927
|
+
onClick={() => setFocusDirectory(prev => (prev === dir ? null : dir))}
|
|
928
|
+
style={{
|
|
929
|
+
padding: '8px 16px',
|
|
930
|
+
background: focusDirectory === dir ? '#3b82f6' : '#334155',
|
|
931
|
+
border: '1px solid #475569',
|
|
932
|
+
borderRadius: 6,
|
|
933
|
+
color: focusDirectory === dir ? '#ffffff' : '#e2e8f0',
|
|
934
|
+
cursor: 'pointer',
|
|
935
|
+
fontSize: 13,
|
|
936
|
+
fontWeight: 500,
|
|
937
|
+
}}
|
|
938
|
+
>
|
|
939
|
+
{dir.split('/').pop()}
|
|
940
|
+
</button>
|
|
941
|
+
))}
|
|
942
|
+
</div>
|
|
943
|
+
{focusDirectory && (
|
|
944
|
+
<div style={{ marginTop: 12, fontSize: 14 }}>
|
|
945
|
+
Focused:{' '}
|
|
946
|
+
<code
|
|
947
|
+
style={{
|
|
948
|
+
color: '#3b82f6',
|
|
949
|
+
background: '#1e293b',
|
|
950
|
+
padding: '4px 8px',
|
|
951
|
+
borderRadius: 4,
|
|
952
|
+
}}
|
|
953
|
+
>
|
|
954
|
+
{focusDirectory}
|
|
955
|
+
</code>
|
|
956
|
+
</div>
|
|
957
|
+
)}
|
|
958
|
+
</div>
|
|
959
|
+
</div>
|
|
960
|
+
);
|
|
961
|
+
};
|
|
962
|
+
|
|
963
|
+
export const DirectorySelection: Story = {
|
|
964
|
+
render: () => <DirectorySelectionTemplate />,
|
|
965
|
+
};
|