feat(ui): standardize responsive control sizing (#3563)

This commit is contained in:
Carlos Florêncio
2026-09-10 18:14:05 +01:00
committed by GitHub
parent 8b64aed179
commit 5ffbda61a9
245 changed files with 2862 additions and 824 deletions
@@ -156,6 +156,7 @@ export const BrowserPanel = memo(function BrowserPanel({ params }: BrowserPanelP
<PanelRoot data-testid="browser-panel">
<PanelHeaderBar>
<Input
controlSize="none"
value={url.displayDraft}
onChange={(e) => url.setUrlDraft(e.target.value)}
onKeyDown={(e) => {
@@ -151,7 +151,7 @@ function DynamicRouteRecoveryActions({
<Button
type="button"
variant="outline"
className="min-h-11 min-w-11 gap-1.5"
className="gap-1.5"
disabled={pendingAction !== null}
onClick={() => onAction("retry")}
data-testid="dynamic-route-retry"
@@ -162,7 +162,7 @@ function DynamicRouteRecoveryActions({
<Button
type="button"
variant="default"
className="min-h-11 min-w-11 gap-1.5"
className="gap-1.5"
disabled={pendingAction !== null}
onClick={() => onAction("skip")}
data-testid="dynamic-route-try-next"
@@ -174,7 +174,6 @@ function DynamicRouteRecoveryActions({
<Button
type="button"
variant="outline"
className="min-h-11 min-w-11"
disabled={pendingAction !== null}
onClick={() => onAction("cancel_wait")}
data-testid="dynamic-route-cancel-wait"
@@ -185,7 +184,6 @@ function DynamicRouteRecoveryActions({
<Button
type="button"
variant="destructive"
className="min-h-11 min-w-11"
disabled={pendingAction !== null}
onClick={() => onAction("stop")}
data-testid="dynamic-route-stop"
@@ -21,6 +21,7 @@ import { useArchiveAndSwitchTask, useTaskActions } from "@/hooks/use-task-action
import { useTaskRemoval } from "@/hooks/use-task-removal";
import type { MessageAction } from "@/components/task/chat/types";
import { TaskDeleteConfirmDialog } from "@/components/task/task-delete-confirm-dialog";
import { controlSizingClassName } from "@kandev/ui/control-sizing";
export const ACTION_ICON_MAP: Record<string, ElementType> = {
archive: IconArchive,
@@ -87,6 +88,71 @@ type StandardActionButtonProps = Omit<ActionButtonProps, "action"> & {
action: MessageAction & { type: "archive_task" | "ws_request" };
};
type ArchiveAndSwitchTask = ReturnType<typeof useArchiveAndSwitchTask>;
async function executeStandardAction(
action: StandardActionButtonProps["action"],
taskId: string | null,
archiveAndSwitch: ArchiveAndSwitchTask,
onCompleted?: () => void,
): Promise<void> {
if (action.type === "archive_task") {
if (taskId) await archiveAndSwitch(taskId);
return;
}
const client = getWebSocketClient();
const params = action.params as { method: string; payload: Record<string, unknown> } | undefined;
// i18n-exempt: technical error for unavailable WebSocket recovery client.
if (!client || !params) throw new Error("WebSocket recovery request is unavailable");
await client.request(params.method, params.payload);
onCompleted?.();
}
type ActionButtonVisualProps = {
action: MessageAction;
compact: boolean;
disabled: boolean;
onClick: () => void;
labelOverride?: string;
destructive?: boolean;
};
function ActionButtonVisual({
action,
compact,
disabled,
onClick,
labelOverride,
destructive = false,
}: ActionButtonVisualProps): ReactElement {
const Icon = action.icon ? ACTION_ICON_MAP[action.icon] : null;
const sizingClassName = controlSizingClassName(
compact ? "compact" : "standard",
compact ? "max-md:min-h-11 [@media(pointer:coarse)]:min-h-11" : undefined,
);
return (
<Button
variant={compact ? "ghost" : "outline"}
size={compact ? "sm" : "default"}
className={cn(
compact
? "shrink-0 px-2 text-xs cursor-pointer"
: "w-full gap-1.5 text-xs cursor-pointer sm:w-auto",
sizingClassName,
destructive && "text-destructive hover:text-destructive",
)}
disabled={disabled}
onClick={onClick}
data-testid={action.test_id}
>
{Icon && <Icon className="h-3 w-3" />}
{labelOverride ?? action.label}
</Button>
);
}
function StandardActionButton({
action,
messageTaskId,
@@ -103,24 +169,8 @@ function StandardActionButton({
if (state === "busy") return;
setState("busy");
try {
switch (action.type) {
case "archive_task": {
if (taskId) await archiveAndSwitch(taskId);
break;
}
case "ws_request": {
const client = getWebSocketClient();
const params = action.params as
| { method: string; payload: Record<string, unknown> }
| undefined;
// i18n-exempt: technical error for unavailable WebSocket recovery client.
if (!client || !params) throw new Error("WebSocket recovery request is unavailable");
await client.request(params.method, params.payload);
break;
}
}
await executeStandardAction(action, taskId, archiveAndSwitch, onCompleted);
setState("done");
if (action.type === "ws_request") onCompleted?.();
} catch {
setState("error");
setTimeout(() => setState("idle"), 3000);
@@ -133,27 +183,18 @@ function StandardActionButton({
// buttons, so this stale one would just confuse the user.
if (state === "done" && action.type === "ws_request") return null;
const Icon = action.icon ? ACTION_ICON_MAP[action.icon] : null;
const disabled = state === "busy" || state === "done";
const isDestructive = action.variant === "destructive";
const button = (
<Button
variant={compact ? "ghost" : "outline"}
size="sm"
className={cn(
compact
? "h-auto min-h-11 shrink-0 px-2 text-xs cursor-pointer sm:min-h-8"
: "h-auto min-h-11 w-full gap-1.5 text-xs cursor-pointer sm:min-h-8 sm:w-auto",
isDestructive && "text-destructive hover:text-destructive",
)}
<ActionButtonVisual
action={action}
compact={compact}
disabled={disabled}
onClick={execute}
data-testid={action.test_id}
>
{Icon && <Icon className="h-3 w-3" />}
{labelOverride ?? action.label}
</Button>
labelOverride={labelOverride}
destructive={isDestructive}
/>
);
if (action.tooltip) {
@@ -171,6 +212,33 @@ type DeleteActionButtonProps = Omit<ActionButtonProps, "action"> & {
action: MessageAction & { type: "delete_task" };
};
type DeleteTaskById = ReturnType<typeof useTaskActions>["deleteTaskById"];
type RemoveTaskFromBoard = ReturnType<typeof useTaskRemoval>["removeTaskFromBoard"];
type TaskStore = ReturnType<typeof useAppStoreApi>;
async function deleteTaskAndRemoveFromBoard({
taskId,
cascade,
discardWorktreeChanges,
store,
deleteTaskById,
removeTaskFromBoard,
}: {
taskId: string;
cascade: boolean;
discardWorktreeChanges: boolean;
store: TaskStore;
deleteTaskById: DeleteTaskById;
removeTaskFromBoard: RemoveTaskFromBoard;
}): Promise<void> {
const { activeTaskId, activeSessionId } = store.getState().tasks;
await deleteTaskById(taskId, { cascade, discardWorktreeChanges });
await removeTaskFromBoard(taskId, {
wasActiveTaskId: activeTaskId,
wasActiveSessionId: activeSessionId,
});
}
function DeleteActionButton({
action,
messageTaskId,
@@ -199,11 +267,13 @@ function DeleteActionButton({
if (!taskId || state === "busy") return;
setState("busy");
try {
const { activeTaskId, activeSessionId } = store.getState().tasks;
await deleteTaskById(taskId, { cascade, discardWorktreeChanges });
await removeTaskFromBoard(taskId, {
wasActiveTaskId: activeTaskId,
wasActiveSessionId: activeSessionId,
await deleteTaskAndRemoveFromBoard({
taskId,
cascade,
discardWorktreeChanges,
store,
deleteTaskById,
removeTaskFromBoard,
});
setState("done");
} catch {
@@ -218,25 +288,16 @@ function DeleteActionButton({
if (state !== "busy") setDeleteDialogOpen(true);
}, [state]);
const Icon = action.icon ? ACTION_ICON_MAP[action.icon] : null;
const disabled = state === "busy" || state === "done";
const button = (
<Button
variant={compact ? "ghost" : "outline"}
size="sm"
className={cn(
compact
? "h-auto min-h-11 shrink-0 px-2 text-xs cursor-pointer sm:min-h-8"
: "h-auto min-h-11 w-full gap-1.5 text-xs cursor-pointer sm:min-h-8 sm:w-auto",
"text-destructive hover:text-destructive",
)}
<ActionButtonVisual
action={action}
compact={compact}
disabled={disabled}
onClick={execute}
data-testid={action.test_id}
>
{Icon && <Icon className="h-3 w-3" />}
{labelOverride ?? action.label}
</Button>
labelOverride={labelOverride}
destructive
/>
);
const dialog = (
<TaskDeleteConfirmDialog
@@ -15,6 +15,7 @@ import {
import type { SessionRecoveryAction } from "@/lib/services/session-recovery-service";
import type { MessageAction } from "@/components/task/chat/types";
import { ACTION_ICON_MAP, ActionButton } from "./action-message-actions";
import { controlSizingClassName } from "@kandev/ui/control-sizing";
export function sessionRecoveryAction(action: MessageAction): SessionRecoveryAction | null {
if (action.type !== "ws_request" || !action.params) return null;
@@ -130,8 +131,10 @@ function SessionRecoveryActionButton({
const button = (
<Button
variant="outline"
size="sm"
className="h-auto min-h-11 w-full gap-1.5 text-xs cursor-pointer sm:min-h-8 sm:w-auto"
className={controlSizingClassName(
"standard",
"w-full gap-1.5 text-xs cursor-pointer sm:w-auto",
)}
disabled={busyAction !== null}
onClick={onClick}
data-testid={action.test_id}
@@ -374,7 +374,8 @@ describe("ActionMessage — running stall notice", () => {
expect(notice.className).not.toContain("text-red");
expect(notice.querySelector("svg")).toBeNull();
const button = screen.getByTestId(STALL_CANCEL_TEST_ID);
expect(button.className).toContain("min-h-11");
expect(button.className).toContain("h-6");
expect(button.className).toContain("max-md:min-h-11");
expect(button.className).not.toContain("w-full");
});
@@ -453,8 +454,8 @@ describe("ActionMessage — missing PR branch", () => {
expect(screen.getByText("codex/enhance-prompt-result-delivery")).toBeTruthy();
const technicalDetails = screen.getByText(TECHNICAL_DETAILS).closest("details");
expect(technicalDetails?.open).toBe(false);
expect(screen.getByTestId("missing-branch-archive-button").className).toContain("min-h-11");
expect(screen.getByTestId("missing-branch-delete-button").className).toContain("min-h-11");
expect(screen.getByTestId("missing-branch-archive-button").className).toContain("h-7");
expect(screen.getByTestId("missing-branch-delete-button").className).toContain("h-7");
fireEvent.click(screen.getByText(TECHNICAL_DETAILS));
expect(technicalDetails?.open).toBe(true);
@@ -521,7 +522,8 @@ describe("ActionMessage — provider quota recovery", () => {
expect(screen.getByText(/kimi-k3/i)).toBeTruthy();
const details = screen.getByText(TECHNICAL_DETAILS).closest("details");
expect(details?.open).toBe(false);
expect(screen.getByTestId(RESUME_TEST_ID).className).toContain("min-h-11");
expect(screen.getByTestId(RESUME_TEST_ID).className).toContain("h-7");
expect(screen.getByTestId(RESUME_TEST_ID).className).toContain("max-md:h-11");
fireEvent.click(screen.getByText(TECHNICAL_DETAILS));
expect(details?.open).toBe(true);
@@ -640,7 +642,8 @@ describe("ActionMessage — remediation link", () => {
expect(link.href).toBe(REMEDIATION_URL);
expect(link.target).toBe("_blank");
expect(link.rel).toBe("noopener noreferrer");
expect(link.className).toContain("min-h-11");
expect(link.className).toContain("h-7");
expect(link.className).toContain("max-md:h-11");
// The sanitized message and collapsed details stay URL-free.
expect(screen.queryByText(/opencode\.ai\/workspace/i)).toBeNull();
expect(screen.getByTestId("provider-quota-recovery").textContent).toContain(QUOTA_OUTPUT);
@@ -14,6 +14,7 @@ import { createXAxisTickFormatter, formatYAxisTick } from "./chart-format";
import { useRichOutputChartAnimations } from "./chart-motion";
import { useChartPlotVisibility } from "./chart-visibility";
import type { RichOutputChartBlock } from "./types";
import { controlSizingClassName } from "@kandev/ui/control-sizing";
const HOST_SERIES_COLORS = [
"var(--chart-1)",
@@ -85,7 +86,7 @@ function SeriesLegend({
key={key}
type="button"
aria-pressed={isVisible}
className={`flex min-h-11 cursor-pointer items-center gap-1.5 rounded-md px-2 text-[11px] text-muted-foreground transition-[opacity,transform] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50 active:scale-[0.98] ${isVisible ? "opacity-100" : "opacity-50"}`}
className={`${controlSizingClassName("standard")} flex cursor-pointer items-center gap-1.5 rounded-md px-2 text-[11px] text-muted-foreground transition-[opacity,transform] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50 active:scale-[0.98] ${isVisible ? "opacity-100" : "opacity-50"}`}
data-testid={`rich-output-chart-legend-${key}`}
onClick={() => onToggle(key)}
>
@@ -111,8 +111,7 @@ export function FilePreviewBlock({ block, sessionId, onOpenFile }: FilePreviewBl
<Button
type="button"
variant="ghost"
size="sm"
className="min-h-11 cursor-pointer gap-1.5 px-2.5 min-[640px]:min-h-9"
className="cursor-pointer gap-1.5 px-2.5"
aria-expanded={expanded}
aria-controls={previewId}
onClick={handleToggle}
@@ -129,8 +128,7 @@ export function FilePreviewBlock({ block, sessionId, onOpenFile }: FilePreviewBl
<Button
type="button"
variant="ghost"
size="sm"
className="min-h-11 cursor-pointer gap-1.5 px-2.5 min-[640px]:min-h-9"
className="cursor-pointer gap-1.5 px-2.5"
onClick={() => onOpenFile(block.path, block.repo)}
data-testid="rich-output-file-open"
>
@@ -234,9 +234,8 @@ function CompletedSessionActions({
>
<Button
variant="default"
size="sm"
data-testid="recovery-resume-button"
className="min-h-11 w-full shrink-0 gap-1.5 cursor-pointer sm:w-auto"
className="w-full shrink-0 gap-1.5 cursor-pointer sm:w-auto"
onClick={handleResume}
disabled={busyAction !== null || !profileExists}
>
@@ -252,8 +251,7 @@ function CompletedSessionActions({
)}
<Button
variant="outline"
size="sm"
className="min-h-11 w-full shrink-0 gap-1.5 cursor-pointer sm:w-auto"
className="w-full shrink-0 gap-1.5 cursor-pointer sm:w-auto"
onClick={() => {
if (taskId) onShowDialog(true);
}}
@@ -300,9 +298,8 @@ function RecoverableSessionButtons({
>
<Button
variant="default"
size="sm"
data-testid="recovery-resume-button"
className="min-h-11 w-full shrink-0 gap-1.5 cursor-pointer sm:w-auto"
className="w-full shrink-0 gap-1.5 cursor-pointer sm:w-auto"
onClick={onResume}
disabled={busyAction !== null || !profileExists}
>
@@ -318,8 +315,7 @@ function RecoverableSessionButtons({
)}
<Button
variant="outline"
size="sm"
className="min-h-11 w-full shrink-0 gap-1.5 cursor-pointer sm:w-auto"
className="w-full shrink-0 gap-1.5 cursor-pointer sm:w-auto"
onClick={onFreshStart}
disabled={busyAction !== null}
data-testid="recovery-fresh-button"
@@ -62,7 +62,11 @@ describe("CommitDiffView error state", () => {
expect(screen.getByRole("alert").textContent).toContain("Commit detail unavailable");
expect(screen.queryByText("No files in this commit")).toBeNull();
fireEvent.click(screen.getByRole("button", { name: "Retry" }));
const retryButton = screen.getByRole("button", { name: "Retry" });
expect(retryButton.getAttribute("data-size")).toBe("default");
expect(retryButton.className).toContain("h-7");
expect(retryButton.className).toContain("pointer:coarse");
fireEvent.click(retryButton);
expect(mocks.refetch).toHaveBeenCalledOnce();
});
});
@@ -256,9 +256,8 @@ function CommitDetailErrorState({
<span>{error}</span>
<Button
type="button"
className="min-h-11 cursor-pointer"
className="cursor-pointer"
variant="outline"
size="sm"
onClick={() => void onRetry()}
>
<IconRefresh className="h-4 w-4" />
@@ -31,6 +31,7 @@ export function FileBrowserSearchHeader({
<Input
ref={searchInputRef}
type="text"
controlSize="none"
value={localSearchQuery}
onChange={onSearchChange}
onKeyDown={(e) => {
@@ -535,6 +535,7 @@ export function TreeNodeName({
return (
<Input
ref={inputRef}
controlSize="none"
value={rename.renameValue}
onChange={(e) => rename.setRenameValue(e.target.value)}
onKeyDown={rename.handleRenameKeyDown}
@@ -53,6 +53,7 @@ export function InlineFileInput({ depth, onSubmit, onCancel }: InlineFileInputPr
<Input
ref={inputRef}
type="text"
controlSize="none"
className="h-5 text-xs px-1 py-0 border-muted-foreground/30"
placeholder={t("task:filename")}
onKeyDown={handleKeyDown}
@@ -5,6 +5,7 @@ import { useTranslation } from "react-i18next";
import { IconExternalLink } from "@tabler/icons-react";
import { cn } from "@/lib/utils";
import { normalizeRemediationUrl } from "@/lib/remediation-url";
import { controlSizingClassName } from "@kandev/ui/control-sizing";
/**
* Shared localized external link for the allowlisted provider remediation
@@ -23,7 +24,8 @@ export function RemediationLink({ url, className }: { url?: string; className?:
rel="noopener noreferrer"
data-testid="remediation-link"
className={cn(
"inline-flex min-h-11 items-center gap-1.5 text-xs text-primary underline underline-offset-4 cursor-pointer sm:min-h-8",
controlSizingClassName("standard", "inline-flex items-center gap-1.5"),
"text-xs text-primary underline underline-offset-4 cursor-pointer",
className,
)}
>
@@ -135,7 +135,7 @@ export function ContextSelect({
<label className="text-xs font-medium text-muted-foreground">{t("task:context")}</label>
<div className="flex min-w-0 items-center gap-2">
<Select value={value} onValueChange={onValueChange} disabled={isSummarizing}>
<SelectTrigger className="!h-11 w-full min-w-0 text-xs sm:!h-7">
<SelectTrigger className="w-full min-w-0 text-xs">
<SelectValue>{isSummarizing ? t("task:summarizing") : displayLabel}</SelectValue>
</SelectTrigger>
<SelectContent>
@@ -12,6 +12,7 @@ import {
CommandList,
CommandSeparator,
} from "@kandev/ui/command";
import { controlSizingClassName } from "@kandev/ui/control-sizing";
import { cn } from "@/lib/utils";
import { buildOptionGroups, hasGroupedOptions } from "./filter-option-groups";
import { useTranslation } from "react-i18next";
@@ -57,7 +58,7 @@ export function FilterMultiSelect({
type="button"
data-testid="filter-value-multi"
className={cn(
"flex h-7 min-w-0 flex-1 cursor-pointer items-center gap-1 rounded-md border border-input bg-transparent px-2 text-xs transition-colors hover:bg-accent/40",
`${controlSizingClassName("standard")} flex min-w-0 flex-1 cursor-pointer items-center gap-1 rounded-md border border-input bg-transparent px-2 text-xs transition-colors hover:bg-accent/40`,
className,
)}
>
@@ -49,7 +49,7 @@ export function GroupPicker({ value, onChange }: Props) {
const { t } = useTranslation();
return (
<Select value={value} onValueChange={(v) => onChange(v as GroupKey)}>
<SelectTrigger size="sm" className="h-7 w-full text-xs" data-testid="group-key-select">
<SelectTrigger className="w-full text-xs" data-testid="group-key-select">
<SelectValue />
</SelectTrigger>
<SelectContent>
@@ -43,7 +43,7 @@ export function TypedSortPicker<Key extends string>({
className={`flex items-center gap-1.5${mobile ? " [&_button]:min-h-11 [&_[role=combobox]]:min-h-11" : ""}`}
>
<Select value={value.key} onValueChange={(key) => onChange({ ...value, key: key as Key })}>
<SelectTrigger size="sm" className="h-7 min-w-0 flex-1 text-xs" data-testid={testIds.key}>
<SelectTrigger className="min-w-0 flex-1 text-xs" data-testid={testIds.key}>
<SelectValue />
</SelectTrigger>
<SelectContent>
@@ -63,8 +63,7 @@ export function TypedSortPicker<Key extends string>({
<Button
type="button"
variant="outline"
size="sm"
className={mobile ? "h-11 cursor-pointer" : "h-7 cursor-pointer"}
className="cursor-pointer"
onClick={() =>
onChange({ ...value, direction: value.direction === "asc" ? "desc" : "asc" })
}
@@ -114,11 +114,7 @@ export function TypedFilterClauseEditor<Dimension extends string, Op extends str
value={clause.dimension}
onValueChange={(value) => changeDimension(value as Dimension)}
>
<SelectTrigger
size="sm"
className="h-7 w-32 shrink-0 text-xs"
data-testid={testIds.dimension}
>
<SelectTrigger className="w-32 shrink-0 text-xs" data-testid={testIds.dimension}>
<SelectValue>{getDimensionLabel(clause.dimension)}</SelectValue>
</SelectTrigger>
<SelectContent>
@@ -131,7 +127,7 @@ export function TypedFilterClauseEditor<Dimension extends string, Op extends str
</Select>
<Select value={clause.op} onValueChange={(value) => changeOp(value as Op)}>
<SelectTrigger size="sm" className="h-7 w-24 shrink-0 text-xs" data-testid={testIds.op}>
<SelectTrigger className="w-24 shrink-0 text-xs" data-testid={testIds.op}>
<SelectValue />
</SelectTrigger>
<SelectContent>
@@ -150,7 +146,6 @@ export function TypedFilterClauseEditor<Dimension extends string, Op extends str
valueTestId={
meta.valueKind === "text" ? (testIds.textValue ?? testIds.value) : testIds.value
}
mobile={mobile}
onChange={(value) => onChange({ ...clause, value })}
valuePlaceholder={meta.placeholder ?? t("task:value")}
selectValuePlaceholder={t("task:selectValue")}
@@ -161,7 +156,7 @@ export function TypedFilterClauseEditor<Dimension extends string, Op extends str
type="button"
variant="ghost"
size="icon"
className="h-6 w-6 shrink-0 cursor-pointer text-muted-foreground hover:text-foreground"
className="shrink-0 cursor-pointer text-muted-foreground hover:text-foreground"
onClick={onRemove}
data-testid={testIds.remove}
aria-label={t("task:removeFilter")}
@@ -191,7 +186,6 @@ function FilterValueInput<Dimension extends string, Op extends string>({
meta,
options,
valueTestId,
mobile,
onChange,
valuePlaceholder,
selectValuePlaceholder,
@@ -201,7 +195,6 @@ function FilterValueInput<Dimension extends string, Op extends string>({
meta: TypedFilterMeta<Dimension, Op>;
options: ValueOption[];
valueTestId: string;
mobile: boolean;
onChange: (value: TypedFilterValue) => void;
valuePlaceholder: string;
selectValuePlaceholder: string;
@@ -215,7 +208,7 @@ function FilterValueInput<Dimension extends string, Op extends string>({
value={String(clause.value ?? "")}
onChange={(event) => onChange(event.target.value)}
placeholder={valuePlaceholder}
className={cn("h-7 min-w-0 flex-1 text-xs", mobile && "h-11")}
className="min-w-0 flex-1 text-xs"
data-testid={valueTestId}
/>
);
@@ -224,20 +217,13 @@ function FilterValueInput<Dimension extends string, Op extends string>({
const multi = clause.op === "in" || clause.op === "not_in";
if (multi) {
const selected = Array.isArray(clause.value) ? clause.value.map(String) : [];
return (
<FilterMultiSelect
options={options}
selected={selected}
onChange={onChange}
className={mobile ? "h-11" : undefined}
/>
);
return <FilterMultiSelect options={options} selected={selected} onChange={onChange} />;
}
const current = String(clause.value ?? "");
return (
<Select value={current} onValueChange={onChange}>
<SelectTrigger size="sm" className="h-7 min-w-0 flex-1 text-xs" data-testid={valueTestId}>
<SelectTrigger className="min-w-0 flex-1 text-xs" data-testid={valueTestId}>
<SelectValue placeholder={selectValuePlaceholder} />
</SelectTrigger>
<SelectContent>
@@ -175,6 +175,7 @@ function NameInput({
return (
<Input
ref={inputRef}
controlSize="none"
autoFocus
aria-label={mode === "rename" ? t("task:viewName") : t("task:newViewName")}
value={value}
@@ -179,7 +179,7 @@ function AttachmentBody({ doc }: { doc: TaskDocument }) {
download={doc.filename ?? doc.key}
className="cursor-pointer"
>
<Button variant="ghost" size="icon" className="h-7 w-7 cursor-pointer">
<Button variant="ghost" size="icon" className="cursor-pointer">
<IconDownload className="h-3.5 w-3.5" />
</Button>
</a>
+1 -1
View File
@@ -426,7 +426,7 @@ function TopBarRight({
)}
{officeTaskHref && (
<TopbarCluster label={t("task:openInOfficeView")} className="[&_a]:h-7 [&_a]:text-xs">
<Button asChild size="sm" variant="outline" className="h-7 cursor-pointer px-2">
<Button asChild size="sm" variant="outline" className="cursor-pointer px-2">
<Link href={officeTaskHref}>{t("task:openInOfficeView")}</Link>
</Button>
</TopbarCluster>
@@ -517,9 +517,9 @@ function StepDisclosureBody({
<Button
type="button"
data-testid={`workflow-step-disclosure-move-${step.id}`}
size="sm"
size="default"
variant="default"
className="h-7 shrink-0 cursor-pointer rounded-sm px-2.5 text-xs [@media(pointer:coarse)]:h-11"
className="shrink-0 cursor-pointer rounded-sm px-2.5 text-xs"
disabled={movingToStepId !== null}
onClick={() => void onMove(step.id)}
>