fix: responsiveness for import app dialog (#1746) (#1786)

The diff appears large due to Prettier formatting, but actual code
changes are minimal.

- Made header sticky with proper flex layout
- Added responsive text sizes (`text-xs sm:text-sm`)
- Improved text wrapping (`break-words overflow-wrap-anywhere`)
- Made tab labels responsive (shorter for small screen)
- Added `flex-shrink-0` to prevent icon/button squishing
- Stack footer buttons vertically on small screen

closes #1746



<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Made the Import App dialog responsive on small screens. Improves
readability and prevents controls from squishing.

- **Bug Fixes**
- Added a sticky header so the title/description stay visible while
scrolling.
- Made text and tab labels responsive, with better word wrapping to
avoid overflow.
- Prevented icon/button compression and stacked footer buttons
vertically on mobile.
- Updated e2e test by removing the AI_RULES snapshot to match the new
UI.

<sup>Written for commit 1025631018964aea37689ab2196e0169755e3739.
Summary will update automatically on new commits.</sup>

<!-- End of auto-generated description by cubic. -->
This commit is contained in:
Adeniji Adekunle James
2025-11-21 15:02:35 +00:00
committed by GitHub
parent 39876f114a
commit 76875fefec
2 changed files with 380 additions and 337 deletions

View File

@@ -102,9 +102,6 @@ test("should import from repository list", async ({ po }) => {
await expect( await expect(
po.page.getByRole("heading", { name: "Import App" }), po.page.getByRole("heading", { name: "Import App" }),
).not.toBeVisible(); ).not.toBeVisible();
// Verify AI_RULES generation prompt
await po.snapshotMessages();
}); });
test("should support advanced options with custom commands", async ({ po }) => { test("should support advanced options with custom commands", async ({ po }) => {

View File

@@ -298,383 +298,429 @@ export function ImportAppDialog({ isOpen, onClose }: ImportAppDialogProps) {
const hasInstallCommand = installCommand.trim().length > 0; const hasInstallCommand = installCommand.trim().length > 0;
const hasStartCommand = startCommand.trim().length > 0; const hasStartCommand = startCommand.trim().length > 0;
const commandsValid = hasInstallCommand === hasStartCommand; const commandsValid = hasInstallCommand === hasStartCommand;
// Add this component inside the ImportAppDialog.tsx file, before the main component
return ( return (
<Dialog open={isOpen} onOpenChange={onClose}> <Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="max-w-2xl max-h-[98vh] overflow-y-auto"> <DialogContent className="max-w-2xl w-[calc(100vw-2rem)] max-h-[98vh] overflow-y-auto flex flex-col p-0">
<DialogHeader> <DialogHeader className="sticky top-0 bg-background border-b px-6 py-4">
<DialogTitle>Import App</DialogTitle> <DialogTitle>Import App</DialogTitle>
<DialogDescription> <DialogDescription className="text-sm">
Import existing app from local folder or clone from Github. Import existing app from local folder or clone from Github.
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<div className="px-6 pb-6 overflow-y-auto flex-1">
<Alert className="border-blue-500/20 text-blue-500"> <Alert className="border-blue-500/20 text-blue-500 mb-2">
<Info className="h-4 w-4" /> <Info className="h-4 w-4 flex-shrink-0" />
<AlertDescription> <AlertDescription className="text-xs sm:text-sm">
App import is an experimental feature. If you encounter any issues, App import is an experimental feature. If you encounter any
please report them using the Help button. issues, please report them using the Help button.
</AlertDescription> </AlertDescription>
</Alert> </Alert>
<Tabs defaultValue="local-folder" className="w-full"> <Tabs defaultValue="local-folder" className="w-full">
<TabsList className="grid w-full grid-cols-3"> <TabsList className="grid w-full grid-cols-3 h-auto">
<TabsTrigger value="local-folder">Local Folder</TabsTrigger> <TabsTrigger
<TabsTrigger value="github-repos">Your GitHub Repos</TabsTrigger> value="local-folder"
<TabsTrigger value="github-url">GitHub URL</TabsTrigger> className="text-xs sm:text-sm px-2 py-2"
</TabsList> >
<TabsContent value="local-folder" className="space-y-4"> Local Folder
<div className="py-4"> </TabsTrigger>
{!selectedPath ? ( <TabsTrigger
<Button value="github-repos"
onClick={handleSelectFolder} className="text-xs sm:text-sm px-2 py-2"
disabled={selectFolderMutation.isPending} >
className="w-full" <span className="hidden sm:inline">Your GitHub Repos</span>
> <span className="sm:hidden">GitHub Repos</span>
{selectFolderMutation.isPending ? ( </TabsTrigger>
<Loader2 className="mr-2 h-4 w-4 animate-spin" /> <TabsTrigger
) : ( value="github-url"
<Folder className="mr-2 h-4 w-4" /> className="text-xs sm:text-sm px-2 py-2"
)} >
{selectFolderMutation.isPending GitHub URL
? "Selecting folder..." </TabsTrigger>
: "Select Folder"} </TabsList>
</Button> <TabsContent value="local-folder" className="space-y-4">
) : ( <div className="py-4">
<div className="space-y-4"> {!selectedPath ? (
<div className="rounded-md border p-4"> <Button
<div className="flex items-start justify-between gap-2"> onClick={handleSelectFolder}
<div className="min-w-0 flex-1"> disabled={selectFolderMutation.isPending}
<p className="text-sm font-medium">Selected folder:</p> className="w-full"
<p className="text-sm text-muted-foreground break-all"> >
{selectedPath} {selectFolderMutation.isPending ? (
</p> <Loader2 className="mr-2 h-4 w-4 animate-spin" />
</div> ) : (
<Button <Folder className="mr-2 h-4 w-4" />
variant="ghost"
size="sm"
onClick={handleClear}
className="h-8 w-8 p-0 flex-shrink-0"
disabled={importAppMutation.isPending}
>
<X className="h-4 w-4" />
<span className="sr-only">Clear selection</span>
</Button>
</div>
</div>
<div className="space-y-2">
{nameExists && (
<p className="text-sm text-yellow-500">
An app with this name already exists. Please choose a
different name:
</p>
)} )}
<div className="relative"> {selectFolderMutation.isPending
<Label className="text-sm ml-2 mb-2">App name</Label> ? "Selecting folder..."
<Input : "Select Folder"}
value={customAppName} </Button>
onChange={handleAppNameChange} ) : (
placeholder="Enter new app name" <div className="space-y-4">
className="w-full pr-8" <div className="rounded-md border p-3 sm:p-4">
disabled={importAppMutation.isPending} <div className="flex items-start justify-between gap-2">
/> <div className="min-w-0 flex-1 overflow-hidden">
{isCheckingName && ( <p className="text-sm font-medium mb-1">
<div className="absolute right-2 top-1/2 -translate-y-1/2"> Selected folder:
<Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
</div>
)}
</div>
</div>
<Accordion type="single" collapsible>
<AccordionItem value="advanced-options">
<AccordionTrigger className="text-sm hover:no-underline">
Advanced options
</AccordionTrigger>
<AccordionContent className="space-y-4">
<div className="grid gap-2">
<Label className="text-sm ml-2 mb-2">
Install command
</Label>
<Input
value={installCommand}
onChange={(e) => setInstallCommand(e.target.value)}
placeholder="pnpm install"
disabled={importAppMutation.isPending}
/>
</div>
<div className="grid gap-2">
<Label className="text-sm ml-2 mb-2">
Start command
</Label>
<Input
value={startCommand}
onChange={(e) => setStartCommand(e.target.value)}
placeholder="pnpm dev"
disabled={importAppMutation.isPending}
/>
</div>
{!commandsValid && (
<p className="text-sm text-red-500">
Both commands are required when customizing.
</p> </p>
)} <p className="text-xs sm:text-sm text-muted-foreground break-words">
</AccordionContent> {selectedPath}
</AccordionItem> </p>
</Accordion> </div>
<Button
{hasAiRules === false && ( variant="ghost"
<Alert className="border-yellow-500/20 text-yellow-500 flex items-start gap-2"> size="sm"
<TooltipProvider> onClick={handleClear}
<Tooltip> className="h-8 w-8 p-0 flex-shrink-0"
<TooltipTrigger asChild> disabled={importAppMutation.isPending}
<Info className="h-4 w-4 flex-shrink-0 mt-1" /> >
</TooltipTrigger> <X className="h-4 w-4" />
<TooltipContent> <span className="sr-only">Clear selection</span>
<p> </Button>
AI_RULES.md lets Dyad know which tech stack to use </div>
for editing the app </div>
</p>
</TooltipContent> <div className="space-y-2">
</Tooltip> {nameExists && (
</TooltipProvider> <p className="text-xs sm:text-sm text-yellow-500">
<AlertDescription> An app with this name already exists. Please choose a
No AI_RULES.md found. Dyad will automatically generate different name:
one after importing. </p>
</AlertDescription> )}
</Alert> <div className="relative">
)} <Label className="text-xs sm:text-sm ml-2 mb-2">
App name
{importAppMutation.isPending && ( </Label>
<div className="flex items-center justify-center space-x-2 text-sm text-muted-foreground animate-pulse"> <Input
<Loader2 className="h-4 w-4 animate-spin" /> value={customAppName}
<span>Importing app...</span> onChange={handleAppNameChange}
</div> placeholder="Enter new app name"
)} className="w-full pr-8 text-sm"
</div> disabled={importAppMutation.isPending}
)} />
</div> {isCheckingName && (
<div className="absolute right-2 top-1/2 -translate-y-1/2">
<DialogFooter> <Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
<Button </div>
variant="outline" )}
onClick={onClose}
disabled={importAppMutation.isPending}
>
Cancel
</Button>
<Button
onClick={handleImport}
disabled={
!selectedPath ||
importAppMutation.isPending ||
nameExists ||
!commandsValid
}
className="min-w-[80px]"
>
{importAppMutation.isPending ? <>Importing...</> : "Import"}
</Button>
</DialogFooter>
</TabsContent>
<TabsContent value="github-repos" className="space-y-4">
{!isAuthenticated ? (
<UnconnectedGitHubConnector
appId={null}
folderName=""
settings={settings}
refreshSettings={refreshSettings}
handleRepoSetupComplete={() => undefined}
expanded={false}
/>
) : (
<>
{loading && (
<div className="flex justify-center py-8">
<Loader2 className="animate-spin h-6 w-6" />
</div>
)}
<div className="space-y-2">
<Label className="text-sm ml-2 mb-2">
App name (optional)
</Label>
<Input
value={githubAppName}
onChange={handleGithubAppNameChange}
placeholder="Leave empty to use repository name"
className="w-full pr-8"
disabled={importing}
/>
{isCheckingGithubName && (
<div className="absolute right-2 top-1/2 -translate-y-1/2">
<Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
</div>
)}
{githubNameExists && (
<p className="text-sm text-yellow-500">
An app with this name already exists. Please choose a
different name.
</p>
)}
</div>
<div className="flex flex-col space-y-2 max-h-64 overflow-y-auto">
{!loading && repos.length === 0 && (
<p className="text-sm text-muted-foreground text-center py-4">
No repositories found
</p>
)}
{repos.map((repo) => (
<div
key={repo.full_name}
className="flex items-center justify-between p-3 border rounded-lg hover:bg-accent/50 transition-colors"
>
<div className="min-w-0 flex-1">
<p className="font-semibold truncate">{repo.name}</p>
<p className="text-sm text-muted-foreground truncate">
{repo.full_name}
</p>
</div> </div>
<Button
variant="outline"
size="sm"
onClick={() => handleSelectRepo(repo)}
disabled={importing}
className="ml-2 flex-shrink-0"
>
{importing ? (
<Loader2 className="animate-spin h-4 w-4" />
) : (
"Import"
)}
</Button>
</div> </div>
))}
</div>
{repos.length > 0 && (
<>
<Accordion type="single" collapsible> <Accordion type="single" collapsible>
<AccordionItem value="advanced-options"> <AccordionItem value="advanced-options">
<AccordionTrigger className="text-sm hover:no-underline"> <AccordionTrigger className="text-xs sm:text-sm hover:no-underline">
Advanced options Advanced options
</AccordionTrigger> </AccordionTrigger>
<AccordionContent className="space-y-4"> <AccordionContent className="space-y-4">
<div className="grid gap-2"> <div className="grid gap-2">
<Label className="text-sm">Install command</Label> <Label className="text-xs sm:text-sm ml-2 mb-2">
Install command
</Label>
<Input <Input
value={installCommand} value={installCommand}
onChange={(e) => onChange={(e) =>
setInstallCommand(e.target.value) setInstallCommand(e.target.value)
} }
placeholder="pnpm install" placeholder="pnpm install"
disabled={importing} className="text-sm"
disabled={importAppMutation.isPending}
/> />
</div> </div>
<div className="grid gap-2"> <div className="grid gap-2">
<Label className="text-sm">Start command</Label> <Label className="text-xs sm:text-sm ml-2 mb-2">
Start command
</Label>
<Input <Input
value={startCommand} value={startCommand}
onChange={(e) => setStartCommand(e.target.value)} onChange={(e) => setStartCommand(e.target.value)}
placeholder="pnpm dev" placeholder="pnpm dev"
disabled={importing} className="text-sm"
disabled={importAppMutation.isPending}
/> />
</div> </div>
{!commandsValid && ( {!commandsValid && (
<p className="text-sm text-red-500"> <p className="text-xs sm:text-sm text-red-500">
Both commands are required when customizing. Both commands are required when customizing.
</p> </p>
)} )}
</AccordionContent> </AccordionContent>
</AccordionItem> </AccordionItem>
</Accordion> </Accordion>
</>
{hasAiRules === false && (
<Alert className="border-yellow-500/20 text-yellow-500 flex items-start gap-2">
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Info className="h-4 w-4 flex-shrink-0 mt-1" />
</TooltipTrigger>
<TooltipContent>
<p className="text-xs">
AI_RULES.md lets Dyad know which tech stack to
use for editing the app
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<AlertDescription className="text-xs sm:text-sm">
No AI_RULES.md found. Dyad will automatically generate
one after importing.
</AlertDescription>
</Alert>
)}
{importAppMutation.isPending && (
<div className="flex items-center justify-center space-x-2 text-xs sm:text-sm text-muted-foreground animate-pulse">
<Loader2 className="h-4 w-4 animate-spin" />
<span>Importing app...</span>
</div>
)}
</div>
)} )}
</> </div>
)}
</TabsContent>
<TabsContent value="github-url" className="space-y-4">
<div className="space-y-2">
<Label className="text-sm">Repository URL</Label>
<Input
placeholder="https://github.com/user/repo.git"
value={url}
onChange={(e) => setUrl(e.target.value)}
disabled={importing}
onBlur={handleUrlBlur}
/>
</div>
<div className="space-y-2">
<Label className="text-sm">App name (optional)</Label>
<Input
value={githubAppName}
onChange={handleGithubAppNameChange}
placeholder="Leave empty to use repository name"
disabled={importing}
/>
{isCheckingGithubName && (
<div className="absolute right-2 top-1/2 -translate-y-1/2">
<Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
</div>
)}
{githubNameExists && (
<p className="text-sm text-yellow-500">
An app with this name already exists. Please choose a
different name.
</p>
)}
</div>
<Accordion type="single" collapsible> <DialogFooter className="flex-col sm:flex-row gap-2">
<AccordionItem value="advanced-options"> <Button
<AccordionTrigger className="text-sm hover:no-underline"> variant="outline"
Advanced options onClick={onClose}
</AccordionTrigger> disabled={importAppMutation.isPending}
<AccordionContent className="space-y-4"> className="w-full sm:w-auto"
<div className="grid gap-2"> >
<Label className="text-sm">Install command</Label> Cancel
<Input </Button>
value={installCommand} <Button
onChange={(e) => setInstallCommand(e.target.value)} onClick={handleImport}
placeholder="pnpm install" disabled={
disabled={importing} !selectedPath ||
/> importAppMutation.isPending ||
</div> nameExists ||
<div className="grid gap-2"> !commandsValid
<Label className="text-sm">Start command</Label> }
<Input className="w-full sm:w-auto min-w-[80px]"
value={startCommand} >
onChange={(e) => setStartCommand(e.target.value)} {importAppMutation.isPending ? <>Importing...</> : "Import"}
placeholder="pnpm dev" </Button>
disabled={importing} </DialogFooter>
/> </TabsContent>
</div> <TabsContent value="github-repos" className="space-y-4">
{!commandsValid && ( {!isAuthenticated ? (
<p className="text-sm text-red-500"> <UnconnectedGitHubConnector
Both commands are required when customizing. appId={null}
</p> folderName=""
)} settings={settings}
</AccordionContent> refreshSettings={refreshSettings}
</AccordionItem> handleRepoSetupComplete={() => undefined}
</Accordion> expanded={false}
/>
<Button
onClick={handleImportFromUrl}
disabled={importing || !url.trim() || !commandsValid}
className="w-full"
>
{importing ? (
<>
<Loader2 className="animate-spin mr-2 h-4 w-4" />
Importing...
</>
) : ( ) : (
"Import" <>
{loading && (
<div className="flex justify-center py-8">
<Loader2 className="animate-spin h-6 w-6" />
</div>
)}
<div className="space-y-2">
<Label className="text-xs sm:text-sm ml-2 mb-2">
App name (optional)
</Label>
<Input
value={githubAppName}
onChange={handleGithubAppNameChange}
placeholder="Leave empty to use repository name"
className="w-full pr-8 text-sm"
disabled={importing}
/>
{isCheckingGithubName && (
<div className="absolute right-2 top-1/2 -translate-y-1/2">
<Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
</div>
)}
{githubNameExists && (
<p className="text-xs sm:text-sm text-yellow-500">
An app with this name already exists. Please choose a
different name.
</p>
)}
</div>
<div className="flex flex-col space-y-2 max-h-64 overflow-y-auto overflow-x-hidden">
{!loading && repos.length === 0 && (
<p className="text-xs sm:text-sm text-muted-foreground text-center py-4">
No repositories found
</p>
)}
{repos.map((repo) => (
<div
key={repo.full_name}
className="flex items-center justify-between p-3 border rounded-lg hover:bg-accent/50 transition-colors min-w-0"
>
<div className="min-w-0 flex-1 overflow-hidden mr-2">
<p className="font-semibold truncate text-sm">
{repo.name}
</p>
<p className="text-xs text-muted-foreground truncate">
{repo.full_name}
</p>
</div>
<Button
variant="outline"
size="sm"
onClick={() => handleSelectRepo(repo)}
disabled={importing}
className="flex-shrink-0 text-xs"
>
{importing ? (
<Loader2 className="animate-spin h-4 w-4" />
) : (
"Import"
)}
</Button>
</div>
))}
</div>
{repos.length > 0 && (
<>
<Accordion type="single" collapsible>
<AccordionItem value="advanced-options">
<AccordionTrigger className="text-xs sm:text-sm hover:no-underline">
Advanced options
</AccordionTrigger>
<AccordionContent className="space-y-4">
<div className="grid gap-2">
<Label className="text-xs sm:text-sm">
Install command
</Label>
<Input
value={installCommand}
onChange={(e) =>
setInstallCommand(e.target.value)
}
placeholder="pnpm install"
className="text-sm"
disabled={importing}
/>
</div>
<div className="grid gap-2">
<Label className="text-xs sm:text-sm">
Start command
</Label>
<Input
value={startCommand}
onChange={(e) =>
setStartCommand(e.target.value)
}
placeholder="pnpm dev"
className="text-sm"
disabled={importing}
/>
</div>
{!commandsValid && (
<p className="text-xs sm:text-sm text-red-500">
Both commands are required when customizing.
</p>
)}
</AccordionContent>
</AccordionItem>
</Accordion>
</>
)}
</>
)} )}
</Button> </TabsContent>
</TabsContent> <TabsContent value="github-url" className="space-y-4">
</Tabs> <div className="space-y-2">
<Label className="text-xs sm:text-sm">Repository URL</Label>
<Input
placeholder="https://github.com/user/repo.git"
value={url}
onChange={(e) => setUrl(e.target.value)}
disabled={importing}
onBlur={handleUrlBlur}
className="text-sm break-all"
/>
</div>
<div className="space-y-2">
<Label className="text-xs sm:text-sm">
App name (optional)
</Label>
<Input
value={githubAppName}
onChange={handleGithubAppNameChange}
placeholder="Leave empty to use repository name"
disabled={importing}
className="text-sm"
/>
{isCheckingGithubName && (
<div className="absolute right-2 top-1/2 -translate-y-1/2">
<Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
</div>
)}
{githubNameExists && (
<p className="text-xs sm:text-sm text-yellow-500">
An app with this name already exists. Please choose a
different name.
</p>
)}
</div>
<Accordion type="single" collapsible>
<AccordionItem value="advanced-options">
<AccordionTrigger className="text-xs sm:text-sm hover:no-underline">
Advanced options
</AccordionTrigger>
<AccordionContent className="space-y-4">
<div className="grid gap-2">
<Label className="text-xs sm:text-sm">
Install command
</Label>
<Input
value={installCommand}
onChange={(e) => setInstallCommand(e.target.value)}
placeholder="pnpm install"
className="text-sm"
disabled={importing}
/>
</div>
<div className="grid gap-2">
<Label className="text-xs sm:text-sm">
Start command
</Label>
<Input
value={startCommand}
onChange={(e) => setStartCommand(e.target.value)}
placeholder="pnpm dev"
className="text-sm"
disabled={importing}
/>
</div>
{!commandsValid && (
<p className="text-xs sm:text-sm text-red-500">
Both commands are required when customizing.
</p>
)}
</AccordionContent>
</AccordionItem>
</Accordion>
<Button
onClick={handleImportFromUrl}
disabled={importing || !url.trim() || !commandsValid}
className="w-full"
>
{importing ? (
<>
<Loader2 className="animate-spin mr-2 h-4 w-4" />
Importing...
</>
) : (
"Import"
)}
</Button>
</TabsContent>
</Tabs>
</div>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
); );