@jetcrab/pi-desk 1.1.21 → 1.1.23-dev
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/package.json +2 -2
- package/src/client/l2_biz/project-preview/l2-project-preview-dialog.tsx +1 -1
- package/src/client/l2_biz/project-preview/l2-project-preview-page.tsx +1 -1
- package/src/client/l2_biz/project-preview/l2-project-preview.tsx +28 -34
- package/src/client/l3_modules/project-files/l3-project-document-view.tsx +162 -151
- package/src/client/l3_modules/project-files/l3-project-file-browser.module.css +30 -0
- package/src/client/l3_modules/project-files/l3-project-file-browser.tsx +137 -111
- package/src/client/l3_modules/project-files/l3-project-file-tabs.tsx +26 -6
- package/src/client/l3_modules/project-files/views/l3-project-search-controls.tsx +118 -123
- package/src/client/l3_modules/project-files/views/l3-project-search-results.tsx +29 -25
- package/temp/build/pi-desk/release/.next/BUILD_ID +1 -1
- package/temp/build/pi-desk/release/.next/build-manifest.json +3 -3
- package/temp/build/pi-desk/release/.next/prerender-manifest.json +3 -3
- package/temp/build/pi-desk/release/.next/required-server-files.js +3 -3
- package/temp/build/pi-desk/release/.next/required-server-files.json +3 -3
- package/temp/build/pi-desk/release/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/temp/build/pi-desk/release/.next/server/app/_global-error.html +1 -1
- package/temp/build/pi-desk/release/.next/server/app/_global-error.rsc +1 -1
- package/temp/build/pi-desk/release/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/temp/build/pi-desk/release/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/temp/build/pi-desk/release/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/temp/build/pi-desk/release/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/temp/build/pi-desk/release/.next/server/app/api/health/route.js +1 -1
- package/temp/build/pi-desk/release/.next/server/app/login/page_client-reference-manifest.js +1 -1
- package/temp/build/pi-desk/release/.next/server/app/page.js +2 -2
- package/temp/build/pi-desk/release/.next/server/app/page_client-reference-manifest.js +1 -1
- package/temp/build/pi-desk/release/.next/server/app/project-preview/page.js +1 -1
- package/temp/build/pi-desk/release/.next/server/app/project-preview/page_client-reference-manifest.js +1 -1
- package/temp/build/pi-desk/release/.next/server/chunks/20460.js +2 -2
- package/temp/build/pi-desk/release/.next/server/middleware-build-manifest.js +1 -1
- package/temp/build/pi-desk/release/.next/server/pages/500.html +1 -1
- package/temp/build/pi-desk/release/.next/server/server-reference-manifest.json +1 -1
- package/temp/build/pi-desk/release/.next/static/chunks/9038-05fd2688407b39c1.js +15 -0
- package/temp/build/pi-desk/release/.next/static/chunks/app/{page-6ebc1c877f6025dd.js → page-54040b9fd54ee8b3.js} +2 -2
- package/temp/build/pi-desk/release/.next/static/chunks/app/project-preview/{page-d70ee4e9409b970a.js → page-bc18765121950e65.js} +1 -1
- package/temp/build/pi-desk/release/.next/static/chunks/{webpack-b60891a214e0925d.js → webpack-48eecbb66781c006.js} +1 -1
- package/temp/build/pi-desk/release/.next/static/css/10e063cfb3cda7bd.css +1 -0
- package/temp/build/pi-desk/release/.next/static/css/2c3b0c0ff1f49c68.css +1 -0
- package/temp/build/pi-desk/release/.next/trace-build +1 -1
- package/temp/build/pi-desk/release/.next/static/chunks/9038-03144ed96d613f75.js +0 -15
- package/temp/build/pi-desk/release/.next/static/css/6dbae2922f88ca31.css +0 -1
- package/temp/build/pi-desk/release/.next/static/css/f646ca6c07245c4c.css +0 -1
- /package/temp/build/pi-desk/release/.next/static/{8Yd33M0zPi4tbRn4xCaRY → gAYTx4KJJR3vTtGYexj4J}/_buildManifest.js +0 -0
- /package/temp/build/pi-desk/release/.next/static/{8Yd33M0zPi4tbRn4xCaRY → gAYTx4KJJR3vTtGYexj4J}/_ssgManifest.js +0 -0
|
@@ -43,6 +43,7 @@ import {
|
|
|
43
43
|
} from './l3-project-git-model'
|
|
44
44
|
import type { L3DirectoryState, L3ProjectFileRuntime } from './l3-project-file-runtime'
|
|
45
45
|
import { L3ProjectPathMenu } from './l3-project-path-menu'
|
|
46
|
+
import styles from './l3-project-file-browser.module.css'
|
|
46
47
|
import {
|
|
47
48
|
buildL3ProjectPathTree,
|
|
48
49
|
compactL3PathTreeDirectory,
|
|
@@ -60,6 +61,7 @@ interface L3ProjectFileBrowserProps {
|
|
|
60
61
|
onOpenGitBranches: (repositoryRoot: string) => void
|
|
61
62
|
onFileOpened?: () => void
|
|
62
63
|
rootTitle?: React.ReactNode
|
|
64
|
+
showRootTitle?: boolean
|
|
63
65
|
gitReadOnly?: boolean
|
|
64
66
|
}
|
|
65
67
|
|
|
@@ -155,6 +157,7 @@ export function L3ProjectFileBrowser({
|
|
|
155
157
|
onOpenGitBranches,
|
|
156
158
|
onFileOpened,
|
|
157
159
|
rootTitle,
|
|
160
|
+
showRootTitle = true,
|
|
158
161
|
gitReadOnly = false
|
|
159
162
|
}: L3ProjectFileBrowserProps): React.JSX.Element {
|
|
160
163
|
const { t } = useTranslation('projectFiles')
|
|
@@ -581,7 +584,7 @@ export function L3ProjectFileBrowser({
|
|
|
581
584
|
type="button"
|
|
582
585
|
disabled={gitReadOnly || repository.state !== 'ready'}
|
|
583
586
|
className={cn(
|
|
584
|
-
buttonVariants({ variant: '
|
|
587
|
+
buttonVariants({ variant: 'ghost', size: 'sm' }),
|
|
585
588
|
'max-w-28 shrink gap-1 font-mono text-xs text-muted-foreground'
|
|
586
589
|
)}
|
|
587
590
|
title={
|
|
@@ -589,6 +592,7 @@ export function L3ProjectFileBrowser({
|
|
|
589
592
|
? t(gitReadOnly ? 'gitBranchReadOnly' : 'gitBranches')
|
|
590
593
|
: selectL4LocalizedText(repository.message, locale)
|
|
591
594
|
}
|
|
595
|
+
aria-label={t(gitReadOnly ? 'gitBranchReadOnly' : 'gitBranches')}
|
|
592
596
|
onClick={() => onOpenGitBranches(root)}
|
|
593
597
|
>
|
|
594
598
|
<GitBranchIcon className="size-3 shrink-0" />
|
|
@@ -678,6 +682,98 @@ export function L3ProjectFileBrowser({
|
|
|
678
682
|
repositories.length === 1 && repositories[0]?.root === '' ? repositories[0] : null
|
|
679
683
|
const showLayoutToggle = repositories.length > 0 && !singleProjectRootRepository
|
|
680
684
|
const effectiveLayout = showLayoutToggle ? (gitWorkspace?.layout ?? 'directory') : 'directory'
|
|
685
|
+
const rootBranchButton = singleProjectRootRepository ? (
|
|
686
|
+
<button
|
|
687
|
+
type="button"
|
|
688
|
+
disabled={gitReadOnly || singleProjectRootRepository.state !== 'ready'}
|
|
689
|
+
className={cn(
|
|
690
|
+
buttonVariants({ variant: 'ghost', size: 'sm' }),
|
|
691
|
+
'max-w-32 shrink gap-1 font-mono text-xs text-muted-foreground',
|
|
692
|
+
!showRootTitle && 'min-w-0 flex-1 justify-start'
|
|
693
|
+
)}
|
|
694
|
+
aria-label={t(gitReadOnly ? 'gitBranchReadOnly' : 'gitBranches')}
|
|
695
|
+
title={
|
|
696
|
+
singleProjectRootRepository.state === 'ready'
|
|
697
|
+
? `${t(gitReadOnly ? 'gitBranchReadOnly' : 'gitBranches')} · ${getL3GitHeadLabel(singleProjectRootRepository.head)}`
|
|
698
|
+
: selectL4LocalizedText(singleProjectRootRepository.message, locale)
|
|
699
|
+
}
|
|
700
|
+
onClick={() => onOpenGitBranches('')}
|
|
701
|
+
>
|
|
702
|
+
<GitBranchIcon className="size-3 shrink-0" />
|
|
703
|
+
<span className="truncate">
|
|
704
|
+
{singleProjectRootRepository.state === 'ready'
|
|
705
|
+
? getL3GitHeadLabel(singleProjectRootRepository.head)
|
|
706
|
+
: t('unavailable')}
|
|
707
|
+
</span>
|
|
708
|
+
</button>
|
|
709
|
+
) : null
|
|
710
|
+
|
|
711
|
+
const controls = (
|
|
712
|
+
<div
|
|
713
|
+
className="flex min-h-8 min-w-0 flex-wrap items-center gap-x-1 gap-y-0.5 outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring"
|
|
714
|
+
tabIndex={showRootTitle ? undefined : 0}
|
|
715
|
+
aria-label={t('projectFiles', { name: projectName })}
|
|
716
|
+
>
|
|
717
|
+
{showLayoutToggle ? (
|
|
718
|
+
<div className="flex shrink-0 gap-0.5" role="group" aria-label={t('layout')}>
|
|
719
|
+
{(['directory', 'repository'] as const).map((layout) => (
|
|
720
|
+
<button
|
|
721
|
+
key={layout}
|
|
722
|
+
data-testid={`project-file-layout-${layout}`}
|
|
723
|
+
type="button"
|
|
724
|
+
aria-pressed={effectiveLayout === layout}
|
|
725
|
+
className={cn(
|
|
726
|
+
buttonVariants({ variant: 'ghost', size: 'sm' }),
|
|
727
|
+
effectiveLayout === layout && 'bg-accent text-accent-foreground'
|
|
728
|
+
)}
|
|
729
|
+
onClick={() => {
|
|
730
|
+
runtime.cancelDirectoryExpansion()
|
|
731
|
+
gitRuntime.setLayout(cwd, layout)
|
|
732
|
+
}}
|
|
733
|
+
>
|
|
734
|
+
{t(layout === 'directory' ? 'directory' : 'repository')}
|
|
735
|
+
</button>
|
|
736
|
+
))}
|
|
737
|
+
</div>
|
|
738
|
+
) : null}
|
|
739
|
+
{repositories.length > 0 ? (
|
|
740
|
+
<button
|
|
741
|
+
data-testid="project-git-changes-toggle"
|
|
742
|
+
type="button"
|
|
743
|
+
aria-pressed={gitWorkspace?.changesOnly ?? false}
|
|
744
|
+
className={cn(
|
|
745
|
+
buttonVariants({ variant: 'ghost', size: 'sm' }),
|
|
746
|
+
'min-w-0 gap-1',
|
|
747
|
+
gitWorkspace?.changesOnly && 'bg-accent text-accent-foreground'
|
|
748
|
+
)}
|
|
749
|
+
onClick={() => {
|
|
750
|
+
runtime.cancelDirectoryExpansion()
|
|
751
|
+
gitRuntime.setChangesOnly(cwd, !(gitWorkspace?.changesOnly ?? false))
|
|
752
|
+
}}
|
|
753
|
+
>
|
|
754
|
+
<ListFilterIcon className="size-3 shrink-0" />
|
|
755
|
+
<span className="truncate">{t('changesOnly')}</span>
|
|
756
|
+
<span className="rounded-md bg-muted px-1 font-mono text-xs">
|
|
757
|
+
{projectChanges.length}
|
|
758
|
+
</span>
|
|
759
|
+
</button>
|
|
760
|
+
) : null}
|
|
761
|
+
{!showRootTitle ? rootBranchButton : null}
|
|
762
|
+
<button
|
|
763
|
+
ref={searchButtonRef}
|
|
764
|
+
type="button"
|
|
765
|
+
className={cn(buttonVariants({ variant: 'ghost', size: 'icon-sm' }), 'ml-auto')}
|
|
766
|
+
aria-label={t('searchProject')}
|
|
767
|
+
title={t('searchProject')}
|
|
768
|
+
onClick={() => {
|
|
769
|
+
setSearchOpen(true)
|
|
770
|
+
requestAnimationFrame(() => searchInputRef.current?.focus())
|
|
771
|
+
}}
|
|
772
|
+
>
|
|
773
|
+
<SearchIcon className="size-3.5" />
|
|
774
|
+
</button>
|
|
775
|
+
</div>
|
|
776
|
+
)
|
|
681
777
|
|
|
682
778
|
return (
|
|
683
779
|
<section
|
|
@@ -706,8 +802,42 @@ export function L3ProjectFileBrowser({
|
|
|
706
802
|
}
|
|
707
803
|
/>
|
|
708
804
|
) : (
|
|
709
|
-
<div className="
|
|
710
|
-
|
|
805
|
+
<div className="border-b px-1">
|
|
806
|
+
{showRootTitle ? (
|
|
807
|
+
<div className="flex min-h-7 min-w-0 items-center gap-1">
|
|
808
|
+
<L3ProjectPathMenu
|
|
809
|
+
cwd={cwd}
|
|
810
|
+
path=""
|
|
811
|
+
type="directory"
|
|
812
|
+
onReveal={
|
|
813
|
+
runtime.canReveal ? (path, type) => runtime.revealFile(path, type) : undefined
|
|
814
|
+
}
|
|
815
|
+
trigger={
|
|
816
|
+
rootTitle ? (
|
|
817
|
+
<div className="flex min-w-0 flex-1">{rootTitle}</div>
|
|
818
|
+
) : (
|
|
819
|
+
<div tabIndex={0} className="flex min-w-0 flex-1 items-baseline gap-1">
|
|
820
|
+
<p className="shrink-0 truncate text-xs font-semibold" title={projectName}>
|
|
821
|
+
{projectName}
|
|
822
|
+
</p>
|
|
823
|
+
<span className="text-xs text-muted-foreground">·</span>
|
|
824
|
+
<p
|
|
825
|
+
className="min-w-0 truncate font-mono text-xs text-muted-foreground"
|
|
826
|
+
title={cwd}
|
|
827
|
+
>
|
|
828
|
+
{compactPath(cwd)}
|
|
829
|
+
</p>
|
|
830
|
+
</div>
|
|
831
|
+
)
|
|
832
|
+
}
|
|
833
|
+
/>
|
|
834
|
+
{rootBranchButton}
|
|
835
|
+
</div>
|
|
836
|
+
) : null}
|
|
837
|
+
|
|
838
|
+
{showRootTitle ? (
|
|
839
|
+
controls
|
|
840
|
+
) : (
|
|
711
841
|
<L3ProjectPathMenu
|
|
712
842
|
cwd={cwd}
|
|
713
843
|
path=""
|
|
@@ -715,113 +845,9 @@ export function L3ProjectFileBrowser({
|
|
|
715
845
|
onReveal={
|
|
716
846
|
runtime.canReveal ? (path, type) => runtime.revealFile(path, type) : undefined
|
|
717
847
|
}
|
|
718
|
-
trigger={
|
|
719
|
-
rootTitle ? (
|
|
720
|
-
<div className="flex min-w-0 flex-1">{rootTitle}</div>
|
|
721
|
-
) : (
|
|
722
|
-
<div tabIndex={0} className="flex min-w-0 flex-1 items-baseline gap-1">
|
|
723
|
-
<p className="shrink-0 truncate text-xs font-semibold" title={projectName}>
|
|
724
|
-
{projectName}
|
|
725
|
-
</p>
|
|
726
|
-
<span className="text-xs text-muted-foreground">·</span>
|
|
727
|
-
<p
|
|
728
|
-
className="min-w-0 truncate font-mono text-xs text-muted-foreground"
|
|
729
|
-
title={cwd}
|
|
730
|
-
>
|
|
731
|
-
{compactPath(cwd)}
|
|
732
|
-
</p>
|
|
733
|
-
</div>
|
|
734
|
-
)
|
|
735
|
-
}
|
|
848
|
+
trigger={controls}
|
|
736
849
|
/>
|
|
737
|
-
|
|
738
|
-
<button
|
|
739
|
-
type="button"
|
|
740
|
-
disabled={gitReadOnly || singleProjectRootRepository.state !== 'ready'}
|
|
741
|
-
className={cn(
|
|
742
|
-
buttonVariants({ variant: 'outline', size: 'sm' }),
|
|
743
|
-
'max-w-32 shrink gap-1 font-mono text-xs text-muted-foreground'
|
|
744
|
-
)}
|
|
745
|
-
title={
|
|
746
|
-
singleProjectRootRepository.state === 'ready'
|
|
747
|
-
? t(gitReadOnly ? 'gitBranchReadOnly' : 'gitBranches')
|
|
748
|
-
: selectL4LocalizedText(singleProjectRootRepository.message, locale)
|
|
749
|
-
}
|
|
750
|
-
onClick={() => onOpenGitBranches('')}
|
|
751
|
-
>
|
|
752
|
-
<GitBranchIcon className="size-3 shrink-0" />
|
|
753
|
-
<span className="truncate">
|
|
754
|
-
{singleProjectRootRepository.state === 'ready'
|
|
755
|
-
? getL3GitHeadLabel(singleProjectRootRepository.head)
|
|
756
|
-
: t('unavailable')}
|
|
757
|
-
</span>
|
|
758
|
-
</button>
|
|
759
|
-
) : null}
|
|
760
|
-
</div>
|
|
761
|
-
|
|
762
|
-
<div className="flex min-h-8 min-w-0 items-center gap-1.5">
|
|
763
|
-
{showLayoutToggle ? (
|
|
764
|
-
<div
|
|
765
|
-
className="flex shrink-0 rounded-md border bg-background/55 p-0.5"
|
|
766
|
-
role="group"
|
|
767
|
-
aria-label={t('layout')}
|
|
768
|
-
>
|
|
769
|
-
{(['directory', 'repository'] as const).map((layout) => (
|
|
770
|
-
<button
|
|
771
|
-
key={layout}
|
|
772
|
-
data-testid={`project-file-layout-${layout}`}
|
|
773
|
-
type="button"
|
|
774
|
-
aria-pressed={effectiveLayout === layout}
|
|
775
|
-
className={cn(
|
|
776
|
-
buttonVariants({ variant: 'ghost', size: 'sm' }),
|
|
777
|
-
effectiveLayout === layout && 'bg-accent text-accent-foreground'
|
|
778
|
-
)}
|
|
779
|
-
onClick={() => {
|
|
780
|
-
runtime.cancelDirectoryExpansion()
|
|
781
|
-
gitRuntime.setLayout(cwd, layout)
|
|
782
|
-
}}
|
|
783
|
-
>
|
|
784
|
-
{t(layout === 'directory' ? 'directory' : 'repository')}
|
|
785
|
-
</button>
|
|
786
|
-
))}
|
|
787
|
-
</div>
|
|
788
|
-
) : null}
|
|
789
|
-
{repositories.length > 0 ? (
|
|
790
|
-
<button
|
|
791
|
-
data-testid="project-git-changes-toggle"
|
|
792
|
-
type="button"
|
|
793
|
-
aria-pressed={gitWorkspace?.changesOnly ?? false}
|
|
794
|
-
className={cn(
|
|
795
|
-
buttonVariants({ variant: 'outline', size: 'sm' }),
|
|
796
|
-
'min-w-0 gap-1',
|
|
797
|
-
gitWorkspace?.changesOnly && 'bg-accent text-accent-foreground'
|
|
798
|
-
)}
|
|
799
|
-
onClick={() => {
|
|
800
|
-
runtime.cancelDirectoryExpansion()
|
|
801
|
-
gitRuntime.setChangesOnly(cwd, !(gitWorkspace?.changesOnly ?? false))
|
|
802
|
-
}}
|
|
803
|
-
>
|
|
804
|
-
<ListFilterIcon className="size-3 shrink-0" />
|
|
805
|
-
<span className="truncate">{t('changesOnly')}</span>
|
|
806
|
-
<span className="rounded-md bg-muted px-1 font-mono text-xs">
|
|
807
|
-
{projectChanges.length}
|
|
808
|
-
</span>
|
|
809
|
-
</button>
|
|
810
|
-
) : null}
|
|
811
|
-
<button
|
|
812
|
-
ref={searchButtonRef}
|
|
813
|
-
type="button"
|
|
814
|
-
className={cn(buttonVariants({ variant: 'outline', size: 'icon-sm' }), 'ml-auto')}
|
|
815
|
-
aria-label={t('searchProject')}
|
|
816
|
-
title={t('searchProject')}
|
|
817
|
-
onClick={() => {
|
|
818
|
-
setSearchOpen(true)
|
|
819
|
-
requestAnimationFrame(() => searchInputRef.current?.focus())
|
|
820
|
-
}}
|
|
821
|
-
>
|
|
822
|
-
<SearchIcon className="size-3.5" />
|
|
823
|
-
</button>
|
|
824
|
-
</div>
|
|
850
|
+
)}
|
|
825
851
|
</div>
|
|
826
852
|
)}
|
|
827
853
|
|
|
@@ -852,8 +878,8 @@ export function L3ProjectFileBrowser({
|
|
|
852
878
|
/>
|
|
853
879
|
) : (
|
|
854
880
|
<L4ScrollArea
|
|
855
|
-
className=
|
|
856
|
-
viewportClassName="py-
|
|
881
|
+
className={cn(styles.tree, 'min-h-0 flex-1')}
|
|
882
|
+
viewportClassName="py-0.5"
|
|
857
883
|
viewportRef={scrollRef}
|
|
858
884
|
onViewportScroll={(event) => runtime.setTreeScrollTop(event.currentTarget.scrollTop)}
|
|
859
885
|
>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import { ChevronDownIcon, ChevronUpIcon, FileCode2Icon, XIcon } from 'lucide-react'
|
|
4
|
-
import { useLayoutEffect, useRef, useState, useSyncExternalStore } from 'react'
|
|
4
|
+
import { useLayoutEffect, useRef, useState, useSyncExternalStore, type ReactNode } from 'react'
|
|
5
5
|
import { useTranslation } from 'react-i18next'
|
|
6
6
|
import { cn } from '@client/l4_foundation/lib/l4-utils'
|
|
7
7
|
import { Button } from '@client/l4_foundation/ui/shadcn/button'
|
|
@@ -23,6 +23,7 @@ interface L3ProjectFileTabsProps {
|
|
|
23
23
|
activePath: string | null
|
|
24
24
|
diffPath: string | null
|
|
25
25
|
mobile: boolean
|
|
26
|
+
actions?: ReactNode
|
|
26
27
|
onActivate: (path: string) => void
|
|
27
28
|
onClose: (path: string | null, scope: L3FileTabCloseScope) => void
|
|
28
29
|
}
|
|
@@ -49,6 +50,7 @@ export function L3ProjectFileTabs({
|
|
|
49
50
|
activePath,
|
|
50
51
|
diffPath,
|
|
51
52
|
mobile,
|
|
53
|
+
actions,
|
|
52
54
|
onActivate,
|
|
53
55
|
onClose
|
|
54
56
|
}: L3ProjectFileTabsProps): React.JSX.Element | null {
|
|
@@ -60,7 +62,9 @@ export function L3ProjectFileTabs({
|
|
|
60
62
|
const [expanded, setExpanded] = useState(false)
|
|
61
63
|
const [overflow, setOverflow] = useState(false)
|
|
62
64
|
const [clippedIndexes, setClippedIndexes] = useState<readonly number[]>([])
|
|
63
|
-
const
|
|
65
|
+
const merged = actions !== undefined
|
|
66
|
+
const rowHeight = merged && !mobile ? 32 : 36
|
|
67
|
+
const maxHeight = (mobile ? 2 : 3) * rowHeight
|
|
64
68
|
const tabs: { path: string | null; label: string; title: string }[] = paths.map((path) => ({
|
|
65
69
|
path,
|
|
66
70
|
label: fileLabel(path, paths),
|
|
@@ -95,8 +99,14 @@ export function L3ProjectFileTabs({
|
|
|
95
99
|
|
|
96
100
|
if (tabs.length === 0) return null
|
|
97
101
|
return (
|
|
98
|
-
<div
|
|
99
|
-
|
|
102
|
+
<div
|
|
103
|
+
className={cn('shrink-0 bg-muted', merged ? 'flex flex-wrap items-start' : 'border-b')}
|
|
104
|
+
data-testid="file-tabs"
|
|
105
|
+
>
|
|
106
|
+
<div
|
|
107
|
+
className={cn('overflow-hidden', merged && 'min-w-0 flex-[1_1_12rem]')}
|
|
108
|
+
style={{ maxHeight: expanded ? undefined : maxHeight }}
|
|
109
|
+
>
|
|
100
110
|
<div
|
|
101
111
|
ref={rowsRef}
|
|
102
112
|
role="tablist"
|
|
@@ -137,7 +147,8 @@ export function L3ProjectFileTabs({
|
|
|
137
147
|
inert={clipped || undefined}
|
|
138
148
|
title={tab.title}
|
|
139
149
|
className={cn(
|
|
140
|
-
'flex
|
|
150
|
+
'flex max-w-full items-center text-sm',
|
|
151
|
+
merged ? (mobile ? 'h-9' : 'h-8') : 'h-9 border-r border-b',
|
|
141
152
|
mobile ? 'w-[min(50%,12rem)]' : 'max-w-60',
|
|
142
153
|
clipped && 'invisible',
|
|
143
154
|
selected
|
|
@@ -154,7 +165,7 @@ export function L3ProjectFileTabs({
|
|
|
154
165
|
type="button"
|
|
155
166
|
role="tab"
|
|
156
167
|
aria-selected={selected}
|
|
157
|
-
className="flex h-
|
|
168
|
+
className="flex h-full min-w-0 flex-1 cursor-pointer items-center gap-2 pl-3 pr-1 outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring"
|
|
158
169
|
onClick={() => {
|
|
159
170
|
if (suppressTouchClickRef.current) {
|
|
160
171
|
suppressTouchClickRef.current = false
|
|
@@ -260,6 +271,15 @@ export function L3ProjectFileTabs({
|
|
|
260
271
|
})}
|
|
261
272
|
</div>
|
|
262
273
|
</div>
|
|
274
|
+
{merged && actions ? (
|
|
275
|
+
<div
|
|
276
|
+
className="ml-auto flex min-h-8 max-w-full flex-wrap items-center justify-end gap-1 px-1"
|
|
277
|
+
role="group"
|
|
278
|
+
aria-label={t('fileActions')}
|
|
279
|
+
>
|
|
280
|
+
{actions}
|
|
281
|
+
</div>
|
|
282
|
+
) : null}
|
|
263
283
|
{overflow ? (
|
|
264
284
|
<Button
|
|
265
285
|
variant="ghost"
|
|
@@ -59,65 +59,54 @@ export function L3ProjectSearchControls({
|
|
|
59
59
|
)
|
|
60
60
|
|
|
61
61
|
return (
|
|
62
|
-
<div className="shrink-0
|
|
63
|
-
<
|
|
64
|
-
<
|
|
65
|
-
<
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
scope === 'all'
|
|
105
|
-
? 'searchScopeAll'
|
|
106
|
-
: scope === 'files'
|
|
107
|
-
? 'searchScopeFiles'
|
|
108
|
-
: 'searchScopeContent'
|
|
109
|
-
)}
|
|
110
|
-
</Button>
|
|
111
|
-
))}
|
|
112
|
-
</div>
|
|
62
|
+
<div className="shrink-0 px-1 pt-1">
|
|
63
|
+
<InputGroup className="rounded-md">
|
|
64
|
+
<InputGroupAddon className="pl-1.5 py-0">
|
|
65
|
+
<SearchIcon className="size-3.5" />
|
|
66
|
+
</InputGroupAddon>
|
|
67
|
+
<InputGroupInput
|
|
68
|
+
ref={inputRef}
|
|
69
|
+
data-testid="project-file-search"
|
|
70
|
+
className="min-h-7 min-w-0 px-1.5 py-0.5 text-sm placeholder:text-xs"
|
|
71
|
+
value={composition ?? input.query}
|
|
72
|
+
placeholder={placeholder}
|
|
73
|
+
aria-label={placeholder}
|
|
74
|
+
aria-invalid={invalid || undefined}
|
|
75
|
+
autoComplete="off"
|
|
76
|
+
spellCheck={false}
|
|
77
|
+
onChange={(event) => {
|
|
78
|
+
if (composing.current) setComposition(event.target.value)
|
|
79
|
+
else onQueryChange(event.target.value)
|
|
80
|
+
}}
|
|
81
|
+
onCompositionStart={(event) => {
|
|
82
|
+
composing.current = true
|
|
83
|
+
setComposition(event.currentTarget.value)
|
|
84
|
+
onComposingChange(true)
|
|
85
|
+
}}
|
|
86
|
+
onCompositionEnd={(event) => {
|
|
87
|
+
composing.current = false
|
|
88
|
+
setComposition(null)
|
|
89
|
+
onQueryChange(event.currentTarget.value)
|
|
90
|
+
onComposingChange(false)
|
|
91
|
+
}}
|
|
92
|
+
onKeyDown={(event) => {
|
|
93
|
+
if (event.nativeEvent.isComposing || composing.current) return
|
|
94
|
+
if (event.key === 'Enter') {
|
|
95
|
+
event.preventDefault()
|
|
96
|
+
onOpenFirst()
|
|
97
|
+
} else if (event.key === 'ArrowDown') {
|
|
98
|
+
event.preventDefault()
|
|
99
|
+
onFocusResults()
|
|
100
|
+
}
|
|
101
|
+
}}
|
|
102
|
+
/>
|
|
103
|
+
<InputGroupAddon align="inline-end" className="pr-0.5 py-0 has-[>button]:mr-0">
|
|
113
104
|
<Tooltip>
|
|
114
105
|
<TooltipTrigger
|
|
115
106
|
render={
|
|
116
|
-
<
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
size="icon-sm"
|
|
120
|
-
className="size-7 shrink-0"
|
|
107
|
+
<InputGroupButton
|
|
108
|
+
size="icon-xs"
|
|
109
|
+
className="min-h-7"
|
|
121
110
|
aria-label={t('closeSearch')}
|
|
122
111
|
onClick={onClose}
|
|
123
112
|
/>
|
|
@@ -127,74 +116,80 @@ export function L3ProjectSearchControls({
|
|
|
127
116
|
</TooltipTrigger>
|
|
128
117
|
<TooltipContent>{t('closeSearch')}</TooltipContent>
|
|
129
118
|
</Tooltip>
|
|
130
|
-
</
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
onCompositionEnd={(event) => {
|
|
155
|
-
composing.current = false
|
|
156
|
-
setComposition(null)
|
|
157
|
-
onQueryChange(event.currentTarget.value)
|
|
158
|
-
onComposingChange(false)
|
|
159
|
-
}}
|
|
160
|
-
onKeyDown={(event) => {
|
|
161
|
-
if (event.nativeEvent.isComposing || composing.current) return
|
|
162
|
-
if (event.key === 'Enter') {
|
|
119
|
+
</InputGroupAddon>
|
|
120
|
+
</InputGroup>
|
|
121
|
+
<div className="flex min-h-7 flex-wrap items-center justify-between gap-x-1">
|
|
122
|
+
<div className="flex min-w-0 items-center" role="radiogroup" aria-label={t('searchScope')}>
|
|
123
|
+
{SCOPES.map((scope, index) => (
|
|
124
|
+
<Button
|
|
125
|
+
key={scope}
|
|
126
|
+
type="button"
|
|
127
|
+
role="radio"
|
|
128
|
+
aria-checked={input.scope === scope}
|
|
129
|
+
tabIndex={input.scope === scope ? 0 : -1}
|
|
130
|
+
disabled={composition !== null}
|
|
131
|
+
variant="ghost"
|
|
132
|
+
size="sm"
|
|
133
|
+
data-testid={`project-search-scope-${scope}`}
|
|
134
|
+
className={cn(
|
|
135
|
+
'px-1 text-xs',
|
|
136
|
+
input.scope === scope && 'bg-accent text-accent-foreground'
|
|
137
|
+
)}
|
|
138
|
+
onClick={() => onOptionsChange({ scope })}
|
|
139
|
+
onKeyDown={(event) => {
|
|
140
|
+
const forward = event.key === 'ArrowRight' || event.key === 'ArrowDown'
|
|
141
|
+
const backward = event.key === 'ArrowLeft' || event.key === 'ArrowUp'
|
|
142
|
+
if (!forward && !backward && event.key !== 'Home' && event.key !== 'End') return
|
|
163
143
|
event.preventDefault()
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
144
|
+
const next =
|
|
145
|
+
event.key === 'Home'
|
|
146
|
+
? 0
|
|
147
|
+
: event.key === 'End'
|
|
148
|
+
? 2
|
|
149
|
+
: (index + (forward ? 1 : 2)) % 3
|
|
150
|
+
onOptionsChange({ scope: SCOPES[next] })
|
|
151
|
+
event.currentTarget.parentElement
|
|
152
|
+
?.querySelectorAll<HTMLButtonElement>('[role="radio"]')
|
|
153
|
+
[next]?.focus()
|
|
154
|
+
}}
|
|
155
|
+
>
|
|
156
|
+
{t(
|
|
157
|
+
scope === 'all'
|
|
158
|
+
? 'searchScopeAll'
|
|
159
|
+
: scope === 'files'
|
|
160
|
+
? 'searchScopeFiles'
|
|
161
|
+
: 'searchScopeContent'
|
|
162
|
+
)}
|
|
163
|
+
</Button>
|
|
164
|
+
))}
|
|
165
|
+
</div>
|
|
166
|
+
<div className="ml-auto flex items-center">
|
|
167
|
+
{OPTIONS.map(({ key, label, text }) => (
|
|
168
|
+
<Tooltip key={key}>
|
|
169
|
+
<TooltipTrigger
|
|
170
|
+
render={
|
|
171
|
+
<Button
|
|
172
|
+
type="button"
|
|
173
|
+
variant="ghost"
|
|
174
|
+
size="icon-sm"
|
|
175
|
+
disabled={composition !== null}
|
|
176
|
+
aria-label={t(label)}
|
|
177
|
+
aria-pressed={input[key]}
|
|
178
|
+
data-testid={`project-search-option-${key}`}
|
|
179
|
+
className={cn(
|
|
180
|
+
'font-mono text-xs text-muted-foreground',
|
|
181
|
+
key === 'wholeWord' && 'underline underline-offset-4'
|
|
182
|
+
)}
|
|
183
|
+
onClick={() => onOptionsChange({ [key]: !input[key] })}
|
|
184
|
+
/>
|
|
185
|
+
}
|
|
186
|
+
>
|
|
187
|
+
{text}
|
|
188
|
+
</TooltipTrigger>
|
|
189
|
+
<TooltipContent>{t(label)}</TooltipContent>
|
|
190
|
+
</Tooltip>
|
|
191
|
+
))}
|
|
192
|
+
</div>
|
|
198
193
|
</div>
|
|
199
194
|
</div>
|
|
200
195
|
)
|