Merge pull request 'Improve quick-add UX and fix visual clipping in board cards' (#3) from Small-UI-fixes into main
Reviewed-on: #3
This commit is contained in:
commit
cfd88b43c3
|
|
@ -177,7 +177,11 @@ export function CategoryLane({ category }: { category: CategoryDTO }) {
|
||||||
className="grid transition-[grid-template-rows] duration-200 ease-in"
|
className="grid transition-[grid-template-rows] duration-200 ease-in"
|
||||||
style={{ gridTemplateRows: holdPhase === "collapsing" ? "0fr" : "1fr" }}
|
style={{ gridTemplateRows: holdPhase === "collapsing" ? "0fr" : "1fr" }}
|
||||||
>
|
>
|
||||||
<div className="overflow-hidden">
|
{/* overflow-hidden only while actually shrinking (grid-template-rows
|
||||||
|
easing to 0fr above) -- at rest it clips nothing, but clipped
|
||||||
|
unconditionally it also cropped the card's own hover lift
|
||||||
|
(hover:-translate-y-0.5 in GroupCard), cutting off its top stroke. */}
|
||||||
|
<div className={cn(holdPhase === "collapsing" ? "overflow-hidden" : "overflow-visible")}>
|
||||||
<div
|
<div
|
||||||
className="transition-opacity duration-1000 ease-in"
|
className="transition-opacity duration-1000 ease-in"
|
||||||
style={{
|
style={{
|
||||||
|
|
|
||||||
|
|
@ -167,7 +167,7 @@ function Board({ title }: { title: string }) {
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<TooltipContent side="bottom">
|
<TooltipContent side="bottom">
|
||||||
{quickTargetGroup ? "Quickly add a to-do to the first group" : "Add a group first, then quickly add to-dos"}
|
{quickTargetGroup ? "Quickly add a to-do to any group" : "Add a group first, then quickly add to-dos"}
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<ViewSwitcher />
|
<ViewSwitcher />
|
||||||
|
|
@ -197,6 +197,7 @@ function Board({ title }: { title: string }) {
|
||||||
<TodoCreateDialog
|
<TodoCreateDialog
|
||||||
groupId={quickTargetGroup.id}
|
groupId={quickTargetGroup.id}
|
||||||
categoryId={quickTargetCategory.id}
|
categoryId={quickTargetCategory.id}
|
||||||
|
categories={categories}
|
||||||
open={quickAddOpen}
|
open={quickAddOpen}
|
||||||
onOpenChange={setQuickAddOpen}
|
onOpenChange={setQuickAddOpen}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -6,9 +6,17 @@ import { Button } from "@/components/ui/button";
|
||||||
import { colorModeFromTheme } from "@/components/theme/use-dark-theme";
|
import { colorModeFromTheme } from "@/components/theme/use-dark-theme";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||||
import { MDEditor } from "@/components/markdown/markdown-widgets";
|
import { MDEditor } from "@/components/markdown/markdown-widgets";
|
||||||
import { useBoard } from "@/components/board/board-context";
|
import { useBoard } from "@/components/board/board-context";
|
||||||
|
import type { CategoryDTO } from "@/types/board";
|
||||||
|
|
||||||
const TITLE_MAX = 20;
|
const TITLE_MAX = 20;
|
||||||
|
|
||||||
|
|
@ -19,11 +27,21 @@ const TITLE_MAX = 20;
|
||||||
export function TodoCreateDialog({
|
export function TodoCreateDialog({
|
||||||
groupId,
|
groupId,
|
||||||
categoryId,
|
categoryId,
|
||||||
|
categories,
|
||||||
open,
|
open,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
}: {
|
}: {
|
||||||
|
// Default/only target group+category. When `categories` is also passed,
|
||||||
|
// this is just the pre-selected option in the "Group" dropdown below,
|
||||||
|
// not the final destination.
|
||||||
groupId: string;
|
groupId: string;
|
||||||
categoryId: string;
|
categoryId: string;
|
||||||
|
// Pass the full board (or leave undefined) to show a "Group" dropdown
|
||||||
|
// letting the user redirect the to-do elsewhere -- used by the quick-add
|
||||||
|
// button, whose default target is otherwise just "whichever group
|
||||||
|
// happens to be first". Omitted from the per-group "+" menu, where the
|
||||||
|
// group is already unambiguous from context.
|
||||||
|
categories?: CategoryDTO[];
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onOpenChange: (open: boolean) => void;
|
onOpenChange: (open: boolean) => void;
|
||||||
}) {
|
}) {
|
||||||
|
|
@ -32,12 +50,23 @@ export function TodoCreateDialog({
|
||||||
|
|
||||||
const [title, setTitle] = useState("");
|
const [title, setTitle] = useState("");
|
||||||
const [details, setDetails] = useState("");
|
const [details, setDetails] = useState("");
|
||||||
|
const [selectedGroupId, setSelectedGroupId] = useState(groupId);
|
||||||
const [pending, setPending] = useState(false);
|
const [pending, setPending] = useState(false);
|
||||||
|
|
||||||
|
const groupOptions = categories?.flatMap((c) =>
|
||||||
|
c.groups.map((g) => ({ groupId: g.id, categoryId: c.id, label: `${c.name} - ${g.title}` }))
|
||||||
|
);
|
||||||
|
// Only meaningful when `categories` is passed -- otherwise the dialog's
|
||||||
|
// single fixed target (the props above) is used as-is.
|
||||||
|
const targetCategoryId =
|
||||||
|
groupOptions?.find((o) => o.groupId === selectedGroupId)?.categoryId ?? categoryId;
|
||||||
|
const targetGroupId = groupOptions ? selectedGroupId : groupId;
|
||||||
|
|
||||||
function handleOpenChange(next: boolean) {
|
function handleOpenChange(next: boolean) {
|
||||||
if (!next) {
|
if (!next) {
|
||||||
setTitle("");
|
setTitle("");
|
||||||
setDetails("");
|
setDetails("");
|
||||||
|
setSelectedGroupId(groupId);
|
||||||
}
|
}
|
||||||
onOpenChange(next);
|
onOpenChange(next);
|
||||||
}
|
}
|
||||||
|
|
@ -47,7 +76,7 @@ export function TodoCreateDialog({
|
||||||
const trimmed = title.trim();
|
const trimmed = title.trim();
|
||||||
if (!trimmed) return;
|
if (!trimmed) return;
|
||||||
setPending(true);
|
setPending(true);
|
||||||
const ok = await addTodo(groupId, categoryId, trimmed, details.trim() || undefined);
|
const ok = await addTodo(targetGroupId, targetCategoryId, trimmed, details.trim() || undefined);
|
||||||
setPending(false);
|
setPending(false);
|
||||||
if (ok) handleOpenChange(false);
|
if (ok) handleOpenChange(false);
|
||||||
}
|
}
|
||||||
|
|
@ -59,6 +88,34 @@ export function TodoCreateDialog({
|
||||||
<DialogTitle>Add to-do</DialogTitle>
|
<DialogTitle>Add to-do</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<form onSubmit={handleSubmit} className="space-y-4">
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
|
{groupOptions && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor={`create-todo-group-${groupId}`}>Group</Label>
|
||||||
|
<Select
|
||||||
|
value={selectedGroupId}
|
||||||
|
// Without `items`, <Select.Value> has no way to look up a
|
||||||
|
// label for the current value until the popup has actually
|
||||||
|
// been opened once (that's what registers each item's
|
||||||
|
// label) -- until then, and again after it closes, it
|
||||||
|
// falls back to displaying the raw value, i.e. the group's
|
||||||
|
// id. Passing the same label list here directly is what
|
||||||
|
// the trigger actually reads from.
|
||||||
|
items={groupOptions.map((o) => ({ value: o.groupId, label: o.label }))}
|
||||||
|
onValueChange={(v) => v && setSelectedGroupId(v)}
|
||||||
|
>
|
||||||
|
<SelectTrigger id={`create-todo-group-${groupId}`} className="w-full">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{groupOptions.map((o) => (
|
||||||
|
<SelectItem key={o.groupId} value={o.groupId}>
|
||||||
|
{o.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor={`create-todo-title-${groupId}`}>Title</Label>
|
<Label htmlFor={`create-todo-title-${groupId}`}>Title</Label>
|
||||||
<Input
|
<Input
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,13 @@ export function TodoProgressPie({
|
||||||
role="img"
|
role="img"
|
||||||
aria-label={`${pct}% of to-dos completed`}
|
aria-label={`${pct}% of to-dos completed`}
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
className="absolute -right-1.5 -bottom-1.5 size-7 shrink-0 cursor-default rounded-full border-2 shadow-sm outline-none transition-transform hover:scale-110 focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1"
|
// z-10: this badge pokes outside its own card's bottom-right
|
||||||
|
// corner, into the gap where the *next* card down starts. That
|
||||||
|
// next card is a later DOM sibling with its own stacking
|
||||||
|
// context (see hover:-translate-y-0.5 in GroupCard), so
|
||||||
|
// without an explicit z-index here it paints on top and crops
|
||||||
|
// the badge's overlapping edge.
|
||||||
|
className="absolute -right-1.5 -bottom-1.5 z-10 size-7 shrink-0 cursor-default rounded-full border-2 shadow-sm outline-none transition-transform hover:scale-110 focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1"
|
||||||
style={{
|
style={{
|
||||||
borderColor: accentColor,
|
borderColor: accentColor,
|
||||||
background: `conic-gradient(${accentColor} ${pct}%, ${emptyColor} ${pct}% 100%)`,
|
background: `conic-gradient(${accentColor} ${pct}%, ${emptyColor} ${pct}% 100%)`,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue