mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 00:02:31 +00:00
Enforce styled scrollbars in renderer
This commit is contained in:
@@ -51,6 +51,9 @@ jobs:
|
||||
- name: Lint
|
||||
run: pnpm exec oxlint --format github
|
||||
|
||||
- name: Check styled scrollbars
|
||||
run: pnpm check:styled-scrollbars
|
||||
|
||||
# Why: project-owned type declarations must live in .ts so tsc
|
||||
# actually checks them. TypeScript's skipLibCheck: true (inherited
|
||||
# from @electron-toolkit/tsconfig) silently widens unresolved names
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
import fs from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
import { pathToFileURL } from 'node:url'
|
||||
import process from 'node:process'
|
||||
import ts from 'typescript'
|
||||
|
||||
const SOURCE_EXTENSIONS = new Set(['.ts', '.tsx', '.js', '.jsx', '.mts', '.cts'])
|
||||
const SKIP_PATH_PARTS = new Set(['node_modules', 'dist', 'out', '.git', '__snapshots__'])
|
||||
const STYLED_SCROLLBAR_CLASSES = new Set([
|
||||
'scrollbar-sleek',
|
||||
'scrollbar-editor',
|
||||
'scrollbar-none',
|
||||
'worktree-sidebar-scrollbar'
|
||||
])
|
||||
// Why: vertical scrolling is where Orca's native scrollbar drift keeps showing
|
||||
// up in cards, dialogs, and menus; horizontal code/table overflow is handled separately.
|
||||
const VERTICAL_SCROLL_CLASSES = new Set([
|
||||
'overflow-auto',
|
||||
'overflow-scroll',
|
||||
'overflow-y-auto',
|
||||
'overflow-y-scroll'
|
||||
])
|
||||
|
||||
export function normalizePath(root, filePath) {
|
||||
return path.relative(root, filePath).split(path.sep).join('/')
|
||||
}
|
||||
|
||||
function isSkippedFile(root, filePath) {
|
||||
const relative = normalizePath(root, filePath)
|
||||
if (relative.includes('.test.') || relative.includes('.spec.')) {
|
||||
return true
|
||||
}
|
||||
return relative.split('/').some((part) => SKIP_PATH_PARTS.has(part))
|
||||
}
|
||||
|
||||
async function collectSourceFiles(root, dir) {
|
||||
const entries = await fs.readdir(dir, { withFileTypes: true })
|
||||
const files = []
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(dir, entry.name)
|
||||
if (entry.isDirectory()) {
|
||||
if (!SKIP_PATH_PARTS.has(entry.name)) {
|
||||
files.push(...(await collectSourceFiles(root, fullPath)))
|
||||
}
|
||||
continue
|
||||
}
|
||||
if (!entry.isFile() || isSkippedFile(root, fullPath)) {
|
||||
continue
|
||||
}
|
||||
if (SOURCE_EXTENSIONS.has(path.extname(entry.name))) {
|
||||
files.push(fullPath)
|
||||
}
|
||||
}
|
||||
|
||||
return files
|
||||
}
|
||||
|
||||
export function plainClassName(token) {
|
||||
const withoutImportant = token.startsWith('!') ? token.slice(1) : token
|
||||
const variantSeparator = withoutImportant.lastIndexOf(':')
|
||||
return variantSeparator === -1 ? withoutImportant : withoutImportant.slice(variantSeparator + 1)
|
||||
}
|
||||
|
||||
function hasVerticalScrollClass(text) {
|
||||
return text.split(/\s+/).some((token) => VERTICAL_SCROLL_CLASSES.has(plainClassName(token)))
|
||||
}
|
||||
|
||||
function hasStyledScrollbarClass(text) {
|
||||
return text.split(/\s+/).some((token) => STYLED_SCROLLBAR_CLASSES.has(plainClassName(token)))
|
||||
}
|
||||
|
||||
function lineAndColumnForPosition(sourceText, position) {
|
||||
let line = 1
|
||||
let lineStart = 0
|
||||
for (let index = 0; index < position; index += 1) {
|
||||
if (sourceText.charCodeAt(index) === 10) {
|
||||
line += 1
|
||||
lineStart = index + 1
|
||||
}
|
||||
}
|
||||
return { line, column: position - lineStart + 1 }
|
||||
}
|
||||
|
||||
function stringFragments(node) {
|
||||
if (ts.isStringLiteralLike(node)) {
|
||||
return [node.text]
|
||||
}
|
||||
if (!ts.isTemplateExpression(node)) {
|
||||
return []
|
||||
}
|
||||
return [node.head.text, ...node.templateSpans.map((span) => span.literal.text)]
|
||||
}
|
||||
|
||||
export function reportUnstyledScrollbars(filePath, sourceText) {
|
||||
const sourceFile = ts.createSourceFile(
|
||||
filePath,
|
||||
sourceText,
|
||||
ts.ScriptTarget.Latest,
|
||||
true,
|
||||
filePath.endsWith('.tsx') || filePath.endsWith('.jsx') ? ts.ScriptKind.TSX : ts.ScriptKind.TS
|
||||
)
|
||||
const reports = []
|
||||
|
||||
function visit(node) {
|
||||
const fragments = stringFragments(node)
|
||||
if (fragments.some(hasVerticalScrollClass) && !fragments.some(hasStyledScrollbarClass)) {
|
||||
const { line, column } = lineAndColumnForPosition(sourceText, node.getStart(sourceFile))
|
||||
reports.push({ filePath, line, column, text: fragments.join('${...}').trim() })
|
||||
}
|
||||
ts.forEachChild(node, visit)
|
||||
}
|
||||
|
||||
visit(sourceFile)
|
||||
return reports
|
||||
}
|
||||
|
||||
export async function collectUnstyledScrollbarReports(root = process.cwd()) {
|
||||
const scanRoot = path.join(root, 'src', 'renderer', 'src')
|
||||
const files = await collectSourceFiles(root, scanRoot)
|
||||
const reports = []
|
||||
|
||||
for (const filePath of files) {
|
||||
const sourceText = await fs.readFile(filePath, 'utf8')
|
||||
reports.push(...reportUnstyledScrollbars(filePath, sourceText))
|
||||
}
|
||||
|
||||
return reports
|
||||
}
|
||||
|
||||
export function formatReports(root, reports) {
|
||||
return reports
|
||||
.map(
|
||||
(report) =>
|
||||
`${normalizePath(root, report.filePath)}:${report.line}:${report.column} ${report.text.replace(/\s+/g, ' ')}`
|
||||
)
|
||||
.join('\n')
|
||||
}
|
||||
|
||||
export async function main(root = process.cwd()) {
|
||||
const reports = await collectUnstyledScrollbarReports(root)
|
||||
if (reports.length === 0) {
|
||||
return 0
|
||||
}
|
||||
|
||||
console.error('Renderer vertical scroll containers must use an Orca scrollbar style.')
|
||||
console.error(
|
||||
'Add scrollbar-sleek, scrollbar-editor, scrollbar-none, or use the shadcn ScrollArea wrapper.'
|
||||
)
|
||||
console.error('')
|
||||
console.error(formatReports(root, reports))
|
||||
return 1
|
||||
}
|
||||
|
||||
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
||||
process.exit(await main())
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { plainClassName, reportUnstyledScrollbars } from './check-styled-scrollbars.mjs'
|
||||
|
||||
describe('check-styled-scrollbars', () => {
|
||||
it('reports renderer vertical scroll containers without an Orca scrollbar style', () => {
|
||||
const reports = reportUnstyledScrollbars(
|
||||
'Example.tsx',
|
||||
'export function Example() { return <div className="max-h-64 overflow-y-auto p-1" /> }'
|
||||
)
|
||||
|
||||
expect(reports).toHaveLength(1)
|
||||
expect(reports[0].text).toContain('overflow-y-auto')
|
||||
})
|
||||
|
||||
it('accepts styled vertical scroll containers', () => {
|
||||
const reports = reportUnstyledScrollbars(
|
||||
'Example.tsx',
|
||||
'export function Example() { return <div className="max-h-64 overflow-auto scrollbar-sleek" /> }'
|
||||
)
|
||||
|
||||
expect(reports).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('does not require a vertical scrollbar style for horizontal-only overflow', () => {
|
||||
const reports = reportUnstyledScrollbars(
|
||||
'Example.tsx',
|
||||
'export function Example() { return <pre className="max-w-full overflow-x-auto" /> }'
|
||||
)
|
||||
|
||||
expect(reports).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('normalizes Tailwind variants and important prefixes before matching', () => {
|
||||
expect(plainClassName('md:overflow-y-auto')).toBe('overflow-y-auto')
|
||||
expect(plainClassName('!scrollbar-editor')).toBe('scrollbar-editor')
|
||||
})
|
||||
})
|
||||
+2
-1
@@ -11,9 +11,10 @@
|
||||
"main": "./out/main/index.js",
|
||||
"scripts": {
|
||||
"format": "oxfmt --write .",
|
||||
"lint": "oxlint",
|
||||
"lint": "oxlint && node config/scripts/check-styled-scrollbars.mjs",
|
||||
"prepare": "husky",
|
||||
"test": "node config/scripts/ensure-native-runtime.mjs --runtime=node && vitest run --config config/vitest.config.ts",
|
||||
"check:styled-scrollbars": "node config/scripts/check-styled-scrollbars.mjs",
|
||||
"check:feature-wall-assets": "node config/scripts/check-feature-wall-assets.mjs",
|
||||
"verify:macos-entitlements": "node config/scripts/verify-macos-entitlements.mjs",
|
||||
"vendor:feature-wall-assets": "node config/scripts/vendor-feature-wall-assets.mjs",
|
||||
|
||||
@@ -318,7 +318,7 @@
|
||||
/* Keep the sidebar gutter reserved; only the thumb fades in on hover. */
|
||||
.worktree-sidebar-scrollbar {
|
||||
/* Why: the scrollbar itself owns the gutter; padding is only the card-to-gutter gap. */
|
||||
padding-right: 6px;
|
||||
padding-right: 2px;
|
||||
scrollbar-gutter: stable;
|
||||
scrollbar-color: transparent transparent;
|
||||
}
|
||||
|
||||
@@ -3749,7 +3749,7 @@ function ChecksTab({
|
||||
<div className="border-b border-border/40 px-2.5 py-1.5 text-[11px] font-medium text-foreground">
|
||||
Annotations
|
||||
</div>
|
||||
<div className="flex max-h-48 flex-col overflow-y-auto">
|
||||
<div className="flex max-h-48 flex-col overflow-y-auto scrollbar-sleek">
|
||||
{details!.annotations.map((annotation, index) => (
|
||||
<div
|
||||
key={`${annotation.path ?? 'annotation'}-${index}`}
|
||||
@@ -3778,7 +3778,7 @@ function ChecksTab({
|
||||
{annotation.message}
|
||||
</div>
|
||||
{annotation.rawDetails && (
|
||||
<pre className="mt-1 max-h-32 overflow-auto whitespace-pre-wrap rounded bg-muted/40 p-2 font-mono text-[11px] text-muted-foreground">
|
||||
<pre className="mt-1 max-h-32 overflow-auto whitespace-pre-wrap rounded bg-muted/40 p-2 font-mono text-[11px] text-muted-foreground scrollbar-sleek">
|
||||
{annotation.rawDetails}
|
||||
</pre>
|
||||
)}
|
||||
@@ -3793,7 +3793,7 @@ function ChecksTab({
|
||||
<div className="border-b border-border/40 px-2.5 py-1.5 text-[11px] font-medium text-foreground">
|
||||
Jobs
|
||||
</div>
|
||||
<div className="flex max-h-64 flex-col overflow-y-auto">
|
||||
<div className="flex max-h-64 flex-col overflow-y-auto scrollbar-sleek">
|
||||
{details!.jobs.map((job, index) => (
|
||||
<div
|
||||
key={`${job.name}-${index}`}
|
||||
@@ -3952,7 +3952,9 @@ function ChecksTab({
|
||||
return (
|
||||
<>
|
||||
{compactHeader}
|
||||
<div className="max-h-[280px] overflow-y-auto p-1">{sorted.map(renderCheckRow)}</div>
|
||||
<div className="max-h-[280px] overflow-y-auto p-1 scrollbar-sleek">
|
||||
{sorted.map(renderCheckRow)}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -4017,7 +4019,7 @@ function MentionTextarea({
|
||||
return (
|
||||
<div className={cn('relative min-w-0 flex-1', wrapperClassName)}>
|
||||
{showSuggestions && (
|
||||
<div className="absolute right-0 bottom-[calc(100%+6px)] left-0 z-50 max-h-64 overflow-y-auto rounded-md border border-border/70 bg-popover p-1 text-popover-foreground shadow-lg">
|
||||
<div className="absolute right-0 bottom-[calc(100%+6px)] left-0 z-50 max-h-64 overflow-y-auto rounded-md border border-border/70 bg-popover p-1 text-popover-foreground shadow-lg scrollbar-sleek">
|
||||
{suggestions.map((option, index) => (
|
||||
<button
|
||||
key={option.login}
|
||||
|
||||
@@ -5413,7 +5413,7 @@ export default function TaskPage(): React.JSX.Element {
|
||||
placeholder="What's going on?"
|
||||
rows={6}
|
||||
disabled={newIssueSubmitting}
|
||||
className="w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-xs transition-[color,box-shadow] outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 resize-none max-h-60 overflow-y-auto"
|
||||
className="w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-xs transition-[color,box-shadow] outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 resize-none max-h-60 overflow-y-auto scrollbar-sleek"
|
||||
/>
|
||||
</div>
|
||||
<p className="text-[10px] text-muted-foreground">Cmd/Ctrl+Enter to submit.</p>
|
||||
@@ -5523,7 +5523,7 @@ export default function TaskPage(): React.JSX.Element {
|
||||
placeholder="What's going on?"
|
||||
rows={6}
|
||||
disabled={newLinearIssueSubmitting}
|
||||
className="w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-xs transition-[color,box-shadow] outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 resize-none max-h-60 overflow-y-auto"
|
||||
className="w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-xs transition-[color,box-shadow] outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 resize-none max-h-60 overflow-y-auto scrollbar-sleek"
|
||||
/>
|
||||
</div>
|
||||
<p className="text-[10px] text-muted-foreground">Cmd/Ctrl+Enter to submit.</p>
|
||||
|
||||
@@ -193,7 +193,7 @@ export function AutomationEditorDialog({
|
||||
</div>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="min-h-0 flex-1 overflow-auto px-5 py-4">
|
||||
<div className="min-h-0 flex-1 overflow-auto px-5 py-4 scrollbar-sleek">
|
||||
{draft.scheduleWarning ? (
|
||||
<div className="mb-3 rounded-md border border-border bg-muted/40 px-3 py-2 text-xs text-muted-foreground">
|
||||
{draft.scheduleWarning}
|
||||
|
||||
@@ -208,7 +208,7 @@ export default function GrabConfirmationSheet({
|
||||
<h3 className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">
|
||||
HTML
|
||||
</h3>
|
||||
<pre className="max-h-32 overflow-auto rounded-lg border border-border/60 bg-muted/20 p-3 font-mono text-xs text-foreground/80">
|
||||
<pre className="max-h-32 overflow-auto rounded-lg border border-border/60 bg-muted/20 p-3 font-mono text-xs text-foreground/80 scrollbar-sleek">
|
||||
<EscapedText text={target.htmlSnippet} />
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
@@ -194,7 +194,7 @@ export function CrashReportDialog(): React.JSX.Element {
|
||||
/>
|
||||
<div className="space-y-1.5">
|
||||
<div className="text-[11px] font-medium text-muted-foreground">Diagnostic text</div>
|
||||
<pre className="max-h-44 overflow-auto whitespace-pre-wrap break-words rounded-md border border-border bg-muted/20 p-3 font-mono text-[11px] leading-5 text-muted-foreground">
|
||||
<pre className="max-h-44 overflow-auto whitespace-pre-wrap break-words rounded-md border border-border bg-muted/20 p-3 font-mono text-[11px] leading-5 text-muted-foreground scrollbar-sleek">
|
||||
{diagnosticText}
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
@@ -1283,7 +1283,7 @@ function DiffNotesPreviewPopover({
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="max-h-72 overflow-y-auto p-2">
|
||||
<div className="max-h-72 overflow-y-auto p-2 scrollbar-sleek">
|
||||
{comments.map((comment) => (
|
||||
<div key={comment.id} className="rounded-md px-2 py-1.5 hover:bg-accent/50">
|
||||
<div className="flex items-center gap-1.5 text-[11px] leading-none text-muted-foreground">
|
||||
|
||||
@@ -89,7 +89,7 @@ export default function ImageDiffViewer({
|
||||
'grid min-h-0 gap-3 p-3',
|
||||
isIntrinsicLayout ? 'h-auto' : 'h-full',
|
||||
sideBySide ? 'grid-cols-2' : 'grid-cols-1',
|
||||
!sideBySide && !isIntrinsicLayout && 'overflow-y-auto'
|
||||
!sideBySide && !isIntrinsicLayout && 'overflow-y-auto scrollbar-editor'
|
||||
)}
|
||||
style={gridRowStyle}
|
||||
>
|
||||
|
||||
@@ -106,7 +106,9 @@ export default function ImageViewer({
|
||||
<div
|
||||
className={cn(
|
||||
'flex justify-center bg-muted/20 p-4 cursor-pointer',
|
||||
isIntrinsicLayout ? 'items-start overflow-visible' : 'flex-1 items-center overflow-auto'
|
||||
isIntrinsicLayout
|
||||
? 'items-start overflow-visible'
|
||||
: 'flex-1 items-center overflow-auto scrollbar-editor'
|
||||
)}
|
||||
onClick={() => setIsPopupOpen(true)}
|
||||
title="Open image in popup"
|
||||
@@ -193,7 +195,7 @@ export default function ImageViewer({
|
||||
<span>Close</span>
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex h-[calc(100%-4.5rem)] w-full min-h-0 items-center justify-center overflow-auto bg-muted/20 p-4">
|
||||
<div className="flex h-[calc(100%-4.5rem)] w-full min-h-0 items-center justify-center overflow-auto bg-muted/20 p-4 scrollbar-editor">
|
||||
<div
|
||||
className="flex items-center justify-center"
|
||||
style={{ transform: `scale(${zoom})`, transformOrigin: 'center center' }}
|
||||
|
||||
@@ -363,7 +363,7 @@ function PreformattedOutput({
|
||||
return (
|
||||
<pre
|
||||
className={cn(
|
||||
'max-h-[420px] overflow-auto whitespace-pre-wrap px-3 py-2 font-mono text-xs leading-5',
|
||||
'max-h-[420px] overflow-auto whitespace-pre-wrap px-3 py-2 font-mono text-xs leading-5 scrollbar-editor',
|
||||
error ? 'text-destructive' : 'text-foreground'
|
||||
)}
|
||||
>
|
||||
@@ -395,7 +395,7 @@ function OutputItem({ item }: { item: IpynbOutputItem }): React.JSX.Element | nu
|
||||
return null
|
||||
}
|
||||
return (
|
||||
<div className="flex max-w-full overflow-auto p-3">
|
||||
<div className="flex max-w-full overflow-auto p-3 scrollbar-editor">
|
||||
<img src={uri} alt={item.mime} className="max-h-[520px] max-w-full object-contain" />
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -388,7 +388,7 @@ export default function ProjectPicker({ activeProject, onSelect }: Props): React
|
||||
{!browseError && partialFailures.length > 0 ? (
|
||||
<PartialFailuresBanner failures={partialFailures} />
|
||||
) : null}
|
||||
<div className="max-h-[340px] overflow-y-auto p-1">
|
||||
<div className="max-h-[340px] overflow-y-auto p-1 scrollbar-sleek">
|
||||
{projectSettings.pinned.length > 0 ? (
|
||||
<Section label="Pinned">
|
||||
{projectSettings.pinned.map((p) => {
|
||||
@@ -619,7 +619,7 @@ function ViewPickStep({
|
||||
<span className="text-xs font-medium">Choose a view</span>
|
||||
<span />
|
||||
</div>
|
||||
<div className="max-h-[340px] overflow-y-auto p-1">
|
||||
<div className="max-h-[340px] overflow-y-auto p-1 scrollbar-sleek">
|
||||
{loading ? (
|
||||
<div className="flex items-center gap-2 px-2 py-2 text-xs text-muted-foreground">
|
||||
<Loader className="size-3 animate-spin" /> Loading views…
|
||||
|
||||
@@ -152,7 +152,7 @@ export default function ProjectRow({
|
||||
<HoverCardContent
|
||||
align="start"
|
||||
sideOffset={4}
|
||||
className="max-h-80 w-96 overflow-y-auto whitespace-pre-wrap text-xs"
|
||||
className="max-h-80 w-96 overflow-y-auto whitespace-pre-wrap text-xs scrollbar-sleek"
|
||||
>
|
||||
{draftBody}
|
||||
</HoverCardContent>
|
||||
|
||||
@@ -232,7 +232,7 @@ export function SlugDialogBody({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-h-0 overflow-y-auto px-4 py-3">
|
||||
<div className="flex-1 min-h-0 overflow-y-auto px-4 py-3 scrollbar-sleek">
|
||||
{loading && !details ? (
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<LoaderCircle className="size-4 animate-spin" /> Loading…
|
||||
|
||||
@@ -3778,7 +3778,7 @@ export function CommitArea({
|
||||
<DialogTitle>Commit Failed</DialogTitle>
|
||||
<DialogDescription>{commitFailureSummary}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<pre className="max-h-[60vh] overflow-auto rounded-md border border-border bg-muted/40 p-3 font-mono text-xs whitespace-pre-wrap text-foreground">
|
||||
<pre className="max-h-[60vh] overflow-auto rounded-md border border-border bg-muted/40 p-3 font-mono text-xs whitespace-pre-wrap text-foreground scrollbar-sleek">
|
||||
{commitError}
|
||||
</pre>
|
||||
<DialogFooter>
|
||||
|
||||
@@ -410,7 +410,7 @@ export function ManageSessionsSection(): React.JSX.Element {
|
||||
No sessions.
|
||||
</div>
|
||||
) : (
|
||||
<div className="max-h-[360px] overflow-y-auto">
|
||||
<div className="max-h-[360px] overflow-y-auto scrollbar-sleek">
|
||||
<table className="w-full text-xs">
|
||||
<tbody>
|
||||
{sessions.map((session) => {
|
||||
|
||||
@@ -251,7 +251,7 @@ export function RemoteStep({
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-1.5 max-h-64 overflow-y-auto pr-1">
|
||||
<div className="space-y-1.5 max-h-64 overflow-y-auto pr-1 scrollbar-sleek">
|
||||
{sshTargets.map((target) => (
|
||||
<SshTargetRow
|
||||
key={target.id}
|
||||
|
||||
@@ -119,7 +119,7 @@ const OrcaYamlTrustDialog = React.memo(function OrcaYamlTrustDialog() {
|
||||
<div className="mb-1 text-[11px] font-medium uppercase tracking-wide text-muted-foreground">
|
||||
{previouslyApproved ? `New ${scriptKind} script` : `${scriptKind} script`}
|
||||
</div>
|
||||
<pre className="max-h-48 overflow-auto whitespace-pre-wrap break-all font-mono text-xs text-foreground">
|
||||
<pre className="max-h-48 overflow-auto whitespace-pre-wrap break-all font-mono text-xs text-foreground scrollbar-sleek">
|
||||
{scriptContent}
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
@@ -271,7 +271,7 @@ export function WorktreeCardDetailsHover({
|
||||
side="right"
|
||||
align="start"
|
||||
sideOffset={8}
|
||||
className="w-80 max-h-[28rem] overflow-y-auto p-3 text-xs"
|
||||
className="w-80 max-h-[28rem] overflow-y-auto p-3 text-xs scrollbar-sleek"
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
>
|
||||
<div className="space-y-3">
|
||||
|
||||
@@ -340,7 +340,7 @@ const WorktreeMetaDialog = React.memo(function WorktreeMetaDialog() {
|
||||
onKeyDown={handleCommentKeyDown}
|
||||
placeholder="Notes about this worktree..."
|
||||
rows={3}
|
||||
className="w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-2 text-xs shadow-xs transition-[color,box-shadow] outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 resize-none max-h-60 overflow-y-auto"
|
||||
className="w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-2 text-xs shadow-xs transition-[color,box-shadow] outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 resize-none max-h-60 overflow-y-auto scrollbar-sleek"
|
||||
/>
|
||||
<p className="text-[10px] text-muted-foreground">
|
||||
Supports **markdown** — bold, lists, `code`, links. Press Enter or{' '}
|
||||
|
||||
@@ -214,7 +214,7 @@ export default function SparseCheckoutPresetSelect({
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
align="start"
|
||||
className="popover-scroll-content max-h-[min(var(--radix-popover-content-available-height),24rem)] w-[var(--radix-popover-trigger-width)] max-w-[calc(100vw-2rem)] overflow-y-auto p-0"
|
||||
className="popover-scroll-content max-h-[min(var(--radix-popover-content-available-height),24rem)] w-[var(--radix-popover-trigger-width)] max-w-[calc(100vw-2rem)] overflow-y-auto p-0 scrollbar-sleek"
|
||||
onOpenAutoFocus={(event) => event.preventDefault()}
|
||||
>
|
||||
{draft ? (
|
||||
|
||||
@@ -167,7 +167,7 @@ function ClaudeSwitcherMenu({
|
||||
<div className="px-2 py-1 text-[10px] font-medium uppercase tracking-[0.08em] text-muted-foreground">
|
||||
Switch to
|
||||
</div>
|
||||
<div className="max-h-[220px] overflow-y-auto rounded-md border border-border/60 bg-accent/5 p-1">
|
||||
<div className="max-h-[220px] overflow-y-auto rounded-md border border-border/60 bg-accent/5 p-1 scrollbar-sleek">
|
||||
{availableSwitchTargets.length === 0 ? (
|
||||
<div className="px-2 py-1.5 text-[11px] text-muted-foreground">No other accounts</div>
|
||||
) : null}
|
||||
@@ -557,7 +557,7 @@ function CodexSwitcherMenu({
|
||||
<div className="px-2 py-1 text-[10px] font-medium uppercase tracking-[0.08em] text-muted-foreground">
|
||||
Switch to
|
||||
</div>
|
||||
<div className="max-h-[220px] overflow-y-auto rounded-md border border-border/60 bg-accent/5 p-1">
|
||||
<div className="max-h-[220px] overflow-y-auto rounded-md border border-border/60 bg-accent/5 p-1 scrollbar-sleek">
|
||||
{availableSwitchTargets.length === 0 ? (
|
||||
<div className="px-2 py-1.5 text-[11px] text-muted-foreground">No other accounts</div>
|
||||
) : null}
|
||||
|
||||
@@ -81,7 +81,7 @@ function ContextMenuContent({
|
||||
<ContextMenuPrimitive.Content
|
||||
data-slot="context-menu-content"
|
||||
className={cn(
|
||||
'z-[70] max-h-(--radix-context-menu-content-available-height) min-w-[11rem] origin-(--radix-context-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-[11px] border border-black/14 bg-[rgba(255,255,255,0.10)] p-1 text-popover-foreground shadow-[0_16px_36px_rgba(0,0,0,0.24),inset_0_1px_0_rgba(255,255,255,0.14)] backdrop-blur-2xl dark:border-white/14 dark:bg-[rgba(0,0,0,0.12)] dark:shadow-[0_20px_44px_rgba(0,0,0,0.42),inset_0_1px_0_rgba(255,255,255,0.04)] data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95',
|
||||
'z-[70] max-h-(--radix-context-menu-content-available-height) min-w-[11rem] origin-(--radix-context-menu-content-transform-origin) overflow-x-hidden overflow-y-auto scrollbar-sleek rounded-[11px] border border-black/14 bg-[rgba(255,255,255,0.10)] p-1 text-popover-foreground shadow-[0_16px_36px_rgba(0,0,0,0.24),inset_0_1px_0_rgba(255,255,255,0.14)] backdrop-blur-2xl dark:border-white/14 dark:bg-[rgba(0,0,0,0.12)] dark:shadow-[0_20px_44px_rgba(0,0,0,0.42),inset_0_1px_0_rgba(255,255,255,0.04)] data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -32,7 +32,7 @@ function DropdownMenuContent({
|
||||
data-slot="dropdown-menu-content"
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
'z-[70] max-h-(--radix-dropdown-menu-content-available-height) min-w-[11rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-[11px] border border-black/14 bg-[rgba(255,255,255,0.82)] p-1 text-black dark:text-white shadow-[0_16px_36px_rgba(0,0,0,0.24),inset_0_1px_0_rgba(255,255,255,0.14)] backdrop-blur-2xl dark:border-white/14 dark:bg-[rgba(0,0,0,0.72)] dark:shadow-[0_20px_44px_rgba(0,0,0,0.42),inset_0_1px_0_rgba(255,255,255,0.04)] data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95',
|
||||
'z-[70] max-h-(--radix-dropdown-menu-content-available-height) min-w-[11rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto scrollbar-sleek rounded-[11px] border border-black/14 bg-[rgba(255,255,255,0.82)] p-1 text-black dark:text-white shadow-[0_16px_36px_rgba(0,0,0,0.24),inset_0_1px_0_rgba(255,255,255,0.14)] backdrop-blur-2xl dark:border-white/14 dark:bg-[rgba(0,0,0,0.72)] dark:shadow-[0_20px_44px_rgba(0,0,0,0.42),inset_0_1px_0_rgba(255,255,255,0.04)] data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95',
|
||||
className
|
||||
)}
|
||||
// Why: Electron's -webkit-app-region: drag on the titlebar captures
|
||||
|
||||
@@ -59,7 +59,7 @@ function SelectContent({
|
||||
<SelectPrimitive.Content
|
||||
data-slot="select-content"
|
||||
className={cn(
|
||||
'relative z-[70] max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border border-border/50 bg-popover text-popover-foreground shadow-md data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95',
|
||||
'relative z-[70] max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto scrollbar-sleek rounded-md border border-border/50 bg-popover text-popover-foreground shadow-md data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95',
|
||||
position === 'popper' &&
|
||||
'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',
|
||||
className
|
||||
|
||||
Reference in New Issue
Block a user