Fix: Redesign Digitale Hefte create dialog - fits viewport properly

Problems fixed:
- Dialog was max-w-lg (narrow) with too much vertical content (16 notebook type
  buttons in a grid, separate sections for each field)
- Content overflowed the viewport on smaller screens

Redesigned approach:
- Dialog: max-w-2xl max-h-[85vh] with flex flex-col layout
- Header: fixed at top with border-b (shrink-0)
- Content: scrollable area (flex-1 overflow-y-auto)
- Footer: fixed at bottom with border-t (shrink-0)

Layout optimizations:
- Title + Description side by side (grid-cols-2)
- Subject + Class side by side (grid-cols-2)
- Notebook type: compact dropdown instead of 16-button grid
  (base types + curriculum separator + curriculum types)
- Color + Icon side by side (grid-cols-2)
- Public toggle + Eco message in one compact row
- Smaller labels (text-xs), tighter spacing (space-y-4)
- Fixed height inputs (h-10 instead of min-h-[44px])

Result: w=512 h=490, fits within viewport (577px)
This commit is contained in:
Z User committed 2026-08-11 11:22:01 +00:00
1 parent b56f840161
commit be1e61d94b
1 file changed
+107 -135
+107 -135
View File
@@ -1179,8 +1179,8 @@ function CreateNotebookDialog({
return (
<Dialog open={open} onOpenChange={(v) => { if (!v) onClose(); }}>
<DialogContent className="max-w-lg">
<DialogHeader>
<DialogContent className="max-w-2xl max-h-[85vh] flex flex-col p-0 gap-0 overflow-hidden">
<DialogHeader className="px-6 py-4 border-b shrink-0">
<DialogTitle className="flex items-center gap-2">
<BookOpen className="w-5 h-5 text-emerald-500" />
{t('notebooks.create_title')}
@@ -1188,88 +1188,35 @@ function CreateNotebookDialog({
<DialogDescription>{t('notebooks.create_desc')}</DialogDescription>
</DialogHeader>
<div className="space-y-5 py-2">
{/* Title */}
<div className="space-y-2">
<Label>{t('notebooks.field_title')}</Label>
<Input
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder={t('notebooks.field_title_placeholder')}
className="min-h-[44px]"
/>
</div>
{/* Description */}
<div className="space-y-2">
<Label>{t('notebooks.field_description')}</Label>
<Textarea
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder={t('notebooks.field_description_placeholder')}
rows={3}
/>
</div>
{/* Notebook Type - Visual Selector */}
<div className="space-y-2">
<Label>{t('notebooks.field_type')}</Label>
{/* Base types */}
<div className="grid grid-cols-3 gap-2">
{NOTEBOOK_TYPES.filter(nt => !nt.isCurriculum).map((nt) => {
const NtIcon = nt.icon;
const isSelected = notebookType === nt.key;
return (
<button
key={nt.key}
onClick={() => setNotebookType(nt.key)}
className={`flex flex-col items-center gap-1 p-3 rounded-xl border-2 transition-all min-h-[64px] ${
isSelected
? 'border-emerald-500 bg-emerald-50 dark:bg-emerald-900/20 text-emerald-700 dark:text-emerald-300'
: 'border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-600 dark:text-gray-400 hover:border-gray-300'
}`}
>
<NtIcon className="w-5 h-5" />
<span className="text-xs font-medium">{t(nt.labelKey)}</span>
</button>
);
})}
<div className="flex-1 overflow-y-auto px-6 py-4 space-y-4">
{/* Title + Description in a row */}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div className="space-y-1.5">
<Label className="text-xs">{t('notebooks.field_title')}</Label>
<Input
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder={t('notebooks.field_title_placeholder')}
className="h-10"
/>
</div>
{/* German curriculum types */}
<div className="mt-3">
<Label className="text-xs text-amber-600 dark:text-amber-400 flex items-center gap-1 mb-2">
<Bookmark className="w-3 h-3" />
{t('notebooks.curriculum_badge')}
</Label>
<div className="grid grid-cols-3 gap-2">
{NOTEBOOK_TYPES.filter(nt => nt.isCurriculum).map((nt) => {
const NtIcon = nt.icon;
const isSelected = notebookType === nt.key;
return (
<button
key={nt.key}
onClick={() => setNotebookType(nt.key)}
className={`flex flex-col items-center gap-1 p-3 rounded-xl border-2 transition-all min-h-[64px] ${
isSelected
? 'border-amber-500 bg-amber-50 dark:bg-amber-900/20 text-amber-700 dark:text-amber-300'
: 'border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-600 dark:text-gray-400 hover:border-amber-300'
}`}
>
<NtIcon className="w-5 h-5" />
<span className="text-xs font-medium">{t(nt.labelKey)}</span>
</button>
);
})}
</div>
<div className="space-y-1.5">
<Label className="text-xs">{t('notebooks.field_description')}</Label>
<Input
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder={t('notebooks.field_description_placeholder')}
className="h-10"
/>
</div>
</div>
{/* Subject + Class */}
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label>{t('notebooks.field_subject')}</Label>
{/* Subject + Class in a row */}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div className="space-y-1.5">
<Label className="text-xs">{t('notebooks.field_subject')}</Label>
<Select value={subjectId} onValueChange={setSubjectId}>
<SelectTrigger className="min-h-[44px]">
<SelectTrigger className="h-10">
<SelectValue placeholder={t('notebooks.no_subject')} />
</SelectTrigger>
<SelectContent>
@@ -1280,10 +1227,10 @@ function CreateNotebookDialog({
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label>{t('notebooks.field_class')}</Label>
<div className="space-y-1.5">
<Label className="text-xs">{t('notebooks.field_class')}</Label>
<Select value={classGroupId} onValueChange={setClassGroupId}>
<SelectTrigger className="min-h-[44px]">
<SelectTrigger className="h-10">
<SelectValue placeholder={t('notebooks.no_class')} />
</SelectTrigger>
<SelectContent>
@@ -1296,72 +1243,97 @@ function CreateNotebookDialog({
</div>
</div>
{/* Cover Color */}
<div className="space-y-2">
<Label>{t('notebooks.field_color')}</Label>
<div className="flex items-center gap-2 flex-wrap">
{COVER_COLORS.map((cc) => (
<button
key={cc.key}
onClick={() => setColor(cc.hex)}
className={`w-8 h-8 rounded-full transition-all border-2 ${
color === cc.hex ? 'border-gray-900 dark:border-white scale-110 shadow-md' : 'border-transparent hover:scale-105'
}`}
style={{ backgroundColor: cc.hex }}
title={t(cc.labelKey)}
/>
))}
</div>
{/* Notebook Type - compact dropdown instead of grid */}
<div className="space-y-1.5">
<Label className="text-xs">{t('notebooks.field_type')}</Label>
<Select value={notebookType} onValueChange={setNotebookType}>
<SelectTrigger className="h-10">
<SelectValue />
</SelectTrigger>
<SelectContent>
{NOTEBOOK_TYPES.filter(nt => !nt.isCurriculum).map((nt) => (
<SelectItem key={nt.key} value={nt.key}>{t(nt.labelKey)}</SelectItem>
))}
{NOTEBOOK_TYPES.filter(nt => nt.isCurriculum).length > 0 && (
<>
<SelectItem value="_separator" disabled className="text-xs text-amber-600 font-semibold">
{t('notebooks.curriculum_badge')}
</SelectItem>
{NOTEBOOK_TYPES.filter(nt => nt.isCurriculum).map((nt) => (
<SelectItem key={nt.key} value={nt.key}>{t(nt.labelKey)}</SelectItem>
))}
</>
)}
</SelectContent>
</Select>
</div>
{/* Icon Selector */}
<div className="space-y-2">
<Label>{t('notebooks.field_icon')}</Label>
<div className="flex items-center gap-2 flex-wrap">
{ICON_OPTIONS.map((iconName) => {
const IconComp = ICON_MAP[iconName];
const isSelected = icon === iconName;
return (
{/* Color + Icon in a row */}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div className="space-y-1.5">
<Label className="text-xs">{t('notebooks.field_color')}</Label>
<div className="flex items-center gap-1.5 flex-wrap">
{COVER_COLORS.map((cc) => (
<button
key={iconName}
onClick={() => setIcon(iconName)}
className={`w-10 h-10 flex items-center justify-center rounded-lg border-2 transition-all ${
isSelected
? 'border-emerald-500 bg-emerald-50 dark:bg-emerald-900/20'
: 'border-gray-200 dark:border-gray-700 hover:border-gray-300'
key={cc.key}
onClick={() => setColor(cc.hex)}
className={`w-7 h-7 rounded-full transition-all border-2 ${
color === cc.hex ? 'border-gray-900 dark:border-white scale-110 shadow-md' : 'border-transparent hover:scale-105'
}`}
>
<IconComp className={`w-4 h-4 ${isSelected ? 'text-emerald-600 dark:text-emerald-300' : 'text-gray-500 dark:text-gray-400'}`} />
</button>
);
})}
style={{ backgroundColor: cc.hex }}
title={t(cc.labelKey)}
/>
))}
</div>
</div>
<div className="space-y-1.5">
<Label className="text-xs">{t('notebooks.field_icon')}</Label>
<div className="flex items-center gap-1 flex-wrap">
{ICON_OPTIONS.map((iconName) => {
const IconComp = ICON_MAP[iconName];
const isSelected = icon === iconName;
return (
<button
key={iconName}
onClick={() => setIcon(iconName)}
className={`w-8 h-8 flex items-center justify-center rounded-lg border-2 transition-all ${
isSelected
? 'border-emerald-500 bg-emerald-50 dark:bg-emerald-900/20'
: 'border-gray-200 dark:border-gray-700 hover:border-gray-300'
}`}
>
<IconComp className={`w-3.5 h-3.5 ${isSelected ? 'text-emerald-600 dark:text-emerald-300' : 'text-gray-500 dark:text-gray-400'}`} />
</button>
);
})}
</div>
</div>
</div>
{/* Public toggle */}
<div className="flex items-center gap-3">
<Switch checked={isPublic} onCheckedChange={setIsPublic} />
<div>
<Label className="flex items-center gap-1">
<Share2 className="w-4 h-4" />
{t('notebooks.field_public')}
</Label>
<p className="text-xs text-gray-500 dark:text-gray-400">{t('notebooks.field_public_hint')}</p>
{/* Public toggle + Eco message in a row */}
<div className="flex items-center justify-between gap-4 rounded-lg bg-muted/50 p-3">
<div className="flex items-center gap-2">
<Switch checked={isPublic} onCheckedChange={setIsPublic} />
<div>
<Label className="text-xs flex items-center gap-1">
<Share2 className="w-3 h-3" />
{t('notebooks.field_public')}
</Label>
<p className="text-[10px] text-gray-500 dark:text-gray-400">{t('notebooks.field_public_hint')}</p>
</div>
</div>
<div className="flex items-center gap-1.5 text-emerald-700 dark:text-emerald-300 text-xs">
<Leaf className="w-3.5 h-3.5 shrink-0" />
<span className="hidden sm:inline">{t('notebooks.eco_message')}</span>
</div>
</div>
</div>
{/* Eco message */}
<div className="flex items-center gap-2 px-4 py-2 bg-emerald-50 dark:bg-emerald-900/20 rounded-lg text-emerald-700 dark:text-emerald-300 text-sm">
<Leaf className="w-4 h-4" />
<span>{t('notebooks.eco_message')}</span>
</div>
<DialogFooter>
<Button variant="outline" onClick={onClose} className="min-h-[44px]">
<DialogFooter className="px-6 py-3 border-t shrink-0">
<Button variant="outline" onClick={onClose} className="h-10">
{t('action.cancel')}
</Button>
<Button onClick={handleCreate} disabled={creating || !title.trim()} className="min-h-[44px] bg-emerald-600 hover:bg-emerald-700">
<Button onClick={handleCreate} disabled={creating || !title.trim()} className="h-10 bg-emerald-600 hover:bg-emerald-700">
{creating ? (
<div className="w-4 h-4 animate-spin rounded-full border-2 border-white border-t-transparent mr-2" />
) : (