feat: introduced a token-based theming system across the UI
* feat: added themes system * feat: smart sidebar auto-hide for files/diff tabs + lower files sidebar threshold * feat: added Checkbox component and update theming - Add reusable Checkbox component for toggles across UI - Replace several inputs with Checkbox in settings and commands panels - Add DiffIcon and apply surface/border theming to key UI areas * feat: Add convert-vscode-theme.cjs to convert VS Code themes to OpenChamber format * refactor: remove unused permission logic from ChatInput - Remove unused permission rules parsing logic from ChatInput - Memoize renderTheme in DiffWorkerProvider to avoid unnecessary recalculations - Remove forceOpaque helper in vscode theme adapter * fix: guard VSCode theme loading in MarkdownRenderer * feat: add custom user themes loading and reload - Load user themes from ~/.config/openchamber/themes at runtime - Expose /api/config/themes to fetch custom themes - Allow theme reloading from Settings → Theme → Reload themes in the UI
This commit is contained in:
committed by
GitHub
parent
5bb2c56bc0
commit
ddedc02687
@@ -236,7 +236,7 @@ export function BranchPickerDialog({ open, onOpenChange, project }: BranchPicker
|
||||
return (
|
||||
<div
|
||||
key={branchName}
|
||||
className="flex items-center gap-2 px-2.5 py-1.5 hover:bg-muted/30 rounded-md overflow-hidden"
|
||||
className="flex items-center gap-2 px-2.5 py-1.5 hover:bg-interactive-hover/30 rounded-md overflow-hidden"
|
||||
>
|
||||
<RiGitBranchLine className="h-4 w-4 text-muted-foreground flex-shrink-0" />
|
||||
|
||||
@@ -327,7 +327,7 @@ export function BranchPickerDialog({ open, onOpenChange, project }: BranchPicker
|
||||
type="button"
|
||||
onClick={() => beginRename(branchName)}
|
||||
disabled={disableRename}
|
||||
className="inline-flex h-7 w-7 items-center justify-center rounded-md hover:bg-muted/40 text-muted-foreground hover:text-foreground transition-colors disabled:opacity-50"
|
||||
className="inline-flex h-7 w-7 items-center justify-center rounded-md hover:bg-interactive-hover/40 text-muted-foreground hover:text-foreground transition-colors disabled:opacity-50"
|
||||
aria-label="Rename"
|
||||
>
|
||||
<RiPencilLine className="h-4 w-4" />
|
||||
@@ -371,7 +371,7 @@ export function BranchPickerDialog({ open, onOpenChange, project }: BranchPicker
|
||||
type="button"
|
||||
onClick={() => void commitRename(branchName)}
|
||||
disabled={isRenaming}
|
||||
className="inline-flex h-7 w-7 items-center justify-center rounded-md hover:bg-muted/40 text-muted-foreground hover:text-foreground transition-colors disabled:opacity-50"
|
||||
className="inline-flex h-7 w-7 items-center justify-center rounded-md hover:bg-interactive-hover/40 text-muted-foreground hover:text-foreground transition-colors disabled:opacity-50"
|
||||
aria-label="Confirm rename"
|
||||
>
|
||||
{isRenaming ? (
|
||||
@@ -383,7 +383,7 @@ export function BranchPickerDialog({ open, onOpenChange, project }: BranchPicker
|
||||
<button
|
||||
type="button"
|
||||
onClick={cancelRename}
|
||||
className="inline-flex h-7 w-7 items-center justify-center rounded-md hover:bg-muted/40 text-muted-foreground hover:text-foreground transition-colors"
|
||||
className="inline-flex h-7 w-7 items-center justify-center rounded-md hover:bg-interactive-hover/40 text-muted-foreground hover:text-foreground transition-colors"
|
||||
aria-label="Cancel rename"
|
||||
>
|
||||
<RiCloseLine className="h-4 w-4" />
|
||||
@@ -420,7 +420,7 @@ export function BranchPickerDialog({ open, onOpenChange, project }: BranchPicker
|
||||
<button
|
||||
type="button"
|
||||
onClick={cancelDelete}
|
||||
className="inline-flex h-7 w-7 items-center justify-center rounded-md hover:bg-muted/40 text-muted-foreground hover:text-foreground transition-colors"
|
||||
className="inline-flex h-7 w-7 items-center justify-center rounded-md hover:bg-interactive-hover/40 text-muted-foreground hover:text-foreground transition-colors"
|
||||
aria-label="Cancel delete"
|
||||
>
|
||||
<RiCloseLine className="h-4 w-4" />
|
||||
|
||||
@@ -292,7 +292,7 @@ export const DirectoryAutocomplete = React.forwardRef<DirectoryAutocompleteHandl
|
||||
ref={(el) => { itemRefs.current[index] = el; }}
|
||||
className={cn(
|
||||
"flex items-center gap-2 px-3 py-1.5 cursor-pointer typography-ui-label",
|
||||
isSelected && "bg-muted"
|
||||
isSelected && "bg-interactive-selection"
|
||||
)}
|
||||
onClick={() => { handleSelectSuggestion(entry); onClose(); }}
|
||||
onMouseEnter={() => setSelectedIndex(index)}
|
||||
|
||||
@@ -208,7 +208,7 @@ export const DirectoryExplorerDialog: React.FC<DirectoryExplorerDialogProps> = (
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggleShowHidden}
|
||||
className="flex items-center gap-2 px-2 py-1 rounded-lg hover:bg-accent/40 transition-colors typography-meta text-muted-foreground flex-shrink-0"
|
||||
className="flex items-center gap-2 px-2 py-1 rounded-lg hover:bg-interactive-hover/40 transition-colors typography-meta text-muted-foreground flex-shrink-0"
|
||||
>
|
||||
{showHidden ? (
|
||||
<RiCheckboxLine className="h-4 w-4 text-primary" />
|
||||
|
||||
@@ -626,7 +626,7 @@ export const DirectoryTree: React.FC<DirectoryTreeProps> = ({
|
||||
e.stopPropagation();
|
||||
toggleExpanded(item);
|
||||
}}
|
||||
className={cn("hover:bg-accent rounded", isMobile ? "p-0.5" : "p-0.5")}
|
||||
className={cn("hover:bg-interactive-hover rounded", isMobile ? "p-0.5" : "p-0.5")}
|
||||
>
|
||||
{isExpanded ? (
|
||||
<RiArrowDownSLine className={isMobile ? "h-3.5 w-3.5" : "h-3 w-3"} />
|
||||
@@ -681,7 +681,7 @@ export const DirectoryTree: React.FC<DirectoryTreeProps> = ({
|
||||
startCreatingDirectory(item);
|
||||
}}
|
||||
className={cn(
|
||||
"hover:bg-accent rounded transition-opacity",
|
||||
"hover:bg-interactive-hover rounded transition-opacity",
|
||||
isMobile ? "p-1.5" : "p-1",
|
||||
alwaysShowActions ? "opacity-60" : "opacity-0 group-hover:opacity-100"
|
||||
)}
|
||||
@@ -696,7 +696,7 @@ export const DirectoryTree: React.FC<DirectoryTreeProps> = ({
|
||||
togglePin(item.path);
|
||||
}}
|
||||
className={cn(
|
||||
"hover:bg-accent rounded transition-opacity",
|
||||
"hover:bg-interactive-hover rounded transition-opacity",
|
||||
isMobile ? "p-1.5" : "p-1",
|
||||
alwaysShowActions ? "opacity-60" : "opacity-0 group-hover:opacity-100"
|
||||
)}
|
||||
@@ -720,7 +720,7 @@ export const DirectoryTree: React.FC<DirectoryTreeProps> = ({
|
||||
isMobile ? 'px-1.5 py-1' : 'px-2 py-1.5',
|
||||
isSelected
|
||||
? 'bg-primary/10 text-primary'
|
||||
: 'hover:bg-accent/50 text-foreground'
|
||||
: 'hover:bg-interactive-hover/50 text-foreground'
|
||||
)}
|
||||
style={{ paddingLeft: `${level * (isMobile ? 12 : 14) + (isMobile ? 4 : 6)}px` }}
|
||||
>
|
||||
@@ -751,7 +751,7 @@ export const DirectoryTree: React.FC<DirectoryTreeProps> = ({
|
||||
}
|
||||
}}
|
||||
onBlur={createDirectory}
|
||||
className="h-6 typography-meta flex-1 selection:bg-muted selection:text-muted-foreground"
|
||||
className="h-6 typography-meta flex-1 selection:bg-interactive-selection selection:text-interactive-selection-foreground"
|
||||
placeholder="new_directory"
|
||||
/>
|
||||
<button
|
||||
@@ -760,7 +760,7 @@ export const DirectoryTree: React.FC<DirectoryTreeProps> = ({
|
||||
e.stopPropagation();
|
||||
createDirectory();
|
||||
}}
|
||||
className="p-1 hover:bg-accent rounded"
|
||||
className="p-1 hover:bg-interactive-hover rounded"
|
||||
title="Create directory"
|
||||
>
|
||||
<RiCheckLine className="h-3 w-3 text-green-600" />
|
||||
@@ -771,7 +771,7 @@ export const DirectoryTree: React.FC<DirectoryTreeProps> = ({
|
||||
e.stopPropagation();
|
||||
cancelCreatingDirectory();
|
||||
}}
|
||||
className="p-1 hover:bg-accent rounded"
|
||||
className="p-1 hover:bg-interactive-hover rounded"
|
||||
title="Cancel"
|
||||
>
|
||||
<RiCloseLine className="h-3 w-3 text-muted-foreground" />
|
||||
@@ -790,7 +790,7 @@ export const DirectoryTree: React.FC<DirectoryTreeProps> = ({
|
||||
<DropdownMenuItem
|
||||
className={cn(
|
||||
'flex items-center gap-1 cursor-pointer group',
|
||||
currentPath === item.path && 'bg-accent'
|
||||
currentPath === item.path && 'bg-interactive-selection'
|
||||
)}
|
||||
style={{ paddingLeft: `${level * 12 + 8}px` }}
|
||||
onSelect={(e) => {
|
||||
@@ -803,7 +803,7 @@ export const DirectoryTree: React.FC<DirectoryTreeProps> = ({
|
||||
e.stopPropagation();
|
||||
toggleExpanded(item);
|
||||
}}
|
||||
className="p-0.5 hover:bg-accent rounded"
|
||||
className="p-0.5 hover:bg-interactive-hover rounded"
|
||||
>
|
||||
{isExpanded ? (
|
||||
<RiArrowDownSLine className="h-3 w-3" />
|
||||
@@ -840,7 +840,7 @@ export const DirectoryTree: React.FC<DirectoryTreeProps> = ({
|
||||
}
|
||||
}}
|
||||
onBlur={createDirectory}
|
||||
className="h-6 typography-meta flex-1 selection:bg-muted selection:text-muted-foreground"
|
||||
className="h-6 typography-meta flex-1 selection:bg-interactive-selection selection:text-interactive-selection-foreground"
|
||||
placeholder="new_directory"
|
||||
/>
|
||||
<button
|
||||
@@ -849,7 +849,7 @@ export const DirectoryTree: React.FC<DirectoryTreeProps> = ({
|
||||
e.stopPropagation();
|
||||
createDirectory();
|
||||
}}
|
||||
className="p-1 hover:bg-accent rounded"
|
||||
className="p-1 hover:bg-interactive-hover rounded"
|
||||
title="Create directory"
|
||||
>
|
||||
<RiCheckLine className="h-3 w-3 text-green-600" />
|
||||
@@ -860,7 +860,7 @@ export const DirectoryTree: React.FC<DirectoryTreeProps> = ({
|
||||
e.stopPropagation();
|
||||
cancelCreatingDirectory();
|
||||
}}
|
||||
className="p-1 hover:bg-accent rounded"
|
||||
className="p-1 hover:bg-interactive-hover rounded"
|
||||
title="Cancel"
|
||||
>
|
||||
<RiCloseLine className="h-3 w-3 text-muted-foreground" />
|
||||
@@ -885,7 +885,7 @@ export const DirectoryTree: React.FC<DirectoryTreeProps> = ({
|
||||
isMobile ? 'px-1.5 py-1' : 'px-2 py-1.5',
|
||||
isSelected
|
||||
? 'bg-primary/10'
|
||||
: 'hover:bg-accent/50'
|
||||
: 'hover:bg-interactive-hover/50'
|
||||
)}
|
||||
>
|
||||
<button
|
||||
@@ -923,7 +923,7 @@ export const DirectoryTree: React.FC<DirectoryTreeProps> = ({
|
||||
<button
|
||||
onClick={() => togglePin(path)}
|
||||
className={cn(
|
||||
"hover:bg-accent rounded-md transition-opacity",
|
||||
"hover:bg-interactive-hover rounded-md transition-opacity",
|
||||
isMobile ? "p-1.5 opacity-60" : "p-1 opacity-0 group-hover:opacity-100"
|
||||
)}
|
||||
title="Unpin directory"
|
||||
@@ -946,7 +946,7 @@ export const DirectoryTree: React.FC<DirectoryTreeProps> = ({
|
||||
}}
|
||||
className={cn(
|
||||
'flex items-start gap-2 cursor-pointer group py-2',
|
||||
currentPath === path && 'bg-accent'
|
||||
currentPath === path && 'bg-interactive-selection'
|
||||
)}
|
||||
>
|
||||
<RiFolder6Line className="h-3.5 w-3.5 text-muted-foreground mt-0.5" />
|
||||
@@ -962,7 +962,7 @@ export const DirectoryTree: React.FC<DirectoryTreeProps> = ({
|
||||
e.preventDefault();
|
||||
togglePin(path);
|
||||
}}
|
||||
className="p-1 opacity-0 group-hover:opacity-100 hover:bg-accent rounded transition-opacity"
|
||||
className="p-1 opacity-0 group-hover:opacity-100 hover:bg-interactive-hover rounded transition-opacity"
|
||||
title="Unpin directory"
|
||||
>
|
||||
<RiPushpin2Line className="h-3 w-3 text-primary" />
|
||||
@@ -985,7 +985,7 @@ export const DirectoryTree: React.FC<DirectoryTreeProps> = ({
|
||||
type="button"
|
||||
onClick={() => setIsPinnedExpanded(prev => !prev)}
|
||||
className={cn(
|
||||
"flex w-full items-center gap-1.5 typography-meta font-medium text-muted-foreground/80 hover:bg-accent/30 rounded transition-colors uppercase tracking-wide",
|
||||
"flex w-full items-center gap-1.5 typography-meta font-medium text-muted-foreground/80 hover:bg-interactive-hover/30 rounded transition-colors uppercase tracking-wide",
|
||||
isMobile ? "px-1.5 py-1" : "px-2 py-1.5"
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -505,8 +505,8 @@ Do not implement changes until I confirm; end with: “Next actions: <1 sentence
|
||||
{directNumber && projectDirectory && github && connected ? (
|
||||
<div
|
||||
className={cn(
|
||||
'group flex items-center gap-2 py-1.5 hover:bg-muted/30 rounded transition-colors cursor-pointer',
|
||||
startingIssueNumber === directNumber && 'bg-muted/30'
|
||||
'group flex items-center gap-2 py-1.5 hover:bg-interactive-hover/30 rounded transition-colors cursor-pointer',
|
||||
startingIssueNumber === directNumber && 'bg-interactive-selection/30'
|
||||
)}
|
||||
onClick={() => void startSession(directNumber)}
|
||||
>
|
||||
@@ -530,8 +530,8 @@ Do not implement changes until I confirm; end with: “Next actions: <1 sentence
|
||||
<div
|
||||
key={issue.number}
|
||||
className={cn(
|
||||
'group flex items-center gap-2 py-1.5 hover:bg-muted/30 rounded transition-colors cursor-pointer',
|
||||
startingIssueNumber === issue.number && 'bg-muted/30'
|
||||
'group flex items-center gap-2 py-1.5 hover:bg-interactive-hover/30 rounded transition-colors cursor-pointer',
|
||||
startingIssueNumber === issue.number && 'bg-interactive-selection/30'
|
||||
)}
|
||||
onClick={() => void startSession(issue.number)}
|
||||
>
|
||||
|
||||
@@ -646,8 +646,8 @@ Nice-to-have:
|
||||
{directNumber && projectDirectory && github && connected ? (
|
||||
<div
|
||||
className={cn(
|
||||
'group flex items-center gap-2 py-1.5 hover:bg-muted/30 rounded transition-colors cursor-pointer',
|
||||
startingNumber === directNumber && 'bg-muted/30'
|
||||
'group flex items-center gap-2 py-1.5 hover:bg-interactive-hover/30 rounded transition-colors cursor-pointer',
|
||||
startingNumber === directNumber && 'bg-interactive-selection/30'
|
||||
)}
|
||||
onClick={() => void startSession(directNumber)}
|
||||
>
|
||||
@@ -675,10 +675,10 @@ Nice-to-have:
|
||||
key={pr.number}
|
||||
className={cn(
|
||||
'group flex items-start gap-2 py-1.5 rounded transition-colors',
|
||||
startingNumber === pr.number && 'bg-muted/30',
|
||||
startingNumber === pr.number && 'bg-interactive-selection/30',
|
||||
disabledByWorktree
|
||||
? 'opacity-50 cursor-not-allowed'
|
||||
: 'hover:bg-muted/30 cursor-pointer'
|
||||
: 'hover:bg-interactive-hover/30 cursor-pointer'
|
||||
)}
|
||||
onClick={() => {
|
||||
if (disabledByWorktree) return;
|
||||
|
||||
@@ -491,7 +491,7 @@ export const SessionDialogs: React.FC = () => {
|
||||
{targetWorktree ? formatPathForDisplay(targetWorktree.path, homeDirectory) : 'Worktree path unavailable.'}
|
||||
</p>
|
||||
{hasDirtyWorktrees && (
|
||||
<p className="typography-micro text-warning">Uncommitted changes will be discarded.</p>
|
||||
<p className="typography-micro text-status-warning">Uncommitted changes will be discarded.</p>
|
||||
)}
|
||||
|
||||
</div>
|
||||
|
||||
@@ -210,7 +210,7 @@ const SortableProjectItem: React.FC<SortableProjectItemProps> = ({
|
||||
? isStuck ? 'var(--sidebar-stuck-bg)' : 'transparent'
|
||||
: undefined,
|
||||
borderColor: isHovered
|
||||
? 'var(--color-border)'
|
||||
? 'var(--color-border-hover)'
|
||||
: isCollapsed
|
||||
? 'color-mix(in srgb, var(--color-border) 35%, transparent)'
|
||||
: 'var(--color-border)'
|
||||
@@ -320,7 +320,7 @@ const SortableProjectItem: React.FC<SortableProjectItemProps> = ({
|
||||
onNewWorktreeSession();
|
||||
}}
|
||||
className={cn(
|
||||
'inline-flex h-6 w-6 items-center justify-center rounded-md text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50 hover:text-foreground flex-shrink-0',
|
||||
'inline-flex h-6 w-6 items-center justify-center rounded-md text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50 hover:text-foreground hover:bg-interactive-hover/50 flex-shrink-0',
|
||||
mobileVariant ? 'opacity-70' : 'opacity-100',
|
||||
)}
|
||||
aria-label="New session in worktree"
|
||||
@@ -342,7 +342,7 @@ const SortableProjectItem: React.FC<SortableProjectItemProps> = ({
|
||||
e.stopPropagation();
|
||||
onNewSession();
|
||||
}}
|
||||
className="inline-flex h-6 w-6 items-center justify-center text-muted-foreground hover:text-foreground flex-shrink-0 rounded-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50"
|
||||
className="inline-flex h-6 w-6 items-center justify-center text-muted-foreground hover:text-foreground hover:bg-interactive-hover/50 flex-shrink-0 rounded-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50"
|
||||
aria-label="New session"
|
||||
>
|
||||
<RiAddLine className="h-4 w-4" />
|
||||
@@ -1133,7 +1133,7 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
|
||||
key={session.id}
|
||||
className={cn(
|
||||
'group relative flex items-center rounded-md px-1.5 py-1',
|
||||
'dark:bg-accent/80 bg-primary/12',
|
||||
'bg-interactive-selection',
|
||||
depth > 0 && 'pl-[20px]',
|
||||
)}
|
||||
>
|
||||
@@ -1209,7 +1209,7 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
|
||||
const streamingIndicator = (() => {
|
||||
if (!memoryState) return null;
|
||||
if (memoryState.isZombie) {
|
||||
return <RiErrorWarningLine className="h-4 w-4 text-warning" />;
|
||||
return <RiErrorWarningLine className="h-4 w-4 text-status-warning" />;
|
||||
}
|
||||
return null;
|
||||
})();
|
||||
@@ -1219,7 +1219,7 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
|
||||
<div
|
||||
className={cn(
|
||||
'group relative flex items-center rounded-md px-1.5 py-1',
|
||||
isActive ? 'dark:bg-accent/80 bg-primary/12' : 'hover:dark:bg-accent/40 hover:bg-primary/6',
|
||||
isActive ? 'bg-interactive-selection' : 'hover:bg-interactive-hover',
|
||||
isMissingDirectory ? 'opacity-75' : '',
|
||||
depth > 0 && 'pl-[20px]',
|
||||
)}
|
||||
@@ -1314,7 +1314,7 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
|
||||
</span>
|
||||
) : null}
|
||||
{isMissingDirectory ? (
|
||||
<span className="inline-flex items-center gap-0.5 text-warning flex-shrink-0">
|
||||
<span className="inline-flex items-center gap-0.5 text-status-warning flex-shrink-0">
|
||||
<RiErrorWarningLine className="h-3 w-3" />
|
||||
Missing
|
||||
</span>
|
||||
@@ -1570,7 +1570,7 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
|
||||
type="button"
|
||||
onClick={handleOpenDirectoryDialog}
|
||||
className={cn(
|
||||
'inline-flex h-8 w-8 items-center justify-center rounded-md text-muted-foreground hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50',
|
||||
'inline-flex h-8 w-8 items-center justify-center rounded-md text-muted-foreground hover:text-foreground hover:bg-interactive-hover/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50',
|
||||
!isDesktopRuntime && 'bg-sidebar/60 hover:bg-sidebar',
|
||||
)}
|
||||
aria-label="Add project"
|
||||
|
||||
Reference in New Issue
Block a user