feat(ui): standardize responsive control sizing (#3563)
This commit is contained in:
@@ -169,7 +169,9 @@ expect(actionsBox).not.toBeNull();
|
||||
expect(metricsBox!.height).toBeCloseTo(actionsBox!.height, 1);
|
||||
```
|
||||
|
||||
Run the assertion in the relevant desktop and mobile projects when responsive layout can change the result. Do not rely on fixed pixels when the product contract is equality or alignment.
|
||||
Run the assertion in the relevant desktop and mobile projects when responsive
|
||||
layout can change the result. Do not rely on fixed pixels when the product
|
||||
contract is equality or alignment. For control-size regressions, read the mobile-parity [sizing contract](../mobile-parity/references/control-sizing.md).
|
||||
For computed colors, parse alpha/opacity semantically or assert a deliberate class/data contract; do not compare serialized `getComputedStyle` strings because browsers may return `rgba()`, `oklab()`, or `color()`.
|
||||
|
||||
**Animation-aware geometry:** Before reading dialog or panel geometry, wait only for currently running Web Animations with finite `effect.getComputedTiming().iterations`; await `animation.finished.catch(() => undefined)` because Radix overlays can cancel animations during close or replacement. Never blanket-await infinite animations or use a fixed sleep; then read bounding boxes and assert the relationship.
|
||||
|
||||
@@ -13,6 +13,10 @@ Use this skill before planning or changing UI. Goal: desktop and mobile deliver
|
||||
|
||||
Before proposing a mobile design, read [Kandev Mobile UI Language](references/kandev-mobile-ui-language.md) and inspect the closest shipped mobile surface. Responsive CSS alone does not establish mobile parity.
|
||||
|
||||
For buttons, single-line inputs, selectors, or control-size audits, also read
|
||||
[Control sizing](references/control-sizing.md). It defines desktop sizes,
|
||||
touch exceptions, and the sweep procedure.
|
||||
|
||||
## When It Applies
|
||||
|
||||
Apply when task changes user-facing UI:
|
||||
@@ -70,6 +74,8 @@ rendered phone surface with the assigned preview's structure and annotations.
|
||||
3. Implement responsive UI.
|
||||
- Reuse domain hooks, state, view-model derivation, filtering/selection, and action handlers across viewports; keep responsive wrappers focused on presentation. Branch composition when the desktop interaction model depends on width or a fine pointer. Do not mount a heavyweight desktop workbench and merely hide or squeeze it on phones.
|
||||
- Use `useResponsiveBreakpoint` and existing `@kandev/ui` or mobile primitives. Reuse current Dropdown/ContextMenu primitives for contextual actions; use `Drawer` or an existing picker shell for structured phone navigation and choices. Use `useTouchDrawer` when a hover disclosure needs a coarse-pointer alternative.
|
||||
- Use 28px for ordinary desktop buttons, single-line inputs, select triggers, and combobox triggers. Use 24px only for deliberate compact inline controls. Match the desktop Start Task dialog and shared default primitives.
|
||||
- Keep phone and coarse-pointer action targets at least 44px. Scope touch dimensions to those conditions. A desktop-visible action must not inherit an unconditional `h-11` or `min-h-11`. Check both height and minimum height: changing `h-*` does not remove an oversized `min-h-*`.
|
||||
- A Radix Tooltip that happens to open after Playwright `.tap()` is not a coarse-pointer alternative. Use `useTouchDrawer` with a Drawer branch and assert the drawer surface in mobile E2E.
|
||||
- Keep coarse-pointer and mobile touch targets large enough for touch use, generally at least 44px in the active dimension. This is an active hit-area rule, not a universal desktop visual-size rule: fine-pointer desktop controls should retain the surrounding design-system density, and touch-sized classes such as `h-11` must not become the shared desktop button size.
|
||||
- When one component serves both pointer modes, keep its fine-pointer size as the base class and add the 44px size only behind a coarse-pointer or phone variant. An unqualified `h-11` or `min-h-11` on a desktop-visible shared action is a sizing bug.
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
# Control sizing
|
||||
|
||||
Use this guide when you add, change, or audit buttons, single-line inputs,
|
||||
select triggers, combobox triggers, or their shared wrappers.
|
||||
|
||||
## Size contract
|
||||
|
||||
| Context | Control height | Usage |
|
||||
| --- | --- | --- |
|
||||
| Desktop, fine pointer | 28px | Ordinary actions and single-line fields |
|
||||
| Compact desktop, fine pointer | 24px | Deliberate inline actions in dense chrome |
|
||||
| Phone or coarse pointer | At least 44px | Action hit areas and single-line fields |
|
||||
|
||||
The pixel values assume the standard 16px root font. Preserve root-font scaling.
|
||||
The desktop Start Task dialog is the reference for ordinary action density.
|
||||
The default `@kandev/ui` Button, Input, and SelectTrigger already use `h-7`.
|
||||
An action's importance changes its variant, not its height.
|
||||
|
||||
Use the same size for neighboring actions, inputs, and selectors within one row.
|
||||
Use matching square dimensions for standalone icon controls.
|
||||
Retain existing specialized controls, such as editor chrome and mobile pills,
|
||||
only with a named purpose and an adjacent precedent.
|
||||
|
||||
Selection cards, navigation rows, multiline fields, and menu options have content
|
||||
geometry. Do not force their entire surface to the ordinary button height.
|
||||
Their embedded ordinary buttons still follow the control contract.
|
||||
|
||||
## Shared implementation
|
||||
|
||||
Use existing shared primitives and sizing helpers before adding local classes.
|
||||
Inspect the current implementation before referencing a new helper from a plan.
|
||||
Keep the fine-pointer desktop size as the base value.
|
||||
Apply larger hit areas through a phone or coarse-pointer condition.
|
||||
Use the canonical 768px phone boundary for new shared sizing rules.
|
||||
Preserve existing overlay boundaries unless the task changes their composition.
|
||||
|
||||
Do not add a global CSS rule that assigns one height to every HTML button.
|
||||
Do not change existing compact or large variant meanings to repair individual callers.
|
||||
Use the standard variant at ordinary action call sites.
|
||||
Keep single-line fields aligned with their attached reveal, clear, and picker buttons.
|
||||
|
||||
Check `height`, `min-height`, `max-height`, padding, line height, and parent stretching together.
|
||||
An `h-7` button with `min-h-11` still occupies at least 44px.
|
||||
A helper with `md:min-h-7` does not shrink a caller's explicit `h-11`.
|
||||
SelectTrigger can also carry a `data-[size=default]:h-*` rule.
|
||||
Verify the CSS cascade in the browser after changing these combinations.
|
||||
|
||||
## Sweep procedure
|
||||
|
||||
1. Search shared primitives, application components, route files, and CSS for height overrides.
|
||||
2. Inspect ordinary Buttons, raw buttons, Inputs, SelectTriggers, combobox wrappers, and inline height styles.
|
||||
3. Follow named class constants, wrapper helpers, size props, and responsive branches to their callers.
|
||||
4. Classify each candidate as standard, compact, touch-only, content-sized, or a documented exception.
|
||||
5. Replace ordinary desktop overrides with the standard size.
|
||||
6. Preserve touch minimums and existing interaction behavior.
|
||||
7. Record each retained exception and its reason in the sweep inventory.
|
||||
8. Repeat the search after migration to find missed wrappers and newly exposed controls.
|
||||
|
||||
This search produces candidates, not confirmed defects:
|
||||
|
||||
```bash
|
||||
rg -n '(min-h-|max-h-|h-|size-)(8|9|10|11|12|\[)|size="(sm|lg)"|height\s*:' \
|
||||
apps/web/components apps/web/app apps/packages/ui/src \
|
||||
-g '*.tsx' -g '*.ts' -g '*.css' -g '!*.test.*'
|
||||
```
|
||||
|
||||
Also inspect controls with padding-only sizing and responsive height resets.
|
||||
Do not apply a blanket replacement to this search output.
|
||||
|
||||
## Rendered verification
|
||||
|
||||
Verify representative dialog, task, settings, and shared-picker surfaces.
|
||||
For each changed family, include normal, disabled, busy, and long-label states.
|
||||
Compare the actual controls, not only their containers or class strings.
|
||||
|
||||
At the standard root font, ordinary desktop controls must measure 28px within 1px.
|
||||
Compact desktop controls must measure 24px within 1px.
|
||||
A desktop assertion of only `height >= 28` permits oversized controls to pass.
|
||||
Touch actions must retain their 44px minimum active dimension.
|
||||
Standalone touch icons need both height and width checks.
|
||||
|
||||
Include desktop fine pointers, phone widths below 768px, and coarse-pointer tablets.
|
||||
Verify keyboard activation, touch activation, and the absence of overlapping hit areas.
|
||||
Preserve the existing coarse-pointer input anti-zoom rule.
|
||||
Check translated labels without reducing their text size to fit a fixed height.
|
||||
|
||||
The durable contract is in
|
||||
[Control sizing requirements](../../../../docs/specs/ui/requirements/control-sizing.md).
|
||||
@@ -35,7 +35,7 @@ Choose based on task frequency and content depth. A frequently revisited primary
|
||||
- Phone breakpoint is below 768px, matching the width at which the desktop sidebar appears. Use `useResponsiveBreakpoint`; it distinguishes phone, tablet, compact desktop, and full desktop using width plus pointer mode. Tablet is a coarse-pointer fallback between 768px and 1024px. The bottom-sheet treatment for Radix menus in `app/globals.css` is deliberately narrower at 640px — pick the boundary that matches what you are changing.
|
||||
- Reuse `@kandev/ui` primitives. `Drawer` is the standard bottom surface; `Sheet` remains useful for tablet/desktop side panels.
|
||||
- Use inset, rounded card surfaces with restrained borders/backgrounds and shadow, matching nearby components. Prefer existing component classes over copying a frozen class list.
|
||||
- Give new primary touch actions, standalone icon buttons, and menu rows an actual hitbox of at least 44px in the active dimension (`min-h-11`, `h-11 w-11`). Existing compact chrome such as `MobilePillButton` is a deliberate density exception, not a sizing precedent for unrelated controls; do not claim surrounding spacing enlarges its hitbox. Provide pressed feedback such as the existing short `active:scale-*` treatment where nearby controls use it.
|
||||
- Give phone and coarse-pointer primary actions, standalone icon buttons, and menu rows an actual hitbox of at least 44px. Apply these dimensions only within the touch condition. Ordinary desktop controls use 28px. See [Control sizing](control-sizing.md) for compact controls, shared components, and geometry checks. Existing compact chrome such as `MobilePillButton` remains a documented exception. Surrounding spacing does not enlarge a hitbox. Preserve nearby pressed feedback.
|
||||
- Keep labels and current context visible. Use truncation for long names, tabular number badges for counts, and accessible names that include enough hierarchy to disambiguate the action.
|
||||
- Use `100dvh`/`h-dvh`, not `100vh`, for viewport-filling phone layouts. Account for `env(safe-area-inset-*)` on fixed controls and drawer content.
|
||||
- In flex surfaces, the usual scroll body is `min-h-0 flex-1 overflow-y-auto overscroll-contain`. Keep headers/actions fixed and avoid nested vertical scrollers unless the interaction truly needs them.
|
||||
|
||||
@@ -3,6 +3,7 @@ import { cva, type VariantProps } from "class-variance-authority";
|
||||
import { Slot } from "radix-ui";
|
||||
|
||||
import { cn } from "./lib/utils";
|
||||
import { controlSizingClassName } from "./control-sizing";
|
||||
|
||||
const buttonVariants = cva(
|
||||
"focus-visible:border-ring focus-visible:ring-ring/35 aria-invalid:border-destructive aria-invalid:ring-destructive/25 rounded-md border border-transparent bg-clip-padding text-xs/relaxed font-medium focus-visible:ring-[2px] aria-invalid:ring-[2px] [&_svg:not([class*='size-'])]:size-4 inline-flex cursor-pointer items-center justify-center whitespace-nowrap transition-colors disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-45 [&_svg]:pointer-events-none shrink-0 [&_svg]:shrink-0 outline-none group/button select-none",
|
||||
@@ -21,12 +22,11 @@ const buttonVariants = cva(
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default:
|
||||
"h-7 gap-1 px-2 text-xs/relaxed has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
|
||||
default: `${controlSizingClassName("standard")} gap-1 px-2 text-xs/relaxed has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5`,
|
||||
xs: "h-5 gap-1 rounded-sm px-2 text-[0.625rem] has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-2.5",
|
||||
sm: "h-6 gap-1 px-2 text-xs/relaxed has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
|
||||
lg: "h-8 gap-1 px-2.5 text-xs/relaxed has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2 [&_svg:not([class*='size-'])]:size-4",
|
||||
icon: "size-7 [&_svg:not([class*='size-'])]:size-3.5",
|
||||
icon: `${controlSizingClassName("icon")} [&_svg:not([class*='size-'])]:size-3.5`,
|
||||
"icon-xs": "size-5 rounded-sm [&_svg:not([class*='size-'])]:size-2.5",
|
||||
"icon-sm": "size-6 [&_svg:not([class*='size-'])]:size-3",
|
||||
"icon-lg": "size-8 [&_svg:not([class*='size-'])]:size-4",
|
||||
|
||||
@@ -71,7 +71,7 @@ function CommandInput({
|
||||
}: React.ComponentProps<typeof CommandPrimitive.Input>) {
|
||||
return (
|
||||
<div data-slot="command-input-wrapper" className="p-1 pb-0">
|
||||
<InputGroup className="bg-input/20 dark:bg-input/30 h-8! border-0">
|
||||
<InputGroup className="bg-input/20 dark:bg-input/30 border-0">
|
||||
<CommandPrimitive.Input
|
||||
data-slot="command-input"
|
||||
className={cn(
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import { cn } from "./lib/utils";
|
||||
|
||||
export const CONTROL_SIZING = {
|
||||
standard: "h-7 max-md:h-11 [@media(pointer:coarse)]:h-11",
|
||||
compact: "h-6",
|
||||
icon: "size-7 max-md:size-11 [@media(pointer:coarse)]:size-11",
|
||||
} as const;
|
||||
|
||||
export type ControlSize = keyof typeof CONTROL_SIZING;
|
||||
|
||||
export function controlSizingClassName(size: ControlSize, className?: string): string {
|
||||
return cn(CONTROL_SIZING[size], className);
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import { cva, type VariantProps } from "class-variance-authority";
|
||||
|
||||
import { cn } from "./lib/utils";
|
||||
import { Button } from "./button";
|
||||
import { controlSizingClassName } from "./control-sizing";
|
||||
import { Input } from "./input";
|
||||
import { Textarea } from "./textarea";
|
||||
|
||||
@@ -14,7 +15,7 @@ function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
|
||||
data-slot="input-group"
|
||||
role="group"
|
||||
className={cn(
|
||||
"border-input bg-input/20 dark:bg-input/30 has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-ring/30 has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[[data-slot][aria-invalid=true]]:border-destructive dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40 h-7 rounded-md border transition-colors has-data-[align=block-end]:rounded-md has-data-[align=block-start]:rounded-md has-[[data-slot=input-group-control]:focus-visible]:ring-[2px] has-[[data-slot][aria-invalid=true]]:ring-[2px] has-[textarea]:rounded-md has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>[data-align=block-end]]:[&>input]:pt-3 has-[>[data-align=block-start]]:[&>input]:pb-3 has-[>[data-align=inline-end]]:[&>input]:pr-1.5 has-[>[data-align=inline-start]]:[&>input]:pl-1.5 [[data-slot=combobox-content]_&]:focus-within:border-inherit [[data-slot=combobox-content]_&]:focus-within:ring-0 group/input-group relative flex w-full min-w-0 items-center outline-none has-[>textarea]:h-auto",
|
||||
`border-input bg-input/20 dark:bg-input/30 has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-ring/30 has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[[data-slot][aria-invalid=true]]:border-destructive dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40 ${controlSizingClassName("standard")} rounded-md border transition-colors has-data-[align=block-end]:rounded-md has-data-[align=block-start]:rounded-md has-[[data-slot=input-group-control]:focus-visible]:ring-[2px] has-[[data-slot][aria-invalid=true]]:ring-[2px] has-[textarea]:rounded-md has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>[data-align=block-end]]:[&>input]:pt-3 has-[>[data-align=block-start]]:[&>input]:pb-3 has-[>[data-align=inline-end]]:[&>input]:pr-1.5 has-[>[data-align=inline-start]]:[&>input]:pl-1.5 [[data-slot=combobox-content]_&]:focus-within:border-inherit [[data-slot=combobox-content]_&]:focus-within:ring-0 group/input-group relative flex w-full min-w-0 items-center outline-none has-[>textarea]:h-auto`,
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -1,15 +1,24 @@
|
||||
import * as React from "react";
|
||||
|
||||
import { controlSizingClassName } from "./control-sizing";
|
||||
import { cn } from "./lib/utils";
|
||||
|
||||
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
|
||||
({ className, type, ...props }, ref) => (
|
||||
export type InputControlSize = "standard" | "compact" | "none";
|
||||
|
||||
type InputProps = React.ComponentProps<"input"> & {
|
||||
/** Keep a fixed caller-provided height for embedded editor chrome. */
|
||||
controlSize?: InputControlSize;
|
||||
};
|
||||
|
||||
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
({ className, type, controlSize = "standard", ...props }, ref) => (
|
||||
<input
|
||||
ref={ref}
|
||||
type={type}
|
||||
data-slot="input"
|
||||
className={cn(
|
||||
"border-input bg-background hover:bg-secondary/50 focus-visible:border-ring focus-visible:ring-ring/35 aria-invalid:border-destructive aria-invalid:ring-destructive/25 h-7 rounded-md border px-2 py-0.5 text-sm transition-colors file:h-6 file:text-xs/relaxed file:font-medium focus-visible:ring-[2px] aria-invalid:ring-[2px] md:text-xs/relaxed file:text-foreground placeholder:text-muted-foreground w-full min-w-0 outline-none file:inline-flex file:border-0 file:bg-transparent disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-muted disabled:opacity-55",
|
||||
"border-input bg-background hover:bg-secondary/50 focus-visible:border-ring focus-visible:ring-ring/35 aria-invalid:border-destructive aria-invalid:ring-destructive/25 rounded-md border px-2 py-0.5 text-sm transition-colors file:h-6 file:text-xs/relaxed file:font-medium focus-visible:ring-[2px] aria-invalid:ring-[2px] md:text-xs/relaxed file:text-foreground placeholder:text-muted-foreground w-full min-w-0 outline-none file:inline-flex file:border-0 file:bg-transparent disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-muted disabled:opacity-55",
|
||||
controlSize === "none" ? undefined : controlSizingClassName(controlSize),
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import * as React from "react";
|
||||
import { Select as SelectPrimitive } from "radix-ui";
|
||||
|
||||
import { controlSizingClassName } from "./control-sizing";
|
||||
import { cn } from "./lib/utils";
|
||||
import { IconSelector, IconCheck, IconChevronUp, IconChevronDown } from "@tabler/icons-react";
|
||||
|
||||
@@ -37,7 +38,7 @@ function SelectTrigger({
|
||||
data-slot="select-trigger"
|
||||
data-size={size}
|
||||
className={cn(
|
||||
"border-input data-[placeholder]:text-muted-foreground bg-background hover:bg-secondary/50 focus-visible:border-ring focus-visible:ring-ring/35 aria-invalid:border-destructive aria-invalid:ring-destructive/25 gap-1.5 rounded-md border px-2 py-1.5 text-xs/relaxed transition-colors focus-visible:ring-[2px] aria-invalid:ring-[2px] data-[size=default]:h-7 data-[size=sm]:h-6 *:data-[slot=select-value]:flex *:data-[slot=select-value]:gap-1.5 [&_svg:not([class*='size-'])]:size-3.5 flex w-fit cursor-pointer items-center justify-between whitespace-nowrap outline-none disabled:cursor-not-allowed disabled:bg-muted disabled:opacity-55 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
`border-input data-[placeholder]:text-muted-foreground bg-background hover:bg-secondary/50 focus-visible:border-ring focus-visible:ring-ring/35 aria-invalid:border-destructive aria-invalid:ring-destructive/25 ${controlSizingClassName(size === "default" ? "standard" : "compact")} gap-1.5 rounded-md border px-2 py-1.5 text-xs/relaxed transition-colors focus-visible:ring-[2px] aria-invalid:ring-[2px] *:data-[slot=select-value]:flex *:data-[slot=select-value]:gap-1.5 [&_svg:not([class*='size-'])]:size-3.5 flex w-fit cursor-pointer items-center justify-between whitespace-nowrap outline-none disabled:cursor-not-allowed disabled:bg-muted disabled:opacity-55 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center [&_svg]:pointer-events-none [&_svg]:shrink-0`,
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
+1
-1
@@ -29,7 +29,7 @@ import { Dialog } from "@kandev/ui/dialog";
|
||||
- Use `hooks/use-responsive-breakpoint.ts` for application layout decisions. Its mobile boundary matches the sidebar's 768px `md` boundary, and it also models tablet, compact desktop, full desktop, and pointer precision; do not substitute the UI package's generic `useIsMobile` hook. Tablet is a coarse-pointer fallback between `md` and `lg`, so no fine-pointer width reports it — a spec that needs the tablet layout has to emulate touch.
|
||||
- When a Tailwind visibility class gates the same surface the hook picks, both must use the same boundary. A `sm:` class paired with a hook-driven mobile branch leaves 640-767px in a state neither side renders.
|
||||
- CSS visibility classes can leave the other viewport tree mounted. Gate live effects such as polling, subscriptions, timers, and measurements with the same responsive or pointer condition, and test both initialization orders when readiness can precede or follow hydration.
|
||||
- Use `useTouchDrawer` when a hover/popover disclosure needs a coarse-pointer `Drawer` alternative. Width-based phone composition and pointer-based disclosure behavior are related but not interchangeable. Apply the 44px minimum to coarse-pointer hit areas, touch rows, and mobile controls only; keep fine-pointer desktop controls at the surrounding design-system density and do not reuse a touch-sized `h-11` class as the shared visual button size.
|
||||
- Use `useTouchDrawer` when a hover/popover disclosure needs a coarse-pointer `Drawer` alternative. Width-based phone composition and pointer-based disclosure behavior are related but not interchangeable. Apply the 44px minimum to coarse-pointer hit areas, touch rows, and mobile controls only; keep fine-pointer desktop controls at the surrounding design-system density, using 28px for ordinary buttons, inputs, and selectors and 24px only for deliberate compact inline controls, and do not reuse a touch-sized `h-11` class as the shared visual button size. See the [sizing guide](../../.agents/skills/mobile-parity/references/control-sizing.md) for exceptions.
|
||||
- Below 640px, shared Radix DropdownMenu/ContextMenu use inset, safe-area-aware bottom sheets in `app/globals.css`. Open `mobile-menu-root` content owns one decorative positioner backdrop matching Drawer dimming and blur; fade it with the sheet's exit motion. Never mark submenus; keep the backdrop outside scrolling content and pointer-transparent so Radix owns dismissal and non-modal interaction. Reuse these primitives and cover long/nested menus instead of adding parallel mobile menus.
|
||||
- Mobile capability parity does not require desktop layout parity. Load `/mobile-parity` for the Kandev surface decision guide, mobile design contract, and verification requirements.
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ import type { AgentProfile, AgentRole } from "@/lib/state/slices/office/types";
|
||||
import { AgentRoutingCard } from "./agent-routing-card";
|
||||
import { reportsToOptions } from "./reports-to-options";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { controlSizingClassName } from "@kandev/ui/control-sizing";
|
||||
|
||||
type AgentConfigurationTabProps = {
|
||||
agent: AgentProfile;
|
||||
@@ -338,7 +339,7 @@ function ReportsToField({
|
||||
>
|
||||
<SelectTrigger
|
||||
id="cfg-reports-to"
|
||||
className="mt-1 min-h-11 w-full cursor-pointer md:min-h-7"
|
||||
className={controlSizingClassName("standard", "mt-1 w-full cursor-pointer")}
|
||||
>
|
||||
<SelectValue placeholder={t("office:noneTopLevel")} />
|
||||
</SelectTrigger>
|
||||
@@ -400,7 +401,9 @@ function IdentityCard({
|
||||
<div className="flex-1">
|
||||
<Label>{t("office:role")}</Label>
|
||||
<Select value={role} onValueChange={(v) => onRoleChange(v as AgentRole)}>
|
||||
<SelectTrigger className="mt-1 min-h-11 w-full cursor-pointer md:min-h-7">
|
||||
<SelectTrigger
|
||||
className={controlSizingClassName("standard", "mt-1 w-full cursor-pointer")}
|
||||
>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
|
||||
@@ -13,6 +13,7 @@ import { CreateAgentDialog } from "./components/create-agent-dialog";
|
||||
import { EmptyState } from "../components/shared/empty-state";
|
||||
import { PageHeader } from "../components/shared/page-header";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { controlSizingClassName } from "@kandev/ui/control-sizing";
|
||||
|
||||
type AgentsPageClientProps = {
|
||||
initialAgents: AgentProfile[];
|
||||
@@ -52,7 +53,10 @@ export function AgentsPageClient({ initialAgents, initialWorkspaceId }: AgentsPa
|
||||
<PageHeader
|
||||
title={t("office:agents")}
|
||||
action={
|
||||
<Button size="sm" className="cursor-pointer" onClick={() => setShowCreate(true)}>
|
||||
<Button
|
||||
className={controlSizingClassName("standard", "cursor-pointer")}
|
||||
onClick={() => setShowCreate(true)}
|
||||
>
|
||||
<IconPlus className="h-4 w-4 mr-1" />
|
||||
{t("office:newAgent")}
|
||||
</Button>
|
||||
@@ -66,8 +70,7 @@ export function AgentsPageClient({ initialAgents, initialWorkspaceId }: AgentsPa
|
||||
action={
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="cursor-pointer"
|
||||
className={controlSizingClassName("standard", "cursor-pointer")}
|
||||
onClick={() => setShowCreate(true)}
|
||||
>
|
||||
<IconPlus className="h-4 w-4 mr-1" />
|
||||
|
||||
@@ -141,7 +141,7 @@ function StatusChip({ draft, onUpdate }: Props) {
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="outline" size="sm" className="cursor-pointer h-7 text-xs">
|
||||
<Button variant="outline" className="cursor-pointer text-xs">
|
||||
<IconCircleDot className={`h-3.5 w-3.5 mr-1 ${current?.className ?? ""}`} />
|
||||
{current?.label ?? draft.status}
|
||||
</Button>
|
||||
@@ -176,7 +176,7 @@ function PriorityChip({ draft, onUpdate }: Props) {
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="outline" size="sm" className="cursor-pointer h-7 text-xs">
|
||||
<Button variant="outline" className="cursor-pointer text-xs">
|
||||
<PriorityIcon className={`h-3.5 w-3.5 mr-1 ${current?.className ?? ""}`} />
|
||||
{current?.label ?? draft.priority}
|
||||
</Button>
|
||||
@@ -207,13 +207,13 @@ export function NewTaskBottomBar({ draft, onUpdate }: Props) {
|
||||
<div className="flex items-center gap-2 pt-2 border-t border-border">
|
||||
<StatusChip draft={draft} onUpdate={onUpdate} />
|
||||
<PriorityChip draft={draft} onUpdate={onUpdate} />
|
||||
<Button variant="outline" size="sm" className="cursor-pointer h-7 text-xs">
|
||||
<Button variant="outline" className="cursor-pointer text-xs">
|
||||
<IconUpload className="h-3.5 w-3.5 mr-1" />
|
||||
{t("office:upload")}
|
||||
</Button>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button variant="ghost" size="icon" className="h-7 w-7 cursor-pointer">
|
||||
<Button variant="ghost" size="icon" className="cursor-pointer">
|
||||
<IconDotsVertical className="h-4 w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
|
||||
@@ -56,7 +56,7 @@ export function ParticipantRow({
|
||||
<span className="w-16 shrink-0">{t(copy.label)}</span>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="outline" size="sm" className="cursor-pointer h-7 text-xs">
|
||||
<Button variant="outline" className="cursor-pointer text-xs">
|
||||
{selectedIds.length > 0
|
||||
? t("office:participantsSelected", { count: selectedIds.length })
|
||||
: t(copy.add)}
|
||||
@@ -80,7 +80,7 @@ export function ParticipantRow({
|
||||
</Popover>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button variant="ghost" size="icon" className="h-6 w-6 cursor-pointer" onClick={onHide}>
|
||||
<Button variant="ghost" size="icon" className="cursor-pointer" onClick={onHide}>
|
||||
<IconX className="h-3 w-3" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
|
||||
@@ -39,7 +39,7 @@ function AgentPickerPopover({
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="outline" size="sm" className="cursor-pointer h-7 text-xs">
|
||||
<Button variant="outline" className="cursor-pointer text-xs">
|
||||
{selected?.name ?? t("office:assignee")}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
@@ -80,7 +80,7 @@ function ProjectPickerPopover({
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="outline" size="sm" className="cursor-pointer h-7 text-xs">
|
||||
<Button variant="outline" className="cursor-pointer text-xs">
|
||||
{selected?.name ?? t("office:project")}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
@@ -146,7 +146,7 @@ export function NewTaskSelectorRow({ draft, onUpdate }: Props) {
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon" className="h-7 w-7 cursor-pointer">
|
||||
<Button variant="ghost" size="icon" className="cursor-pointer">
|
||||
<IconDotsVertical className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
|
||||
@@ -112,7 +112,7 @@ function AgentSingleSelect({
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="outline" size="sm" className="cursor-pointer h-7 text-xs">
|
||||
<Button variant="outline" className="cursor-pointer text-xs">
|
||||
{selected?.name ?? placeholder}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
|
||||
@@ -95,7 +95,7 @@ function RouteAttemptRow({ attempt }: { attempt: RouteAttempt }) {
|
||||
{durationLabel(t, attempt.started_at, attempt.finished_at)}
|
||||
</span>
|
||||
<CollapsibleTrigger asChild>
|
||||
<Button variant="ghost" size="icon" className="h-6 w-6 cursor-pointer">
|
||||
<Button variant="ghost" size="icon" className="cursor-pointer">
|
||||
{open ? (
|
||||
<IconChevronDown className="h-3.5 w-3.5" />
|
||||
) : (
|
||||
|
||||
@@ -12,6 +12,7 @@ import { loadOfficeAgents, loadOfficeInbox } from "@/hooks/use-office-workspace-
|
||||
import type { InboxItem } from "@/lib/state/slices/office/types";
|
||||
import { InboxItemRow } from "./inbox-item-row";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { controlSizingClassName } from "@kandev/ui/control-sizing";
|
||||
|
||||
type TabValue = "mine" | "recent" | "all";
|
||||
|
||||
@@ -122,7 +123,7 @@ function InboxToolbar({
|
||||
<IconSearch className="absolute left-2.5 top-2 h-3.5 w-3.5 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder={t("office:search")}
|
||||
className="w-[220px] h-8 pl-8 text-xs"
|
||||
className={controlSizingClassName("standard", "w-[220px] pl-8 text-xs")}
|
||||
value={search}
|
||||
onChange={(e) => onSearchChange(e.target.value)}
|
||||
/>
|
||||
|
||||
@@ -12,6 +12,7 @@ import { useAppStore } from "@/components/state-provider";
|
||||
import type { Project, ProjectStatus } from "@/lib/state/slices/office/types";
|
||||
import { PROJECT_STATUS_LABEL_KEYS } from "../../lib/label-keys";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { controlSizingClassName } from "@kandev/ui/control-sizing";
|
||||
|
||||
// `labelKey`, not `label` — module scope freezes a `t()` at the boot locale.
|
||||
// The `value`s are the persisted project-status ids and stay untranslated.
|
||||
@@ -67,7 +68,7 @@ export function ProjectHeader({ project }: ProjectHeaderProps) {
|
||||
setName(e.target.value);
|
||||
setDirty(true);
|
||||
}}
|
||||
className="text-lg font-semibold h-9 px-2.5"
|
||||
className={controlSizingClassName("standard", "text-lg font-semibold px-2.5")}
|
||||
/>
|
||||
<Select
|
||||
value={status}
|
||||
@@ -76,7 +77,7 @@ export function ProjectHeader({ project }: ProjectHeaderProps) {
|
||||
setDirty(true);
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="w-[140px] data-[size=default]:h-9 cursor-pointer">
|
||||
<SelectTrigger className={controlSizingClassName("standard", "w-[140px] cursor-pointer")}>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -88,7 +89,11 @@ export function ProjectHeader({ project }: ProjectHeaderProps) {
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{dirty && (
|
||||
<Button size="sm" onClick={handleSave} disabled={saving} className="cursor-pointer">
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
disabled={saving}
|
||||
className={controlSizingClassName("standard", "cursor-pointer")}
|
||||
>
|
||||
<IconDeviceFloppy className="h-4 w-4 mr-1" />
|
||||
{saving ? t("office:saving") : t("common:save")}
|
||||
</Button>
|
||||
|
||||
@@ -16,6 +16,7 @@ import type { AgentProfile } from "@/lib/state/slices/office/types";
|
||||
import { ProjectRepositoryPicker } from "./project-repository-picker";
|
||||
import { RepoChip } from "./repo-chip";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { controlSizingClassName } from "@kandev/ui/control-sizing";
|
||||
|
||||
const COLOR_OPTIONS = [
|
||||
"#ef4444",
|
||||
@@ -345,14 +346,14 @@ export function CreateProjectDialog({ open, onOpenChange, workspaceId }: CreateP
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => onOpenChange(false)}
|
||||
className="min-h-11 w-full cursor-pointer sm:min-h-9 sm:w-auto"
|
||||
className={controlSizingClassName("standard", "w-full cursor-pointer sm:w-auto")}
|
||||
>
|
||||
{t("common:cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleCreate}
|
||||
disabled={!form.name.trim() || submitting}
|
||||
className="min-h-11 w-full cursor-pointer sm:min-h-9 sm:w-auto"
|
||||
className={controlSizingClassName("standard", "w-full cursor-pointer sm:w-auto")}
|
||||
>
|
||||
{submitting ? t("office:creating") : t("office:createProject")}
|
||||
</Button>
|
||||
|
||||
@@ -19,6 +19,7 @@ import { normalizeRepoValue, shouldShowCustomEntry } from "./repo-entry";
|
||||
import { useDiscoveredRepositories } from "./use-discovered-repositories";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { RepositoryDiscoveryControls } from "@/components/repository-discovery-controls";
|
||||
import { controlSizingClassName } from "@kandev/ui/control-sizing";
|
||||
|
||||
type Props = {
|
||||
workspaceId: string | null;
|
||||
@@ -128,7 +129,7 @@ export function ProjectRepositoryPicker({
|
||||
placeholder={t("office:searchOrPasteAUrlOr")}
|
||||
value={query}
|
||||
onValueChange={setQuery}
|
||||
className="h-9"
|
||||
className={controlSizingClassName("standard")}
|
||||
/>
|
||||
<PickerCommandList
|
||||
workspaceOptions={workspaceOptions}
|
||||
@@ -150,7 +151,10 @@ function PickerTriggerButton({ label, ...rest }: { label: string }) {
|
||||
type="button"
|
||||
data-testid="project-add-repository"
|
||||
className={cn(
|
||||
"h-8 inline-flex items-center gap-1.5 rounded-md border border-input bg-input/20 dark:bg-input/30 px-2.5 text-xs cursor-pointer",
|
||||
controlSizingClassName(
|
||||
"standard",
|
||||
"inline-flex items-center gap-1.5 rounded-md border border-input bg-input/20 dark:bg-input/30 px-2.5 text-xs cursor-pointer",
|
||||
),
|
||||
"hover:bg-muted/60",
|
||||
)}
|
||||
{...rest}
|
||||
|
||||
@@ -131,7 +131,7 @@ export function RoutineRow({
|
||||
asChild
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 cursor-pointer"
|
||||
className="cursor-pointer"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<Link href={`/office/routines/${routine.id}`}>
|
||||
@@ -163,7 +163,7 @@ function RoutineActions({ onRunNow, onDelete }: { onRunNow: () => void; onDelete
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 cursor-pointer"
|
||||
className="cursor-pointer"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<IconDots className="h-4 w-4" />
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { useRouter } from "@/lib/routing/client-router";
|
||||
import { IconX } from "@tabler/icons-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { controlSizingClassName } from "@kandev/ui/control-sizing";
|
||||
|
||||
type CloseButtonProps = {
|
||||
/** Where to navigate when the user dismisses the wizard. */
|
||||
@@ -17,7 +18,10 @@ export function CloseButton({ href }: CloseButtonProps) {
|
||||
type="button"
|
||||
onClick={() => router.push(href)}
|
||||
aria-label={t("common:cancel")}
|
||||
className="fixed top-4 right-4 z-10 inline-flex h-9 w-9 items-center justify-center rounded-full border border-border bg-background text-muted-foreground hover:bg-accent hover:text-foreground cursor-pointer lg:absolute lg:bg-transparent lg:-top-12 lg:-left-12 lg:right-auto"
|
||||
className={controlSizingClassName(
|
||||
"icon",
|
||||
"fixed top-4 right-4 z-10 rounded-full border border-border bg-background text-muted-foreground hover:bg-accent hover:text-foreground cursor-pointer lg:absolute lg:bg-transparent lg:-top-12 lg:-left-12 lg:right-auto",
|
||||
)}
|
||||
>
|
||||
<IconX className="h-4 w-4" />
|
||||
</button>
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
} from "./agent-profile-setup-controls";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import { TIER_NAME_KEYS } from "../lib/label-keys";
|
||||
import { controlSizingClassName } from "@kandev/ui/control-sizing";
|
||||
|
||||
type ProfileSelectOption = ReturnType<typeof useSelectableProfileOptions>["profileOptions"][number];
|
||||
|
||||
@@ -166,7 +167,10 @@ function ProfileSelectorSection({
|
||||
onValueChange={(v) => onChange({ agentProfileId: v })}
|
||||
disabled={profileOptions.length === 0}
|
||||
placeholder={t("office:selectAnAgentProfile")}
|
||||
triggerClassName="mt-1 border border-input rounded-md px-3 h-9"
|
||||
triggerClassName={controlSizingClassName(
|
||||
"standard",
|
||||
"mt-1 border border-input rounded-md px-3",
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{!showCreate && (
|
||||
@@ -320,7 +324,10 @@ function ExecutorSelector({
|
||||
onValueChange={onChange}
|
||||
placeholder={t("office:selectExecutor")}
|
||||
showSearch={false}
|
||||
triggerClassName="mt-1 border border-input rounded-md px-3 h-9"
|
||||
triggerClassName={controlSizingClassName(
|
||||
"standard",
|
||||
"mt-1 border border-input rounded-md px-3",
|
||||
)}
|
||||
/>
|
||||
{selected ? (
|
||||
<p className="text-xs text-muted-foreground mt-1">{selected.description}</p>
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
useSelectableProfileOptions,
|
||||
} from "./agent-profile-setup-controls";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { controlSizingClassName } from "@kandev/ui/control-sizing";
|
||||
|
||||
type StepTierProfilesProps = {
|
||||
tierProfileIds: Partial<Record<Tier, string>>;
|
||||
@@ -162,7 +163,10 @@ function TierProfileSelector({
|
||||
onValueChange={onChange}
|
||||
disabled={options.length === 0}
|
||||
placeholder={t("office:selectProfile")}
|
||||
triggerClassName="border border-input rounded-md px-3 h-9 w-full"
|
||||
triggerClassName={controlSizingClassName(
|
||||
"standard",
|
||||
"border border-input rounded-md px-3 w-full",
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -279,7 +279,7 @@ function ChatInput({
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
size="icon"
|
||||
className="h-9 w-9 cursor-pointer shrink-0"
|
||||
className="cursor-pointer shrink-0"
|
||||
disabled={disabled || !message.trim()}
|
||||
onClick={onSend}
|
||||
>
|
||||
|
||||
@@ -13,6 +13,7 @@ import { useTranslation } from "react-i18next";
|
||||
// putting the hook's `t` in that dep array would re-issue the request on every
|
||||
// locale switch.
|
||||
import { t as staticT } from "@/lib/i18n";
|
||||
import { controlSizingClassName } from "@kandev/ui/control-sizing";
|
||||
|
||||
// Catalog keys, not copy — module scope freezes a `t()` at the boot locale.
|
||||
// The `value`s are the wire filter ids sent to `listActivity`.
|
||||
@@ -45,7 +46,9 @@ export function ActivityFeed({ workspaceId }: { workspaceId: string }) {
|
||||
title={t("office:activity")}
|
||||
action={
|
||||
<Select value={filterType} onValueChange={setFilterType}>
|
||||
<SelectTrigger className="w-[140px] h-8 text-xs cursor-pointer">
|
||||
<SelectTrigger
|
||||
className={controlSizingClassName("standard", "w-[140px] text-xs cursor-pointer")}
|
||||
>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
|
||||
@@ -8,6 +8,7 @@ import { Card, CardContent } from "@kandev/ui/card";
|
||||
import { toast } from "@/lib/toast/sonner";
|
||||
import { createBudget } from "@/lib/api/domains/office-api";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { controlSizingClassName } from "@kandev/ui/control-sizing";
|
||||
|
||||
type Props = {
|
||||
workspaceId: string;
|
||||
@@ -45,7 +46,7 @@ function FormFields({
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<FormField label={t("office:scopeType")}>
|
||||
<Select value={state.scopeType} onValueChange={(v) => onChange({ scopeType: v })}>
|
||||
<SelectTrigger className="h-8 text-sm cursor-pointer">
|
||||
<SelectTrigger className={controlSizingClassName("standard", "text-sm cursor-pointer")}>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -63,7 +64,7 @@ function FormFields({
|
||||
</FormField>
|
||||
<FormField label={t("office:scopeId")}>
|
||||
<Input
|
||||
className="h-8 text-sm"
|
||||
className={controlSizingClassName("standard", "text-sm")}
|
||||
value={state.scopeId}
|
||||
onChange={(e) => onChange({ scopeId: e.target.value })}
|
||||
placeholder={t("office:entityId")}
|
||||
@@ -71,7 +72,7 @@ function FormFields({
|
||||
</FormField>
|
||||
<FormField label={t("office:limit")}>
|
||||
<Input
|
||||
className="h-8 text-sm"
|
||||
className={controlSizingClassName("standard", "text-sm")}
|
||||
type="number"
|
||||
value={state.limitDollars}
|
||||
onChange={(e) => onChange({ limitDollars: e.target.value })}
|
||||
@@ -80,7 +81,7 @@ function FormFields({
|
||||
</FormField>
|
||||
<FormField label={t("office:period")}>
|
||||
<Select value={state.period} onValueChange={(v) => onChange({ period: v })}>
|
||||
<SelectTrigger className="h-8 text-sm cursor-pointer">
|
||||
<SelectTrigger className={controlSizingClassName("standard", "text-sm cursor-pointer")}>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -101,7 +102,7 @@ function FormFields({
|
||||
</FormField>
|
||||
<FormField label={t("office:alertThreshold")}>
|
||||
<Input
|
||||
className="h-8 text-sm"
|
||||
className={controlSizingClassName("standard", "text-sm")}
|
||||
type="number"
|
||||
value={state.alertPct}
|
||||
onChange={(e) => onChange({ alertPct: e.target.value })}
|
||||
@@ -109,7 +110,7 @@ function FormFields({
|
||||
</FormField>
|
||||
<FormField label={t("office:actionOnExceed")}>
|
||||
<Select value={state.action} onValueChange={(v) => onChange({ action: v })}>
|
||||
<SelectTrigger className="h-8 text-sm cursor-pointer">
|
||||
<SelectTrigger className={controlSizingClassName("standard", "text-sm cursor-pointer")}>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
|
||||
@@ -18,12 +18,7 @@ export function OrgZoomControls({ onZoomIn, onZoomOut, onFit, onExport }: OrgZoo
|
||||
<div className="absolute top-4 right-4 z-10 flex flex-col gap-1">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="h-8 w-8 cursor-pointer"
|
||||
onClick={onZoomIn}
|
||||
>
|
||||
<Button variant="outline" size="icon" className="cursor-pointer" onClick={onZoomIn}>
|
||||
<IconPlus className="h-4 w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
@@ -31,12 +26,7 @@ export function OrgZoomControls({ onZoomIn, onZoomOut, onFit, onExport }: OrgZoo
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="h-8 w-8 cursor-pointer"
|
||||
onClick={onZoomOut}
|
||||
>
|
||||
<Button variant="outline" size="icon" className="cursor-pointer" onClick={onZoomOut}>
|
||||
<IconMinus className="h-4 w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
@@ -44,7 +34,7 @@ export function OrgZoomControls({ onZoomIn, onZoomOut, onFit, onExport }: OrgZoo
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button variant="outline" size="icon" className="h-8 w-8 cursor-pointer" onClick={onFit}>
|
||||
<Button variant="outline" size="icon" className="cursor-pointer" onClick={onFit}>
|
||||
<IconArrowsMaximize className="h-4 w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
@@ -53,12 +43,7 @@ export function OrgZoomControls({ onZoomIn, onZoomOut, onFit, onExport }: OrgZoo
|
||||
{onExport && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="h-8 w-8 cursor-pointer"
|
||||
onClick={onExport}
|
||||
>
|
||||
<Button variant="outline" size="icon" className="cursor-pointer" onClick={onExport}>
|
||||
<IconDownload className="h-4 w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
|
||||
@@ -16,6 +16,7 @@ import { providerLabel } from "./provider-order-editor";
|
||||
import { TIER_NAME_KEYS } from "../../../lib/label-keys";
|
||||
import { firstProviderWithTier } from "./wake-reason-tier-card";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { controlSizingClassName } from "@kandev/ui/control-sizing";
|
||||
|
||||
// USE_WORKSPACE_DEFAULT is the sentinel used in the per-role tier dropdown
|
||||
// to mean "no role entry — use the workspace default tier." Persisting this
|
||||
@@ -103,7 +104,7 @@ function RowLabel({ role, labelId }: { role: RoleMeta; labelId: string }) {
|
||||
<button
|
||||
type="button"
|
||||
aria-label={t("office:moreInfoAbout", { label: role.label })}
|
||||
className="inline-flex h-11 w-11 shrink-0 cursor-pointer items-center justify-center text-muted-foreground hover:text-foreground"
|
||||
className={`${controlSizingClassName("icon")} shrink-0 cursor-pointer text-muted-foreground hover:text-foreground`}
|
||||
>
|
||||
<IconInfoCircle className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
@@ -142,7 +143,10 @@ function TierSelect({
|
||||
onValueChange={(v) => onChange(v as Tier | typeof USE_WORKSPACE_DEFAULT)}
|
||||
disabled={disabled}
|
||||
>
|
||||
<SelectTrigger aria-labelledby={ariaLabelledBy} className="min-h-11 w-[220px] cursor-pointer">
|
||||
<SelectTrigger
|
||||
aria-labelledby={ariaLabelledBy}
|
||||
className={controlSizingClassName("standard", "w-[220px] cursor-pointer")}
|
||||
>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
|
||||
@@ -16,6 +16,7 @@ import { useTree, type VisibleRow } from "@/hooks/use-tree";
|
||||
import type { FileTreeNode } from "./export-types";
|
||||
import { getDescendantFilePaths } from "./export-utils";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { controlSizingClassName } from "@kandev/ui/control-sizing";
|
||||
|
||||
interface ExportFileTreeProps {
|
||||
tree: FileTreeNode[];
|
||||
@@ -82,7 +83,7 @@ export function ExportFileTree({
|
||||
placeholder={t("office:searchFiles")}
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
className="pl-8 h-8 text-sm"
|
||||
className={controlSizingClassName("standard", "pl-8 text-sm")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -21,6 +21,7 @@ import { Tooltip, TooltipContent, TooltipTrigger } from "@kandev/ui/tooltip";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { Skill, SkillSourceType } from "@/lib/state/slices/office/types";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { controlSizingClassName } from "@kandev/ui/control-sizing";
|
||||
|
||||
interface SkillListProps {
|
||||
skills: Skill[];
|
||||
@@ -86,7 +87,7 @@ export function SkillList(props: SkillListProps) {
|
||||
placeholder={t("office:filterSkills")}
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
className="h-8 text-xs"
|
||||
className={controlSizingClassName("standard", "text-xs")}
|
||||
/>
|
||||
</div>
|
||||
<SkillItems items={filtered} selectedId={selectedId} onSelect={onSelect} search={search} />
|
||||
@@ -126,12 +127,7 @@ function SkillListHeader({
|
||||
<div className="flex items-center gap-1">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={onRefresh}
|
||||
className="h-7 w-7 p-0 cursor-pointer"
|
||||
>
|
||||
<Button variant="ghost" size="icon" onClick={onRefresh} className="p-0 cursor-pointer">
|
||||
<IconRefresh className="h-4 w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
@@ -139,12 +135,7 @@ function SkillListHeader({
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={onAdd}
|
||||
className="h-7 w-7 p-0 cursor-pointer"
|
||||
>
|
||||
<Button variant="ghost" size="icon" onClick={onAdd} className="p-0 cursor-pointer">
|
||||
<IconPlus className="h-4 w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
@@ -175,14 +166,13 @@ function SkillImportInput({
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
onKeyDown={(e) => e.key === "Enter" && onImport()}
|
||||
className="h-8 text-xs"
|
||||
className={controlSizingClassName("standard", "text-xs")}
|
||||
/>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={onImport}
|
||||
disabled={!value.trim() || importing}
|
||||
className="h-8 shrink-0 cursor-pointer"
|
||||
className={controlSizingClassName("standard", "shrink-0 cursor-pointer")}
|
||||
>
|
||||
{importing ? <IconLoader2 className="h-3.5 w-3.5 animate-spin" /> : t("office:add")}
|
||||
</Button>
|
||||
|
||||
@@ -46,6 +46,8 @@ import { toAgentProfileOption } from "@/lib/state/slices/settings/types";
|
||||
import { HideDisabledAgentProfilesSetting } from "@/app/settings/agents/hide-disabled-agent-profiles-setting";
|
||||
import type { AgentDiscovery, Agent, AvailableAgent, RuntimeUpdate } from "@/lib/types/http";
|
||||
|
||||
const installedAgentsActionClassName = settingsActionClassName("cursor-pointer");
|
||||
|
||||
type InstalledAgentsSectionProps = {
|
||||
installedAgents: AgentDiscovery[];
|
||||
/** The full scan, detected or not — the backend's ranking of every agent. */
|
||||
@@ -99,9 +101,8 @@ function InstalledAgentsHeader({
|
||||
<div className="flex w-full flex-wrap gap-2 md:w-auto" data-testid="installed-agents-actions">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={onOpenShell}
|
||||
className="h-11 cursor-pointer md:h-6"
|
||||
className={installedAgentsActionClassName}
|
||||
data-testid="open-host-shell"
|
||||
>
|
||||
<IconTerminal2 className="h-4 w-4 mr-2" />
|
||||
@@ -109,10 +110,9 @@ function InstalledAgentsHeader({
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={onRescan}
|
||||
disabled={rescanning}
|
||||
className="h-11 cursor-pointer md:h-6"
|
||||
className={installedAgentsActionClassName}
|
||||
data-testid="rescan-agents-button"
|
||||
>
|
||||
{rescanning ? (
|
||||
@@ -125,9 +125,8 @@ function InstalledAgentsHeader({
|
||||
{onOpenTuiDialog && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={onOpenTuiDialog}
|
||||
className="h-11 cursor-pointer md:h-6"
|
||||
className={installedAgentsActionClassName}
|
||||
data-testid="new-agent-button"
|
||||
>
|
||||
<IconPlus className="h-4 w-4 mr-2" />
|
||||
@@ -408,8 +407,7 @@ export default function AgentsSettingsPage() {
|
||||
is already installed. */}
|
||||
{canManage && (
|
||||
<Button
|
||||
size="sm"
|
||||
className={settingsActionClassName("cursor-pointer")}
|
||||
className={installedAgentsActionClassName}
|
||||
asChild
|
||||
data-testid="install-agents-button"
|
||||
>
|
||||
|
||||
@@ -26,6 +26,7 @@ import { useSettingsSaveContributor } from "@/components/settings/settings-save-
|
||||
import type { Executor, ExecutorType } from "@/lib/types/http";
|
||||
import { EXECUTOR_ICON_MAP } from "@/lib/executor-icons";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { settingsActionClassName } from "@/components/settings/settings-control";
|
||||
|
||||
const EXECUTORS_ROUTE = "/settings/executors";
|
||||
// The word the user must type to arm the delete button. It is compared with
|
||||
@@ -341,9 +342,8 @@ function ExecutorEditForm({ executor }: { executor: Executor }) {
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => router.push(EXECUTORS_ROUTE)}
|
||||
className="min-h-11 w-full cursor-pointer text-sm md:min-h-7 md:w-auto md:text-xs"
|
||||
className={settingsActionClassName("w-full cursor-pointer text-sm md:w-auto md:text-xs")}
|
||||
>
|
||||
{t("executors:backToExecutors")}
|
||||
</Button>
|
||||
|
||||
@@ -206,7 +206,7 @@ function EnvVarRow({
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => onRemove(index)}
|
||||
className="cursor-pointer h-9 w-9 shrink-0"
|
||||
className="cursor-pointer shrink-0"
|
||||
>
|
||||
<IconTrash className="h-3.5 w-3.5 text-muted-foreground" />
|
||||
</Button>
|
||||
@@ -241,7 +241,6 @@ function EnvVarsCard({
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={onAdd}
|
||||
className={settingsActionClassName("cursor-pointer")}
|
||||
>
|
||||
@@ -283,7 +282,11 @@ function ProfileActions({
|
||||
const router = useRouter();
|
||||
return (
|
||||
<div className="flex items-center justify-between">
|
||||
<Button variant="destructive" size="sm" onClick={onRequestDelete} className="cursor-pointer">
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={onRequestDelete}
|
||||
className={settingsActionClassName("cursor-pointer")}
|
||||
>
|
||||
<IconTrash className="h-4 w-4 mr-1" />
|
||||
{t("executors:deleteProfile")}
|
||||
</Button>
|
||||
@@ -463,7 +466,6 @@ function ProfileEditHeader({ executor, profileName }: { executor: Executor; prof
|
||||
actions={
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className={settingsActionClassName("cursor-pointer")}
|
||||
onClick={() => router.push(`/settings/executor/${executor.id}`)}
|
||||
>
|
||||
|
||||
@@ -22,6 +22,7 @@ import { KubernetesDiagnosticsCard } from "@/components/settings/kubernetes-diag
|
||||
import { KubernetesReadOnlyNotice } from "@/components/settings/kubernetes-read-only-notice";
|
||||
import { saveWithKubernetesSessionConfirmation } from "@/components/settings/kubernetes-save-confirmation";
|
||||
import { KubernetesSessionsCard } from "@/components/settings/kubernetes-sessions-card";
|
||||
import { settingsActionClassName } from "@/components/settings/settings-control";
|
||||
import { useAppStore } from "@/components/state-provider";
|
||||
import {
|
||||
SettingsSaveCancelledError,
|
||||
@@ -91,7 +92,7 @@ function ExecutorMessage({ message }: { message: string }) {
|
||||
<CardContent className="py-12 text-center">
|
||||
<p className="break-words text-sm text-muted-foreground">{message}</p>
|
||||
<Button
|
||||
className="mt-4 min-h-11 cursor-pointer"
|
||||
className={settingsActionClassName("mt-4 cursor-pointer")}
|
||||
onClick={() => router.push(EXECUTORS_ROUTE)}
|
||||
>
|
||||
{t("executors:backToExecutors")}
|
||||
@@ -251,19 +252,17 @@ function KubernetesExecutorHeader({
|
||||
<div className="flex w-full flex-col gap-2 md:w-auto md:flex-row">
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
onClick={onDelete}
|
||||
disabled={!canManage}
|
||||
className="min-h-11 cursor-pointer text-sm md:min-h-7 md:text-xs"
|
||||
className={settingsActionClassName("cursor-pointer text-sm md:text-xs")}
|
||||
>
|
||||
<IconTrash className="mr-1.5 h-4 w-4" />
|
||||
{t("executors:deleteExecutor")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => router.push(EXECUTORS_ROUTE)}
|
||||
className="min-h-11 cursor-pointer text-sm md:min-h-7 md:text-xs"
|
||||
className={settingsActionClassName("cursor-pointer text-sm md:text-xs")}
|
||||
>
|
||||
{t("executors:backToExecutors")}
|
||||
</Button>
|
||||
@@ -299,7 +298,7 @@ function DeleteExecutorDialog({
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => onOpenChange(false)}
|
||||
className="min-h-11 cursor-pointer md:min-h-9"
|
||||
className={settingsActionClassName("cursor-pointer")}
|
||||
>
|
||||
{t("common:cancel")}
|
||||
</Button>
|
||||
@@ -307,7 +306,7 @@ function DeleteExecutorDialog({
|
||||
variant="destructive"
|
||||
onClick={onDelete}
|
||||
disabled={deleting}
|
||||
className="min-h-11 cursor-pointer md:min-h-9"
|
||||
className={settingsActionClassName("cursor-pointer")}
|
||||
>
|
||||
{deleting ? t("executors:deleting") : t("executors:delete")}
|
||||
</Button>
|
||||
|
||||
@@ -15,6 +15,7 @@ import { KubernetesWorkloadCard } from "@/components/settings/kubernetes-workloa
|
||||
import { KubernetesWorkspaceCard } from "@/components/settings/kubernetes-workspace-card";
|
||||
import { ProfileDetailsCard } from "@/components/settings/profile-edit/profile-details-card";
|
||||
import { useSettingsSaveContributor } from "@/components/settings/settings-save-provider";
|
||||
import { settingsActionClassName } from "@/components/settings/settings-control";
|
||||
import { serializeSettingsRevision } from "@/components/settings/settings-save-revision";
|
||||
import {
|
||||
createDefaultKubernetesExecutorForm,
|
||||
@@ -157,9 +158,8 @@ function KubernetesCreateHeader() {
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => router.push(EXECUTORS_ROUTE)}
|
||||
className="min-h-11 w-full cursor-pointer text-sm md:min-h-7 md:w-auto md:text-xs"
|
||||
className={settingsActionClassName("w-full cursor-pointer text-sm md:w-auto md:text-xs")}
|
||||
>
|
||||
{t("executors:backToExecutors")}
|
||||
</Button>
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
import type { ScriptPlaceholder } from "@/lib/api/domains/settings-api";
|
||||
import { EXECUTOR_ICON_MAP, getExecutorLabel } from "@/lib/executor-icons";
|
||||
import { useSettingsSaveContributor } from "@/components/settings/settings-save-provider";
|
||||
import { settingsActionClassName } from "@/components/settings/settings-control";
|
||||
import { serializeSettingsRevision } from "@/components/settings/settings-save-revision";
|
||||
import { ProfileDetailsCard } from "@/components/settings/profile-edit/profile-details-card";
|
||||
import {
|
||||
@@ -113,9 +114,8 @@ function CreateProfileHeader({ type, typeInfo }: { type: string; typeInfo: Execu
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => router.push(EXECUTORS_ROUTE)}
|
||||
className="min-h-11 w-full cursor-pointer text-sm md:min-h-7 md:w-auto md:text-xs"
|
||||
className={settingsActionClassName("w-full cursor-pointer text-sm md:w-auto md:text-xs")}
|
||||
>
|
||||
{t("executors:backToExecutors")}
|
||||
</Button>
|
||||
|
||||
@@ -12,6 +12,7 @@ import { createExecutor, createExecutorProfile } from "@/lib/api/domains/setting
|
||||
import { SSHConnectionCard } from "@/components/settings/ssh-connection-card";
|
||||
import type { SSHExecutorConfig } from "@/components/settings/ssh-connection-card";
|
||||
import { getExecutorLabel } from "@/lib/executor-icons";
|
||||
import { settingsActionClassName } from "@/components/settings/settings-control";
|
||||
import { buildSSHExecutorConfig } from "./ssh-config";
|
||||
import type { Executor } from "@/lib/types/http";
|
||||
|
||||
@@ -102,9 +103,8 @@ function SSHCreateHeader() {
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => router.push(EXECUTORS_ROUTE)}
|
||||
className="min-h-11 w-full cursor-pointer text-sm md:min-h-7 md:w-auto md:text-xs"
|
||||
className={settingsActionClassName("w-full cursor-pointer text-sm md:w-auto md:text-xs")}
|
||||
>
|
||||
{t("executors:backToExecutors")}
|
||||
</Button>
|
||||
|
||||
@@ -21,6 +21,7 @@ import { deleteExecutorProfile } from "@/lib/api/domains/settings-api";
|
||||
import { EXECUTOR_ICON_MAP, getExecutorLabel } from "@/lib/executor-icons";
|
||||
import type { Executor, ExecutorProfile } from "@/lib/types/http";
|
||||
import { KubernetesReadOnlyNotice } from "@/components/settings/kubernetes-read-only-notice";
|
||||
import { settingsActionClassName } from "@/components/settings/settings-control";
|
||||
|
||||
type ProfileWithExecutor = ExecutorProfile & {
|
||||
executor_type: string;
|
||||
@@ -126,7 +127,9 @@ function ProfileCard({
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="min-h-11 w-11 shrink-0 cursor-pointer md:h-8 md:min-h-8 md:w-8 md:opacity-0 md:group-hover:opacity-100 md:focus-visible:opacity-100"
|
||||
className={settingsActionClassName(
|
||||
"shrink-0 cursor-pointer md:opacity-0 md:group-hover:opacity-100 md:focus-visible:opacity-100",
|
||||
)}
|
||||
disabled={!canDelete}
|
||||
aria-label={t("executors:deleteProfile")}
|
||||
onClick={(e) => {
|
||||
@@ -207,7 +210,7 @@ function DeleteProfileDialog({
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => onOpenChange(false)}
|
||||
className="min-h-11 cursor-pointer md:min-h-9"
|
||||
className={settingsActionClassName("cursor-pointer")}
|
||||
>
|
||||
{t("common:cancel")}
|
||||
</Button>
|
||||
@@ -215,7 +218,7 @@ function DeleteProfileDialog({
|
||||
variant="destructive"
|
||||
onClick={onDelete}
|
||||
disabled={deleting}
|
||||
className="min-h-11 cursor-pointer md:min-h-9"
|
||||
className={settingsActionClassName("cursor-pointer")}
|
||||
>
|
||||
{deleting ? t("executors:deleting") : t("executors:delete")}
|
||||
</Button>
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
parseSSHExecutorConfig,
|
||||
} from "@/app/settings/executors/new/[type]/ssh-config";
|
||||
import type { Executor } from "@/lib/types/http";
|
||||
import { settingsActionClassName } from "@/components/settings/settings-control";
|
||||
|
||||
const EXECUTORS_ROUTE = "/settings/executors";
|
||||
|
||||
@@ -159,9 +160,8 @@ function SSHExecutorHeader({ executorName }: { executorName: string }) {
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => router.push(EXECUTORS_ROUTE)}
|
||||
className="min-h-11 w-full cursor-pointer text-sm md:min-h-7 md:w-auto md:text-xs"
|
||||
className={settingsActionClassName("w-full cursor-pointer text-sm md:w-auto md:text-xs")}
|
||||
>
|
||||
{t("executors:backToExecutors")}
|
||||
</Button>
|
||||
|
||||
@@ -8,6 +8,7 @@ import { IconGitBranch } from "@tabler/icons-react";
|
||||
import { Button } from "@kandev/ui/button";
|
||||
import { SettingsSection } from "@/components/settings/settings-section";
|
||||
import { RepositoryCard } from "@/components/settings/repository-card";
|
||||
import { settingsActionClassName } from "@/components/settings/settings-control";
|
||||
import { WorkspaceRepositorySetsSection } from "./workspace-repository-sets-section";
|
||||
import { AddLocalRepositoryDialog } from "./workspace-add-local-repository-dialog";
|
||||
import { generateUUID } from "@/lib/utils";
|
||||
@@ -538,7 +539,7 @@ export function WorkspaceRepositoriesClient({
|
||||
}
|
||||
action={
|
||||
isImproveWorkspace ? undefined : (
|
||||
<Button size="sm" className="cursor-pointer" onClick={openDialog}>
|
||||
<Button className={settingsActionClassName("cursor-pointer")} onClick={openDialog}>
|
||||
{t("workspaces:addLocalRepository")}
|
||||
</Button>
|
||||
)
|
||||
|
||||
@@ -201,7 +201,7 @@ function MemberRow({
|
||||
{checked ? (
|
||||
<div className="flex shrink-0 gap-0.5">
|
||||
<Button
|
||||
size="sm"
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="cursor-pointer"
|
||||
disabled={!canMoveUp}
|
||||
@@ -212,7 +212,7 @@ function MemberRow({
|
||||
<IconArrowUp className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="cursor-pointer"
|
||||
disabled={!canMoveDown}
|
||||
|
||||
@@ -74,7 +74,6 @@ export function WorkspaceRepositorySetsSection({
|
||||
action={
|
||||
readOnly ? undefined : (
|
||||
<Button
|
||||
size="sm"
|
||||
className="cursor-pointer"
|
||||
onClick={() => manager.startCreate()}
|
||||
data-testid="repository-set-create"
|
||||
@@ -256,7 +255,7 @@ function RepositorySetRowActions({
|
||||
return (
|
||||
<div className="flex shrink-0 gap-1">
|
||||
<Button
|
||||
size="sm"
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="cursor-pointer"
|
||||
aria-label={t("workspaces:repositorySetsEdit")}
|
||||
@@ -267,7 +266,7 @@ function RepositorySetRowActions({
|
||||
</Button>
|
||||
<Button
|
||||
ref={deleteAnchorRef}
|
||||
size="sm"
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="cursor-pointer"
|
||||
aria-label={t("workspaces:repositorySetsDelete")}
|
||||
|
||||
@@ -167,7 +167,7 @@ export function WorkspacesPageClient() {
|
||||
{t("workspaces:manageYourWorkspacesAndWorkflows")}
|
||||
</p>
|
||||
</div>
|
||||
<Button size="sm" className={settingsActionClassName()} onClick={() => setIsAdding(true)}>
|
||||
<Button className={settingsActionClassName()} onClick={() => setIsAdding(true)}>
|
||||
<IconPlus className="h-4 w-4 mr-2" />
|
||||
{t("workspaces:addWorkspace")}
|
||||
</Button>
|
||||
|
||||
@@ -92,8 +92,8 @@ function ActionsCell({ row, ctx }: { row: Row<TaskWithResolution>; ctx: ActionsC
|
||||
<Button
|
||||
ref={archiveAnchorRef}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="cursor-pointer h-7 w-7 p-0"
|
||||
size="icon"
|
||||
className="cursor-pointer p-0"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setShowArchiveConfirm(true);
|
||||
@@ -109,8 +109,8 @@ function ActionsCell({ row, ctx }: { row: Row<TaskWithResolution>; ctx: ActionsC
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="cursor-pointer h-7 w-7 p-0"
|
||||
size="icon"
|
||||
className="cursor-pointer p-0"
|
||||
disabled={isDeleting}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
TASKS_LIST_GROUP_OPTIONS,
|
||||
TASKS_LIST_SORT_OPTIONS,
|
||||
} from "@/lib/tasks/tasks-list-options";
|
||||
import { controlSizingClassName } from "@kandev/ui/control-sizing";
|
||||
|
||||
export function TasksListControls({
|
||||
showArchived,
|
||||
@@ -59,7 +60,9 @@ export function TasksListControls({
|
||||
onChange={onTasksListGroupChange}
|
||||
testId="tasks-list-group"
|
||||
/>
|
||||
<Label className="flex h-11 items-center gap-2 text-sm text-muted-foreground cursor-pointer select-none lg:h-9">
|
||||
<Label
|
||||
className={`${controlSizingClassName("standard")} flex items-center gap-2 text-sm text-muted-foreground cursor-pointer select-none`}
|
||||
>
|
||||
<Checkbox
|
||||
checked={showArchived}
|
||||
onCheckedChange={(checked) => onShowArchivedChange(checked === true)}
|
||||
@@ -88,7 +91,10 @@ function ListOptionSelect<T extends string>({
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-muted-foreground">{label}</span>
|
||||
<Select value={value} onValueChange={(next) => onChange(next as T)}>
|
||||
<SelectTrigger data-testid={testId} className="h-10 w-[150px] cursor-pointer lg:h-9">
|
||||
<SelectTrigger
|
||||
data-testid={testId}
|
||||
className={controlSizingClassName("standard", "w-[150px] cursor-pointer")}
|
||||
>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
|
||||
@@ -495,7 +495,7 @@ function UnarchiveRowAction({
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-9 w-9 cursor-pointer"
|
||||
className="cursor-pointer"
|
||||
data-testid="tasks-list-unarchive"
|
||||
disabled={isPending}
|
||||
onClick={async () => {
|
||||
@@ -562,7 +562,7 @@ function TaskRowActions({
|
||||
ref={archiveAnchorRef}
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-9 w-9 cursor-pointer"
|
||||
className="cursor-pointer"
|
||||
onClick={() => onArchiveOpenChange(true)}
|
||||
>
|
||||
<IconArchive className="h-4 w-4 text-muted-foreground" />
|
||||
@@ -579,7 +579,7 @@ function TaskRowActions({
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-9 w-9 cursor-pointer"
|
||||
className="cursor-pointer"
|
||||
disabled={isDeleting}
|
||||
onClick={() => onDeleteOpenChange(true)}
|
||||
>
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
PaginationItem,
|
||||
} from "@kandev/ui/pagination";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@kandev/ui/select";
|
||||
import { controlSizingClassName } from "@kandev/ui/control-sizing";
|
||||
import {
|
||||
IconChevronLeft,
|
||||
IconChevronRight,
|
||||
@@ -121,7 +122,7 @@ function RowsPerPageSelect({
|
||||
>
|
||||
<SelectTrigger
|
||||
data-testid="tasks-pagination-page-size"
|
||||
className="h-10 w-[76px] cursor-pointer sm:h-8"
|
||||
className={controlSizingClassName("standard", "w-[76px] cursor-pointer")}
|
||||
>
|
||||
<SelectValue placeholder={pagination.pageSize} />
|
||||
</SelectTrigger>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Button } from "@kandev/ui/button";
|
||||
import { Input } from "@kandev/ui/input";
|
||||
import type { ChangeEvent, KeyboardEvent, MouseEvent } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { controlSizingClassName } from "@kandev/ui/control-sizing";
|
||||
import Link from "@/components/routing/app-link";
|
||||
import { useRouter } from "@/lib/routing/client-router";
|
||||
import { navigateToSettingsDiscovery } from "@/lib/settings-discovery/navigation";
|
||||
@@ -98,7 +99,10 @@ export function SettingsSearch({
|
||||
className={
|
||||
floating
|
||||
? "h-full appearance-none border-0 bg-transparent pl-9 pr-11 text-base shadow-none [&::-webkit-search-cancel-button]:hidden"
|
||||
: "h-11 appearance-none bg-background pl-9 pr-11 text-base md:h-8 md:pl-8 md:pr-8 md:text-xs [&::-webkit-search-cancel-button]:hidden"
|
||||
: controlSizingClassName(
|
||||
"standard",
|
||||
"appearance-none bg-background pl-9 pr-11 text-base md:pl-8 md:pr-8 md:text-xs [&::-webkit-search-cancel-button]:hidden",
|
||||
)
|
||||
}
|
||||
/>
|
||||
{query && (
|
||||
|
||||
@@ -8,6 +8,7 @@ import { useAppStore } from "@/components/state-provider";
|
||||
import { useKandevRestart } from "@/hooks/domains/system/use-kandev-restart";
|
||||
import { useRestartCapability } from "@/hooks/domains/system/use-restart-capability";
|
||||
import { RestartProgressDialog } from "@/components/settings/system/restart-progress-dialog";
|
||||
import { controlSizingClassName } from "@kandev/ui/control-sizing";
|
||||
|
||||
export function AgentRuntimeUnavailableAlert() {
|
||||
const agentRuntime = useAppStore((state) => state.agentRuntime);
|
||||
@@ -41,8 +42,10 @@ function UnavailableAgentRuntimeContent() {
|
||||
{restartSupported ? (
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
className="h-11 w-full shrink-0 cursor-pointer sm:w-auto"
|
||||
className={controlSizingClassName(
|
||||
"standard",
|
||||
"w-full shrink-0 cursor-pointer sm:w-auto",
|
||||
)}
|
||||
disabled={restart.isRestarting}
|
||||
onClick={() => void restart.start()}
|
||||
>
|
||||
|
||||
@@ -6,6 +6,7 @@ import { IconAlertTriangle } from "@tabler/icons-react";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@kandev/ui/alert";
|
||||
import { Button } from "@kandev/ui/button";
|
||||
import { backendReloadCoordinator } from "@/lib/platform/backend-reload-coordinator";
|
||||
import { controlSizingClassName } from "@kandev/ui/control-sizing";
|
||||
|
||||
export function BackendReloadRequiredAlert() {
|
||||
const { t } = useTranslation();
|
||||
@@ -31,8 +32,10 @@ export function BackendReloadRequiredAlert() {
|
||||
<p className="min-w-0 break-words">{t("system:backendReloadRequiredBody")}</p>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
className="h-11 w-full shrink-0 cursor-pointer sm:w-auto"
|
||||
className={controlSizingClassName(
|
||||
"standard",
|
||||
"w-full shrink-0 cursor-pointer sm:w-auto",
|
||||
)}
|
||||
onClick={() => window.location.reload()}
|
||||
>
|
||||
{t("system:backendReloadRequiredAction")}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { Button } from "@kandev/ui/button";
|
||||
import { toast } from "@/lib/toast/sonner";
|
||||
import { exportAutomationsZip } from "@/lib/api/domains/automations-export-api";
|
||||
import { triggerBlobDownload } from "@/lib/utils/file-download";
|
||||
import { controlSizingClassName } from "@kandev/ui/control-sizing";
|
||||
|
||||
// i18n-exempt: fixed download file name, not user-facing copy.
|
||||
const EXPORT_FILE_NAME = "kandev-automations.zip";
|
||||
@@ -42,9 +43,8 @@ export function AutomationsExportButton({ workspaceId }: AutomationsExportButton
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
data-testid="export-automations-button"
|
||||
className="min-h-11 cursor-pointer md:min-h-7"
|
||||
className={controlSizingClassName("standard", "cursor-pointer")}
|
||||
onClick={() => void handleExport()}
|
||||
disabled={downloading}
|
||||
>
|
||||
|
||||
@@ -15,6 +15,7 @@ import { useAutomationEnabledDrafts } from "./use-automation-enabled-drafts";
|
||||
import { WorkspaceSectionHeader } from "@/components/settings/workspaces/workspace-section-header";
|
||||
import { AutomationDeleteConfirmDialog } from "./automation-delete-confirm-dialog";
|
||||
import type { Automation } from "@/lib/types/automation";
|
||||
import { controlSizingClassName } from "@kandev/ui/control-sizing";
|
||||
|
||||
type AutomationsListPageProps = {
|
||||
workspaceId: string;
|
||||
@@ -79,9 +80,8 @@ export function AutomationsListPage({ workspaceId }: AutomationsListPageProps) {
|
||||
<AutomationsExportButton workspaceId={workspaceId} />
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
data-testid="new-automation-button"
|
||||
className="min-h-11 cursor-pointer md:min-h-7"
|
||||
className={controlSizingClassName("standard", "cursor-pointer")}
|
||||
onClick={() => router.push(`/settings/workspaces/${workspaceId}/automations/new`)}
|
||||
>
|
||||
<IconPlus className="h-4 w-4 mr-2" />
|
||||
|
||||
@@ -309,7 +309,7 @@ function FrequencySelect({
|
||||
|
||||
return (
|
||||
<Select value={value} onValueChange={(next) => onChange(next as ScheduleFrequency)}>
|
||||
<SelectTrigger className="w-[170px] h-8" data-testid="schedule-frequency">
|
||||
<SelectTrigger className="w-[170px]" data-testid="schedule-frequency">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -355,7 +355,7 @@ function FrequencyDetail({
|
||||
data-testid="schedule-custom-input"
|
||||
data-settings-dirty={isDirty}
|
||||
placeholder="0 9 * * 1-5"
|
||||
className={`font-mono h-8 w-[170px] ${hasCustomError ? "border-destructive" : ""}`}
|
||||
className={`font-mono w-[170px] ${hasCustomError ? "border-destructive" : ""}`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -376,7 +376,7 @@ function FrequencyDetail({
|
||||
}}
|
||||
data-testid="schedule-minute"
|
||||
data-settings-dirty={isDirty}
|
||||
className="w-[75px] h-8"
|
||||
className="w-[75px]"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
@@ -393,7 +393,7 @@ function FrequencyDetail({
|
||||
value={String(spec.weekday)}
|
||||
onValueChange={(value) => onSpecChange({ ...spec, weekday: Number(value) })}
|
||||
>
|
||||
<SelectTrigger className="w-[125px] h-8" data-testid="schedule-weekday">
|
||||
<SelectTrigger className="w-[125px]" data-testid="schedule-weekday">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -417,7 +417,7 @@ function FrequencyDetail({
|
||||
}}
|
||||
data-testid="schedule-time"
|
||||
data-settings-dirty={isDirty}
|
||||
className="w-[110px] h-8"
|
||||
className="w-[110px]"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
@@ -486,9 +486,9 @@ function TimezoneField({ value, onChange }: { value: string; onChange: (zone: st
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
size="default"
|
||||
role="combobox"
|
||||
className="h-8 cursor-pointer font-normal justify-between gap-1"
|
||||
className="cursor-pointer font-normal justify-between gap-1"
|
||||
data-testid="schedule-timezone"
|
||||
>
|
||||
{value || "UTC"}
|
||||
|
||||
@@ -111,12 +111,12 @@ function MobileColumnNavigator({
|
||||
setOpen(false);
|
||||
};
|
||||
return (
|
||||
<div className="flex min-h-11 items-center gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="h-11 w-11 cursor-pointer"
|
||||
className="cursor-pointer"
|
||||
aria-label={t("azuredevops:previousBoardColumn")}
|
||||
disabled={index === 0}
|
||||
onClick={() => onChange(columns[index - 1].id)}
|
||||
@@ -126,7 +126,7 @@ function MobileColumnNavigator({
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
className="min-h-11 flex-1 cursor-pointer justify-between"
|
||||
className="flex-1 cursor-pointer justify-between"
|
||||
data-testid="azure-board-column-picker"
|
||||
onClick={() => setOpen(true)}
|
||||
>
|
||||
@@ -138,7 +138,7 @@ function MobileColumnNavigator({
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="h-11 w-11 cursor-pointer"
|
||||
className="cursor-pointer"
|
||||
aria-label={t("azuredevops:nextBoardColumn")}
|
||||
disabled={index >= columns.length - 1}
|
||||
onClick={() => onChange(columns[index + 1].id)}
|
||||
|
||||
@@ -21,6 +21,11 @@ import {
|
||||
DEFAULT_AZURE_PULL_REQUEST_QUERIES,
|
||||
DEFAULT_AZURE_WORK_ITEM_QUERIES,
|
||||
} from "./azure-devops-workspace-defaults";
|
||||
import { controlSizingClassName } from "@kandev/ui/control-sizing";
|
||||
import {
|
||||
settingsActionClassName,
|
||||
settingsControlClassName,
|
||||
} from "@/components/settings/settings-control";
|
||||
|
||||
/**
|
||||
* Wire discriminant for the two query kinds. These used to be the English
|
||||
@@ -93,7 +98,7 @@ function QueryGroupSelect({
|
||||
return (
|
||||
<Select value={value === "created" ? "created" : "inbox"} onValueChange={onChange}>
|
||||
<SelectTrigger
|
||||
className="h-11 w-full cursor-pointer sm:h-8"
|
||||
className={settingsControlClassName("w-full cursor-pointer")}
|
||||
aria-label={t("azuredevops:queryGroupAria", {
|
||||
kind: t(KIND_TITLE_KEYS[kind]),
|
||||
index: index + 1,
|
||||
@@ -156,7 +161,7 @@ function RemoveQueryButton({
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-11 w-full cursor-pointer text-destructive sm:h-8 sm:w-8"
|
||||
className={controlSizingClassName("icon", "cursor-pointer text-destructive")}
|
||||
aria-label={t("azuredevops:removeQueryAria", {
|
||||
kind: t(KIND_LOWER_KEYS[kind]),
|
||||
index: index + 1,
|
||||
@@ -186,7 +191,7 @@ function WorkItemQueryRow({
|
||||
>
|
||||
<QueryField label={t("azuredevops:label")}>
|
||||
<Input
|
||||
className="h-11 w-full sm:h-8"
|
||||
className={settingsControlClassName("w-full")}
|
||||
value={preset.label}
|
||||
aria-label={t("azuredevops:queryLabelAria", { kind, index: index + 1 })}
|
||||
data-settings-dirty={preset.label !== baseline?.label}
|
||||
@@ -196,7 +201,7 @@ function WorkItemQueryRow({
|
||||
{/* WIQL is the Azure DevOps query language, not copy. */}
|
||||
<QueryField label="WIQL" className="col-span-2 row-start-2 sm:col-span-1 sm:row-auto">
|
||||
<Input
|
||||
className="h-11 w-full font-mono text-xs sm:h-8"
|
||||
className={settingsControlClassName("w-full font-mono text-xs")}
|
||||
value={stringFilter(preset, "wiql")}
|
||||
aria-label={t("azuredevops:queryWiqlAria", { kind, index: index + 1 })}
|
||||
data-settings-dirty={
|
||||
@@ -210,7 +215,7 @@ function WorkItemQueryRow({
|
||||
type="number"
|
||||
min={1}
|
||||
max={200}
|
||||
className="h-11 w-full sm:h-8"
|
||||
className={settingsControlClassName("w-full")}
|
||||
value={numberFilter(preset, "top", 50)}
|
||||
aria-label={t("azuredevops:queryTopAria", { kind, index: index + 1 })}
|
||||
onChange={(event) => onPatchFilters({ top: Number(event.target.value) || 1 })}
|
||||
@@ -249,7 +254,7 @@ function PullRequestQueryRow({
|
||||
>
|
||||
<QueryField label={t("azuredevops:label")}>
|
||||
<Input
|
||||
className="h-11 w-full sm:h-8"
|
||||
className={settingsControlClassName("w-full")}
|
||||
value={preset.label}
|
||||
aria-label={t("azuredevops:queryLabelAria", { kind, index: index + 1 })}
|
||||
data-settings-dirty={preset.label !== baseline?.label}
|
||||
@@ -262,7 +267,7 @@ function PullRequestQueryRow({
|
||||
onValueChange={(status) => onPatchFilters({ status })}
|
||||
>
|
||||
<SelectTrigger
|
||||
className="h-11 w-full cursor-pointer sm:h-8"
|
||||
className={settingsControlClassName("w-full cursor-pointer")}
|
||||
aria-label={t("azuredevops:queryStatusAria", { kind, index: index + 1 })}
|
||||
>
|
||||
<SelectValue />
|
||||
@@ -279,7 +284,7 @@ function PullRequestQueryRow({
|
||||
</QueryField>
|
||||
<QueryField label={t("azuredevops:creator")} className="col-span-2 sm:col-span-1">
|
||||
<Input
|
||||
className="h-11 w-full sm:h-8"
|
||||
className={settingsControlClassName("w-full")}
|
||||
value={stringFilter(preset, "creator")}
|
||||
aria-label={t("azuredevops:queryCreatorAria", { kind, index: index + 1 })}
|
||||
placeholder={identityPlaceholder}
|
||||
@@ -288,7 +293,7 @@ function PullRequestQueryRow({
|
||||
</QueryField>
|
||||
<QueryField label={t("azuredevops:reviewer")} className="col-span-2 sm:col-span-1">
|
||||
<Input
|
||||
className="h-11 w-full sm:h-8"
|
||||
className={settingsControlClassName("w-full")}
|
||||
value={stringFilter(preset, "reviewer")}
|
||||
aria-label={t("azuredevops:queryReviewerAria", { kind, index: index + 1 })}
|
||||
placeholder={identityPlaceholder}
|
||||
@@ -343,9 +348,8 @@ function QueryEditor({
|
||||
))}
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="h-11 w-full cursor-pointer sm:h-8 sm:w-auto"
|
||||
className={settingsActionClassName("w-full cursor-pointer sm:w-auto")}
|
||||
onClick={() => onChange([...queries, newQuery(kind)])}
|
||||
>
|
||||
<IconPlus className="h-4 w-4" />{" "}
|
||||
@@ -477,9 +481,8 @@ export function AzureDevOpsDefaultQueriesSection({ workspaceId }: { workspaceId:
|
||||
action={
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="h-11 w-full cursor-pointer sm:h-8 sm:w-auto"
|
||||
className={settingsActionClassName("w-full cursor-pointer sm:w-auto")}
|
||||
disabled={drafts.loading}
|
||||
onClick={drafts.reset}
|
||||
>
|
||||
|
||||
@@ -27,6 +27,11 @@ import {
|
||||
DEFAULT_AZURE_PULL_REQUEST_ACTIONS,
|
||||
DEFAULT_AZURE_WORK_ITEM_ACTIONS,
|
||||
} from "./azure-devops-workspace-defaults";
|
||||
import { controlSizingClassName } from "@kandev/ui/control-sizing";
|
||||
import {
|
||||
settingsActionClassName,
|
||||
settingsControlClassName,
|
||||
} from "@/components/settings/settings-control";
|
||||
|
||||
type Translate = (key: string, values?: Record<string, unknown>) => string;
|
||||
|
||||
@@ -111,7 +116,7 @@ function ActionIconSelect({
|
||||
return (
|
||||
<Select value={value} onValueChange={onChange}>
|
||||
<SelectTrigger
|
||||
className="h-11 w-full cursor-pointer sm:h-8"
|
||||
className={settingsControlClassName("w-full cursor-pointer")}
|
||||
aria-label={t("azuredevops:icon")}
|
||||
data-settings-dirty={dirty}
|
||||
>
|
||||
@@ -233,7 +238,7 @@ function ActionRow({
|
||||
</Field>
|
||||
<Field label={t("azuredevops:label")}>
|
||||
<Input
|
||||
className="h-11 w-full sm:h-8"
|
||||
className={settingsControlClassName("w-full")}
|
||||
value={action.label}
|
||||
aria-label={t("azuredevops:actionLabelAria", { kind: kindTitle, index: index + 1 })}
|
||||
data-settings-dirty={action.label !== baseline?.label}
|
||||
@@ -242,7 +247,7 @@ function ActionRow({
|
||||
</Field>
|
||||
<Field label={t("azuredevops:hint")} className="col-span-2 sm:col-span-1">
|
||||
<Input
|
||||
className="h-11 w-full sm:h-8"
|
||||
className={settingsControlClassName("w-full")}
|
||||
value={action.hint}
|
||||
aria-label={t("azuredevops:actionHintAria", { kind: kindTitle, index: index + 1 })}
|
||||
placeholder={t("azuredevops:hintOptional")}
|
||||
@@ -253,8 +258,7 @@ function ActionRow({
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-11 cursor-pointer text-xs sm:h-8"
|
||||
className={settingsActionClassName("cursor-pointer")}
|
||||
onClick={onToggle}
|
||||
>
|
||||
{expanded ? t("azuredevops:hidePrompt") : t("azuredevops:editPrompt")}
|
||||
@@ -263,7 +267,7 @@ function ActionRow({
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-11 w-full cursor-pointer text-destructive sm:h-8 sm:w-8"
|
||||
className={controlSizingClassName("icon", "cursor-pointer text-destructive")}
|
||||
onClick={onRemove}
|
||||
aria-label={t("azuredevops:removeActionAria", {
|
||||
kind: t(KIND_LOWER_KEYS[kind]),
|
||||
@@ -317,9 +321,8 @@ function ActionEditor({
|
||||
))}
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="h-11 w-full cursor-pointer sm:h-8 sm:w-auto"
|
||||
className={settingsActionClassName("w-full cursor-pointer sm:w-auto")}
|
||||
onClick={add}
|
||||
>
|
||||
<IconPlus className="h-4 w-4" />{" "}
|
||||
@@ -454,9 +457,8 @@ export function AzureDevOpsQuickActionsSection({ workspaceId }: { workspaceId: s
|
||||
action={
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="h-11 w-full cursor-pointer sm:h-8 sm:w-auto"
|
||||
className={settingsActionClassName("w-full cursor-pointer sm:w-auto")}
|
||||
disabled={drafts.loading || !!drafts.loadError}
|
||||
onClick={drafts.reset}
|
||||
>
|
||||
|
||||
@@ -479,7 +479,7 @@ function ConnectionActions({ state, disabled }: { state: SettingsState; disabled
|
||||
type="button"
|
||||
variant="destructive"
|
||||
onClick={() => setConfirmingRemove(true)}
|
||||
className="min-h-11 w-full cursor-pointer sm:ml-auto sm:w-auto"
|
||||
className="w-full cursor-pointer sm:ml-auto sm:w-auto"
|
||||
data-testid="azure-devops-delete-button"
|
||||
>
|
||||
<IconTrash className="h-4 w-4" />
|
||||
|
||||
@@ -32,6 +32,7 @@ import type {
|
||||
AzureDevOpsWorkItemWatch,
|
||||
AzureDevOpsWorkItemWatchInput,
|
||||
} from "@/lib/types/azure-devops";
|
||||
import { controlSizingClassName } from "@kandev/ui/control-sizing";
|
||||
|
||||
type Kind = "work-item" | "pull-request";
|
||||
|
||||
@@ -152,9 +153,8 @@ function WatchActions({
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="min-h-11 cursor-pointer"
|
||||
className="cursor-pointer"
|
||||
onClick={onEdit}
|
||||
data-testid={`azure-${kind}-watch-edit-${watch.id}`}
|
||||
>
|
||||
@@ -162,9 +162,8 @@ function WatchActions({
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="min-h-11 cursor-pointer"
|
||||
className="cursor-pointer"
|
||||
onClick={onToggle}
|
||||
data-testid={`azure-${kind}-watch-toggle-${watch.id}`}
|
||||
>
|
||||
@@ -172,21 +171,14 @@ function WatchActions({
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="min-h-11 cursor-pointer"
|
||||
className="cursor-pointer"
|
||||
onClick={onTrigger}
|
||||
data-testid={`azure-${kind}-watch-trigger-${watch.id}`}
|
||||
>
|
||||
<IconPlayerPlay className="h-4 w-4" /> {t("azuredevops:runNow")}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="min-h-11 cursor-pointer"
|
||||
onClick={onReset}
|
||||
>
|
||||
<Button type="button" variant="outline" className="cursor-pointer" onClick={onReset}>
|
||||
<IconRefresh className="h-4 w-4" /> {t("common:reset")}
|
||||
</Button>
|
||||
<WatcherDeleteAction
|
||||
@@ -406,7 +398,7 @@ function WatchEditor({
|
||||
value={pullRequest.status || "active"}
|
||||
onValueChange={(value) => set("status", value)}
|
||||
>
|
||||
<SelectTrigger className="min-h-11">
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
{/* `value` is the Azure DevOps pull-request status sent on the
|
||||
@@ -506,7 +498,7 @@ function WatchEditor({
|
||||
value={current.cleanupPolicy}
|
||||
onValueChange={(value: AzureDevOpsCleanupPolicy) => set("cleanupPolicy", value)}
|
||||
>
|
||||
<SelectTrigger className="min-h-11">
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
{/* `value` is the persisted `AzureDevOpsCleanupPolicy`; only the
|
||||
@@ -530,7 +522,7 @@ function WatchEditor({
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
className="min-h-11 w-full cursor-pointer"
|
||||
className="w-full cursor-pointer"
|
||||
onClick={() => void submit()}
|
||||
disabled={saving}
|
||||
>
|
||||
@@ -548,7 +540,7 @@ function WatchEditor({
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="min-h-11 min-w-11 cursor-pointer"
|
||||
className={controlSizingClassName("icon", "cursor-pointer")}
|
||||
aria-label={t("azuredevops:closeWatchEditor")}
|
||||
data-testid="azure-watch-editor-close"
|
||||
onClick={() => onOpenChange(false)}
|
||||
@@ -569,7 +561,7 @@ function WatchEditor({
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="min-h-11 min-w-11 cursor-pointer"
|
||||
className={controlSizingClassName("icon", "cursor-pointer")}
|
||||
aria-label={t("azuredevops:closeWatchEditor")}
|
||||
data-testid="azure-watch-editor-close"
|
||||
onClick={() => onOpenChange(false)}
|
||||
@@ -658,7 +650,7 @@ export function AzureDevOpsWatchSettings({ workspaceId }: { workspaceId: string
|
||||
action={
|
||||
<Button
|
||||
type="button"
|
||||
className="min-h-11 w-full cursor-pointer sm:w-auto"
|
||||
className="w-full cursor-pointer sm:w-auto"
|
||||
onClick={() => setEditor({ kind: "pull-request" })}
|
||||
data-testid="azure-add-pull-request-watch"
|
||||
>
|
||||
@@ -694,7 +686,7 @@ export function AzureDevOpsWatchSettings({ workspaceId }: { workspaceId: string
|
||||
action={
|
||||
<Button
|
||||
type="button"
|
||||
className="min-h-11 w-full cursor-pointer sm:w-auto"
|
||||
className="w-full cursor-pointer sm:w-auto"
|
||||
onClick={() => setEditor({ kind: "work-item" })}
|
||||
data-testid="azure-add-work-item-watch"
|
||||
>
|
||||
|
||||
@@ -29,6 +29,7 @@ import type {
|
||||
import { markdownComponents, remarkPlugins } from "@/components/shared/markdown-components";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { azureActionDisplayCopy } from "./azure-devops-workspace-defaults";
|
||||
import { controlSizingClassName } from "@kandev/ui/control-sizing";
|
||||
|
||||
type BoardContext = {
|
||||
board: AzureDevOpsBoard;
|
||||
@@ -100,8 +101,7 @@ function AssignmentActions({
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
className="min-h-11 cursor-pointer"
|
||||
className="cursor-pointer"
|
||||
disabled={saving}
|
||||
onClick={() => onChange("assign_current_user")}
|
||||
data-testid="azure-work-item-assign-current-user"
|
||||
@@ -111,9 +111,8 @@ function AssignmentActions({
|
||||
{assignedTo && (
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="min-h-11 cursor-pointer"
|
||||
className="cursor-pointer"
|
||||
disabled={saving}
|
||||
onClick={() => onChange("unassign")}
|
||||
data-testid="azure-work-item-unassign"
|
||||
@@ -155,7 +154,7 @@ function BoardActions({
|
||||
<Select value={columnId} onValueChange={setColumnId}>
|
||||
<SelectTrigger
|
||||
id="azure-work-item-column"
|
||||
className="min-h-11 flex-1"
|
||||
className="flex-1"
|
||||
data-testid="azure-work-item-column"
|
||||
>
|
||||
<SelectValue />
|
||||
@@ -173,7 +172,7 @@ function BoardActions({
|
||||
value={String(columnDone)}
|
||||
onValueChange={(value) => setColumnDone(value === "true")}
|
||||
>
|
||||
<SelectTrigger className="min-h-11 sm:w-40" data-testid="azure-work-item-column-done">
|
||||
<SelectTrigger className="sm:w-40" data-testid="azure-work-item-column-done">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -184,7 +183,7 @@ function BoardActions({
|
||||
)}
|
||||
<Button
|
||||
type="button"
|
||||
className="min-h-11 cursor-pointer"
|
||||
className="cursor-pointer"
|
||||
disabled={
|
||||
saving || (columnId === context.item.columnId && columnDone === context.item.columnDone)
|
||||
}
|
||||
@@ -223,7 +222,7 @@ function QuickActions({
|
||||
key={action.id}
|
||||
type="button"
|
||||
variant="outline"
|
||||
className="min-h-11 cursor-pointer"
|
||||
className="cursor-pointer"
|
||||
onClick={() => onStartTask(item, action)}
|
||||
>
|
||||
<IconPlus className="h-4 w-4" />
|
||||
@@ -285,9 +284,8 @@ function DetailBody({
|
||||
{state.error}
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="min-h-11 cursor-pointer"
|
||||
className="cursor-pointer"
|
||||
onClick={() => void state.refresh()}
|
||||
>
|
||||
<IconRefresh className="h-4 w-4" /> {t("azuredevops:retry")}
|
||||
@@ -371,9 +369,8 @@ function DetailBody({
|
||||
<h3 className="text-sm font-semibold">{t("azuredevops:discussion")}</h3>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="min-h-11 cursor-pointer"
|
||||
className="cursor-pointer"
|
||||
onClick={() => void state.retryComments()}
|
||||
disabled={state.commentsLoading}
|
||||
>
|
||||
@@ -395,7 +392,7 @@ function DetailBody({
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
className="min-h-11 w-full cursor-pointer"
|
||||
className="w-full cursor-pointer"
|
||||
onClick={state.loadOlderComments}
|
||||
disabled={state.commentsLoading}
|
||||
>
|
||||
@@ -458,7 +455,7 @@ export function AzureDevOpsWorkItemDetail({
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="min-h-11 min-w-11 cursor-pointer"
|
||||
className={controlSizingClassName("icon", "cursor-pointer")}
|
||||
>
|
||||
<a
|
||||
href={state.item?.webUrl ?? initialItem?.webUrl}
|
||||
@@ -474,7 +471,7 @@ export function AzureDevOpsWorkItemDetail({
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="min-h-11 min-w-11 cursor-pointer"
|
||||
className={controlSizingClassName("icon", "cursor-pointer")}
|
||||
aria-label={t("azuredevops:closeWorkItemDetails")}
|
||||
data-testid="azure-work-item-detail-close"
|
||||
onClick={() => onOpenChange(false)}
|
||||
|
||||
@@ -9,6 +9,7 @@ import { useFeature } from "@/hooks/domains/features/use-feature";
|
||||
import { useRouter } from "@/lib/routing/client-router";
|
||||
import { linkToTask } from "@/lib/links";
|
||||
import type { Task } from "@/lib/types/http";
|
||||
import { controlSizingClassName } from "@kandev/ui/control-sizing";
|
||||
|
||||
export type CanvasTaskCreateLauncherProps = {
|
||||
workspaceId: string | null;
|
||||
@@ -36,7 +37,7 @@ export function CanvasTaskCreateLauncher({ workspaceId }: CanvasTaskCreateLaunch
|
||||
<>
|
||||
<Button
|
||||
type="button"
|
||||
className="min-h-11 w-full cursor-pointer md:min-h-7 md:w-auto"
|
||||
className={controlSizingClassName("standard", "w-full cursor-pointer md:w-auto")}
|
||||
data-testid="settings-create-canvas"
|
||||
onClick={() => setOpen(true)}
|
||||
>
|
||||
|
||||
@@ -6,6 +6,7 @@ import { IconCheck, IconChevronDown, IconLoader2 } from "@tabler/icons-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { prioritizeSelectedOption, selectorOptionClassName } from "@/lib/utils/selector-options";
|
||||
import { Button } from "@kandev/ui/button";
|
||||
import { controlSizingClassName } from "@kandev/ui/control-sizing";
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
@@ -198,9 +199,10 @@ function ComboboxTrigger({
|
||||
aria-label={ariaLabel}
|
||||
aria-expanded={open}
|
||||
className={cn(
|
||||
controlSizingClassName("standard"),
|
||||
"w-full justify-between",
|
||||
!disabled && "cursor-pointer",
|
||||
touchTarget && "min-h-12",
|
||||
touchTarget && "max-md:min-h-12 [@media(pointer:coarse)]:min-h-12",
|
||||
triggerClassName,
|
||||
)}
|
||||
disabled={disabled}
|
||||
@@ -300,7 +302,12 @@ export const Combobox = memo(function Combobox({
|
||||
{headerAction}
|
||||
</div>
|
||||
) : null}
|
||||
{showSearch && <CommandInput placeholder={searchPlaceholder} className="h-9" />}
|
||||
{showSearch && (
|
||||
<CommandInput
|
||||
placeholder={searchPlaceholder}
|
||||
className={controlSizingClassName("standard")}
|
||||
/>
|
||||
)}
|
||||
<CommandList>
|
||||
<CommandEmpty>{emptyMessage}</CommandEmpty>
|
||||
<OptionsList
|
||||
|
||||
@@ -154,7 +154,7 @@ export const ConfigChatPanel = memo(function ConfigChatPanel({
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
size="icon"
|
||||
className="fixed bottom-[calc(1.5rem+var(--app-status-bar-height))] right-6 z-50 h-12 w-12 cursor-pointer rounded-full shadow-lg"
|
||||
className="fixed bottom-[calc(1.5rem+var(--app-status-bar-height))] right-6 z-50 size-12 max-md:size-12 [@media(pointer:coarse)]:size-12 cursor-pointer rounded-full shadow-lg"
|
||||
aria-label={t("common:configurationChat")}
|
||||
>
|
||||
<IconSparkles className="h-6 w-6" />
|
||||
|
||||
@@ -33,6 +33,7 @@ import type { Repository } from "@/lib/types/http";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { TFunction } from "i18next";
|
||||
import { t } from "@/lib/i18n";
|
||||
import { controlSizingClassName } from "@kandev/ui/control-sizing";
|
||||
|
||||
// The returned strings are never rendered: the only caller uses this as a
|
||||
// boolean gate (`!nameError`), so they stay English rather than becoming dead
|
||||
@@ -113,7 +114,7 @@ function RepositoryLocationFields({
|
||||
onChange={(event) => onNameChange(event.target.value)}
|
||||
placeholder="new-project"
|
||||
autoFocus
|
||||
className="h-11 sm:h-8"
|
||||
className={controlSizingClassName("standard")}
|
||||
/>
|
||||
</label>
|
||||
<div className="space-y-1.5">
|
||||
@@ -131,13 +132,12 @@ function RepositoryLocationFields({
|
||||
onLoadTypedDirectory();
|
||||
}}
|
||||
placeholder="/Users/you/Projects"
|
||||
className="h-11 min-w-0 flex-1 font-mono sm:h-8"
|
||||
className={controlSizingClassName("standard", "min-w-0 flex-1 font-mono")}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="icon-lg"
|
||||
className="size-11 sm:size-8"
|
||||
size="icon"
|
||||
onClick={onLoadTypedDirectory}
|
||||
disabled={!parentPath}
|
||||
aria-label={t("common:browseParentDirectory")}
|
||||
@@ -211,7 +211,7 @@ function CreateRepositoryFooter({
|
||||
<div className="flex shrink-0 justify-end border-t border-border px-4 pt-3 pb-[max(1rem,env(safe-area-inset-bottom))]">
|
||||
<Button
|
||||
type="submit"
|
||||
className="min-h-11 w-full cursor-pointer sm:w-auto sm:min-w-40"
|
||||
className="w-full cursor-pointer sm:w-auto sm:min-w-40"
|
||||
disabled={!canSubmit}
|
||||
>
|
||||
<IconFolderPlus className="h-4 w-4" />
|
||||
|
||||
@@ -23,6 +23,11 @@ import { SettingsPromptEditor } from "@/components/settings/settings-prompt-edit
|
||||
import type { ScriptPlaceholder } from "@/components/settings/profile-edit/script-editor-completions";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import type { TFunction } from "i18next";
|
||||
import { controlSizingClassName } from "@kandev/ui/control-sizing";
|
||||
import {
|
||||
settingsActionClassName,
|
||||
settingsControlClassName,
|
||||
} from "@/components/settings/settings-control";
|
||||
|
||||
// A function, not a const: `description` is copy, and a module-scope `t()` call
|
||||
// would freeze at the boot locale (see docs/i18n.md). `key` is the substitution
|
||||
@@ -72,7 +77,7 @@ function PresetIconSelect({
|
||||
return (
|
||||
<Select value={value} onValueChange={onChange}>
|
||||
<SelectTrigger
|
||||
className="!h-8 py-0.5 text-sm cursor-pointer"
|
||||
className={settingsControlClassName("py-0.5 text-sm cursor-pointer")}
|
||||
aria-label={t("github:icon")}
|
||||
data-settings-dirty={isDirty}
|
||||
>
|
||||
@@ -131,7 +136,7 @@ function PresetRow({
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<span className="text-[10px] text-muted-foreground">{t("github:label")}</span>
|
||||
<Input
|
||||
className="h-8 w-40"
|
||||
className={settingsControlClassName("w-40")}
|
||||
value={preset.label}
|
||||
data-settings-dirty={preset.label !== baseline?.label}
|
||||
placeholder={t("github:label")}
|
||||
@@ -141,7 +146,7 @@ function PresetRow({
|
||||
<div className="flex flex-col gap-0.5 flex-1">
|
||||
<span className="text-[10px] text-muted-foreground">{t("github:hint")}</span>
|
||||
<Input
|
||||
className="h-8"
|
||||
className={settingsControlClassName()}
|
||||
value={preset.hint}
|
||||
data-settings-dirty={preset.hint !== baseline?.hint}
|
||||
placeholder={t("github:hintOptional")}
|
||||
@@ -150,8 +155,7 @@ function PresetRow({
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-8 cursor-pointer text-xs"
|
||||
className={settingsActionClassName("cursor-pointer")}
|
||||
onClick={onToggle}
|
||||
>
|
||||
{expanded ? t("github:hidePrompt") : t("github:editPrompt")}
|
||||
@@ -159,7 +163,7 @@ function PresetRow({
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 cursor-pointer text-destructive"
|
||||
className={controlSizingClassName("icon", "cursor-pointer text-destructive")}
|
||||
onClick={onRemove}
|
||||
aria-label={t("github:remove")}
|
||||
>
|
||||
@@ -256,7 +260,7 @@ function PresetEditor({
|
||||
onRemove={() => remove(index)}
|
||||
/>
|
||||
))}
|
||||
<Button size="sm" variant="outline" onClick={add} className="cursor-pointer">
|
||||
<Button variant="outline" onClick={add} className={settingsActionClassName("cursor-pointer")}>
|
||||
<IconPlus className="h-3.5 w-3.5 mr-1" />
|
||||
{addLabel}
|
||||
</Button>
|
||||
@@ -385,11 +389,10 @@ export function ActionPresetsSection({ workspaceId }: { workspaceId: string }) {
|
||||
action={
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={reset}
|
||||
disabled={loading}
|
||||
className="cursor-pointer"
|
||||
className={settingsActionClassName("cursor-pointer")}
|
||||
>
|
||||
<IconRefresh className="h-3.5 w-3.5 mr-1" />
|
||||
{t("common:reset")}
|
||||
|
||||
@@ -21,6 +21,11 @@ import {
|
||||
ISSUE_PRESETS as BUILTIN_ISSUE_PRESETS,
|
||||
} from "@/components/github/my-github/search-bar";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { controlSizingClassName } from "@kandev/ui/control-sizing";
|
||||
import {
|
||||
settingsActionClassName,
|
||||
settingsControlClassName,
|
||||
} from "@/components/settings/settings-control";
|
||||
|
||||
// `label` is PERSISTED as part of `StoredQueryPreset` (github_default_query_presets)
|
||||
// and is editable in the row below, so it must stay locale-neutral — see
|
||||
@@ -56,7 +61,7 @@ function QueryRow({
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<span className="text-[10px] text-muted-foreground">{t("github:label")}</span>
|
||||
<Input
|
||||
className="h-8 w-36"
|
||||
className={settingsControlClassName("w-36")}
|
||||
value={preset.label}
|
||||
data-settings-dirty={preset.label !== baseline?.label}
|
||||
placeholder={t("github:label")}
|
||||
@@ -66,7 +71,7 @@ function QueryRow({
|
||||
<div className="flex flex-col gap-0.5 flex-1">
|
||||
<span className="text-[10px] text-muted-foreground">{t("github:query")}</span>
|
||||
<Input
|
||||
className="h-8 font-mono text-xs"
|
||||
className={settingsControlClassName("font-mono text-xs")}
|
||||
value={preset.filter}
|
||||
data-settings-dirty={preset.filter !== baseline?.filter}
|
||||
placeholder={t("github:queryExample", { query: "review-requested:@me is:open" })}
|
||||
@@ -80,7 +85,7 @@ function QueryRow({
|
||||
onValueChange={(v) => onPatch({ group: v as "inbox" | "created" })}
|
||||
>
|
||||
<SelectTrigger
|
||||
className="h-8 w-28 cursor-pointer"
|
||||
className={settingsControlClassName("w-28 cursor-pointer")}
|
||||
data-settings-dirty={preset.group !== baseline?.group}
|
||||
>
|
||||
<SelectValue />
|
||||
@@ -98,7 +103,7 @@ function QueryRow({
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 cursor-pointer text-destructive mt-3.5"
|
||||
className={controlSizingClassName("icon", "cursor-pointer text-destructive mt-3.5")}
|
||||
onClick={onRemove}
|
||||
aria-label={t("github:remove")}
|
||||
>
|
||||
@@ -142,7 +147,7 @@ function QueryEditor({
|
||||
onRemove={() => remove(index)}
|
||||
/>
|
||||
))}
|
||||
<Button size="sm" variant="outline" onClick={add} className="cursor-pointer">
|
||||
<Button variant="outline" onClick={add} className={settingsActionClassName("cursor-pointer")}>
|
||||
<IconPlus className="h-3.5 w-3.5 mr-1" />
|
||||
{addLabel}
|
||||
</Button>
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
} from "@kandev/ui/drawer";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@kandev/ui/tooltip";
|
||||
import { useTouchDrawer } from "@/hooks/use-compact-task-chrome";
|
||||
import { controlSizingClassName } from "@kandev/ui/control-sizing";
|
||||
|
||||
export function GitHubAccessHelp({
|
||||
label,
|
||||
@@ -40,7 +41,7 @@ export function GitHubAccessHelp({
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-11 w-11 shrink-0 cursor-pointer text-muted-foreground sm:h-6 sm:w-6"
|
||||
className={controlSizingClassName("icon", "shrink-0 cursor-pointer text-muted-foreground")}
|
||||
aria-haspopup={usesTouchDrawer ? "dialog" : undefined}
|
||||
aria-expanded={usesTouchDrawer ? open : undefined}
|
||||
aria-label={label}
|
||||
|
||||
@@ -91,17 +91,17 @@ export function GitHubAppConnectionPanel({ workspaceId }: { workspaceId: string
|
||||
<Button
|
||||
disabled={selectedRegistration?.status !== "active" || registrations.mutating}
|
||||
onClick={() => void install()}
|
||||
className="h-11 cursor-pointer"
|
||||
className="cursor-pointer"
|
||||
data-testid="github-app-install-button"
|
||||
>
|
||||
{registrations.mutating && <Spinner className="mr-2 h-4 w-4" />}
|
||||
{t("github:installForThisWorkspace")}
|
||||
<IconExternalLink className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
<Button variant="outline" className="h-11 cursor-pointer" onClick={() => setView("import")}>
|
||||
<Button variant="outline" className="cursor-pointer" onClick={() => setView("import")}>
|
||||
<IconPlus className="mr-2 h-4 w-4" /> {t("github:addExistingApp")}
|
||||
</Button>
|
||||
<Button variant="outline" className="h-11 cursor-pointer" onClick={() => setView("create")}>
|
||||
<Button variant="outline" className="cursor-pointer" onClick={() => setView("create")}>
|
||||
<IconPlus className="mr-2 h-4 w-4" /> {t("github:createNewApp")}
|
||||
</Button>
|
||||
</div>
|
||||
@@ -131,7 +131,7 @@ function useAppRegistrationSelection(workspaceId: string, registrations: Registr
|
||||
function BackButton({ onClick }: { onClick: () => void }) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Button variant="ghost" className="h-11 cursor-pointer px-2" onClick={onClick}>
|
||||
<Button variant="ghost" className="cursor-pointer px-2" onClick={onClick}>
|
||||
{t("github:backToRegisteredApps")}
|
||||
</Button>
|
||||
);
|
||||
|
||||
@@ -120,13 +120,13 @@ function ManifestHandoff({
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 sm:flex-row">
|
||||
<Button
|
||||
className="h-11 cursor-pointer"
|
||||
className="cursor-pointer"
|
||||
onClick={() => submitManifestToGitHub(handoff.registration_url, handoff.manifest)}
|
||||
>
|
||||
{t("github:continueOnGithub")}
|
||||
<IconExternalLink className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
<Button variant="outline" className="h-11 cursor-pointer" onClick={onEdit}>
|
||||
<Button variant="outline" className="cursor-pointer" onClick={onEdit}>
|
||||
{t("github:editDetails")}
|
||||
</Button>
|
||||
</div>
|
||||
@@ -163,14 +163,12 @@ function CreateAppFields(props: CreateFieldsProps) {
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
<Field label={t("github:nameInKandev")} error={props.errors.displayName}>
|
||||
<Input
|
||||
className="h-11"
|
||||
value={props.displayName}
|
||||
onChange={(event) => props.onDisplayName(event.target.value)}
|
||||
/>
|
||||
</Field>
|
||||
<Field label={t("github:githubOwnerLogin")} error={props.errors.ownerLogin}>
|
||||
<Input
|
||||
className="h-11"
|
||||
value={props.ownerLogin}
|
||||
onChange={(event) => props.onOwnerLogin(event.target.value)}
|
||||
/>
|
||||
@@ -190,7 +188,6 @@ function CreateAppFields(props: CreateFieldsProps) {
|
||||
</RadioGroup>
|
||||
<Field label={t("github:publicKandevUrl")} error={props.errors.publicBaseUrl}>
|
||||
<Input
|
||||
className="h-11"
|
||||
type="url"
|
||||
placeholder="https://kandev.example.com"
|
||||
value={props.publicBaseUrl}
|
||||
@@ -200,7 +197,7 @@ function CreateAppFields(props: CreateFieldsProps) {
|
||||
<p className="text-xs text-muted-foreground">{t("github:githubMustReachThisHttpsOrigin")}</p>
|
||||
<GitHubAppVisibilityField value={props.visibility} onChange={props.onVisibility} />
|
||||
<div className="flex flex-col gap-2 sm:flex-row">
|
||||
<Button type="submit" disabled={props.mutating} className="h-11 cursor-pointer">
|
||||
<Button type="submit" disabled={props.mutating} className="cursor-pointer">
|
||||
{props.mutating && <Spinner className="mr-2 h-4 w-4" />}
|
||||
{t("github:prepareAppOnGithub")}
|
||||
<IconExternalLink className="ml-2 h-4 w-4" />
|
||||
|
||||
@@ -37,24 +37,15 @@ export function GitHubAppImportIdentityFields({
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
<Field label={t("github:nameInKandev")} error={errors.displayName}>
|
||||
<Input
|
||||
className="h-11"
|
||||
value={values.displayName}
|
||||
onChange={(e) => update("displayName", e.target.value)}
|
||||
/>
|
||||
</Field>
|
||||
<Field label={t("github:githubAppSlug")} error={errors.slug}>
|
||||
<Input
|
||||
className="h-11"
|
||||
value={values.slug}
|
||||
onChange={(e) => update("slug", e.target.value)}
|
||||
/>
|
||||
<Input value={values.slug} onChange={(e) => update("slug", e.target.value)} />
|
||||
</Field>
|
||||
<Field label={t("github:githubOwnerLogin")} error={errors.ownerLogin}>
|
||||
<Input
|
||||
className="h-11"
|
||||
value={values.ownerLogin}
|
||||
onChange={(e) => update("ownerLogin", e.target.value)}
|
||||
/>
|
||||
<Input value={values.ownerLogin} onChange={(e) => update("ownerLogin", e.target.value)} />
|
||||
</Field>
|
||||
</div>
|
||||
<RadioGroup
|
||||
@@ -96,7 +87,6 @@ export function GitHubAppImportSecretFields({
|
||||
{(Object.keys(labels) as (keyof typeof labels)[]).map((name) => (
|
||||
<Field key={name} label={labels[name]} error={errors[name]}>
|
||||
<Input
|
||||
className="h-11"
|
||||
type={name.includes("Secret") || name === "webhookSecret" ? "password" : "text"}
|
||||
autoComplete="off"
|
||||
value={values[name]}
|
||||
|
||||
@@ -149,14 +149,13 @@ function PrepareImportForm(props: {
|
||||
</div>
|
||||
<Field label={t("github:publicKandevUrl")} error={props.error}>
|
||||
<Input
|
||||
className="h-11"
|
||||
type="url"
|
||||
placeholder="https://kandev.example.com"
|
||||
value={props.publicBaseUrl}
|
||||
onChange={(event) => props.onPublicBaseUrl(event.target.value)}
|
||||
/>
|
||||
</Field>
|
||||
<Button type="submit" disabled={props.mutating} className="h-11 cursor-pointer">
|
||||
<Button type="submit" disabled={props.mutating} className="cursor-pointer">
|
||||
{props.mutating && <Spinner className="mr-2 h-4 w-4" />}
|
||||
{t("github:generateSetupInstructions")}
|
||||
</Button>
|
||||
@@ -193,14 +192,14 @@ function PreparedImportForm(props: PreparedImportProps) {
|
||||
update={props.update}
|
||||
/>
|
||||
<div className="flex flex-col gap-2 sm:flex-row">
|
||||
<Button type="submit" disabled={props.mutating} className="h-11 cursor-pointer">
|
||||
<Button type="submit" disabled={props.mutating} className="cursor-pointer">
|
||||
{props.mutating && <Spinner className="mr-2 h-4 w-4" />}
|
||||
{t("github:verifyAndImportApp")}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
className="h-11 cursor-pointer"
|
||||
className="cursor-pointer"
|
||||
onClick={props.onStartOver}
|
||||
>
|
||||
{t("github:startOver")}
|
||||
|
||||
@@ -67,7 +67,7 @@ export function GitHubAppImportGuide({
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-11 w-11 shrink-0 cursor-pointer"
|
||||
className="shrink-0 cursor-pointer"
|
||||
aria-label={t("github:copy", { label })}
|
||||
onClick={() => void copyValue(value)}
|
||||
>
|
||||
@@ -84,14 +84,14 @@ export function GitHubAppImportGuide({
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 sm:flex-row">
|
||||
{settingsUrl ? (
|
||||
<Button asChild variant="outline" className="h-11 cursor-pointer">
|
||||
<Button asChild variant="outline" className="cursor-pointer">
|
||||
<a href={settingsUrl} target="_blank" rel="noreferrer">
|
||||
{t("github:openGithubAppSettings")}
|
||||
<IconExternalLink className="ml-2 h-4 w-4" />
|
||||
</a>
|
||||
</Button>
|
||||
) : (
|
||||
<Button type="button" variant="outline" className="h-11" disabled>
|
||||
<Button type="button" variant="outline" disabled>
|
||||
{t("github:openGithubAppSettings")}
|
||||
<IconExternalLink className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
|
||||
@@ -42,7 +42,7 @@ export function GitHubAppPolicyDialog({
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline" className="h-11 cursor-pointer">
|
||||
<Button variant="outline" className="cursor-pointer">
|
||||
<IconShieldCheck className="mr-2 h-4 w-4" />
|
||||
{t("github:reviewPermissions")}
|
||||
</Button>
|
||||
|
||||
@@ -113,7 +113,7 @@ export function GitHubCLIForm({
|
||||
onValueChange={setSelected}
|
||||
disabled={disabled || loading || !accounts.length}
|
||||
>
|
||||
<SelectTrigger id="github-cli-account" className="min-h-11 min-w-0 flex-1">
|
||||
<SelectTrigger id="github-cli-account" className="min-w-0 sm:flex-1">
|
||||
<SelectValue placeholder={accountPlaceholder(t, loading, loadError)} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
|
||||
@@ -25,6 +25,7 @@ import type { GitHubStatus } from "@/lib/types/github";
|
||||
import type { GitHubAutomationMethod } from "./github-auth-method-list";
|
||||
import { GitHubConnectionSettingsForm } from "./github-connection-settings-form";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { controlSizingClassName } from "@kandev/ui/control-sizing";
|
||||
|
||||
function methodForStatus(status: GitHubStatus): GitHubAutomationMethod {
|
||||
if (status.automation?.source === "github_app_installation") return "app";
|
||||
@@ -65,7 +66,10 @@ export function GitHubConnectionDialog({
|
||||
[status],
|
||||
);
|
||||
const trigger = (
|
||||
<Button variant={connected ? "outline" : "default"} className="h-11 cursor-pointer">
|
||||
<Button
|
||||
variant={connected ? "outline" : "default"}
|
||||
className={controlSizingClassName("standard", "cursor-pointer")}
|
||||
>
|
||||
<IconPlug className="mr-2 h-4 w-4" />
|
||||
{connected ? t("github:changeConnection") : t("github:connectGithub")}
|
||||
</Button>
|
||||
|
||||
@@ -284,7 +284,7 @@ export function GitHubConnectionSettingsForm(props: SettingsFormProps) {
|
||||
type="button"
|
||||
disabled={!canSave}
|
||||
onClick={save}
|
||||
className="h-11 w-full cursor-pointer sm:w-auto"
|
||||
className="w-full cursor-pointer sm:w-auto"
|
||||
data-dialog-default-action
|
||||
>
|
||||
{saving && <Spinner className="mr-2 h-4 w-4" />}
|
||||
|
||||
@@ -42,13 +42,13 @@ export function GitHubPATForm({
|
||||
placeholder="ghp_xxxxxxxxxxxx"
|
||||
autoComplete="off"
|
||||
disabled={disabled}
|
||||
className={settingsCredentialClassName("h-11 pr-11")}
|
||||
className={settingsCredentialClassName("pr-11")}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="absolute right-0 top-0 h-11 w-11 cursor-pointer"
|
||||
className="absolute right-0 top-0 cursor-pointer"
|
||||
onClick={() => setVisible((current) => !current)}
|
||||
disabled={disabled}
|
||||
aria-label={visible ? t("github:hideToken") : t("github:showToken")}
|
||||
|
||||
@@ -47,7 +47,7 @@ export function GitHubPermissionsDialog({ status }: { status: GitHubStatus }) {
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline" className="h-11 cursor-pointer">
|
||||
<Button variant="outline" className="cursor-pointer">
|
||||
{missingCount > 0 ? (
|
||||
<IconAlertTriangle className="mr-2 h-4 w-4 text-amber-500" />
|
||||
) : (
|
||||
|
||||
@@ -33,6 +33,7 @@ import type {
|
||||
} from "@/lib/types/github";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { t as translate } from "@/lib/i18n";
|
||||
import { controlSizingClassName } from "@kandev/ui/control-sizing";
|
||||
|
||||
function splitCSV(value: string): string[] {
|
||||
return value
|
||||
@@ -86,7 +87,7 @@ function RepositoryScopeHelp() {
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-11 w-11 cursor-pointer text-muted-foreground sm:h-7 sm:w-7"
|
||||
className={controlSizingClassName("icon", "cursor-pointer text-muted-foreground")}
|
||||
aria-haspopup="dialog"
|
||||
aria-expanded={open}
|
||||
aria-label={t("github:explainRepositoryScope")}
|
||||
|
||||
@@ -39,6 +39,7 @@ import { GitHubRateLimitDisplay } from "./github-rate-limit";
|
||||
import { GitHubTaskAccessSummary } from "./github-task-credentials-section";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { TFunction } from "i18next";
|
||||
import { controlSizingClassName } from "@kandev/ui/control-sizing";
|
||||
|
||||
// Keyed by the wire enum, which is never translated; only the label is copy.
|
||||
// Catalog keys rather than `t()` calls because this is module scope — a `t()`
|
||||
@@ -237,7 +238,7 @@ function AutomationActions({
|
||||
onClick={onRefresh}
|
||||
disabled={refreshing}
|
||||
aria-busy={refreshing}
|
||||
className="h-11 w-11 cursor-pointer"
|
||||
className={controlSizingClassName("icon", "cursor-pointer")}
|
||||
aria-label={t("github:refreshGithubConnection")}
|
||||
>
|
||||
<IconRefresh className={`h-4 w-4 ${refreshing ? "animate-spin" : ""}`} />
|
||||
@@ -247,7 +248,7 @@ function AutomationActions({
|
||||
variant="outline"
|
||||
onClick={onDisconnect}
|
||||
disabled={busy}
|
||||
className="h-11 cursor-pointer text-destructive"
|
||||
className="cursor-pointer text-destructive"
|
||||
>
|
||||
<IconTrash className="mr-2 h-4 w-4" />
|
||||
{t("github:disconnect")}
|
||||
@@ -357,7 +358,7 @@ function PersonalIdentityActions({
|
||||
return (
|
||||
<div className="flex flex-col gap-2 sm:flex-row">
|
||||
{status.app_available && status.automation?.source === "github_app_installation" && (
|
||||
<Button disabled={busy} onClick={onConnect} className="h-11 cursor-pointer">
|
||||
<Button disabled={busy} onClick={onConnect} className="cursor-pointer">
|
||||
<IconBrandGithub className="mr-2 h-4 w-4" />
|
||||
{status.personal ? t("github:reconnectIdentity") : t("github:connectIdentity")}
|
||||
<IconExternalLink className="ml-2 h-4 w-4" />
|
||||
@@ -368,7 +369,7 @@ function PersonalIdentityActions({
|
||||
variant="outline"
|
||||
onClick={onDisconnect}
|
||||
disabled={busy}
|
||||
className="h-11 cursor-pointer text-destructive"
|
||||
className="cursor-pointer text-destructive"
|
||||
>
|
||||
<IconTrash className="mr-2 h-4 w-4" />
|
||||
{t("github:disconnect")}
|
||||
|
||||
@@ -41,7 +41,7 @@ function StartTaskMenu({
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button size="sm" variant="outline" className="h-7 gap-1 cursor-pointer">
|
||||
<Button size="sm" variant="outline" className="gap-1 cursor-pointer">
|
||||
<IconPlus className="h-3.5 w-3.5" />
|
||||
{t("github:task")}
|
||||
<IconChevronDown className="h-3 w-3" />
|
||||
|
||||
@@ -51,7 +51,7 @@ export function ListToolbar({
|
||||
onRepoFilterChange={onRepoFilterChange}
|
||||
repoOptions={repoOptions}
|
||||
ariaLabel={t("github:filterGithubResultsByRepository")}
|
||||
triggerClassName="h-8 w-full border border-input bg-background px-2 py-1.5 text-xs/relaxed hover:bg-secondary/50 md:w-[220px]"
|
||||
triggerClassName="w-full border border-input bg-background px-2 text-xs/relaxed hover:bg-secondary/50 md:w-[220px]"
|
||||
className="md:min-w-[360px]"
|
||||
testId="github-repo-filter-trigger"
|
||||
dropdownTestId="github-repo-filter-dropdown"
|
||||
|
||||
@@ -96,7 +96,7 @@ function SavePresetForm({
|
||||
onRepoFilterChange={setDefaultRepoFilter}
|
||||
repoOptions={repoOptions}
|
||||
ariaLabel={t("github:defaultRepository")}
|
||||
triggerClassName="h-11 border border-input bg-background px-3 py-2 text-sm hover:bg-secondary/50 md:h-9 md:py-1.5"
|
||||
triggerClassName="border border-input bg-background px-3 text-sm hover:bg-secondary/50"
|
||||
testId="github-save-query-repo-trigger"
|
||||
dropdownTestId="github-save-query-repo-dropdown"
|
||||
/>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useTranslation } from "react-i18next";
|
||||
import { setPanelTitle } from "@/lib/layout/panel-portal-manager";
|
||||
import { IconCheck } from "@tabler/icons-react";
|
||||
import { Button } from "@kandev/ui/button";
|
||||
import { controlSizingClassName } from "@kandev/ui/control-sizing";
|
||||
import { useAppStore } from "@/components/state-provider";
|
||||
import {
|
||||
ChangeRequestDetail,
|
||||
@@ -230,8 +231,10 @@ function ApproveButton({
|
||||
return (
|
||||
<Button
|
||||
data-testid="pr-approve-button"
|
||||
size="sm"
|
||||
className="cursor-pointer gap-1.5 border-0 bg-green-600 text-white hover:bg-green-700 dark:bg-green-600 dark:hover:bg-green-500"
|
||||
className={controlSizingClassName(
|
||||
"standard",
|
||||
"cursor-pointer gap-1.5 border-0 bg-green-600 text-white hover:bg-green-700 dark:bg-green-600 dark:hover:bg-green-500",
|
||||
)}
|
||||
onClick={handleApprove}
|
||||
disabled={submitting}
|
||||
>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { useState } from "react";
|
||||
import { IconChevronDown, IconGitMerge } from "@tabler/icons-react";
|
||||
import { Button } from "@kandev/ui/button";
|
||||
import { controlSizingClassName } from "@kandev/ui/control-sizing";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -213,8 +214,10 @@ function MergeButtonShell({
|
||||
const primaryBtn = (
|
||||
<Button
|
||||
data-testid="pr-merge-button"
|
||||
size="sm"
|
||||
className={`min-h-11 cursor-pointer gap-1.5 border-0 bg-green-600 text-white hover:bg-green-700 dark:bg-green-600 dark:hover:bg-green-500 ${showDropdown ? "rounded-r-none" : ""}`}
|
||||
className={controlSizingClassName(
|
||||
"standard",
|
||||
`cursor-pointer gap-1.5 border-0 bg-green-600 text-white hover:bg-green-700 dark:bg-green-600 dark:hover:bg-green-500 ${showDropdown ? "rounded-r-none" : ""}`,
|
||||
)}
|
||||
onClick={onPrimaryClick}
|
||||
disabled={disabled}
|
||||
>
|
||||
|
||||
@@ -62,7 +62,9 @@ describe("ReviewsSection re-request action", () => {
|
||||
|
||||
const action = screen.getByRole("button", { name: "Re-request review from octocat" });
|
||||
expect(action).toHaveProperty("disabled", true);
|
||||
expect(action.className).toContain("min-h-11");
|
||||
expect(action.className).toContain("h-7");
|
||||
expect(action.className).toContain("max-md:h-11");
|
||||
expect(action.className).toContain("[@media(pointer:coarse)]:h-11");
|
||||
fireEvent.click(action);
|
||||
expect(onReRequest).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -86,7 +88,8 @@ describe("ReviewsSection re-request action", () => {
|
||||
name: `Re-request review from ${longLogin}`,
|
||||
});
|
||||
const author = screen.getByRole("link", { name: longLogin });
|
||||
expect(action.className).toContain("min-h-11");
|
||||
expect(action.className).toContain("max-md:h-11");
|
||||
expect(action.className).toContain("[@media(pointer:coarse)]:h-11");
|
||||
expect(action.parentElement?.className).toContain("basis-full");
|
||||
expect(author.className).toContain("inline-block");
|
||||
expect(author.className).toContain("max-w-full");
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Button } from "@kandev/ui/button";
|
||||
import type { PRReview, RequestedReviewer } from "@/lib/types/github";
|
||||
import { CollapsibleSection, FeedbackItemRow } from "./pr-shared";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { controlSizingClassName } from "@kandev/ui/control-sizing";
|
||||
|
||||
function ReviewStateIcon({ state }: { state: string }) {
|
||||
if (state === "APPROVED") return <IconCheck className="h-3.5 w-3.5 text-green-500 shrink-0" />;
|
||||
@@ -157,9 +158,11 @@ function SubmittedReviewRow({
|
||||
<div className="basis-full w-full sm:basis-auto sm:w-auto">
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="h-8 min-h-11 w-full max-w-full sm:w-auto sm:min-h-0 shrink-0 cursor-pointer px-2 text-[10px] [@media(pointer:coarse)]:min-h-11"
|
||||
className={controlSizingClassName(
|
||||
"standard",
|
||||
"w-full max-w-full shrink-0 cursor-pointer px-2 text-[10px] sm:w-auto",
|
||||
)}
|
||||
data-testid={`pr-rerequest-review-${normalizeLogin(review.author)}`}
|
||||
aria-label={t("github:reRequestReviewFrom", { author: review.author })}
|
||||
onClick={() => onReRequest(review.author)}
|
||||
|
||||
@@ -15,6 +15,8 @@ import { useGitLabActionPresets } from "@/hooks/domains/gitlab/use-gitlab-action
|
||||
import type { GitLabActionPreset, GitLabActionPresets } from "@/lib/types/gitlab";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { TFunction } from "i18next";
|
||||
import { controlSizingClassName } from "@kandev/ui/control-sizing";
|
||||
import { settingsActionClassName } from "@/components/settings/settings-control";
|
||||
|
||||
function actionPromptPlaceholders(t: TFunction) {
|
||||
return [
|
||||
@@ -84,7 +86,7 @@ function PresetList({
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-11 w-11 cursor-pointer text-destructive sm:h-8 sm:w-8"
|
||||
className={controlSizingClassName("icon", "cursor-pointer text-destructive")}
|
||||
aria-label={t("gitlab:remove", { label: preset.label })}
|
||||
onClick={() => onChange(presets.filter((_, current) => current !== index))}
|
||||
>
|
||||
@@ -103,9 +105,8 @@ function PresetList({
|
||||
))}
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="h-11 w-full cursor-pointer sm:h-8 sm:w-auto"
|
||||
className={settingsActionClassName("w-full cursor-pointer sm:w-auto")}
|
||||
onClick={() => onChange([...presets, newPreset()])}
|
||||
>
|
||||
<IconPlus className="h-4 w-4" />
|
||||
@@ -193,9 +194,8 @@ export function GitLabActionPresetsSection({ workspaceId }: { workspaceId: strin
|
||||
action={
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="h-11 cursor-pointer sm:h-8"
|
||||
className={settingsActionClassName("cursor-pointer")}
|
||||
disabled={loading}
|
||||
aria-label={t("gitlab:resetQuickActionsToDefaults")}
|
||||
onClick={() => void resetDefaults()}
|
||||
|
||||
@@ -55,6 +55,7 @@ import { MRDiscussionsSection } from "./mr-discussions-section";
|
||||
import { MRReviewerControl } from "./mr-reviewer-control";
|
||||
import { SubscriptionToggle } from "./subscription-toggle";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { controlSizingClassName } from "@kandev/ui/control-sizing";
|
||||
|
||||
type MRKeyInput = Pick<TaskMR, "host" | "project_path" | "mr_iid">;
|
||||
const EMPTY_LABELS: string[] = [];
|
||||
@@ -114,9 +115,8 @@ function LabelsControl({
|
||||
aria-label={t("gitlab:mergeRequestLabels")}
|
||||
/>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="h-11 shrink-0 cursor-pointer sm:h-9"
|
||||
className={controlSizingClassName("standard", "shrink-0 cursor-pointer")}
|
||||
disabled={busy}
|
||||
onClick={() =>
|
||||
onSave(
|
||||
@@ -159,27 +159,27 @@ function PanelHeader({
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
size="icon-sm"
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="h-10 w-10 cursor-pointer sm:h-8 sm:w-8"
|
||||
className={controlSizingClassName("icon", "cursor-pointer")}
|
||||
aria-label={t("gitlab:refreshMergeRequest")}
|
||||
onClick={onRefresh}
|
||||
>
|
||||
<IconRefresh className={`h-4 w-4 ${loading ? "animate-spin" : ""}`} />
|
||||
</Button>
|
||||
<Button
|
||||
size="icon-sm"
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="h-10 w-10 cursor-pointer sm:h-8 sm:w-8"
|
||||
className={controlSizingClassName("icon", "cursor-pointer")}
|
||||
aria-label={t("gitlab:openMergeRequestInGitlab")}
|
||||
onClick={() => void openExternalLink(mr.mr_url).catch(() => undefined)}
|
||||
>
|
||||
<IconExternalLink className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
size="icon-sm"
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="h-10 w-10 cursor-pointer text-destructive sm:h-8 sm:w-8"
|
||||
className={controlSizingClassName("icon", "cursor-pointer text-destructive")}
|
||||
aria-label={t("gitlab:unlinkMergeRequest")}
|
||||
onClick={onUnlink}
|
||||
>
|
||||
@@ -210,26 +210,26 @@ function MRActionButtons({
|
||||
return (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
className="h-11 cursor-pointer gap-1.5 bg-green-600 text-white hover:bg-green-700 sm:h-8"
|
||||
className={controlSizingClassName(
|
||||
"standard",
|
||||
"cursor-pointer gap-1.5 bg-green-600 text-white hover:bg-green-700",
|
||||
)}
|
||||
disabled={disabled}
|
||||
onClick={() => void run("approve", () => approveMR(identity))}
|
||||
>
|
||||
<IconCheck className="h-4 w-4" /> {t("gitlab:approve")}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="h-11 cursor-pointer sm:h-8"
|
||||
className={controlSizingClassName("standard", "cursor-pointer")}
|
||||
disabled={disabled}
|
||||
onClick={() => void run("unapprove", () => unapproveMR(identity))}
|
||||
>
|
||||
{t("gitlab:unapprove")}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="h-11 cursor-pointer gap-1.5 sm:h-8"
|
||||
className={controlSizingClassName("standard", "cursor-pointer gap-1.5")}
|
||||
disabled={disabled || hasConflicts}
|
||||
onClick={onMerge}
|
||||
>
|
||||
@@ -330,7 +330,11 @@ function FeedbackPlaceholder({ error, onRetry }: { error: string | null; onRetry
|
||||
return (
|
||||
<div className="p-4 text-sm text-destructive">
|
||||
{error}
|
||||
<Button variant="outline" size="sm" className="ml-2 cursor-pointer" onClick={onRetry}>
|
||||
<Button
|
||||
variant="outline"
|
||||
className={controlSizingClassName("standard", "ml-2 cursor-pointer")}
|
||||
onClick={onRetry}
|
||||
>
|
||||
{t("gitlab:retry")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -8,6 +8,7 @@ import { Textarea } from "@kandev/ui/textarea";
|
||||
import type { GitLabMRDiscussion } from "@/lib/types/gitlab";
|
||||
import { CollapsibleSection, formatTimeAgo, PRMarkdownBody } from "@/components/github/pr-shared";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { controlSizingClassName } from "@kandev/ui/control-sizing";
|
||||
|
||||
function discussionLocation(discussion: GitLabMRDiscussion): string {
|
||||
if (!discussion.path) return "";
|
||||
@@ -78,9 +79,9 @@ function Discussion({
|
||||
</Badge>
|
||||
)}
|
||||
<Button
|
||||
size="icon-sm"
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="ml-auto h-9 w-9 shrink-0 cursor-pointer sm:h-7 sm:w-7"
|
||||
className={controlSizingClassName("icon", "ml-auto shrink-0 cursor-pointer")}
|
||||
aria-label={t("gitlab:addDiscussionToTaskContext")}
|
||||
onClick={() => onAddContext(buildDiscussionContext(discussion, mrUrl))}
|
||||
>
|
||||
@@ -111,9 +112,8 @@ function Discussion({
|
||||
<div className="flex gap-2">
|
||||
{discussion.resolvable && !discussion.resolved && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="h-11 flex-1 cursor-pointer gap-1 sm:h-9"
|
||||
className={controlSizingClassName("standard", "flex-1 cursor-pointer gap-1")}
|
||||
disabled={busy}
|
||||
onClick={() => void onResolve(discussion.id)}
|
||||
>
|
||||
@@ -121,8 +121,7 @@ function Discussion({
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
size="sm"
|
||||
className="h-11 flex-1 cursor-pointer gap-1 sm:h-9"
|
||||
className={controlSizingClassName("standard", "flex-1 cursor-pointer gap-1")}
|
||||
disabled={busy || !reply.trim()}
|
||||
onClick={() => void submitReply()}
|
||||
>
|
||||
|
||||
@@ -10,6 +10,7 @@ import { listProjectMembers } from "@/lib/api/domains/gitlab-api";
|
||||
import type { GitLabMRUser, GitLabProjectMember } from "@/lib/types/gitlab";
|
||||
import { isCurrentIdentityRequest } from "@/hooks/domains/gitlab/request-identity";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { controlSizingClassName } from "@kandev/ui/control-sizing";
|
||||
|
||||
export function toggleMemberId(ids: number[], id: number): number[] {
|
||||
return ids.includes(id) ? ids.filter((candidate) => candidate !== id) : [...ids, id];
|
||||
@@ -49,7 +50,7 @@ function SelectedMemberBadges({
|
||||
{member.username}
|
||||
<button
|
||||
type="button"
|
||||
className="flex h-11 w-11 cursor-pointer items-center justify-center sm:h-6 sm:w-6"
|
||||
className={`${controlSizingClassName("icon")} cursor-pointer`}
|
||||
aria-label={t(
|
||||
kind === "reviewers" ? "gitlab:removeFromReviewers" : "gitlab:removeFromAssignees",
|
||||
{ username: member.username },
|
||||
@@ -146,9 +147,8 @@ export function MRReviewerControl({
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<h4 className="text-xs font-semibold">{sectionLabel}</h4>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="h-11 cursor-pointer sm:h-8"
|
||||
className={controlSizingClassName("standard", "cursor-pointer")}
|
||||
disabled={busy || !changed}
|
||||
onClick={() => void onSave(selectedIds)}
|
||||
>
|
||||
@@ -178,7 +178,7 @@ export function MRReviewerControl({
|
||||
<Button
|
||||
size="icon"
|
||||
variant="outline"
|
||||
className="h-11 w-11 shrink-0 cursor-pointer sm:h-9 sm:w-9"
|
||||
className={controlSizingClassName("icon", "shrink-0 cursor-pointer")}
|
||||
aria-label={searchLabel}
|
||||
disabled={searching}
|
||||
onClick={() => void search()}
|
||||
|
||||
@@ -84,7 +84,7 @@ function MilestoneFilterInput({
|
||||
}}
|
||||
placeholder={t("gitlab:eGSprint42")}
|
||||
aria-label={t("gitlab:milestoneFilterLabel")}
|
||||
className="h-11 w-full md:h-8 md:w-[180px]"
|
||||
className="w-full md:w-[180px]"
|
||||
data-testid="gitlab-milestone-filter"
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -135,9 +135,8 @@ export function SubscriptionToggle(identity: SubscriptionIdentity) {
|
||||
const Icon = subscribed ? IconBellOff : IconBell;
|
||||
return (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="h-11 cursor-pointer gap-1.5 sm:h-8"
|
||||
className="cursor-pointer gap-1.5"
|
||||
disabled={loading}
|
||||
aria-label={label}
|
||||
title={label}
|
||||
|
||||
@@ -440,17 +440,13 @@ export function GitLabWatchDialog({
|
||||
<ScheduleFields kind={kind} form={form} setForm={setForm} />
|
||||
</div>
|
||||
<DialogFooter className="gap-2 sm:gap-0">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => onOpenChange(false)}
|
||||
className="min-h-11 cursor-pointer sm:min-h-9"
|
||||
>
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)} className="cursor-pointer">
|
||||
{t("common:cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => void save()}
|
||||
disabled={!payload || saving}
|
||||
className="min-h-11 cursor-pointer sm:min-h-9"
|
||||
className="cursor-pointer"
|
||||
>
|
||||
{saveLabel}
|
||||
</Button>
|
||||
|
||||
@@ -39,11 +39,7 @@ function ActionError({ message }: { message: string }) {
|
||||
function NewWatchButton({ onClick }: { onClick: () => void }) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={onClick}
|
||||
className="min-h-11 w-full cursor-pointer sm:min-h-8 sm:w-auto"
|
||||
>
|
||||
<Button onClick={onClick} className="w-full cursor-pointer sm:w-auto">
|
||||
<IconPlus className="mr-1 h-4 w-4" />
|
||||
{t("gitlab:newWatch")}
|
||||
</Button>
|
||||
|
||||
@@ -56,7 +56,7 @@ export function IntegrationAuthErrorMessage({
|
||||
: cleanIntegrationErrorMessage(error)}
|
||||
</span>
|
||||
{isAuth && (
|
||||
<Button asChild size="sm" variant="outline" className="cursor-pointer h-7 text-xs">
|
||||
<Button asChild size="sm" variant="outline" className="cursor-pointer text-xs">
|
||||
<Link href={reconnectHref}>{t("integrations:reconnectProvider", { name })}</Link>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
@@ -46,9 +46,9 @@ export function ChangeRequestDetailCopyButton({
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="h-6 w-6 min-h-11 min-w-11 cursor-pointer p-0 text-muted-foreground hover:text-foreground [@media(min-width:640px)_and_(pointer:fine)]:min-h-0 [@media(min-width:640px)_and_(pointer:fine)]:min-w-0"
|
||||
className="cursor-pointer p-0 text-muted-foreground hover:text-foreground"
|
||||
onClick={() => {
|
||||
void copyToClipboard(url).then((success) => {
|
||||
if (success) setCopied(true);
|
||||
|
||||
@@ -53,9 +53,8 @@ export function ChangeRequestActionButton({
|
||||
return (
|
||||
<Button
|
||||
type={type}
|
||||
size="sm"
|
||||
variant={actionVariant(action.tone)}
|
||||
className={`min-h-11 cursor-pointer sm:min-h-0 ${successClass}`}
|
||||
className={`cursor-pointer ${successClass}`}
|
||||
disabled={action.disabled || busy}
|
||||
onClick={onAction}
|
||||
aria-label={ariaLabel}
|
||||
@@ -182,19 +181,15 @@ export function ChangeRequestDetailHeader({
|
||||
onAction={() => void props.onAction?.({ actionId: action.id })}
|
||||
/>
|
||||
))}
|
||||
{props.headerActions ? (
|
||||
<div className="contents [&_button]:min-h-11 sm:[&_button]:min-h-0">
|
||||
{props.headerActions}
|
||||
</div>
|
||||
) : null}
|
||||
{props.headerActions ? <div className="contents">{props.headerActions}</div> : null}
|
||||
{props.onRefresh ? (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="h-11 w-11 cursor-pointer p-0 text-muted-foreground sm:h-6 sm:w-6"
|
||||
size="icon"
|
||||
className="cursor-pointer p-0 text-muted-foreground"
|
||||
onClick={props.onRefresh}
|
||||
disabled={props.loading}
|
||||
aria-label={t("integrations:refresh")}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user