| author | |
| committer | |
| log | 4c9fbf1f647c83179af1c2acd78eded04ef07fe8 |
| tree | f71d7ba480e2521fd09e53e4944d2db8af0db2b0 |
| parent | 18aa7036732c0fc2e2ab79e54d5d8f59a160843d |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>9 files changed, 65 insertions(+), 62 deletions(-)
apps/web/src/components/Sidebar.tsx+1-1| ... | @@ -1374,7 +1374,7 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: { | ... | @@ -1374,7 +1374,7 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: { |
| 1374 | if (!showSnoozeButton) setSnoozeMenuOpen(false); | 1374 | if (!showSnoozeButton) setSnoozeMenuOpen(false); |
| 1375 | }, [showSnoozeButton]); | 1375 | }, [showSnoozeButton]); |
| 1376 | const handlePrClick = useCallback( | 1376 | const handlePrClick = useCallback( |
| 1377 | (event: ReactMouseEvent<HTMLAnchorElement>) => { | 1377 | (event: ReactMouseEvent<HTMLElement>) => { |
| 1378 | const url = pr?.url ?? currentLinkedPr?.url; | 1378 | const url = pr?.url ?? currentLinkedPr?.url; |
| 1379 | if (!url) return; | 1379 | if (!url) return; |
| 1380 | const openedInRightPanel = openPrLink( | 1380 | const openedInRightPanel = openPrLink( |
apps/web/src/components/ThreadStatusIndicators.tsx+27-25| ... | @@ -16,7 +16,7 @@ import { | ... | @@ -16,7 +16,7 @@ import { |
| 16 | } from "@t3tools/shared/threadPullRequests"; | 16 | } from "@t3tools/shared/threadPullRequests"; |
| 17 | import { FolderGit2Icon, TerminalIcon } from "lucide-react"; | 17 | import { FolderGit2Icon, TerminalIcon } from "lucide-react"; |
| 18 | import { useMemo, type MouseEvent } from "react"; | 18 | import { useMemo, type MouseEvent } from "react"; |
| 19 | import { buttonVariants, InlineButton } from "./ui/button"; | 19 | import { Button, InlineButton } from "./ui/button"; |
| 20 | import { cn } from "../lib/utils"; | 20 | import { cn } from "../lib/utils"; |
| 21 | import { useEnvironment, usePrimaryEnvironmentId } from "../state/environments"; | 21 | import { useEnvironment, usePrimaryEnvironmentId } from "../state/environments"; |
| 22 | import { EnvironmentMachineIcon } from "./EnvironmentMachineIcon"; | 22 | import { EnvironmentMachineIcon } from "./EnvironmentMachineIcon"; |
| ... | @@ -207,50 +207,52 @@ export function ThreadPullRequestBadgeControl({ | ... | @@ -207,50 +207,52 @@ export function ThreadPullRequestBadgeControl({ |
| 207 | url?: string | undefined; | 207 | url?: string | undefined; |
| 208 | status: PrStatusIndicator | null; | 208 | status: PrStatusIndicator | null; |
| 209 | onOpenStack: () => void; | 209 | onOpenStack: () => void; |
| 210 | onOpenPullRequest: (event: MouseEvent<HTMLAnchorElement>) => void; | 210 | onOpenPullRequest: (event: MouseEvent<HTMLElement>) => void; |
| 211 | }) { | 211 | }) { |
| 212 | const presentation = resolveThreadPullRequestBadgePresentation({ badge, number, url, status }); | 212 | const presentation = resolveThreadPullRequestBadgePresentation({ badge, number, url, status }); |
| 213 | if (presentation === null) return null; | 213 | if (presentation === null) return null; |
| 214 | const isStack = badge?.kind === "stack"; | 214 | const isStack = badge?.kind === "stack"; |
| 215 | const className = cn( | ||
| 216 | variant === "ghost" | ||
| 217 | ? buttonVariants({ variant: "ghost", size: "xs" }) | ||
| 218 | : "inline-flex shrink-0 cursor-pointer items-center gap-0.5 whitespace-nowrap border-b border-transparent hover:border-current focus-visible:outline-2 focus-visible:outline-ring", | ||
| 219 | "text-xs tabular-nums", | ||
| 220 | variant === "ghost" && | ||
| 221 | "font-normal text-xs! active:scale-100 [--control-icon-color:currentColor]", | ||
| 222 | presentation.toneClassName, | ||
| 223 | ); | ||
| 224 | const content = ( | 215 | const content = ( |
| 225 | <> | 216 | <> |
| 226 | <presentation.Icon aria-hidden className="size-3 shrink-0" /> | 217 | <presentation.Icon aria-hidden className="size-3 shrink-0" /> |
| 227 | {presentation.text} | 218 | {presentation.text} |
| 228 | </> | 219 | </> |
| 229 | ); | 220 | ); |
| 221 | const linkProps = isStack | ||
| 222 | ? { | ||
| 223 | onClick: (event: MouseEvent<HTMLElement>) => { | ||
| 224 | event.preventDefault(); | ||
| 225 | event.stopPropagation(); | ||
| 226 | onOpenStack(); | ||
| 227 | }, | ||
| 228 | } | ||
| 229 | : { onClick: onOpenPullRequest }; | ||
| 230 | const element = isStack ? ( | ||
| 231 | <button type="button" /> | ||
| 232 | ) : ( | ||
| 233 | <a href={url} target="_blank" rel="noopener noreferrer" /> | ||
| 234 | ); | ||
| 230 | return ( | 235 | return ( |
| 231 | <Tooltip> | 236 | <Tooltip> |
| 232 | <TooltipTrigger | 237 | <TooltipTrigger |
| 233 | render={ | 238 | render={ |
| 234 | isStack ? ( | 239 | variant === "ghost" ? ( |
| 235 | <InlineButton | 240 | <Button |
| 236 | className={className} | 241 | render={element} |
| 242 | variant="ghost" | ||
| 243 | size="xs" | ||
| 244 | className={presentation.toneClassName} | ||
| 237 | aria-label={presentation.label} | 245 | aria-label={presentation.label} |
| 238 | onPointerDown={(event) => event.stopPropagation()} | 246 | onPointerDown={(event) => event.stopPropagation()} |
| 239 | onClick={(event) => { | 247 | {...linkProps} |
| 240 | event.preventDefault(); | ||
| 241 | event.stopPropagation(); | ||
| 242 | onOpenStack(); | ||
| 243 | }} | ||
| 244 | /> | 248 | /> |
| 245 | ) : ( | 249 | ) : ( |
| 246 | <a | 250 | <InlineButton |
| 247 | href={url} | 251 | render={element} |
| 248 | target="_blank" | 252 | className={presentation.toneClassName} |
| 249 | rel="noopener noreferrer" | ||
| 250 | className={className} | ||
| 251 | aria-label={presentation.label} | 253 | aria-label={presentation.label} |
| 252 | onPointerDown={(event) => event.stopPropagation()} | 254 | onPointerDown={(event) => event.stopPropagation()} |
| 253 | onClick={onOpenPullRequest} | 255 | {...linkProps} |
| 254 | /> | 256 | /> |
| 255 | ) | 257 | ) |
| 256 | } | 258 | } |
apps/web/src/components/pullRequest/PullRequestGhosts.tsx+1-4| ... | @@ -169,10 +169,7 @@ export function PullRequestDetailGhost({ | ... | @@ -169,10 +169,7 @@ export function PullRequestDetailGhost({ |
| 169 | <span className="min-w-0 truncate font-medium">{seed.repository}</span> | 169 | <span className="min-w-0 truncate font-medium">{seed.repository}</span> |
| 170 | <InlineButton | 170 | <InlineButton |
| 171 | onClick={() => void readLocalApi()?.shell.openExternal(seed.url)} | 171 | onClick={() => void readLocalApi()?.shell.openExternal(seed.url)} |
| 172 | className={cn( | 172 | className={statePresentation?.toneClassName} |
| 173 | "font-medium underline-offset-2 hover:underline", | ||
| 174 | statePresentation?.toneClassName, | ||
| 175 | )} | ||
| 176 | aria-label={`Open pull request #${seed.number} on host`} | 173 | aria-label={`Open pull request #${seed.number} on host`} |
| 177 | > | 174 | > |
| 178 | #{seed.number} | 175 | #{seed.number} |
apps/web/src/components/settings/ExpandableText.tsx+2-1| ... | @@ -37,7 +37,8 @@ export function ExpandableText({ | ... | @@ -37,7 +37,8 @@ export function ExpandableText({ |
| 37 | <InlineButton | 37 | <InlineButton |
| 38 | aria-expanded={expanded} | 38 | aria-expanded={expanded} |
| 39 | aria-controls={textId} | 39 | aria-controls={textId} |
| 40 | className="mt-1 text-[11px] font-medium text-foreground/70 underline-offset-2 hover:text-foreground hover:underline" | 40 | tone="muted" |
| 41 | className="mt-1" | ||
| 41 | onClick={() => setExpanded((value) => !value)} | 42 | onClick={() => setExpanded((value) => !value)} |
| 42 | > | 43 | > |
| 43 | {expanded ? "Show less" : expandLabel} | 44 | {expanded ? "Show less" : expandLabel} |
apps/web/src/components/settings/ProcessSignalActions.tsx+2-2| ... | @@ -19,7 +19,7 @@ export function ProcessSignalActions({ | ... | @@ -19,7 +19,7 @@ export function ProcessSignalActions({ |
| 19 | <InlineButton | 19 | <InlineButton |
| 20 | disabled={disabled} | 20 | disabled={disabled} |
| 21 | aria-label="Send SIGINT" | 21 | aria-label="Send SIGINT" |
| 22 | className="text-[11px] font-medium text-muted-foreground underline-offset-2 hover:text-foreground hover:underline" | 22 | tone="muted" |
| 23 | onClick={() => onSignal("SIGINT")} | 23 | onClick={() => onSignal("SIGINT")} |
| 24 | > | 24 | > |
| 25 | INT | 25 | INT |
| ... | @@ -34,7 +34,7 @@ export function ProcessSignalActions({ | ... | @@ -34,7 +34,7 @@ export function ProcessSignalActions({ |
| 34 | <InlineButton | 34 | <InlineButton |
| 35 | disabled={disabled} | 35 | disabled={disabled} |
| 36 | aria-label="Send SIGKILL" | 36 | aria-label="Send SIGKILL" |
| 37 | className="text-[11px] font-medium text-destructive underline-offset-2 hover:underline" | 37 | tone="destructive" |
| 38 | onClick={() => onSignal("SIGKILL")} | 38 | onClick={() => onSignal("SIGKILL")} |
| 39 | > | 39 | > |
| 40 | KILL | 40 | KILL |
apps/web/src/components/settings/SettingInheritance.tsx+2-8| ... | @@ -286,10 +286,7 @@ export function SettingInheritance({ | ... | @@ -286,10 +286,7 @@ export function SettingInheritance({ |
| 286 | <div className="flex items-center justify-between gap-3 px-2 text-xs text-muted-foreground"> | 286 | <div className="flex items-center justify-between gap-3 px-2 text-xs text-muted-foreground"> |
| 287 | <span>Overridden by</span> | 287 | <span>Overridden by</span> |
| 288 | {onClearOverrides ? ( | 288 | {onClearOverrides ? ( |
| 289 | <InlineButton | 289 | <InlineButton onClick={() => onClearOverrides(overriding)}> |
| 290 | className="font-medium text-foreground underline-offset-2 hover:underline" | ||
| 291 | onClick={() => onClearOverrides(overriding)} | ||
| 292 | > | ||
| 293 | Reset {overriding.length === 1 ? "it" : "all"} | 290 | Reset {overriding.length === 1 ? "it" : "all"} |
| 294 | </InlineButton> | 291 | </InlineButton> |
| 295 | ) : null} | 292 | ) : null} |
| ... | @@ -300,10 +297,7 @@ export function SettingInheritance({ | ... | @@ -300,10 +297,7 @@ export function SettingInheritance({ |
| 300 | key={project.projectId} | 297 | key={project.projectId} |
| 301 | className="grid grid-cols-[minmax(0,1fr)_auto] items-center gap-x-3 px-2 py-1" | 298 | className="grid grid-cols-[minmax(0,1fr)_auto] items-center gap-x-3 px-2 py-1" |
| 302 | > | 299 | > |
| 303 | <InlineButton | 300 | <InlineButton className="min-w-0 justify-start" onClick={project.open}> |
| 304 | className="min-w-0 justify-start text-left text-foreground underline-offset-2 hover:underline" | ||
| 305 | onClick={project.open} | ||
| 306 | > | ||
| 307 | <span className="truncate">{project.label}</span> | 301 | <span className="truncate">{project.label}</span> |
| 308 | </InlineButton> | 302 | </InlineButton> |
| 309 | <span className="max-w-32 truncate text-muted-foreground tabular-nums"> | 303 | <span className="max-w-32 truncate text-muted-foreground tabular-nums"> |
apps/web/src/components/sidebar/SidebarThreadUndoNotice.tsx+1-5| ... | @@ -17,11 +17,7 @@ export function SidebarThreadUndoNotice() { | ... | @@ -17,11 +17,7 @@ export function SidebarThreadUndoNotice() { |
| 17 | <Alert role="status" variant="sidebar"> | 17 | <Alert role="status" variant="sidebar"> |
| 18 | <AlertDescription> | 18 | <AlertDescription> |
| 19 | {notice.action} {notice.count} thread{notice.count === 1 ? "" : "s"},{" "} | 19 | {notice.action} {notice.count} thread{notice.count === 1 ? "" : "s"},{" "} |
| 20 | <InlineButton | 20 | <InlineButton onClick={undoLatestThreadAction}> |
| 21 | underline | ||
| 22 | onClick={undoLatestThreadAction} | ||
| 23 | className="hover:text-sidebar-foreground" | ||
| 24 | > | ||
| 25 | {shortcut ? `${shortcut} to undo` : "Undo"} | 21 | {shortcut ? `${shortcut} to undo` : "Undo"} |
| 26 | </InlineButton> | 22 | </InlineButton> |
| 27 | </AlertDescription> | 23 | </AlertDescription> |
apps/web/src/components/ui/button.tsx+28-15| ... | @@ -97,22 +97,35 @@ function Button({ className, variant, size, render, ...props }: ButtonProps) { | ... | @@ -97,22 +97,35 @@ function Button({ className, variant, size, render, ...props }: ButtonProps) { |
| 97 | 97 | ||
| 98 | export { Button, buttonVariants }; | 98 | export { Button, buttonVariants }; |
| 99 | 99 | ||
| 100 | /** An inline action that keeps the geometry of surrounding text or a graph node. */ | 100 | const inlineButtonVariants = cva( |
| 101 | "inline-flex shrink-0 cursor-pointer items-center gap-0.5 whitespace-nowrap font-medium underline-offset-2 hover:underline focus-visible:outline-2 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-64", | ||
| 102 | { | ||
| 103 | defaultVariants: { tone: "default" }, | ||
| 104 | variants: { | ||
| 105 | tone: { | ||
| 106 | default: "text-foreground", | ||
| 107 | muted: "text-muted-foreground hover:text-foreground", | ||
| 108 | destructive: "text-destructive/80 hover:text-destructive", | ||
| 109 | }, | ||
| 110 | }, | ||
| 111 | }, | ||
| 112 | ); | ||
| 113 | |||
| 114 | /** An inline text action that keeps the size of the surrounding text and underlines on hover. */ | ||
| 101 | export function InlineButton({ | 115 | export function InlineButton({ |
| 102 | className, | 116 | className, |
| 103 | underline = false, | 117 | tone, |
| 118 | render, | ||
| 104 | ...props | 119 | ...props |
| 105 | }: React.ComponentProps<"button"> & { underline?: boolean }) { | 120 | }: useRender.ComponentProps<"button"> & VariantProps<typeof inlineButtonVariants>) { |
| 106 | return ( | 121 | const defaultProps = { |
| 107 | <button | 122 | className: cn(inlineButtonVariants({ tone }), className), |
| 108 | type="button" | 123 | "data-slot": "inline-button", |
| 109 | data-slot="inline-button" | 124 | type: render ? undefined : ("button" as const), |
| 110 | className={cn( | 125 | }; |
| 111 | "inline-flex shrink-0 cursor-pointer items-center gap-0.5 whitespace-nowrap focus-visible:outline-2 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-64", | 126 | return useRender({ |
| 112 | underline && "border-b border-transparent hover:border-current", | 127 | defaultTagName: "button", |
| 113 | className, | 128 | props: mergeProps<"button">(defaultProps, props), |
| 114 | )} | 129 | render, |
| 115 | {...props} | 130 | }); |
| 116 | /> | ||
| 117 | ); | ||
| 118 | } | 131 | } |
scripts/lint-restyle-ceiling.ts+1-1| ... | @@ -8,7 +8,7 @@ import * as NodeURL from "node:url"; | ... | @@ -8,7 +8,7 @@ import * as NodeURL from "node:url"; |
| 8 | // components/ui exports are migrated to variants (see vite.config.ts). This gate keeps the | 8 | // components/ui exports are migrated to variants (see vite.config.ts). This gate keeps the |
| 9 | // count from growing: CI fails when findings exceed the ceiling. Lower the ceiling when you | 9 | // count from growing: CI fails when findings exceed the ceiling. Lower the ceiling when you |
| 10 | // migrate a file, and delete this script when the rule becomes an error. | 10 | // migrate a file, and delete this script when the rule becomes an error. |
| 11 | export const RESTYLE_CEILING = 865; | 11 | export const RESTYLE_CEILING = 826; |
| 12 | 12 | ||
| 13 | const RULE = "shadcn(no-restyle)"; | 13 | const RULE = "shadcn(no-restyle)"; |
| 14 | 14 |