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,29 +298,45 @@ 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"
>
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> </TabsList>
<TabsContent value="local-folder" className="space-y-4"> <TabsContent value="local-folder" className="space-y-4">
<div className="py-4"> <div className="py-4">
@@ -341,11 +357,13 @@ export function ImportAppDialog({ isOpen, onClose }: ImportAppDialogProps) {
</Button> </Button>
) : ( ) : (
<div className="space-y-4"> <div className="space-y-4">
<div className="rounded-md border p-4"> <div className="rounded-md border p-3 sm:p-4">
<div className="flex items-start justify-between gap-2"> <div className="flex items-start justify-between gap-2">
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1 overflow-hidden">
<p className="text-sm font-medium">Selected folder:</p> <p className="text-sm font-medium mb-1">
<p className="text-sm text-muted-foreground break-all"> Selected folder:
</p>
<p className="text-xs sm:text-sm text-muted-foreground break-words">
{selectedPath} {selectedPath}
</p> </p>
</div> </div>
@@ -364,18 +382,20 @@ export function ImportAppDialog({ isOpen, onClose }: ImportAppDialogProps) {
<div className="space-y-2"> <div className="space-y-2">
{nameExists && ( {nameExists && (
<p className="text-sm text-yellow-500"> <p className="text-xs sm:text-sm text-yellow-500">
An app with this name already exists. Please choose a An app with this name already exists. Please choose a
different name: different name:
</p> </p>
)} )}
<div className="relative"> <div className="relative">
<Label className="text-sm ml-2 mb-2">App name</Label> <Label className="text-xs sm:text-sm ml-2 mb-2">
App name
</Label>
<Input <Input
value={customAppName} value={customAppName}
onChange={handleAppNameChange} onChange={handleAppNameChange}
placeholder="Enter new app name" placeholder="Enter new app name"
className="w-full pr-8" className="w-full pr-8 text-sm"
disabled={importAppMutation.isPending} disabled={importAppMutation.isPending}
/> />
{isCheckingName && ( {isCheckingName && (
@@ -388,34 +408,38 @@ export function ImportAppDialog({ isOpen, onClose }: ImportAppDialogProps) {
<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 ml-2 mb-2"> <Label className="text-xs sm:text-sm ml-2 mb-2">
Install command Install command
</Label> </Label>
<Input <Input
value={installCommand} value={installCommand}
onChange={(e) => setInstallCommand(e.target.value)} onChange={(e) =>
setInstallCommand(e.target.value)
}
placeholder="pnpm install" placeholder="pnpm install"
className="text-sm"
disabled={importAppMutation.isPending} disabled={importAppMutation.isPending}
/> />
</div> </div>
<div className="grid gap-2"> <div className="grid gap-2">
<Label className="text-sm ml-2 mb-2"> <Label className="text-xs sm:text-sm ml-2 mb-2">
Start command Start command
</Label> </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"
className="text-sm"
disabled={importAppMutation.isPending} 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>
)} )}
@@ -431,14 +455,14 @@ export function ImportAppDialog({ isOpen, onClose }: ImportAppDialogProps) {
<Info className="h-4 w-4 flex-shrink-0 mt-1" /> <Info className="h-4 w-4 flex-shrink-0 mt-1" />
</TooltipTrigger> </TooltipTrigger>
<TooltipContent> <TooltipContent>
<p> <p className="text-xs">
AI_RULES.md lets Dyad know which tech stack to use AI_RULES.md lets Dyad know which tech stack to
for editing the app use for editing the app
</p> </p>
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
</TooltipProvider> </TooltipProvider>
<AlertDescription> <AlertDescription className="text-xs sm:text-sm">
No AI_RULES.md found. Dyad will automatically generate No AI_RULES.md found. Dyad will automatically generate
one after importing. one after importing.
</AlertDescription> </AlertDescription>
@@ -446,7 +470,7 @@ export function ImportAppDialog({ isOpen, onClose }: ImportAppDialogProps) {
)} )}
{importAppMutation.isPending && ( {importAppMutation.isPending && (
<div className="flex items-center justify-center space-x-2 text-sm text-muted-foreground animate-pulse"> <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" /> <Loader2 className="h-4 w-4 animate-spin" />
<span>Importing app...</span> <span>Importing app...</span>
</div> </div>
@@ -455,11 +479,12 @@ export function ImportAppDialog({ isOpen, onClose }: ImportAppDialogProps) {
)} )}
</div> </div>
<DialogFooter> <DialogFooter className="flex-col sm:flex-row gap-2">
<Button <Button
variant="outline" variant="outline"
onClick={onClose} onClick={onClose}
disabled={importAppMutation.isPending} disabled={importAppMutation.isPending}
className="w-full sm:w-auto"
> >
Cancel Cancel
</Button> </Button>
@@ -471,7 +496,7 @@ export function ImportAppDialog({ isOpen, onClose }: ImportAppDialogProps) {
nameExists || nameExists ||
!commandsValid !commandsValid
} }
className="min-w-[80px]" className="w-full sm:w-auto min-w-[80px]"
> >
{importAppMutation.isPending ? <>Importing...</> : "Import"} {importAppMutation.isPending ? <>Importing...</> : "Import"}
</Button> </Button>
@@ -496,14 +521,14 @@ export function ImportAppDialog({ isOpen, onClose }: ImportAppDialogProps) {
)} )}
<div className="space-y-2"> <div className="space-y-2">
<Label className="text-sm ml-2 mb-2"> <Label className="text-xs sm:text-sm ml-2 mb-2">
App name (optional) App name (optional)
</Label> </Label>
<Input <Input
value={githubAppName} value={githubAppName}
onChange={handleGithubAppNameChange} onChange={handleGithubAppNameChange}
placeholder="Leave empty to use repository name" placeholder="Leave empty to use repository name"
className="w-full pr-8" className="w-full pr-8 text-sm"
disabled={importing} disabled={importing}
/> />
{isCheckingGithubName && ( {isCheckingGithubName && (
@@ -512,27 +537,29 @@ export function ImportAppDialog({ isOpen, onClose }: ImportAppDialogProps) {
</div> </div>
)} )}
{githubNameExists && ( {githubNameExists && (
<p className="text-sm text-yellow-500"> <p className="text-xs sm:text-sm text-yellow-500">
An app with this name already exists. Please choose a An app with this name already exists. Please choose a
different name. different name.
</p> </p>
)} )}
</div> </div>
<div className="flex flex-col space-y-2 max-h-64 overflow-y-auto"> <div className="flex flex-col space-y-2 max-h-64 overflow-y-auto overflow-x-hidden">
{!loading && repos.length === 0 && ( {!loading && repos.length === 0 && (
<p className="text-sm text-muted-foreground text-center py-4"> <p className="text-xs sm:text-sm text-muted-foreground text-center py-4">
No repositories found No repositories found
</p> </p>
)} )}
{repos.map((repo) => ( {repos.map((repo) => (
<div <div
key={repo.full_name} key={repo.full_name}
className="flex items-center justify-between p-3 border rounded-lg hover:bg-accent/50 transition-colors" 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"> <div className="min-w-0 flex-1 overflow-hidden mr-2">
<p className="font-semibold truncate">{repo.name}</p> <p className="font-semibold truncate text-sm">
<p className="text-sm text-muted-foreground truncate"> {repo.name}
</p>
<p className="text-xs text-muted-foreground truncate">
{repo.full_name} {repo.full_name}
</p> </p>
</div> </div>
@@ -541,7 +568,7 @@ export function ImportAppDialog({ isOpen, onClose }: ImportAppDialogProps) {
size="sm" size="sm"
onClick={() => handleSelectRepo(repo)} onClick={() => handleSelectRepo(repo)}
disabled={importing} disabled={importing}
className="ml-2 flex-shrink-0" className="flex-shrink-0 text-xs"
> >
{importing ? ( {importing ? (
<Loader2 className="animate-spin h-4 w-4" /> <Loader2 className="animate-spin h-4 w-4" />
@@ -557,32 +584,40 @@ export function ImportAppDialog({ isOpen, onClose }: ImportAppDialogProps) {
<> <>
<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">
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"
className="text-sm"
disabled={importing} disabled={importing}
/> />
</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">
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"
className="text-sm"
disabled={importing} disabled={importing}
/> />
</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>
)} )}
@@ -596,22 +631,26 @@ export function ImportAppDialog({ isOpen, onClose }: ImportAppDialogProps) {
</TabsContent> </TabsContent>
<TabsContent value="github-url" className="space-y-4"> <TabsContent value="github-url" className="space-y-4">
<div className="space-y-2"> <div className="space-y-2">
<Label className="text-sm">Repository URL</Label> <Label className="text-xs sm:text-sm">Repository URL</Label>
<Input <Input
placeholder="https://github.com/user/repo.git" placeholder="https://github.com/user/repo.git"
value={url} value={url}
onChange={(e) => setUrl(e.target.value)} onChange={(e) => setUrl(e.target.value)}
disabled={importing} disabled={importing}
onBlur={handleUrlBlur} onBlur={handleUrlBlur}
className="text-sm break-all"
/> />
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label className="text-sm">App name (optional)</Label> <Label className="text-xs sm:text-sm">
App name (optional)
</Label>
<Input <Input
value={githubAppName} value={githubAppName}
onChange={handleGithubAppNameChange} onChange={handleGithubAppNameChange}
placeholder="Leave empty to use repository name" placeholder="Leave empty to use repository name"
disabled={importing} disabled={importing}
className="text-sm"
/> />
{isCheckingGithubName && ( {isCheckingGithubName && (
<div className="absolute right-2 top-1/2 -translate-y-1/2"> <div className="absolute right-2 top-1/2 -translate-y-1/2">
@@ -619,7 +658,7 @@ export function ImportAppDialog({ isOpen, onClose }: ImportAppDialogProps) {
</div> </div>
)} )}
{githubNameExists && ( {githubNameExists && (
<p className="text-sm text-yellow-500"> <p className="text-xs sm:text-sm text-yellow-500">
An app with this name already exists. Please choose a An app with this name already exists. Please choose a
different name. different name.
</p> </p>
@@ -628,30 +667,36 @@ export function ImportAppDialog({ isOpen, onClose }: ImportAppDialogProps) {
<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">
Install command
</Label>
<Input <Input
value={installCommand} value={installCommand}
onChange={(e) => setInstallCommand(e.target.value)} onChange={(e) => setInstallCommand(e.target.value)}
placeholder="pnpm install" placeholder="pnpm install"
className="text-sm"
disabled={importing} disabled={importing}
/> />
</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">
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"
className="text-sm"
disabled={importing} disabled={importing}
/> />
</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>
)} )}
@@ -675,6 +720,7 @@ export function ImportAppDialog({ isOpen, onClose }: ImportAppDialogProps) {
</Button> </Button>
</TabsContent> </TabsContent>
</Tabs> </Tabs>
</div>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
); );