@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.
@@ -1,18 +1,17 @@
1
1
  import React from 'react';
2
2
  import type { Meta, StoryObj } from '@storybook/react';
3
- import { ThemeProvider } from '@principal-ade/industry-theme';
4
- import { FileCity3D, type CityData, type CityBuilding, type CityDistrict } from '../components/FileCity3D';
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: { text: 'src', bounds: { minX: -2, maxX: 42, minZ: 42, maxZ: 46 }, position: 'bottom' },
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: { text: 'components', bounds: { minX: 48, maxX: 82, minZ: 32, maxZ: 36 }, position: 'bottom' },
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: { text: 'utils', bounds: { minX: 48, maxX: 77, minZ: 67, maxZ: 71 }, position: 'bottom' },
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: { text: 'tests', bounds: { minX: -2, maxX: 32, minZ: 72, maxZ: 76 }, position: 'bottom' },
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: { totalFiles: buildings.length, totalDirectories: districts.length, rootPath: '/large-project' },
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: { totalFiles: buildings.length, totalDirectories: districts.length, rootPath: '/monorepo' },
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: (building) => {
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
+ };