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(
po.page.getByRole("heading", { name: "Import App" }),
).not.toBeVisible();
// Verify AI_RULES generation prompt
await po.snapshotMessages();
});
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 hasStartCommand = startCommand.trim().length > 0;
const commandsValid = hasInstallCommand === hasStartCommand;
// Add this component inside the ImportAppDialog.tsx file, before the main component
return (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="max-w-2xl max-h-[98vh] overflow-y-auto">
<DialogHeader>
<DialogContent className="max-w-2xl w-[calc(100vw-2rem)] max-h-[98vh] overflow-y-auto flex flex-col p-0">
<DialogHeader className="sticky top-0 bg-background border-b px-6 py-4">
<DialogTitle>Import App</DialogTitle>
<DialogDescription>
<DialogDescription className="text-sm">
Import existing app from local folder or clone from Github.
</DialogDescription>
</DialogHeader>
<Alert className="border-blue-500/20 text-blue-500">
<Info className="h-4 w-4" />
<AlertDescription>
App import is an experimental feature. If you encounter any issues,
please report them using the Help button.
</AlertDescription>
</Alert>
<Tabs defaultValue="local-folder" className="w-full">
<TabsList className="grid w-full grid-cols-3">
<TabsTrigger value="local-folder">Local Folder</TabsTrigger>
<TabsTrigger value="github-repos">Your GitHub Repos</TabsTrigger>
<TabsTrigger value="github-url">GitHub URL</TabsTrigger>
</TabsList>
<TabsContent value="local-folder" className="space-y-4">
<div className="py-4">
{!selectedPath ? (
<Button
onClick={handleSelectFolder}
disabled={selectFolderMutation.isPending}
className="w-full"
>
{selectFolderMutation.isPending ? (
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
) : (
<Folder className="mr-2 h-4 w-4" />
)}
{selectFolderMutation.isPending
? "Selecting folder..."
: "Select Folder"}
</Button>
) : (
<div className="space-y-4">
<div className="rounded-md border p-4">
<div className="flex items-start justify-between gap-2">
<div className="min-w-0 flex-1">
<p className="text-sm font-medium">Selected folder:</p>
<p className="text-sm text-muted-foreground break-all">
{selectedPath}
</p>
</div>
<Button
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="px-6 pb-6 overflow-y-auto flex-1">
<Alert className="border-blue-500/20 text-blue-500 mb-2">
<Info className="h-4 w-4 flex-shrink-0" />
<AlertDescription className="text-xs sm:text-sm">
App import is an experimental feature. If you encounter any
issues, please report them using the Help button.
</AlertDescription>
</Alert>
<Tabs defaultValue="local-folder" className="w-full">
<TabsList className="grid w-full grid-cols-3 h-auto">
<TabsTrigger
value="local-folder"
className="text-xs sm:text-sm px-2 py-2"
>
Local Folder
</TabsTrigger>
<TabsTrigger
value="github-repos"
className="text-xs sm:text-sm px-2 py-2"
>
<span className="hidden sm:inline">Your GitHub Repos</span>
<span className="sm:hidden">GitHub Repos</span>
</TabsTrigger>
<TabsTrigger
value="github-url"
className="text-xs sm:text-sm px-2 py-2"
>
GitHub URL
</TabsTrigger>
</TabsList>
<TabsContent value="local-folder" className="space-y-4">
<div className="py-4">
{!selectedPath ? (
<Button
onClick={handleSelectFolder}
disabled={selectFolderMutation.isPending}
className="w-full"
>
{selectFolderMutation.isPending ? (
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
) : (
<Folder className="mr-2 h-4 w-4" />
)}
<div className="relative">
<Label className="text-sm ml-2 mb-2">App name</Label>
<Input
value={customAppName}
onChange={handleAppNameChange}
placeholder="Enter new app name"
className="w-full pr-8"
disabled={importAppMutation.isPending}
/>
{isCheckingName && (
<div className="absolute right-2 top-1/2 -translate-y-1/2">
<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.
{selectFolderMutation.isPending
? "Selecting folder..."
: "Select Folder"}
</Button>
) : (
<div className="space-y-4">
<div className="rounded-md border p-3 sm:p-4">
<div className="flex items-start justify-between gap-2">
<div className="min-w-0 flex-1 overflow-hidden">
<p className="text-sm font-medium mb-1">
Selected folder:
</p>
)}
</AccordionContent>
</AccordionItem>
</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>
AI_RULES.md lets Dyad know which tech stack to use
for editing the app
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<AlertDescription>
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-sm text-muted-foreground animate-pulse">
<Loader2 className="h-4 w-4 animate-spin" />
<span>Importing app...</span>
</div>
)}
</div>
)}
</div>
<DialogFooter>
<Button
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>
<p className="text-xs sm:text-sm text-muted-foreground break-words">
{selectedPath}
</p>
</div>
<Button
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-xs sm:text-sm text-yellow-500">
An app with this name already exists. Please choose a
different name:
</p>
)}
<div className="relative">
<Label className="text-xs sm:text-sm ml-2 mb-2">
App name
</Label>
<Input
value={customAppName}
onChange={handleAppNameChange}
placeholder="Enter new app name"
className="w-full pr-8 text-sm"
disabled={importAppMutation.isPending}
/>
{isCheckingName && (
<div className="absolute right-2 top-1/2 -translate-y-1/2">
<Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
</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>
{repos.length > 0 && (
<>
<Accordion type="single" collapsible>
<AccordionItem value="advanced-options">
<AccordionTrigger className="text-sm hover:no-underline">
<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-sm">Install command</Label>
<Label className="text-xs sm:text-sm ml-2 mb-2">
Install command
</Label>
<Input
value={installCommand}
onChange={(e) =>
setInstallCommand(e.target.value)
}
placeholder="pnpm install"
disabled={importing}
className="text-sm"
disabled={importAppMutation.isPending}
/>
</div>
<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
value={startCommand}
onChange={(e) => setStartCommand(e.target.value)}
placeholder="pnpm dev"
disabled={importing}
className="text-sm"
disabled={importAppMutation.isPending}
/>
</div>
{!commandsValid && (
<p className="text-sm text-red-500">
<p className="text-xs sm:text-sm text-red-500">
Both commands are required when customizing.
</p>
)}
</AccordionContent>
</AccordionItem>
</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>
)}
</>
)}
</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>
</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">Install command</Label>
<Input
value={installCommand}
onChange={(e) => setInstallCommand(e.target.value)}
placeholder="pnpm install"
disabled={importing}
/>
</div>
<div className="grid gap-2">
<Label className="text-sm">Start command</Label>
<Input
value={startCommand}
onChange={(e) => setStartCommand(e.target.value)}
placeholder="pnpm dev"
disabled={importing}
/>
</div>
{!commandsValid && (
<p className="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...
</>
<DialogFooter className="flex-col sm:flex-row gap-2">
<Button
variant="outline"
onClick={onClose}
disabled={importAppMutation.isPending}
className="w-full sm:w-auto"
>
Cancel
</Button>
<Button
onClick={handleImport}
disabled={
!selectedPath ||
importAppMutation.isPending ||
nameExists ||
!commandsValid
}
className="w-full sm:w-auto 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}
/>
) : (
"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>
</Tabs>
</TabsContent>
<TabsContent value="github-url" className="space-y-4">
<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>
</Dialog>
);