@pennyfarthing/cyclist 10.4.0 → 11.0.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/dist/api/agent-load.d.ts +1 -2
- package/dist/api/agent-load.d.ts.map +1 -1
- package/dist/api/agent-load.js +2 -123
- package/dist/api/agent-load.js.map +1 -1
- package/dist/api/audit-log.d.ts +1 -17
- package/dist/api/audit-log.d.ts.map +1 -1
- package/dist/api/audit-log.js +2 -162
- package/dist/api/audit-log.js.map +1 -1
- package/dist/api/background-tasks.d.ts +1 -26
- package/dist/api/background-tasks.d.ts.map +1 -1
- package/dist/api/background-tasks.js +2 -55
- package/dist/api/background-tasks.js.map +1 -1
- package/dist/api/bell.d.ts +1 -18
- package/dist/api/bell.d.ts.map +1 -1
- package/dist/api/bell.js +2 -33
- package/dist/api/bell.js.map +1 -1
- package/dist/api/code-markers.d.ts +1 -8
- package/dist/api/code-markers.d.ts.map +1 -1
- package/dist/api/code-markers.js +2 -61
- package/dist/api/code-markers.js.map +1 -1
- package/dist/api/complexity.d.ts +1 -2
- package/dist/api/complexity.d.ts.map +1 -1
- package/dist/api/complexity.js +2 -46
- package/dist/api/complexity.js.map +1 -1
- package/dist/api/context.d.ts +1 -37
- package/dist/api/context.d.ts.map +1 -1
- package/dist/api/context.js +2 -143
- package/dist/api/context.js.map +1 -1
- package/dist/api/dead-code.d.ts +1 -2
- package/dist/api/dead-code.d.ts.map +1 -1
- package/dist/api/dead-code.js +2 -69
- package/dist/api/dead-code.js.map +1 -1
- package/dist/api/dependencies.d.ts +1 -2
- package/dist/api/dependencies.d.ts.map +1 -1
- package/dist/api/dependencies.js +2 -42
- package/dist/api/dependencies.js.map +1 -1
- package/dist/api/evaluation.d.ts +1 -19
- package/dist/api/evaluation.d.ts.map +1 -1
- package/dist/api/evaluation.js +2 -127
- package/dist/api/evaluation.js.map +1 -1
- package/dist/api/file-browser.d.ts +1 -8
- package/dist/api/file-browser.d.ts.map +1 -1
- package/dist/api/file-browser.js +2 -114
- package/dist/api/file-browser.js.map +1 -1
- package/dist/api/git.d.ts +1 -46
- package/dist/api/git.d.ts.map +1 -1
- package/dist/api/git.js +2 -354
- package/dist/api/git.js.map +1 -1
- package/dist/api/health-score.d.ts +1 -2
- package/dist/api/health-score.d.ts.map +1 -1
- package/dist/api/health-score.js +2 -46
- package/dist/api/health-score.js.map +1 -1
- package/dist/api/hook-request.d.ts +1 -40
- package/dist/api/hook-request.d.ts.map +1 -1
- package/dist/api/hook-request.js +2 -277
- package/dist/api/hook-request.js.map +1 -1
- package/dist/api/hotspots.d.ts +1 -2
- package/dist/api/hotspots.d.ts.map +1 -1
- package/dist/api/hotspots.js +2 -61
- package/dist/api/hotspots.js.map +1 -1
- package/dist/api/identity.d.ts +1 -16
- package/dist/api/identity.d.ts.map +1 -1
- package/dist/api/identity.js +2 -78
- package/dist/api/identity.js.map +1 -1
- package/dist/api/index.d.ts +1 -34
- package/dist/api/index.d.ts.map +1 -1
- package/dist/api/index.js +2 -44
- package/dist/api/index.js.map +1 -1
- package/dist/api/mode.d.ts +1 -22
- package/dist/api/mode.d.ts.map +1 -1
- package/dist/api/mode.js +2 -37
- package/dist/api/mode.js.map +1 -1
- package/dist/api/otlp.d.ts +1 -2
- package/dist/api/otlp.d.ts.map +1 -1
- package/dist/api/otlp.js +2 -46
- package/dist/api/otlp.js.map +1 -1
- package/dist/api/permissions.d.ts +1 -15
- package/dist/api/permissions.d.ts.map +1 -1
- package/dist/api/permissions.js +2 -66
- package/dist/api/permissions.js.map +1 -1
- package/dist/api/persona.d.ts +1 -8
- package/dist/api/persona.d.ts.map +1 -1
- package/dist/api/persona.js +2 -67
- package/dist/api/persona.js.map +1 -1
- package/dist/api/portrait.d.ts +1 -5
- package/dist/api/portrait.d.ts.map +1 -1
- package/dist/api/portrait.js +2 -27
- package/dist/api/portrait.js.map +1 -1
- package/dist/api/settings.d.ts +1 -53
- package/dist/api/settings.d.ts.map +1 -1
- package/dist/api/settings.js +2 -464
- package/dist/api/settings.js.map +1 -1
- package/dist/api/spans.d.ts +1 -16
- package/dist/api/spans.d.ts.map +1 -1
- package/dist/api/spans.js +2 -244
- package/dist/api/spans.js.map +1 -1
- package/dist/api/stats.d.ts +1 -12
- package/dist/api/stats.d.ts.map +1 -1
- package/dist/api/stats.js +2 -84
- package/dist/api/stats.js.map +1 -1
- package/dist/api/story.d.ts +1 -2
- package/dist/api/story.d.ts.map +1 -1
- package/dist/api/story.js +2 -14
- package/dist/api/story.js.map +1 -1
- package/dist/api/telemetry.d.ts +1 -18
- package/dist/api/telemetry.d.ts.map +1 -1
- package/dist/api/telemetry.js +2 -164
- package/dist/api/telemetry.js.map +1 -1
- package/dist/api/theme-agents.d.ts +1 -60
- package/dist/api/theme-agents.d.ts.map +1 -1
- package/dist/api/theme-agents.js +2 -213
- package/dist/api/theme-agents.js.map +1 -1
- package/dist/api/todos.d.ts +1 -32
- package/dist/api/todos.d.ts.map +1 -1
- package/dist/api/todos.js +2 -43
- package/dist/api/todos.js.map +1 -1
- package/dist/api/token-stats.d.ts +1 -7
- package/dist/api/token-stats.d.ts.map +1 -1
- package/dist/api/token-stats.js +2 -35
- package/dist/api/token-stats.js.map +1 -1
- package/dist/api/welcome.d.ts +1 -21
- package/dist/api/welcome.d.ts.map +1 -1
- package/dist/api/welcome.js +2 -34
- package/dist/api/welcome.js.map +1 -1
- package/dist/bikerack.js +2 -2
- package/dist/bikerack.js.map +1 -1
- package/dist/env.d.ts +6 -0
- package/dist/env.d.ts.map +1 -0
- package/dist/env.js +10 -0
- package/dist/env.js.map +1 -0
- package/dist/focus.d.ts +53 -0
- package/dist/focus.d.ts.map +1 -0
- package/dist/focus.js +122 -0
- package/dist/focus.js.map +1 -0
- package/dist/git-cache.d.ts +1 -0
- package/dist/git-cache.d.ts.map +1 -1
- package/dist/git-cache.js +3 -1
- package/dist/git-cache.js.map +1 -1
- package/dist/menu-builder.d.ts.map +1 -1
- package/dist/menu-builder.js +0 -1
- package/dist/menu-builder.js.map +1 -1
- package/dist/prime.d.ts +3 -3
- package/dist/prime.d.ts.map +1 -1
- package/dist/prime.js +38 -14
- package/dist/prime.js.map +1 -1
- package/dist/public/css/react.css +1 -1
- package/dist/public/js/react/react.js +53 -61
- package/dist/server.d.ts +18 -85
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +105 -405
- package/dist/server.js.map +1 -1
- package/dist/sprint-data.d.ts +1 -1
- package/dist/sprint-data.d.ts.map +1 -1
- package/dist/sprint-data.js +2 -2
- package/dist/sprint-data.js.map +1 -1
- package/dist/theme-metadata.d.ts +3 -3
- package/dist/theme-metadata.d.ts.map +1 -1
- package/dist/theme-metadata.js +4 -4
- package/dist/theme-metadata.js.map +1 -1
- package/dist/websocket.d.ts +2 -0
- package/dist/websocket.d.ts.map +1 -1
- package/dist/websocket.js +53 -75
- package/dist/websocket.js.map +1 -1
- package/package.json +2 -6
- package/portraits/hogans-heroes/large/burkhalter-35312.png +0 -0
- package/portraits/hogans-heroes/large/carter-34352.png +0 -0
- package/portraits/hogans-heroes/large/hochstetter-45314.png +0 -0
- package/portraits/hogans-heroes/large/hogan-44541.png +0 -0
- package/portraits/hogans-heroes/large/kinch-35241.png +0 -0
- package/portraits/hogans-heroes/large/klink-23434.png +0 -0
- package/portraits/hogans-heroes/large/lebeau-45443.png +0 -0
- package/portraits/hogans-heroes/large/marya-53543.png +0 -0
- package/portraits/hogans-heroes/large/newkirk-54432.png +0 -0
- package/portraits/hogans-heroes/large/schultz-42453.png +0 -0
- package/portraits/hogans-heroes/large/underground-55131.png +0 -0
- package/portraits/hogans-heroes/medium/burkhalter-35312.png +0 -0
- package/portraits/hogans-heroes/medium/carter-34352.png +0 -0
- package/portraits/hogans-heroes/medium/hochstetter-45314.png +0 -0
- package/portraits/hogans-heroes/medium/hogan-44541.png +0 -0
- package/portraits/hogans-heroes/medium/kinch-35241.png +0 -0
- package/portraits/hogans-heroes/medium/klink-23434.png +0 -0
- package/portraits/hogans-heroes/medium/lebeau-45443.png +0 -0
- package/portraits/hogans-heroes/medium/marya-53543.png +0 -0
- package/portraits/hogans-heroes/medium/newkirk-54432.png +0 -0
- package/portraits/hogans-heroes/medium/schultz-42453.png +0 -0
- package/portraits/hogans-heroes/medium/underground-55131.png +0 -0
- package/portraits/monty-python/large/announcer-44441.png +0 -0
- package/portraits/monty-python/large/arguer-35412.png +0 -0
- package/portraits/monty-python/large/bicycle-repair-man-35241.png +0 -0
- package/portraits/monty-python/large/colonel-35423.png +0 -0
- package/portraits/monty-python/large/counsellor-45341.png +0 -0
- package/portraits/monty-python/large/gumbys-23524.png +0 -0
- package/portraits/monty-python/large/nudge-43533.png +0 -0
- package/portraits/monty-python/large/praline-45413.png +0 -0
- package/portraits/monty-python/large/silly-walks-55322.png +0 -0
- package/portraits/monty-python/large/wensleydale-54451.png +0 -0
- package/portraits/monty-python/large/xim-nez-43534.png +0 -0
- package/portraits/monty-python/medium/announcer-44441.png +0 -0
- package/portraits/monty-python/medium/arguer-35412.png +0 -0
- package/portraits/monty-python/medium/bicycle-repair-man-35241.png +0 -0
- package/portraits/monty-python/medium/colonel-35423.png +0 -0
- package/portraits/monty-python/medium/counsellor-45341.png +0 -0
- package/portraits/monty-python/medium/gumbys-23524.png +0 -0
- package/portraits/monty-python/medium/nudge-43533.png +0 -0
- package/portraits/monty-python/medium/praline-45413.png +0 -0
- package/portraits/monty-python/medium/silly-walks-55322.png +0 -0
- package/portraits/monty-python/medium/wensleydale-54451.png +0 -0
- package/portraits/monty-python/medium/xim-nez-43534.png +0 -0
- package/portraits/stephen-king/large/andy-55231.png +0 -0
- package/portraits/stephen-king/large/christine-25112.png +0 -0
- package/portraits/stephen-king/large/danny-53243.png +0 -0
- package/portraits/stephen-king/large/flagg-55311.png +0 -0
- package/portraits/stephen-king/large/gaunt-54421.png +0 -0
- package/portraits/stephen-king/large/jack-44224.png +0 -0
- package/portraits/stephen-king/large/johnny-44353.png +0 -0
- package/portraits/stephen-king/large/margaret-15415.png +0 -0
- package/portraits/stephen-king/large/paul-45233.png +0 -0
- package/portraits/stephen-king/large/pennywise-54411.png +0 -0
- package/portraits/stephen-king/large/roland-35121.png +0 -0
- package/portraits/stephen-king/medium/andy-55231.png +0 -0
- package/portraits/stephen-king/medium/christine-25112.png +0 -0
- package/portraits/stephen-king/medium/danny-53243.png +0 -0
- package/portraits/stephen-king/medium/flagg-55311.png +0 -0
- package/portraits/stephen-king/medium/gaunt-54421.png +0 -0
- package/portraits/stephen-king/medium/jack-44224.png +0 -0
- package/portraits/stephen-king/medium/johnny-44353.png +0 -0
- package/portraits/stephen-king/medium/margaret-15415.png +0 -0
- package/portraits/stephen-king/medium/paul-45233.png +0 -0
- package/portraits/stephen-king/medium/pennywise-54411.png +0 -0
- package/portraits/stephen-king/medium/roland-35121.png +0 -0
- package/portraits/star-trek-tng/large/beverly-44352.png +0 -0
- package/portraits/star-trek-tng/large/data-55241.png +0 -0
- package/portraits/star-trek-tng/large/deanna-43353.png +0 -0
- package/portraits/star-trek-tng/large/geordi-54342.png +0 -0
- package/portraits/star-trek-tng/large/jean-luc-45342.png +0 -0
- package/portraits/star-trek-tng/large/kathryn-45332.png +0 -0
- package/portraits/star-trek-tng/large/miles-35342.png +0 -0
- package/portraits/star-trek-tng/large/q-53521.png +0 -0
- package/portraits/star-trek-tng/large/spock-45231.png +0 -0
- package/portraits/star-trek-tng/large/troi-44352.png +0 -0
- package/portraits/star-trek-tng/medium/beverly-44352.png +0 -0
- package/portraits/star-trek-tng/medium/data-55241.png +0 -0
- package/portraits/star-trek-tng/medium/deanna-43353.png +0 -0
- package/portraits/star-trek-tng/medium/geordi-54342.png +0 -0
- package/portraits/star-trek-tng/medium/jean-luc-45342.png +0 -0
- package/portraits/star-trek-tng/medium/kathryn-45332.png +0 -0
- package/portraits/star-trek-tng/medium/miles-35342.png +0 -0
- package/portraits/star-trek-tng/medium/q-53521.png +0 -0
- package/portraits/star-trek-tng/medium/spock-45231.png +0 -0
- package/portraits/star-trek-tng/medium/troi-44352.png +0 -0
- package/src/public/App.tsx +0 -340
- package/src/public/components/AgentLoadDialog.tsx +0 -202
- package/src/public/components/AgentPopup.tsx +0 -308
- package/src/public/components/ApprovalModal/ApprovalModal.css +0 -35
- package/src/public/components/ApprovalModal/index.tsx +0 -632
- package/src/public/components/BikeRackIndex.tsx +0 -54
- package/src/public/components/BikeRackWorkspace.tsx +0 -142
- package/src/public/components/CommandPalette.tsx +0 -555
- package/src/public/components/ConfirmDialog.tsx +0 -168
- package/src/public/components/ContextIndicator/ContextIndicator.css +0 -85
- package/src/public/components/ContextIndicator/index.tsx +0 -330
- package/src/public/components/ContextSparkline.tsx +0 -56
- package/src/public/components/ControlBar.tsx +0 -636
- package/src/public/components/DeadCodeDialog.tsx +0 -169
- package/src/public/components/DiffViewer.tsx +0 -585
- package/src/public/components/DockviewWorkspace.tsx +0 -737
- package/src/public/components/Editor.tsx +0 -630
- package/src/public/components/ErrorBoundary.tsx +0 -67
- package/src/public/components/FileTree.tsx +0 -379
- package/src/public/components/FontPicker/FontPicker.css +0 -276
- package/src/public/components/FontPicker/index.tsx +0 -430
- package/src/public/components/FullFileTree.tsx +0 -237
- package/src/public/components/HealthGauge.tsx +0 -181
- package/src/public/components/Message.tsx +0 -225
- package/src/public/components/MessageList.tsx +0 -98
- package/src/public/components/MessageView.tsx +0 -400
- package/src/public/components/ModeSwitch/ModeSwitch.css +0 -165
- package/src/public/components/ModeSwitch/index.tsx +0 -372
- package/src/public/components/PersonaHeader.tsx +0 -240
- package/src/public/components/QuickActions.tsx +0 -267
- package/src/public/components/SpanTimeline.tsx +0 -352
- package/src/public/components/StandalonePanel.tsx +0 -84
- package/src/public/components/StatsStrip.tsx +0 -162
- package/src/public/components/StreamingContent.tsx +0 -77
- package/src/public/components/SubagentSpan.tsx +0 -180
- package/src/public/components/TandemPortrait.tsx +0 -72
- package/src/public/components/ThemePalette/ThemePalette.css +0 -179
- package/src/public/components/ThemePalette/index.tsx +0 -326
- package/src/public/components/ToolCallBlock.tsx +0 -252
- package/src/public/components/ToolStack.tsx +0 -209
- package/src/public/components/ToolStatus.tsx +0 -57
- package/src/public/components/dialogs/CodeMarkersDialog.tsx +0 -169
- package/src/public/components/dialogs/ComplexityDialog.tsx +0 -163
- package/src/public/components/dialogs/DependenciesDialog.tsx +0 -120
- package/src/public/components/dialogs/HotspotsDialog.tsx +0 -451
- package/src/public/components/dialogs/ToolDialog.tsx +0 -43
- package/src/public/components/panel-registry.ts +0 -11
- package/src/public/components/panels/ACPanel.tsx +0 -93
- package/src/public/components/panels/AcceptanceCriteriaPanel.tsx +0 -104
- package/src/public/components/panels/AuditLogPanel.tsx +0 -465
- package/src/public/components/panels/BackgroundPanel.tsx +0 -115
- package/src/public/components/panels/BikeLanePanel.tsx +0 -214
- package/src/public/components/panels/ChangedPanel.tsx +0 -65
- package/src/public/components/panels/DebugPanel.tsx +0 -344
- package/src/public/components/panels/DiffsPanel.tsx +0 -155
- package/src/public/components/panels/GitPanel.tsx +0 -216
- package/src/public/components/panels/HotspotsPanel.tsx +0 -365
- package/src/public/components/panels/MessagePanel.tsx +0 -497
- package/src/public/components/panels/SettingsPanel.tsx +0 -453
- package/src/public/components/panels/SprintPanel.tsx +0 -670
- package/src/public/components/panels/TTYPanel.tsx +0 -299
- package/src/public/components/panels/TodoPanel.tsx +0 -142
- package/src/public/components/panels/WorkflowPanel.tsx +0 -224
- package/src/public/components/panels/index.ts +0 -24
- package/src/public/components/ui/alert-dialog.tsx +0 -139
- package/src/public/components/ui/badge.tsx +0 -36
- package/src/public/components/ui/button.tsx +0 -57
- package/src/public/components/ui/checkbox.tsx +0 -28
- package/src/public/components/ui/collapsible.tsx +0 -9
- package/src/public/components/ui/command.tsx +0 -151
- package/src/public/components/ui/dialog.tsx +0 -120
- package/src/public/components/ui/popover.tsx +0 -31
- package/src/public/components/ui/progress.tsx +0 -28
- package/src/public/components/ui/scroll-area.tsx +0 -46
- package/src/public/components/ui/select.tsx +0 -157
- package/src/public/components/ui/separator.tsx +0 -29
- package/src/public/components/ui/skeleton.tsx +0 -15
- package/src/public/components/ui/switch.tsx +0 -27
- package/src/public/components/ui/toggle-group.tsx +0 -59
- package/src/public/components/ui/toggle.tsx +0 -43
- package/src/public/components/ui/tooltip.tsx +0 -30
- package/src/public/contexts/ClaudeContext.tsx +0 -311
- package/src/public/contexts/MessageQueueContext.tsx +0 -143
- package/src/public/css/theme-browser.css +0 -550
- package/src/public/css/theme-system.css +0 -630
- package/src/public/hooks/index.ts +0 -49
- package/src/public/hooks/useAgentLoad.ts +0 -105
- package/src/public/hooks/useBackgroundTasks.ts +0 -131
- package/src/public/hooks/useClaude.ts +0 -234
- package/src/public/hooks/useCodeMarkers.ts +0 -101
- package/src/public/hooks/useColorScheme.ts +0 -42
- package/src/public/hooks/useCommandHistory.ts +0 -99
- package/src/public/hooks/useComplexity.ts +0 -80
- package/src/public/hooks/useDeadCode.ts +0 -99
- package/src/public/hooks/useDependencies.ts +0 -82
- package/src/public/hooks/useDiffs.ts +0 -143
- package/src/public/hooks/useFileBrowser.ts +0 -71
- package/src/public/hooks/useGitStatus.ts +0 -233
- package/src/public/hooks/useHealthScore.ts +0 -69
- package/src/public/hooks/useHotspots.ts +0 -123
- package/src/public/hooks/useLayoutPersistence.ts +0 -138
- package/src/public/hooks/useMarkdownParser.ts +0 -36
- package/src/public/hooks/useMarkerActions.ts +0 -234
- package/src/public/hooks/useMessageQueue.ts +0 -380
- package/src/public/hooks/useMessageStream.ts +0 -131
- package/src/public/hooks/usePersona.ts +0 -112
- package/src/public/hooks/usePlanModeExit.ts +0 -105
- package/src/public/hooks/useResponsiveLayout.ts +0 -173
- package/src/public/hooks/useSprint.ts +0 -147
- package/src/public/hooks/useStatsStrip.ts +0 -204
- package/src/public/hooks/useStory.ts +0 -135
- package/src/public/hooks/useSubagentHelper.ts +0 -64
- package/src/public/hooks/useSyntaxHighlighter.ts +0 -52
- package/src/public/hooks/useTabCompletion.ts +0 -124
- package/src/public/hooks/useTodos.ts +0 -93
- package/src/public/hooks/useUserAvatar.ts +0 -54
- package/src/public/index.tsx +0 -10
- package/src/public/lib/utils.ts +0 -6
- package/src/public/styles/dockview-theme.css +0 -459
- package/src/public/styles/tailwind.css +0 -4396
- package/src/public/types/electron.d.ts +0 -18
- package/src/public/types/message.ts +0 -51
- package/src/public/utils/avatar-service.ts +0 -73
- package/src/public/utils/color-presets.ts +0 -940
- package/src/public/utils/font-presets.ts +0 -362
- package/src/public/utils/formatDuration.ts +0 -14
- package/src/public/utils/markdown.ts +0 -249
- package/src/public/utils/messageFilters.ts +0 -128
- package/src/public/utils/slash-commands.ts +0 -353
- package/src/public/utils/subagent-display.ts +0 -146
- package/src/public/utils/syntax.ts +0 -219
- package/src/public/utils/toolIntentSummarizer.ts +0 -199
- package/src/public/utils/toolStackGrouper.ts +0 -106
- package/src/public/utils/toolTypeColors.ts +0 -45
|
@@ -1,430 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* FontPicker Component
|
|
3
|
-
*
|
|
4
|
-
* Font selection with system font browser via queryLocalFonts() API.
|
|
5
|
-
* Story MSSCI-12769 - Font Customization
|
|
6
|
-
*
|
|
7
|
-
* Features:
|
|
8
|
-
* - shadcn Select-based dropdown with system font discovery
|
|
9
|
-
* - Live font preview in each option
|
|
10
|
-
* - Monospace detection for code font filtering
|
|
11
|
-
* - Graceful fallback when queryLocalFonts() unavailable
|
|
12
|
-
* - Font size picker (segmented control)
|
|
13
|
-
* - ARIA accessibility via Radix Select
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
import React, { useState, useEffect, useCallback, useMemo } from 'react';
|
|
17
|
-
import { Button } from '@/components/ui/button';
|
|
18
|
-
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
|
19
|
-
import {
|
|
20
|
-
Select,
|
|
21
|
-
SelectContent,
|
|
22
|
-
SelectGroup,
|
|
23
|
-
SelectItem,
|
|
24
|
-
SelectLabel,
|
|
25
|
-
SelectTrigger,
|
|
26
|
-
SelectValue,
|
|
27
|
-
} from '@/components/ui/select';
|
|
28
|
-
import {
|
|
29
|
-
UI_FONT_PRESETS,
|
|
30
|
-
CODE_FONT_PRESETS,
|
|
31
|
-
FONT_SIZE_SCALE,
|
|
32
|
-
FontSize,
|
|
33
|
-
} from '../../utils/font-presets';
|
|
34
|
-
import './FontPicker.css';
|
|
35
|
-
|
|
36
|
-
// =============================================================================
|
|
37
|
-
// Types
|
|
38
|
-
// =============================================================================
|
|
39
|
-
|
|
40
|
-
export interface FontPickerProps {
|
|
41
|
-
type: 'ui' | 'code';
|
|
42
|
-
currentFont: string;
|
|
43
|
-
customFont?: string;
|
|
44
|
-
onSelect: (presetId: string, customFamily?: string) => void;
|
|
45
|
-
onCustomFontChange?: (family: string) => void;
|
|
46
|
-
className?: string;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export interface FontSizePickerProps {
|
|
50
|
-
currentSize: FontSize;
|
|
51
|
-
onSelect: (size: FontSize) => void;
|
|
52
|
-
className?: string;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
interface SystemFont {
|
|
56
|
-
family: string;
|
|
57
|
-
isMonospace: boolean;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// =============================================================================
|
|
61
|
-
// Monospace Detection via OpenType `post` table
|
|
62
|
-
// =============================================================================
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Parse SFNT table directory to find a table's offset and length.
|
|
66
|
-
* SFNT header: version(4) + numTables(2) + searchRange(2) + entrySelector(2) + rangeShift(2) = 12
|
|
67
|
-
* Each table record: tag(4) + checksum(4) + offset(4) + length(4) = 16
|
|
68
|
-
*/
|
|
69
|
-
function findSfntTable(view: DataView, tag: string): { offset: number; length: number } | null {
|
|
70
|
-
const numTables = view.getUint16(4);
|
|
71
|
-
for (let i = 0; i < numTables; i++) {
|
|
72
|
-
const recordOffset = 12 + i * 16;
|
|
73
|
-
const tableTag = String.fromCharCode(
|
|
74
|
-
view.getUint8(recordOffset),
|
|
75
|
-
view.getUint8(recordOffset + 1),
|
|
76
|
-
view.getUint8(recordOffset + 2),
|
|
77
|
-
view.getUint8(recordOffset + 3),
|
|
78
|
-
);
|
|
79
|
-
if (tableTag === tag) {
|
|
80
|
-
return {
|
|
81
|
-
offset: view.getUint32(recordOffset + 8),
|
|
82
|
-
length: view.getUint32(recordOffset + 12),
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
return null;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Read `isFixedPitch` from the `post` table.
|
|
91
|
-
* post layout: version(4) + italicAngle(4) + underlinePosition(2) + underlineThickness(2) = offset 12
|
|
92
|
-
* isFixedPitch is uint32 at offset 12: 0 = proportional, non-zero = monospace.
|
|
93
|
-
*/
|
|
94
|
-
async function detectMonospaceFromBlob(fontData: { blob: () => Promise<Blob> }): Promise<boolean> {
|
|
95
|
-
try {
|
|
96
|
-
const blob = await fontData.blob();
|
|
97
|
-
const buffer = await blob.arrayBuffer();
|
|
98
|
-
const view = new DataView(buffer);
|
|
99
|
-
|
|
100
|
-
const post = findSfntTable(view, 'post');
|
|
101
|
-
if (post) {
|
|
102
|
-
const isFixedPitch = view.getUint32(post.offset + 12);
|
|
103
|
-
return isFixedPitch !== 0;
|
|
104
|
-
}
|
|
105
|
-
return false;
|
|
106
|
-
} catch {
|
|
107
|
-
return false;
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// =============================================================================
|
|
112
|
-
// System Font Discovery
|
|
113
|
-
// =============================================================================
|
|
114
|
-
|
|
115
|
-
interface FontDataEntry {
|
|
116
|
-
family: string;
|
|
117
|
-
fullName: string;
|
|
118
|
-
postscriptName: string;
|
|
119
|
-
style: string;
|
|
120
|
-
blob: () => Promise<Blob>;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
const FONT_CACHE_KEY = 'cyclist-system-fonts';
|
|
124
|
-
|
|
125
|
-
interface FontCacheData {
|
|
126
|
-
fonts: SystemFont[];
|
|
127
|
-
count: number; // number of font families — if it changes, fonts were installed/removed
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
function loadCachedFonts(): SystemFont[] | null {
|
|
131
|
-
try {
|
|
132
|
-
const raw = localStorage.getItem(FONT_CACHE_KEY);
|
|
133
|
-
if (!raw) return null;
|
|
134
|
-
const data: FontCacheData = JSON.parse(raw);
|
|
135
|
-
if (data.fonts?.length > 0) return data.fonts;
|
|
136
|
-
return null;
|
|
137
|
-
} catch {
|
|
138
|
-
return null;
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
function saveCachedFonts(fonts: SystemFont[], count: number): void {
|
|
143
|
-
try {
|
|
144
|
-
const data: FontCacheData = { fonts, count };
|
|
145
|
-
localStorage.setItem(FONT_CACHE_KEY, JSON.stringify(data));
|
|
146
|
-
} catch {
|
|
147
|
-
// localStorage full or unavailable — not critical
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
let systemFontsPromise: Promise<SystemFont[]> | null = null;
|
|
152
|
-
|
|
153
|
-
async function getSystemFonts(): Promise<SystemFont[]> {
|
|
154
|
-
if (systemFontsPromise) return systemFontsPromise;
|
|
155
|
-
|
|
156
|
-
systemFontsPromise = (async () => {
|
|
157
|
-
if (!('queryLocalFonts' in window)) {
|
|
158
|
-
return [];
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
try {
|
|
162
|
-
const fonts: FontDataEntry[] = await (window as unknown as { queryLocalFonts: () => Promise<FontDataEntry[]> }).queryLocalFonts();
|
|
163
|
-
|
|
164
|
-
// Deduplicate by family name, keep one FontData per family for mono detection
|
|
165
|
-
const familyMap = new Map<string, FontDataEntry>();
|
|
166
|
-
for (const font of fonts) {
|
|
167
|
-
if (!familyMap.has(font.family)) {
|
|
168
|
-
familyMap.set(font.family, font);
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
const familyCount = familyMap.size;
|
|
173
|
-
|
|
174
|
-
// Check localStorage cache — reuse if font count hasn't changed
|
|
175
|
-
const cached = loadCachedFonts();
|
|
176
|
-
if (cached && cached.length > 0) {
|
|
177
|
-
// Load raw cached data to check count
|
|
178
|
-
try {
|
|
179
|
-
const raw = localStorage.getItem(FONT_CACHE_KEY);
|
|
180
|
-
if (raw) {
|
|
181
|
-
const data: FontCacheData = JSON.parse(raw);
|
|
182
|
-
if (data.count === familyCount) {
|
|
183
|
-
return cached;
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
} catch {
|
|
187
|
-
// Fall through to re-detect
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
// Detect monospace via post table in parallel
|
|
192
|
-
const entries = Array.from(familyMap.entries());
|
|
193
|
-
const monoResults = await Promise.all(
|
|
194
|
-
entries.map(([, fontData]) => detectMonospaceFromBlob(fontData))
|
|
195
|
-
);
|
|
196
|
-
|
|
197
|
-
const result: SystemFont[] = entries.map(([family], i) => ({
|
|
198
|
-
family,
|
|
199
|
-
isMonospace: monoResults[i],
|
|
200
|
-
}));
|
|
201
|
-
|
|
202
|
-
result.sort((a, b) => a.family.localeCompare(b.family));
|
|
203
|
-
saveCachedFonts(result, familyCount);
|
|
204
|
-
return result;
|
|
205
|
-
} catch {
|
|
206
|
-
// Permission denied or API error
|
|
207
|
-
return [];
|
|
208
|
-
}
|
|
209
|
-
})();
|
|
210
|
-
|
|
211
|
-
return systemFontsPromise;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
// Prefix for system font values to distinguish from preset IDs
|
|
215
|
-
const SYSTEM_FONT_PREFIX = 'system-font:';
|
|
216
|
-
|
|
217
|
-
// =============================================================================
|
|
218
|
-
// FontPicker Component
|
|
219
|
-
// =============================================================================
|
|
220
|
-
|
|
221
|
-
export function FontPicker({
|
|
222
|
-
type,
|
|
223
|
-
currentFont,
|
|
224
|
-
customFont,
|
|
225
|
-
onSelect,
|
|
226
|
-
onCustomFontChange,
|
|
227
|
-
className = '',
|
|
228
|
-
}: FontPickerProps): React.ReactElement {
|
|
229
|
-
const [customValue, setCustomValue] = useState(customFont || '');
|
|
230
|
-
const [systemFonts, setSystemFonts] = useState<SystemFont[]>([]);
|
|
231
|
-
const [fontsLoaded, setFontsLoaded] = useState(false);
|
|
232
|
-
|
|
233
|
-
const presets = type === 'ui' ? UI_FONT_PRESETS : CODE_FONT_PRESETS;
|
|
234
|
-
const currentPreset = presets.find(p => p.id === currentFont);
|
|
235
|
-
|
|
236
|
-
// Load system fonts eagerly
|
|
237
|
-
useEffect(() => {
|
|
238
|
-
if (!fontsLoaded) {
|
|
239
|
-
getSystemFonts().then(fonts => {
|
|
240
|
-
setSystemFonts(fonts);
|
|
241
|
-
setFontsLoaded(true);
|
|
242
|
-
});
|
|
243
|
-
}
|
|
244
|
-
}, [fontsLoaded]);
|
|
245
|
-
|
|
246
|
-
// Filter system fonts: English-only (Latin names), monospace-only for code
|
|
247
|
-
const filteredSystemFonts = useMemo(() => {
|
|
248
|
-
let fonts = systemFonts;
|
|
249
|
-
|
|
250
|
-
// Filter to fonts with Latin-script names (excludes CJK, Arabic, Devanagari, etc.)
|
|
251
|
-
fonts = fonts.filter(f => /^[\x20-\x7E\u00C0-\u024F]+$/.test(f.family));
|
|
252
|
-
|
|
253
|
-
// For code fonts, only show monospace
|
|
254
|
-
if (type === 'code') {
|
|
255
|
-
fonts = fonts.filter(f => f.isMonospace);
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
return fonts;
|
|
259
|
-
}, [systemFonts, type]);
|
|
260
|
-
|
|
261
|
-
// Non-custom presets for display
|
|
262
|
-
const displayPresets = useMemo(() => {
|
|
263
|
-
return presets.filter(p => !p.isCustom);
|
|
264
|
-
}, [presets]);
|
|
265
|
-
|
|
266
|
-
// Update custom value when prop changes
|
|
267
|
-
useEffect(() => {
|
|
268
|
-
if (customFont !== undefined) {
|
|
269
|
-
setCustomValue(customFont);
|
|
270
|
-
}
|
|
271
|
-
}, [customFont]);
|
|
272
|
-
|
|
273
|
-
// Compute the Select value: for system fonts we encode as "system-font:FamilyName"
|
|
274
|
-
const selectValue = useMemo(() => {
|
|
275
|
-
if (currentFont === 'custom' && customValue) {
|
|
276
|
-
// Check if this matches a system font
|
|
277
|
-
const isSystemFont = systemFonts.some(f => f.family === customValue);
|
|
278
|
-
if (isSystemFont) {
|
|
279
|
-
return `${SYSTEM_FONT_PREFIX}${customValue}`;
|
|
280
|
-
}
|
|
281
|
-
return 'custom';
|
|
282
|
-
}
|
|
283
|
-
return currentFont;
|
|
284
|
-
}, [currentFont, customValue, systemFonts]);
|
|
285
|
-
|
|
286
|
-
const handleValueChange = useCallback(
|
|
287
|
-
(value: string) => {
|
|
288
|
-
if (value.startsWith(SYSTEM_FONT_PREFIX)) {
|
|
289
|
-
const family = value.slice(SYSTEM_FONT_PREFIX.length);
|
|
290
|
-
onSelect('custom', family);
|
|
291
|
-
} else if (value === 'custom') {
|
|
292
|
-
onSelect('custom');
|
|
293
|
-
} else {
|
|
294
|
-
onSelect(value);
|
|
295
|
-
}
|
|
296
|
-
},
|
|
297
|
-
[onSelect]
|
|
298
|
-
);
|
|
299
|
-
|
|
300
|
-
const handleCustomChange = useCallback(
|
|
301
|
-
(e: React.ChangeEvent<HTMLInputElement>) => {
|
|
302
|
-
const value = e.target.value;
|
|
303
|
-
setCustomValue(value);
|
|
304
|
-
onCustomFontChange?.(value);
|
|
305
|
-
if (currentFont === 'custom') {
|
|
306
|
-
onSelect('custom', value);
|
|
307
|
-
}
|
|
308
|
-
},
|
|
309
|
-
[currentFont, onSelect, onCustomFontChange]
|
|
310
|
-
);
|
|
311
|
-
|
|
312
|
-
const hasSystemFonts = systemFonts.length > 0;
|
|
313
|
-
const showCustomInput = currentFont === 'custom' && !hasSystemFonts;
|
|
314
|
-
|
|
315
|
-
return (
|
|
316
|
-
<div className={`font-picker ${className}`}>
|
|
317
|
-
<Select value={selectValue} onValueChange={handleValueChange}>
|
|
318
|
-
<SelectTrigger
|
|
319
|
-
className="font-picker-trigger"
|
|
320
|
-
aria-label={`Select ${type} font`}
|
|
321
|
-
style={{ fontFamily: currentPreset?.fontFamily || (customValue ? `"${customValue}"` : 'inherit') }}
|
|
322
|
-
>
|
|
323
|
-
<SelectValue placeholder="Select font..." />
|
|
324
|
-
</SelectTrigger>
|
|
325
|
-
<SelectContent className="max-h-[300px]">
|
|
326
|
-
{/* Presets section */}
|
|
327
|
-
{displayPresets.length > 0 && (
|
|
328
|
-
<SelectGroup>
|
|
329
|
-
<SelectLabel>Presets</SelectLabel>
|
|
330
|
-
{displayPresets.map((preset) => (
|
|
331
|
-
<SelectItem
|
|
332
|
-
key={preset.id}
|
|
333
|
-
value={preset.id}
|
|
334
|
-
style={{ fontFamily: preset.fontFamily || 'inherit' }}
|
|
335
|
-
>
|
|
336
|
-
<span className="font-picker-item-content">
|
|
337
|
-
<span className="font-name">{preset.name}</span>
|
|
338
|
-
<span className="font-preview" style={{ fontFamily: preset.fontFamily }}>
|
|
339
|
-
Aa
|
|
340
|
-
</span>
|
|
341
|
-
</span>
|
|
342
|
-
</SelectItem>
|
|
343
|
-
))}
|
|
344
|
-
</SelectGroup>
|
|
345
|
-
)}
|
|
346
|
-
|
|
347
|
-
{/* System fonts section */}
|
|
348
|
-
{hasSystemFonts && filteredSystemFonts.length > 0 && (
|
|
349
|
-
<SelectGroup>
|
|
350
|
-
<SelectLabel>System Fonts ({filteredSystemFonts.length})</SelectLabel>
|
|
351
|
-
{filteredSystemFonts.map((font) => (
|
|
352
|
-
<SelectItem
|
|
353
|
-
key={font.family}
|
|
354
|
-
value={`${SYSTEM_FONT_PREFIX}${font.family}`}
|
|
355
|
-
style={{ fontFamily: `"${font.family}", inherit` }}
|
|
356
|
-
>
|
|
357
|
-
<span className="font-picker-item-content">
|
|
358
|
-
<span className="font-name">{font.family}</span>
|
|
359
|
-
<span className="font-preview" style={{ fontFamily: `"${font.family}"` }}>
|
|
360
|
-
Aa
|
|
361
|
-
</span>
|
|
362
|
-
</span>
|
|
363
|
-
</SelectItem>
|
|
364
|
-
))}
|
|
365
|
-
</SelectGroup>
|
|
366
|
-
)}
|
|
367
|
-
|
|
368
|
-
{/* Custom option (fallback when no system fonts) */}
|
|
369
|
-
{!hasSystemFonts && fontsLoaded && (
|
|
370
|
-
<SelectGroup>
|
|
371
|
-
<SelectLabel>Custom</SelectLabel>
|
|
372
|
-
<SelectItem value="custom">
|
|
373
|
-
Custom...
|
|
374
|
-
</SelectItem>
|
|
375
|
-
</SelectGroup>
|
|
376
|
-
)}
|
|
377
|
-
</SelectContent>
|
|
378
|
-
</Select>
|
|
379
|
-
|
|
380
|
-
{showCustomInput && (
|
|
381
|
-
<input
|
|
382
|
-
type="text"
|
|
383
|
-
className="font-picker-custom-input"
|
|
384
|
-
value={customValue}
|
|
385
|
-
onChange={handleCustomChange}
|
|
386
|
-
placeholder="Enter font family..."
|
|
387
|
-
aria-label="Custom font family"
|
|
388
|
-
/>
|
|
389
|
-
)}
|
|
390
|
-
</div>
|
|
391
|
-
);
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
// =============================================================================
|
|
395
|
-
// FontSizePicker Component
|
|
396
|
-
// =============================================================================
|
|
397
|
-
|
|
398
|
-
const SIZES: FontSize[] = ['xs', 'sm', 'base', 'lg', 'xl'];
|
|
399
|
-
|
|
400
|
-
export function FontSizePicker({
|
|
401
|
-
currentSize,
|
|
402
|
-
onSelect,
|
|
403
|
-
className = '',
|
|
404
|
-
}: FontSizePickerProps): React.ReactElement {
|
|
405
|
-
return (
|
|
406
|
-
<TooltipProvider delayDuration={300}>
|
|
407
|
-
<div className={`font-size-picker ${className}`} role="group" aria-label="Font size">
|
|
408
|
-
{SIZES.map((size) => (
|
|
409
|
-
<Tooltip key={size}>
|
|
410
|
-
<TooltipTrigger asChild>
|
|
411
|
-
<Button
|
|
412
|
-
variant={size === currentSize ? 'secondary' : 'ghost'}
|
|
413
|
-
size="sm"
|
|
414
|
-
className={`font-size-option ${size === currentSize ? 'active' : ''}`}
|
|
415
|
-
data-size={size}
|
|
416
|
-
onClick={() => onSelect(size)}
|
|
417
|
-
aria-pressed={size === currentSize}
|
|
418
|
-
>
|
|
419
|
-
{size.toUpperCase()}
|
|
420
|
-
</Button>
|
|
421
|
-
</TooltipTrigger>
|
|
422
|
-
<TooltipContent>{`${FONT_SIZE_SCALE[size]} (${size})`}</TooltipContent>
|
|
423
|
-
</Tooltip>
|
|
424
|
-
))}
|
|
425
|
-
</div>
|
|
426
|
-
</TooltipProvider>
|
|
427
|
-
);
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
export default FontPicker;
|
|
@@ -1,237 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* FullFileTree - Complete directory tree with changed file highlighting
|
|
3
|
-
*
|
|
4
|
-
* Displays the full project file tree with lazy-loaded directories.
|
|
5
|
-
* Changed files are highlighted with git status colors (created/modified/deleted).
|
|
6
|
-
* Uses /api/files for directory listing and /ws/git for change status.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import React, { useState, useCallback, useEffect } from 'react';
|
|
10
|
-
import { Badge } from '@/components/ui/badge';
|
|
11
|
-
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
|
12
|
-
import { ScrollArea } from '@/components/ui/scroll-area';
|
|
13
|
-
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
|
|
14
|
-
import { useFileBrowser, DirectoryEntry } from '../hooks/useFileBrowser';
|
|
15
|
-
import type { FileStatus } from './FileTree';
|
|
16
|
-
|
|
17
|
-
// =============================================================================
|
|
18
|
-
// Types
|
|
19
|
-
// =============================================================================
|
|
20
|
-
|
|
21
|
-
export interface FullFileTreeProps {
|
|
22
|
-
/** Map of file path → git status for highlighting */
|
|
23
|
-
changedFiles: Map<string, FileStatus>;
|
|
24
|
-
/** Callback when a file is clicked */
|
|
25
|
-
onFileClick?: (entry: DirectoryEntry, status?: FileStatus) => void;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
// =============================================================================
|
|
29
|
-
// File Item Component
|
|
30
|
-
// =============================================================================
|
|
31
|
-
|
|
32
|
-
function TreeFileItem({
|
|
33
|
-
entry,
|
|
34
|
-
status,
|
|
35
|
-
depth,
|
|
36
|
-
onFileClick,
|
|
37
|
-
}: {
|
|
38
|
-
entry: DirectoryEntry;
|
|
39
|
-
status?: FileStatus;
|
|
40
|
-
depth: number;
|
|
41
|
-
onFileClick?: (entry: DirectoryEntry, status?: FileStatus) => void;
|
|
42
|
-
}): React.ReactElement {
|
|
43
|
-
const statusIcon = status === 'created' ? '+' : status === 'modified' ? '~' : status === 'deleted' ? '-' : null;
|
|
44
|
-
|
|
45
|
-
return (
|
|
46
|
-
<Tooltip>
|
|
47
|
-
<TooltipTrigger asChild>
|
|
48
|
-
<div
|
|
49
|
-
role="treeitem"
|
|
50
|
-
className={`file-item${status ? ` file-${status}` : ''}`}
|
|
51
|
-
style={{ paddingLeft: `${12 + depth * 16}px` }}
|
|
52
|
-
tabIndex={0}
|
|
53
|
-
aria-label={`${entry.name}${status ? `, ${status}` : ''}`}
|
|
54
|
-
onClick={() => onFileClick?.(entry, status)}
|
|
55
|
-
onKeyDown={(e) => {
|
|
56
|
-
if (e.key === 'Enter' || e.key === ' ') {
|
|
57
|
-
e.preventDefault();
|
|
58
|
-
onFileClick?.(entry, status);
|
|
59
|
-
}
|
|
60
|
-
}}
|
|
61
|
-
>
|
|
62
|
-
{statusIcon && (
|
|
63
|
-
<span className={`status-icon status-${status}`} aria-hidden="true">
|
|
64
|
-
{statusIcon}
|
|
65
|
-
</span>
|
|
66
|
-
)}
|
|
67
|
-
<span className={`file-name${status === 'deleted' ? '' : ''}`}>{entry.name}</span>
|
|
68
|
-
</div>
|
|
69
|
-
</TooltipTrigger>
|
|
70
|
-
<TooltipContent>{entry.path}</TooltipContent>
|
|
71
|
-
</Tooltip>
|
|
72
|
-
);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// =============================================================================
|
|
76
|
-
// Directory Node Component (recursive)
|
|
77
|
-
// =============================================================================
|
|
78
|
-
|
|
79
|
-
function TreeDirectoryNode({
|
|
80
|
-
entry,
|
|
81
|
-
depth,
|
|
82
|
-
changedFiles,
|
|
83
|
-
onFileClick,
|
|
84
|
-
fetchDirectory,
|
|
85
|
-
cache,
|
|
86
|
-
loading,
|
|
87
|
-
}: {
|
|
88
|
-
entry: DirectoryEntry;
|
|
89
|
-
depth: number;
|
|
90
|
-
changedFiles: Map<string, FileStatus>;
|
|
91
|
-
onFileClick?: (entry: DirectoryEntry, status?: FileStatus) => void;
|
|
92
|
-
fetchDirectory: (path: string) => Promise<void>;
|
|
93
|
-
cache: Record<string, DirectoryEntry[]>;
|
|
94
|
-
loading: Set<string>;
|
|
95
|
-
}): React.ReactElement {
|
|
96
|
-
// Check if this directory contains any changed files
|
|
97
|
-
const hasChanges = Array.from(changedFiles.keys()).some(
|
|
98
|
-
(filePath) => filePath.startsWith(entry.path + '/')
|
|
99
|
-
);
|
|
100
|
-
|
|
101
|
-
const [isOpen, setIsOpen] = useState(hasChanges);
|
|
102
|
-
const children = cache[entry.path];
|
|
103
|
-
const isLoading = loading.has(entry.path);
|
|
104
|
-
|
|
105
|
-
// Auto-fetch children when directory has changes and is opened by default
|
|
106
|
-
useEffect(() => {
|
|
107
|
-
if (hasChanges && !children && !loading.has(entry.path)) {
|
|
108
|
-
fetchDirectory(entry.path);
|
|
109
|
-
}
|
|
110
|
-
}, [hasChanges, children, entry.path, fetchDirectory, loading]);
|
|
111
|
-
|
|
112
|
-
// Auto-open when changes appear in this directory
|
|
113
|
-
useEffect(() => {
|
|
114
|
-
if (hasChanges) {
|
|
115
|
-
setIsOpen(true);
|
|
116
|
-
}
|
|
117
|
-
}, [hasChanges]);
|
|
118
|
-
|
|
119
|
-
const handleToggle = useCallback(() => {
|
|
120
|
-
const willOpen = !isOpen;
|
|
121
|
-
setIsOpen(willOpen);
|
|
122
|
-
if (willOpen && !children) {
|
|
123
|
-
fetchDirectory(entry.path);
|
|
124
|
-
}
|
|
125
|
-
}, [isOpen, children, entry.path, fetchDirectory]);
|
|
126
|
-
|
|
127
|
-
return (
|
|
128
|
-
<Collapsible open={isOpen} onOpenChange={handleToggle}>
|
|
129
|
-
<CollapsibleTrigger asChild>
|
|
130
|
-
<div
|
|
131
|
-
className={`directory-header${hasChanges ? ' has-changes' : ''}`}
|
|
132
|
-
style={{ paddingLeft: `${4 + depth * 16}px` }}
|
|
133
|
-
>
|
|
134
|
-
<span className="directory-toggle">
|
|
135
|
-
<span className="toggle-icon">{isOpen ? '▼' : '▶'}</span>
|
|
136
|
-
</span>
|
|
137
|
-
<span className="directory-name">{entry.name}</span>
|
|
138
|
-
</div>
|
|
139
|
-
</CollapsibleTrigger>
|
|
140
|
-
<CollapsibleContent>
|
|
141
|
-
{isLoading && (
|
|
142
|
-
<div
|
|
143
|
-
className="tree-loading"
|
|
144
|
-
style={{ paddingLeft: `${12 + (depth + 1) * 16}px` }}
|
|
145
|
-
>
|
|
146
|
-
Loading...
|
|
147
|
-
</div>
|
|
148
|
-
)}
|
|
149
|
-
{children?.map((child) =>
|
|
150
|
-
child.type === 'directory' ? (
|
|
151
|
-
<TreeDirectoryNode
|
|
152
|
-
key={child.path}
|
|
153
|
-
entry={child}
|
|
154
|
-
depth={depth + 1}
|
|
155
|
-
changedFiles={changedFiles}
|
|
156
|
-
onFileClick={onFileClick}
|
|
157
|
-
fetchDirectory={fetchDirectory}
|
|
158
|
-
cache={cache}
|
|
159
|
-
loading={loading}
|
|
160
|
-
/>
|
|
161
|
-
) : (
|
|
162
|
-
<TreeFileItem
|
|
163
|
-
key={child.path}
|
|
164
|
-
entry={child}
|
|
165
|
-
status={changedFiles.get(child.path)}
|
|
166
|
-
depth={depth + 1}
|
|
167
|
-
onFileClick={onFileClick}
|
|
168
|
-
/>
|
|
169
|
-
)
|
|
170
|
-
)}
|
|
171
|
-
</CollapsibleContent>
|
|
172
|
-
</Collapsible>
|
|
173
|
-
);
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
// =============================================================================
|
|
177
|
-
// FullFileTree Component
|
|
178
|
-
// =============================================================================
|
|
179
|
-
|
|
180
|
-
export function FullFileTree({ changedFiles, onFileClick }: FullFileTreeProps): React.ReactElement {
|
|
181
|
-
const { cache, loading, error, fetchDirectory } = useFileBrowser();
|
|
182
|
-
|
|
183
|
-
// Load root directory on mount
|
|
184
|
-
useEffect(() => {
|
|
185
|
-
fetchDirectory('');
|
|
186
|
-
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
|
187
|
-
|
|
188
|
-
const rootEntries = cache['__root__'];
|
|
189
|
-
const changedCount = changedFiles.size;
|
|
190
|
-
|
|
191
|
-
return (
|
|
192
|
-
<TooltipProvider delayDuration={300}>
|
|
193
|
-
<div role="tree" aria-label="Project files" className="filetree full-filetree">
|
|
194
|
-
{changedCount > 0 && (
|
|
195
|
-
<Badge
|
|
196
|
-
variant="secondary"
|
|
197
|
-
data-testid="file-count-badge"
|
|
198
|
-
className="file-count-badge"
|
|
199
|
-
aria-label={`${changedCount} files changed`}
|
|
200
|
-
>
|
|
201
|
-
{changedCount}
|
|
202
|
-
</Badge>
|
|
203
|
-
)}
|
|
204
|
-
<ScrollArea className="filetree-scroll">
|
|
205
|
-
{error && <div className="tree-error">{error}</div>}
|
|
206
|
-
{!rootEntries && !error && (
|
|
207
|
-
<div className="tree-loading">Loading project files...</div>
|
|
208
|
-
)}
|
|
209
|
-
{rootEntries?.map((entry) =>
|
|
210
|
-
entry.type === 'directory' ? (
|
|
211
|
-
<TreeDirectoryNode
|
|
212
|
-
key={entry.path}
|
|
213
|
-
entry={entry}
|
|
214
|
-
depth={0}
|
|
215
|
-
changedFiles={changedFiles}
|
|
216
|
-
onFileClick={onFileClick}
|
|
217
|
-
fetchDirectory={fetchDirectory}
|
|
218
|
-
cache={cache}
|
|
219
|
-
loading={loading}
|
|
220
|
-
/>
|
|
221
|
-
) : (
|
|
222
|
-
<TreeFileItem
|
|
223
|
-
key={entry.path}
|
|
224
|
-
entry={entry}
|
|
225
|
-
status={changedFiles.get(entry.path)}
|
|
226
|
-
depth={0}
|
|
227
|
-
onFileClick={onFileClick}
|
|
228
|
-
/>
|
|
229
|
-
)
|
|
230
|
-
)}
|
|
231
|
-
</ScrollArea>
|
|
232
|
-
</div>
|
|
233
|
-
</TooltipProvider>
|
|
234
|
-
);
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
export default FullFileTree;
|