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
@@ -0,0 +1,205 @@
|
|||||||
|
---
|
||||||
|
name: theme-system
|
||||||
|
description: Use when creating or modifying UI components, styling, or visual elements in OpenChamber. All UI colors must use theme tokens - never hardcoded values or Tailwind color classes.
|
||||||
|
license: MIT
|
||||||
|
compatibility: opencode
|
||||||
|
---
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
OpenChamber uses a JSON-based theme system. Themes are defined in `packages/ui/src/lib/theme/themes/`. Users can also add custom themes via `~/.config/openchamber/themes/`.
|
||||||
|
|
||||||
|
**Core principle:** UI colors must use theme tokens - never hardcoded hex colors or Tailwind color classes.
|
||||||
|
|
||||||
|
## When to Use
|
||||||
|
|
||||||
|
- Creating or modifying UI components
|
||||||
|
- Working with colors, backgrounds, borders, or text
|
||||||
|
|
||||||
|
## Quick Decision Tree
|
||||||
|
|
||||||
|
1. **Code display?** → `syntax.*`
|
||||||
|
2. **Feedback/status?** → `status.*`
|
||||||
|
3. **Primary CTA?** → `primary.*`
|
||||||
|
4. **Interactive/clickable?** → `interactive.*`
|
||||||
|
5. **Background layer?** → `surface.*`
|
||||||
|
6. **Text?** → `surface.foreground` or `surface.mutedForeground`
|
||||||
|
|
||||||
|
## Critical Rules
|
||||||
|
|
||||||
|
- `surface.elevated` = inputs, cards, panels
|
||||||
|
- `interactive.hover` = **ONLY on clickable elements**
|
||||||
|
- `interactive.selection` = active/selected states (not primary!)
|
||||||
|
- Status colors = **ONLY for actual feedback** (errors, warnings, success)
|
||||||
|
- Input footers = `bg-transparent` on elevated background
|
||||||
|
|
||||||
|
### Never Use
|
||||||
|
|
||||||
|
- Hardcoded hex colors (`#FF0000`)
|
||||||
|
- Tailwind colors (`bg-white`, `text-blue-500`, `bg-gray-*`)
|
||||||
|
- Deprecated: `bg-secondary`, `bg-muted`
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Via Hook
|
||||||
|
```tsx
|
||||||
|
import { useThemeSystem } from '@/contexts/useThemeSystem';
|
||||||
|
const { currentTheme } = useThemeSystem();
|
||||||
|
|
||||||
|
<div style={{ backgroundColor: currentTheme.colors.surface.elevated }}>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Via CSS Variables
|
||||||
|
```tsx
|
||||||
|
<div className="bg-[var(--surface-elevated)] hover:bg-[var(--interactive-hover)]">
|
||||||
|
```
|
||||||
|
|
||||||
|
## Color Tokens
|
||||||
|
|
||||||
|
### Surface Colors
|
||||||
|
|
||||||
|
| Token | Usage |
|
||||||
|
|-------|-------|
|
||||||
|
| `surface.background` | Main app background |
|
||||||
|
| `surface.elevated` | Inputs, cards, panels, popovers |
|
||||||
|
| `surface.muted` | Secondary backgrounds, sidebars |
|
||||||
|
| `surface.foreground` | Primary text |
|
||||||
|
| `surface.mutedForeground` | Secondary text, hints |
|
||||||
|
| `surface.subtle` | Subtle dividers |
|
||||||
|
|
||||||
|
### Interactive Colors
|
||||||
|
|
||||||
|
| Token | Usage |
|
||||||
|
|-------|-------|
|
||||||
|
| `interactive.border` | Default borders |
|
||||||
|
| `interactive.hover` | Hover on **clickable elements only** |
|
||||||
|
| `interactive.selection` | Active/selected items |
|
||||||
|
| `interactive.selectionForeground` | Text on selection |
|
||||||
|
| `interactive.focusRing` | Focus indicators |
|
||||||
|
|
||||||
|
### Status Colors
|
||||||
|
|
||||||
|
| Token | Usage |
|
||||||
|
|-------|-------|
|
||||||
|
| `status.error` | Errors, validation failures |
|
||||||
|
| `status.warning` | Warnings, cautions |
|
||||||
|
| `status.success` | Success messages |
|
||||||
|
| `status.info` | Informational messages |
|
||||||
|
|
||||||
|
Each has variants: `*`, `*Foreground`, `*Background`, `*Border`.
|
||||||
|
|
||||||
|
### Primary Colors
|
||||||
|
|
||||||
|
| Token | Usage |
|
||||||
|
|-------|-------|
|
||||||
|
| `primary.base` | Primary CTA buttons |
|
||||||
|
| `primary.hover` | Hover on primary elements |
|
||||||
|
| `primary.foreground` | Text on primary background |
|
||||||
|
|
||||||
|
**Primary vs Selection:** Primary = "click me" (CTA), Selection = "currently active" (state).
|
||||||
|
|
||||||
|
### Syntax Colors
|
||||||
|
|
||||||
|
For code display only. Never use for UI elements.
|
||||||
|
|
||||||
|
| Token | Usage |
|
||||||
|
|-------|-------|
|
||||||
|
| `syntax.base.background` | Code block background |
|
||||||
|
| `syntax.base.foreground` | Default code text |
|
||||||
|
| `syntax.base.keyword` | Keywords |
|
||||||
|
| `syntax.base.string` | Strings |
|
||||||
|
| `syntax.highlights.diffAdded` | Added lines |
|
||||||
|
| `syntax.highlights.diffRemoved` | Removed lines |
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### Input Area
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
const { currentTheme } = useThemeSystem();
|
||||||
|
|
||||||
|
<div style={{ backgroundColor: currentTheme.colors.surface.elevated }}>
|
||||||
|
<textarea className="bg-transparent" />
|
||||||
|
<div className="bg-transparent">{/* Footer - transparent! */}</div>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Active Tab
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<button className={isActive
|
||||||
|
? 'bg-interactive-selection text-interactive-selection-foreground'
|
||||||
|
: 'hover:bg-interactive-hover/50'
|
||||||
|
}>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Error Message
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<div style={{
|
||||||
|
color: currentTheme.colors.status.error,
|
||||||
|
backgroundColor: currentTheme.colors.status.errorBackground
|
||||||
|
}}>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Card
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<div style={{ backgroundColor: currentTheme.colors.surface.elevated }}>
|
||||||
|
<h3 style={{ color: currentTheme.colors.surface.foreground }}>Title</h3>
|
||||||
|
<p style={{ color: currentTheme.colors.surface.mutedForeground }}>Description</p>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Wrong vs Right
|
||||||
|
|
||||||
|
### Wrong
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// Hardcoded colors
|
||||||
|
<div style={{ backgroundColor: '#F2F0E5' }}>
|
||||||
|
<button className="bg-blue-500">
|
||||||
|
|
||||||
|
// Primary for active tab
|
||||||
|
<Tab className="bg-primary">Active</Tab>
|
||||||
|
|
||||||
|
// Hover on static element
|
||||||
|
<div className="hover:bg-interactive-hover">Static card</div>
|
||||||
|
|
||||||
|
// Colored footer on input
|
||||||
|
<div style={{ backgroundColor: currentTheme.colors.surface.elevated }}>
|
||||||
|
<textarea />
|
||||||
|
<div style={{ backgroundColor: currentTheme.colors.surface.muted }}>Footer</div>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Right
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// Theme tokens
|
||||||
|
<div style={{ backgroundColor: currentTheme.colors.surface.elevated }}>
|
||||||
|
<button style={{ backgroundColor: currentTheme.colors.primary.base }}>
|
||||||
|
|
||||||
|
// Selection for active tab
|
||||||
|
<Tab style={{ backgroundColor: currentTheme.colors.interactive.selection }}>Active</Tab>
|
||||||
|
|
||||||
|
// Hover only on clickable
|
||||||
|
<button className="hover:bg-[var(--interactive-hover)]">Click</button>
|
||||||
|
|
||||||
|
// Transparent footer
|
||||||
|
<div style={{ backgroundColor: currentTheme.colors.surface.elevated }}>
|
||||||
|
<textarea className="bg-transparent" />
|
||||||
|
<div className="bg-transparent">Footer</div>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- **[Adding Themes](references/adding-themes.md)** - Built-in and custom themes
|
||||||
|
|
||||||
|
## Key Files
|
||||||
|
|
||||||
|
- Theme types: `packages/ui/src/types/theme.ts`
|
||||||
|
- Theme hook: `packages/ui/src/contexts/useThemeSystem.ts`
|
||||||
|
- CSS generator: `packages/ui/src/lib/theme/cssGenerator.ts`
|
||||||
|
- Built-in themes: `packages/ui/src/lib/theme/themes/`
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
---
|
||||||
|
title: Adding Themes
|
||||||
|
---
|
||||||
|
|
||||||
|
# Adding Themes
|
||||||
|
|
||||||
|
## Custom Themes (User)
|
||||||
|
|
||||||
|
Drop a JSON file into `~/.config/openchamber/themes/`. No rebuild needed.
|
||||||
|
|
||||||
|
1. Create theme file (e.g., `my-theme.json`)
|
||||||
|
2. In app: **Settings → Theme → Reload themes**
|
||||||
|
3. Select from dropdown
|
||||||
|
|
||||||
|
See `docs/CUSTOM_THEMES.md` for full format reference.
|
||||||
|
|
||||||
|
## Built-in Themes (Development)
|
||||||
|
|
||||||
|
### 1. Create JSON Files
|
||||||
|
|
||||||
|
Add to `packages/ui/src/lib/theme/themes/`:
|
||||||
|
- `<id>-light.json`
|
||||||
|
- `<id>-dark.json`
|
||||||
|
|
||||||
|
Use existing themes (e.g., `flexoki-dark.json`) as reference for the full structure.
|
||||||
|
|
||||||
|
### 2. Register in presets.ts
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import mytheme_light_Raw from './mytheme-light.json';
|
||||||
|
import mytheme_dark_Raw from './mytheme-dark.json';
|
||||||
|
|
||||||
|
export const presetThemes: Theme[] = [
|
||||||
|
// ... existing themes
|
||||||
|
mytheme_light_Raw as Theme,
|
||||||
|
mytheme_dark_Raw as Theme,
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Validate
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun run type-check && bun run lint && bun run build
|
||||||
|
```
|
||||||
|
|
||||||
|
## Key Files
|
||||||
|
|
||||||
|
- Theme types: `packages/ui/src/types/theme.ts`
|
||||||
|
- Presets: `packages/ui/src/lib/theme/themes/presets.ts`
|
||||||
|
- Example: `packages/ui/src/lib/theme/themes/flexoki-dark.json`
|
||||||
|
- Custom themes doc: `docs/CUSTOM_THEMES.md`
|
||||||
@@ -72,6 +72,17 @@ All scripts are in `package.json`.
|
|||||||
- No new deps unless asked.
|
- No new deps unless asked.
|
||||||
- Never add secrets (`.env`, keys) or log sensitive data.
|
- Never add secrets (`.env`, keys) or log sensitive data.
|
||||||
|
|
||||||
|
## Theme System (MANDATORY for UI work)
|
||||||
|
|
||||||
|
When working on any UI components, styling, or visual changes, agents **MUST** study the theme system skill first.
|
||||||
|
|
||||||
|
**Before starting any UI work:**
|
||||||
|
```
|
||||||
|
skill({ name: "theme-system" })
|
||||||
|
```
|
||||||
|
|
||||||
|
This skill contains all color tokens, semantic logic, decision tree, and usage patterns. All UI colors must use theme tokens - never hardcoded values or Tailwind color classes.
|
||||||
|
|
||||||
## Recent changes
|
## Recent changes
|
||||||
- Releases + high-level changes: `CHANGELOG.md`
|
- Releases + high-level changes: `CHANGELOG.md`
|
||||||
- Recent commits: `git log --oneline` (latest tags: `v1.4.6`, `v1.4.5`)
|
- Recent commits: `git log --oneline` (latest tags: `v1.4.6`, `v1.4.5`)
|
||||||
|
|||||||
@@ -86,6 +86,12 @@ Run OpenChamber remotely using GitHub Actions. No local computer required.
|
|||||||
|
|
||||||
[**Read the Guide: OpenChamber for Actions**](docs/OPENCHAMBER_FOR_ACTIONS.md)
|
[**Read the Guide: OpenChamber for Actions**](docs/OPENCHAMBER_FOR_ACTIONS.md)
|
||||||
|
|
||||||
|
### Custom Themes
|
||||||
|
|
||||||
|
Create your own color schemes by dropping JSON files into `~/.config/openchamber/themes/`. Hot reload supported — no restart needed.
|
||||||
|
|
||||||
|
[**Read the Guide: Custom Themes**](docs/CUSTOM_THEMES.md)
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
### VS Code Extension
|
### VS Code Extension
|
||||||
|
|||||||
@@ -0,0 +1,218 @@
|
|||||||
|
# Custom Themes
|
||||||
|
|
||||||
|
OpenChamber supports user-defined themes. Drop a JSON file into the themes directory and reload — no app restart required.
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
1. Create the themes directory:
|
||||||
|
```bash
|
||||||
|
mkdir -p ~/.config/openchamber/themes
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Create a theme JSON file (e.g., `my-theme.json`) with the format below.
|
||||||
|
|
||||||
|
3. In OpenChamber: **Settings → Theme → Reload themes**.
|
||||||
|
|
||||||
|
4. Select your theme from the dropdown.
|
||||||
|
|
||||||
|
## Theme Location
|
||||||
|
|
||||||
|
| Platform | Path |
|
||||||
|
|----------|------|
|
||||||
|
| macOS/Linux | `~/.config/openchamber/themes/` |
|
||||||
|
|
||||||
|
## Theme Format
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"metadata": {
|
||||||
|
"id": "my-custom-theme",
|
||||||
|
"name": "My Custom Theme",
|
||||||
|
"description": "A custom theme for OpenChamber",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"variant": "dark",
|
||||||
|
"tags": ["dark", "custom"]
|
||||||
|
},
|
||||||
|
"colors": {
|
||||||
|
"primary": {
|
||||||
|
"base": "#EC8B49",
|
||||||
|
"hover": "#DA702C",
|
||||||
|
"active": "#F9AE77",
|
||||||
|
"foreground": "#100F0F",
|
||||||
|
"muted": "#EC8B4980",
|
||||||
|
"emphasis": "#F9AE77"
|
||||||
|
},
|
||||||
|
"surface": {
|
||||||
|
"background": "#100F0F",
|
||||||
|
"foreground": "#CECDC3",
|
||||||
|
"muted": "#1C1B1A",
|
||||||
|
"mutedForeground": "#878580",
|
||||||
|
"elevated": "#1C1A19",
|
||||||
|
"elevatedForeground": "#CECDC3",
|
||||||
|
"overlay": "#00000080",
|
||||||
|
"subtle": "#1e1d1c"
|
||||||
|
},
|
||||||
|
"interactive": {
|
||||||
|
"border": "#343331",
|
||||||
|
"borderHover": "#403E3C",
|
||||||
|
"borderFocus": "#EC8B49",
|
||||||
|
"selection": "#f4f4f41f",
|
||||||
|
"selectionForeground": "#CECDC3",
|
||||||
|
"focus": "#EC8B49",
|
||||||
|
"focusRing": "#EC8B4950",
|
||||||
|
"cursor": "#CECDC3",
|
||||||
|
"hover": "#ffffff18",
|
||||||
|
"active": "#ffffff1f"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"error": "#D14D41",
|
||||||
|
"errorForeground": "#100F0F",
|
||||||
|
"errorBackground": "#AF302920",
|
||||||
|
"errorBorder": "#AF302950",
|
||||||
|
"warning": "#DA702C",
|
||||||
|
"warningForeground": "#100F0F",
|
||||||
|
"warningBackground": "#BC521520",
|
||||||
|
"warningBorder": "#BC521550",
|
||||||
|
"success": "#A0AF54",
|
||||||
|
"successForeground": "#100F0F",
|
||||||
|
"successBackground": "#66800B20",
|
||||||
|
"successBorder": "#66800B50",
|
||||||
|
"info": "#4385BE",
|
||||||
|
"infoForeground": "#100F0F",
|
||||||
|
"infoBackground": "#205EA620",
|
||||||
|
"infoBorder": "#205EA650"
|
||||||
|
},
|
||||||
|
"syntax": {
|
||||||
|
"base": {
|
||||||
|
"background": "#1C1B1A",
|
||||||
|
"foreground": "#CECDC3",
|
||||||
|
"comment": "#878580",
|
||||||
|
"keyword": "#4385BE",
|
||||||
|
"string": "#3AA99F",
|
||||||
|
"number": "#8B7EC8",
|
||||||
|
"function": "#DA702C",
|
||||||
|
"variable": "#CECDC3",
|
||||||
|
"type": "#D0A215",
|
||||||
|
"operator": "#D14D41"
|
||||||
|
},
|
||||||
|
"tokens": {
|
||||||
|
"commentDoc": "#575653",
|
||||||
|
"stringEscape": "#CECDC3",
|
||||||
|
"keywordImport": "#D14D41",
|
||||||
|
"storageModifier": "#4385BE",
|
||||||
|
"functionCall": "#DA702C",
|
||||||
|
"method": "#879A39",
|
||||||
|
"variableProperty": "#4385BE",
|
||||||
|
"variableOther": "#879A39",
|
||||||
|
"variableGlobal": "#CE5D97",
|
||||||
|
"variableLocal": "#282726",
|
||||||
|
"parameter": "#CECDC3",
|
||||||
|
"constant": "#CECDC3",
|
||||||
|
"class": "#DA702C",
|
||||||
|
"className": "#DA702C",
|
||||||
|
"interface": "#D0A215",
|
||||||
|
"struct": "#DA702C",
|
||||||
|
"enum": "#DA702C",
|
||||||
|
"typeParameter": "#DA702C",
|
||||||
|
"namespace": "#D0A215",
|
||||||
|
"module": "#D14D41",
|
||||||
|
"tag": "#4385BE",
|
||||||
|
"jsxTag": "#CE5D97",
|
||||||
|
"tagAttribute": "#D0A215",
|
||||||
|
"tagAttributeValue": "#3AA99F",
|
||||||
|
"boolean": "#D0A215",
|
||||||
|
"decorator": "#D0A215",
|
||||||
|
"label": "#CE5D97",
|
||||||
|
"punctuation": "#878580",
|
||||||
|
"macro": "#4385BE",
|
||||||
|
"preprocessor": "#CE5D97",
|
||||||
|
"regex": "#3AA99F",
|
||||||
|
"url": "#4385BE",
|
||||||
|
"key": "#DA702C",
|
||||||
|
"exception": "#CE5D97"
|
||||||
|
},
|
||||||
|
"highlights": {
|
||||||
|
"diffAdded": "#879A39",
|
||||||
|
"diffAddedBackground": "#66800B20",
|
||||||
|
"diffRemoved": "#D14D41",
|
||||||
|
"diffRemovedBackground": "#AF302920",
|
||||||
|
"diffModified": "#4385BE",
|
||||||
|
"diffModifiedBackground": "#205EA620",
|
||||||
|
"lineNumber": "#403E3C",
|
||||||
|
"lineNumberActive": "#CECDC3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"markdown": {
|
||||||
|
"heading1": "#fbf9e6",
|
||||||
|
"heading2": "#e6e4d2",
|
||||||
|
"heading3": "#CECDC3",
|
||||||
|
"heading4": "#CECDC3",
|
||||||
|
"link": "#4385BE",
|
||||||
|
"linkHover": "#205EA6",
|
||||||
|
"inlineCode": "#A0AF53",
|
||||||
|
"inlineCodeBackground": "#1C1B1A",
|
||||||
|
"blockquote": "#878580",
|
||||||
|
"blockquoteBorder": "#343331",
|
||||||
|
"listMarker": "#D0A21599"
|
||||||
|
},
|
||||||
|
"chat": {
|
||||||
|
"userMessage": "#CECDC3",
|
||||||
|
"userMessageBackground": "#2d1d15",
|
||||||
|
"assistantMessage": "#CECDC3",
|
||||||
|
"assistantMessageBackground": "#100F0F",
|
||||||
|
"timestamp": "#878580",
|
||||||
|
"divider": "#343331"
|
||||||
|
},
|
||||||
|
"tools": {
|
||||||
|
"background": "#1C1B1A50",
|
||||||
|
"border": "#42403e9d",
|
||||||
|
"headerHover": "#34333150",
|
||||||
|
"icon": "#aca7a1",
|
||||||
|
"title": "#CECDC3",
|
||||||
|
"description": "#878580",
|
||||||
|
"edit": {
|
||||||
|
"added": "#879A39",
|
||||||
|
"addedBackground": "#66800B25",
|
||||||
|
"removed": "#D14D41",
|
||||||
|
"removedBackground": "#AF302925",
|
||||||
|
"lineNumber": "#403E3C"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"fonts": {
|
||||||
|
"sans": "\"IBM Plex Mono\", monospace",
|
||||||
|
"mono": "\"IBM Plex Mono\", monospace",
|
||||||
|
"heading": "\"IBM Plex Mono\", monospace"
|
||||||
|
},
|
||||||
|
"radius": {
|
||||||
|
"none": "0",
|
||||||
|
"sm": "0.125rem",
|
||||||
|
"md": "0.375rem",
|
||||||
|
"lg": "0.5rem",
|
||||||
|
"xl": "0.75rem",
|
||||||
|
"full": "9999px"
|
||||||
|
},
|
||||||
|
"transitions": {
|
||||||
|
"fast": "150ms ease",
|
||||||
|
"normal": "250ms ease",
|
||||||
|
"slow": "350ms ease"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
Themes are validated on load. Invalid themes are skipped with a console warning.
|
||||||
|
|
||||||
|
Common issues:
|
||||||
|
- Missing required fields
|
||||||
|
- Invalid `variant` (must be `"light"` or `"dark"`)
|
||||||
|
- File size > 512KB
|
||||||
|
|
||||||
|
## Tips
|
||||||
|
|
||||||
|
- Use hex with alpha for transparency (e.g., `#FFFFFF20`)
|
||||||
|
- Reference built-in themes in `packages/ui/src/lib/theme/themes/` for more examples
|
||||||
|
- Theme `id` must be unique; duplicates are skipped
|
||||||
@@ -26,6 +26,17 @@
|
|||||||
document.documentElement.style.setProperty('color-scheme', isDark ? 'dark' : 'light');
|
document.documentElement.style.setProperty('color-scheme', isDark ? 'dark' : 'light');
|
||||||
// Store for use in inline styles
|
// Store for use in inline styles
|
||||||
window.__INITIAL_THEME_DARK__ = isDark;
|
window.__INITIAL_THEME_DARK__ = isDark;
|
||||||
|
|
||||||
|
// Splash colors persisted by the app theme system
|
||||||
|
var splashBgLight = localStorage.getItem('splashBgLight');
|
||||||
|
var splashFgLight = localStorage.getItem('splashFgLight');
|
||||||
|
var splashBgDark = localStorage.getItem('splashBgDark');
|
||||||
|
var splashFgDark = localStorage.getItem('splashFgDark');
|
||||||
|
|
||||||
|
if (splashBgLight) document.documentElement.style.setProperty('--splash-background-light', splashBgLight);
|
||||||
|
if (splashFgLight) document.documentElement.style.setProperty('--splash-stroke-light', splashFgLight);
|
||||||
|
if (splashBgDark) document.documentElement.style.setProperty('--splash-background-dark', splashBgDark);
|
||||||
|
if (splashFgDark) document.documentElement.style.setProperty('--splash-stroke-dark', splashFgDark);
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
<style>
|
<style>
|
||||||
@@ -54,18 +65,44 @@
|
|||||||
font-family: 'IBM Plex Mono', monospace;
|
font-family: 'IBM Plex Mono', monospace;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
/* Theme-aware colors for loading screen */
|
:root {
|
||||||
html.dark {
|
--splash-background-dark: #151313;
|
||||||
--splash-stroke: white;
|
--splash-stroke-dark: white;
|
||||||
|
--splash-background-light: #F6F4EF;
|
||||||
|
--splash-stroke-light: black;
|
||||||
|
|
||||||
|
--splash-background: var(--splash-background-dark);
|
||||||
|
--splash-stroke: var(--splash-stroke-dark);
|
||||||
|
|
||||||
|
/* Fallback fills (overridden below when supported) */
|
||||||
--splash-face-fill: rgba(255, 255, 255, 0.15);
|
--splash-face-fill: rgba(255, 255, 255, 0.15);
|
||||||
--splash-cell-fill: rgba(255, 255, 255, 0.35);
|
--splash-cell-fill: rgba(255, 255, 255, 0.35);
|
||||||
--splash-logo-fill: white;
|
--splash-logo-fill: var(--splash-stroke);
|
||||||
}
|
}
|
||||||
|
|
||||||
html.light {
|
html.light {
|
||||||
--splash-stroke: black;
|
--splash-background: var(--splash-background-light);
|
||||||
|
--splash-stroke: var(--splash-stroke-light);
|
||||||
--splash-face-fill: rgba(0, 0, 0, 0.15);
|
--splash-face-fill: rgba(0, 0, 0, 0.15);
|
||||||
--splash-cell-fill: rgba(0, 0, 0, 0.4);
|
--splash-cell-fill: rgba(0, 0, 0, 0.4);
|
||||||
--splash-logo-fill: black;
|
--splash-logo-fill: var(--splash-stroke);
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark {
|
||||||
|
--splash-background: var(--splash-background-dark);
|
||||||
|
--splash-stroke: var(--splash-stroke-dark);
|
||||||
|
--splash-logo-fill: var(--splash-stroke);
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: var(--splash-background);
|
||||||
|
}
|
||||||
|
|
||||||
|
@supports (color: color-mix(in srgb, white 50%, transparent)) {
|
||||||
|
:root {
|
||||||
|
--splash-face-fill: color-mix(in srgb, var(--splash-stroke) 15%, transparent);
|
||||||
|
--splash-cell-fill: color-mix(in srgb, var(--splash-stroke) 35%, transparent);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.loading {
|
.loading {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ pub async fn load_settings(state: State<'_, DesktopRuntime>) -> Result<SettingsL
|
|||||||
.settings()
|
.settings()
|
||||||
.update_with(|mut settings| {
|
.update_with(|mut settings| {
|
||||||
migrate_legacy_project_settings(&mut settings);
|
migrate_legacy_project_settings(&mut settings);
|
||||||
|
migrate_legacy_theme_settings(&mut settings);
|
||||||
normalize_project_selection(&mut settings);
|
normalize_project_selection(&mut settings);
|
||||||
(settings, ())
|
(settings, ())
|
||||||
})
|
})
|
||||||
@@ -52,6 +53,7 @@ pub async fn save_settings(
|
|||||||
.settings()
|
.settings()
|
||||||
.update_with(|current| {
|
.update_with(|current| {
|
||||||
let mut merged = merge_persisted_settings(¤t, &sanitized_changes);
|
let mut merged = merge_persisted_settings(¤t, &sanitized_changes);
|
||||||
|
migrate_legacy_theme_settings(&mut merged);
|
||||||
normalize_project_selection(&mut merged);
|
normalize_project_selection(&mut merged);
|
||||||
(merged, ())
|
(merged, ())
|
||||||
})
|
})
|
||||||
@@ -610,6 +612,70 @@ fn migrate_legacy_project_settings(settings: &mut Value) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn migrate_legacy_theme_settings(settings: &mut Value) {
|
||||||
|
if !settings.is_object() {
|
||||||
|
*settings = json!({});
|
||||||
|
}
|
||||||
|
|
||||||
|
let obj = settings.as_object_mut().unwrap();
|
||||||
|
|
||||||
|
let theme_id = obj
|
||||||
|
.get("themeId")
|
||||||
|
.and_then(|value| value.as_str())
|
||||||
|
.map(str::trim)
|
||||||
|
.filter(|value| !value.is_empty())
|
||||||
|
.map(|value| value.to_string());
|
||||||
|
|
||||||
|
let theme_variant = obj
|
||||||
|
.get("themeVariant")
|
||||||
|
.and_then(|value| value.as_str())
|
||||||
|
.map(str::trim)
|
||||||
|
.filter(|value| *value == "light" || *value == "dark")
|
||||||
|
.map(|value| value.to_string());
|
||||||
|
|
||||||
|
let has_light = obj
|
||||||
|
.get("lightThemeId")
|
||||||
|
.and_then(|value| value.as_str())
|
||||||
|
.is_some_and(|value| !value.trim().is_empty());
|
||||||
|
let has_dark = obj
|
||||||
|
.get("darkThemeId")
|
||||||
|
.and_then(|value| value.as_str())
|
||||||
|
.is_some_and(|value| !value.trim().is_empty());
|
||||||
|
|
||||||
|
if has_light && has_dark {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let default_light = "flexoki-light".to_string();
|
||||||
|
let default_dark = "flexoki-dark".to_string();
|
||||||
|
|
||||||
|
if !has_light {
|
||||||
|
let next = if let (Some(id), Some(variant)) = (theme_id.as_ref(), theme_variant.as_ref()) {
|
||||||
|
if variant == "light" {
|
||||||
|
id.clone()
|
||||||
|
} else {
|
||||||
|
default_light.clone()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
default_light.clone()
|
||||||
|
};
|
||||||
|
obj.insert("lightThemeId".to_string(), json!(next));
|
||||||
|
}
|
||||||
|
|
||||||
|
if !has_dark {
|
||||||
|
let next = if let (Some(id), Some(variant)) = (theme_id.as_ref(), theme_variant.as_ref()) {
|
||||||
|
if variant == "dark" {
|
||||||
|
id.clone()
|
||||||
|
} else {
|
||||||
|
default_dark.clone()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
default_dark.clone()
|
||||||
|
};
|
||||||
|
obj.insert("darkThemeId".to_string(), json!(next));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn normalize_project_selection(settings: &mut Value) {
|
fn normalize_project_selection(settings: &mut Value) {
|
||||||
let Some(obj) = settings.as_object_mut() else {
|
let Some(obj) = settings.as_object_mut() else {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -92,6 +92,8 @@ const MODELS_DEV_API_URL: &str = "https://models.dev/api.json";
|
|||||||
const MODELS_METADATA_CACHE_TTL: Duration = Duration::from_secs(5 * 60);
|
const MODELS_METADATA_CACHE_TTL: Duration = Duration::from_secs(5 * 60);
|
||||||
const MODELS_METADATA_REQUEST_TIMEOUT: Duration = Duration::from_secs(8);
|
const MODELS_METADATA_REQUEST_TIMEOUT: Duration = Duration::from_secs(8);
|
||||||
|
|
||||||
|
const MAX_THEME_JSON_BYTES: u64 = 512 * 1024;
|
||||||
|
|
||||||
const CHECK_FOR_UPDATES_EVENT: &str = "openchamber:check-for-updates";
|
const CHECK_FOR_UPDATES_EVENT: &str = "openchamber:check-for-updates";
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
@@ -2059,6 +2061,14 @@ async fn handle_config_routes(
|
|||||||
method: Method,
|
method: Method,
|
||||||
mut req: Request,
|
mut req: Request,
|
||||||
) -> Result<Response, StatusCode> {
|
) -> Result<Response, StatusCode> {
|
||||||
|
if path == "/api/config/themes" && method == Method::GET {
|
||||||
|
let themes = read_custom_themes_from_disk().await;
|
||||||
|
return Ok(json_response(
|
||||||
|
StatusCode::OK,
|
||||||
|
serde_json::json!({ "themes": themes }),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(name) = path.strip_prefix("/api/config/agents/") {
|
if let Some(name) = path.strip_prefix("/api/config/agents/") {
|
||||||
let trimmed = name.trim();
|
let trimmed = name.trim();
|
||||||
if trimmed.is_empty() {
|
if trimmed.is_empty() {
|
||||||
@@ -2564,6 +2574,7 @@ async fn proxy_to_opencode(
|
|||||||
let is_desktop_config_route = origin_path.starts_with("/api/config/agents/")
|
let is_desktop_config_route = origin_path.starts_with("/api/config/agents/")
|
||||||
|| origin_path.starts_with("/api/config/commands/")
|
|| origin_path.starts_with("/api/config/commands/")
|
||||||
|| origin_path.starts_with("/api/config/skills")
|
|| origin_path.starts_with("/api/config/skills")
|
||||||
|
|| origin_path == "/api/config/themes"
|
||||||
|| origin_path == "/api/config/reload"
|
|| origin_path == "/api/config/reload"
|
||||||
|| is_provider_auth_delete
|
|| is_provider_auth_delete
|
||||||
|| is_provider_source_get;
|
|| is_provider_source_get;
|
||||||
@@ -2724,3 +2735,232 @@ impl SettingsStore {
|
|||||||
Ok(candidate)
|
Ok(candidate)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn openchamber_user_config_root() -> Option<PathBuf> {
|
||||||
|
let home = dirs::home_dir()?;
|
||||||
|
Some(home.join(".config").join("openchamber"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn openchamber_themes_dir() -> Option<PathBuf> {
|
||||||
|
openchamber_user_config_root().map(|root| root.join("themes"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn value_non_empty_string(value: &Value) -> Option<String> {
|
||||||
|
value
|
||||||
|
.as_str()
|
||||||
|
.map(str::trim)
|
||||||
|
.filter(|s| !s.is_empty())
|
||||||
|
.map(|s| s.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_nested<'a>(value: &'a Value, path: &[&str]) -> Option<&'a Value> {
|
||||||
|
let mut current = value;
|
||||||
|
for key in path {
|
||||||
|
current = current.get(*key)?;
|
||||||
|
}
|
||||||
|
Some(current)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn has_required_theme_fields(theme: &Value) -> bool {
|
||||||
|
let required_paths: [&[&str]; 46] = [
|
||||||
|
&["metadata", "id"],
|
||||||
|
&["metadata", "name"],
|
||||||
|
&["metadata", "variant"],
|
||||||
|
&["colors", "primary", "base"],
|
||||||
|
&["colors", "primary", "foreground"],
|
||||||
|
&["colors", "surface", "background"],
|
||||||
|
&["colors", "surface", "foreground"],
|
||||||
|
&["colors", "surface", "muted"],
|
||||||
|
&["colors", "surface", "mutedForeground"],
|
||||||
|
&["colors", "surface", "elevated"],
|
||||||
|
&["colors", "surface", "elevatedForeground"],
|
||||||
|
&["colors", "surface", "subtle"],
|
||||||
|
&["colors", "interactive", "border"],
|
||||||
|
&["colors", "interactive", "selection"],
|
||||||
|
&["colors", "interactive", "selectionForeground"],
|
||||||
|
&["colors", "interactive", "focusRing"],
|
||||||
|
&["colors", "interactive", "hover"],
|
||||||
|
&["colors", "status", "error"],
|
||||||
|
&["colors", "status", "errorForeground"],
|
||||||
|
&["colors", "status", "errorBackground"],
|
||||||
|
&["colors", "status", "errorBorder"],
|
||||||
|
&["colors", "status", "warning"],
|
||||||
|
&["colors", "status", "warningForeground"],
|
||||||
|
&["colors", "status", "warningBackground"],
|
||||||
|
&["colors", "status", "warningBorder"],
|
||||||
|
&["colors", "status", "success"],
|
||||||
|
&["colors", "status", "successForeground"],
|
||||||
|
&["colors", "status", "successBackground"],
|
||||||
|
&["colors", "status", "successBorder"],
|
||||||
|
&["colors", "status", "info"],
|
||||||
|
&["colors", "status", "infoForeground"],
|
||||||
|
&["colors", "status", "infoBackground"],
|
||||||
|
&["colors", "status", "infoBorder"],
|
||||||
|
&["colors", "syntax", "base", "background"],
|
||||||
|
&["colors", "syntax", "base", "foreground"],
|
||||||
|
&["colors", "syntax", "base", "keyword"],
|
||||||
|
&["colors", "syntax", "base", "string"],
|
||||||
|
&["colors", "syntax", "base", "number"],
|
||||||
|
&["colors", "syntax", "base", "function"],
|
||||||
|
&["colors", "syntax", "base", "variable"],
|
||||||
|
&["colors", "syntax", "base", "type"],
|
||||||
|
&["colors", "syntax", "base", "comment"],
|
||||||
|
&["colors", "syntax", "base", "operator"],
|
||||||
|
&["colors", "syntax", "highlights", "diffAdded"],
|
||||||
|
&["colors", "syntax", "highlights", "diffRemoved"],
|
||||||
|
&["colors", "syntax", "highlights", "lineNumber"],
|
||||||
|
];
|
||||||
|
|
||||||
|
for path in required_paths {
|
||||||
|
let Some(value) = get_nested(theme, path) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
if value_non_empty_string(value).is_none() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let variant = get_nested(theme, &["metadata", "variant"])
|
||||||
|
.and_then(value_non_empty_string)
|
||||||
|
.unwrap_or_default();
|
||||||
|
if variant != "light" && variant != "dark" {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn normalize_theme_json(mut theme: Value) -> Option<Value> {
|
||||||
|
if !theme.is_object() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !has_required_theme_fields(&theme) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let id = get_nested(&theme, &["metadata", "id"]).and_then(value_non_empty_string)?;
|
||||||
|
let name = get_nested(&theme, &["metadata", "name"]).and_then(value_non_empty_string)?;
|
||||||
|
let variant = get_nested(&theme, &["metadata", "variant"]).and_then(value_non_empty_string)?;
|
||||||
|
|
||||||
|
// Ensure metadata exists and is an object.
|
||||||
|
let metadata = theme
|
||||||
|
.get_mut("metadata")
|
||||||
|
.and_then(|v| v.as_object_mut())?;
|
||||||
|
|
||||||
|
metadata.insert("id".to_string(), Value::String(id.trim().to_string()));
|
||||||
|
metadata.insert("name".to_string(), Value::String(name.trim().to_string()));
|
||||||
|
metadata.insert("variant".to_string(), Value::String(variant));
|
||||||
|
|
||||||
|
if !metadata
|
||||||
|
.get("description")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.is_some()
|
||||||
|
{
|
||||||
|
metadata.insert("description".to_string(), Value::String("".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let version_ok = metadata
|
||||||
|
.get("version")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.map(str::trim)
|
||||||
|
.filter(|s| !s.is_empty())
|
||||||
|
.is_some();
|
||||||
|
if !version_ok {
|
||||||
|
metadata.insert("version".to_string(), Value::String("1.0.0".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(tags_value) = metadata.get_mut("tags") {
|
||||||
|
if let Some(tags) = tags_value.as_array_mut() {
|
||||||
|
tags.retain(|tag| tag.as_str().map(str::trim).filter(|s| !s.is_empty()).is_some());
|
||||||
|
} else {
|
||||||
|
*tags_value = Value::Array(vec![]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
metadata.insert("tags".to_string(), Value::Array(vec![]));
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(theme)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn read_custom_themes_from_disk() -> Vec<Value> {
|
||||||
|
let Some(dir) = openchamber_themes_dir() else {
|
||||||
|
return vec![];
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut results: Vec<Value> = vec![];
|
||||||
|
let mut seen_ids: std::collections::HashSet<String> = std::collections::HashSet::new();
|
||||||
|
|
||||||
|
let mut entries = match fs::read_dir(&dir).await {
|
||||||
|
Ok(entries) => entries,
|
||||||
|
Err(err) if err.kind() == std::io::ErrorKind::NotFound => return vec![],
|
||||||
|
Err(err) => {
|
||||||
|
warn!("[desktop:themes] Failed to list themes dir {:?}: {}", dir, err);
|
||||||
|
return vec![];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
while let Ok(Some(entry)) = entries.next_entry().await {
|
||||||
|
let path = entry.path();
|
||||||
|
let is_json = path
|
||||||
|
.extension()
|
||||||
|
.and_then(|ext| ext.to_str())
|
||||||
|
.map(|ext| ext.eq_ignore_ascii_case("json"))
|
||||||
|
.unwrap_or(false);
|
||||||
|
if !is_json {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let metadata = match entry.metadata().await {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(_) => continue,
|
||||||
|
};
|
||||||
|
if !metadata.is_file() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if metadata.len() > MAX_THEME_JSON_BYTES {
|
||||||
|
warn!(
|
||||||
|
"[desktop:themes] Skip {:?}: too large ({} bytes)",
|
||||||
|
path,
|
||||||
|
metadata.len()
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let bytes = match fs::read(&path).await {
|
||||||
|
Ok(bytes) => bytes,
|
||||||
|
Err(err) => {
|
||||||
|
warn!("[desktop:themes] Failed to read {:?}: {}", path, err);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let parsed: Value = match serde_json::from_slice(&bytes) {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(err) => {
|
||||||
|
warn!("[desktop:themes] Invalid JSON {:?}: {}", path, err);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let normalized = match normalize_theme_json(parsed) {
|
||||||
|
Some(v) => v,
|
||||||
|
None => {
|
||||||
|
warn!("[desktop:themes] Invalid theme JSON {:?}", path);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let id = get_nested(&normalized, &["metadata", "id"])
|
||||||
|
.and_then(value_non_empty_string)
|
||||||
|
.unwrap_or_default();
|
||||||
|
if id.is_empty() || seen_ids.contains(&id) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
seen_ids.insert(id);
|
||||||
|
|
||||||
|
results.push(normalized);
|
||||||
|
}
|
||||||
|
|
||||||
|
results
|
||||||
|
}
|
||||||
|
|||||||
@@ -137,10 +137,10 @@ export const AgentMentionAutocomplete = React.forwardRef<AgentMentionAutocomplet
|
|||||||
ref={(el) => {
|
ref={(el) => {
|
||||||
itemRefs.current[index] = el;
|
itemRefs.current[index] = el;
|
||||||
}}
|
}}
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex items-start gap-2 px-3 py-1.5 cursor-pointer rounded-lg typography-ui-label',
|
'flex items-start gap-2 px-3 py-1.5 cursor-pointer rounded-lg typography-ui-label',
|
||||||
index === selectedIndex && 'bg-muted'
|
index === selectedIndex && 'bg-interactive-selection'
|
||||||
)}
|
)}
|
||||||
onClick={() => onAgentSelect(agent.name)}
|
onClick={() => onAgentSelect(agent.name)}
|
||||||
onMouseEnter={() => setSelectedIndex(index)}
|
onMouseEnter={() => setSelectedIndex(index)}
|
||||||
>
|
>
|
||||||
@@ -175,7 +175,7 @@ export const AgentMentionAutocomplete = React.forwardRef<AgentMentionAutocomplet
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
className="absolute z-[100] min-w-0 w-full max-w-[360px] max-h-60 bg-background border border-border rounded-xl shadow-none bottom-full mb-2 left-0 flex flex-col"
|
className="absolute z-[100] min-w-0 w-full max-w-[360px] max-h-60 bg-background border-2 border-border/60 rounded-xl shadow-md bottom-full mb-2 left-0 flex flex-col"
|
||||||
>
|
>
|
||||||
<ScrollableOverlay outerClassName="flex-1 min-h-0" className="px-0 pb-2" fillContainer={false}>
|
<ScrollableOverlay outerClassName="flex-1 min-h-0" className="px-0 pb-2" fillContainer={false}>
|
||||||
{agents.length ? (
|
{agents.length ? (
|
||||||
|
|||||||
@@ -298,13 +298,13 @@ export const ChatContainer: React.FC = () => {
|
|||||||
<div className="relative bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/80 z-10">
|
<div className="relative bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/80 z-10">
|
||||||
{showScrollButton && sessionMessages.length > 0 && (
|
{showScrollButton && sessionMessages.length > 0 && (
|
||||||
<div className="absolute bottom-full left-1/2 -translate-x-1/2 mb-2">
|
<div className="absolute bottom-full left-1/2 -translate-x-1/2 mb-2">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => scrollToBottom({ force: true })}
|
onClick={() => scrollToBottom({ force: true })}
|
||||||
className="rounded-full h-8 w-8 p-0 shadow-none bg-background/95 hover:bg-accent"
|
className="rounded-full h-8 w-8 p-0 shadow-none bg-background/95 hover:bg-interactive-hover"
|
||||||
aria-label="Scroll to bottom"
|
aria-label="Scroll to bottom"
|
||||||
>
|
>
|
||||||
|
|
||||||
<RiArrowDownLine className="h-4 w-4" />
|
<RiArrowDownLine className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import React from 'react';
|
|||||||
|
|
||||||
import { OpenChamberLogo } from '@/components/ui/OpenChamberLogo';
|
import { OpenChamberLogo } from '@/components/ui/OpenChamberLogo';
|
||||||
import { TextLoop } from '@/components/ui/TextLoop';
|
import { TextLoop } from '@/components/ui/TextLoop';
|
||||||
import { useOptionalThemeSystem } from '@/contexts/useThemeSystem';
|
import { useThemeSystem } from '@/contexts/useThemeSystem';
|
||||||
|
|
||||||
const phrases = [
|
const phrases = [
|
||||||
"Fix the failing tests",
|
"Fix the failing tests",
|
||||||
@@ -24,17 +24,10 @@ const phrases = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const ChatEmptyState: React.FC = () => {
|
const ChatEmptyState: React.FC = () => {
|
||||||
const themeContext = useOptionalThemeSystem();
|
const { currentTheme } = useThemeSystem();
|
||||||
|
|
||||||
let isDark = true;
|
// Use theme's muted foreground for secondary text
|
||||||
if (themeContext) {
|
const textColor = currentTheme?.colors?.surface?.mutedForeground || 'var(--muted-foreground)';
|
||||||
isDark = themeContext.currentTheme.metadata.variant !== 'light';
|
|
||||||
} else if (typeof window !== 'undefined') {
|
|
||||||
isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Same colors as face fill in OpenChamberLogo, but higher opacity for text readability
|
|
||||||
const textColor = isDark ? 'rgba(255,255,255,0.4)' : 'rgba(0,0,0,0.4)';
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center justify-center min-h-full w-full gap-6">
|
<div className="flex flex-col items-center justify-center min-h-full w-full gap-6">
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ export class ChatErrorBoundary extends React.Component<ChatErrorBoundaryProps, C
|
|||||||
|
|
||||||
{this.state.error && (
|
{this.state.error && (
|
||||||
<details className="text-xs font-mono bg-muted p-3 rounded">
|
<details className="text-xs font-mono bg-muted p-3 rounded">
|
||||||
<summary className="cursor-pointer hover:bg-muted/80">Error details</summary>
|
<summary className="cursor-pointer hover:bg-interactive-hover/80">Error details</summary>
|
||||||
<pre className="mt-2 overflow-x-auto">
|
<pre className="mt-2 overflow-x-auto">
|
||||||
{this.state.error.toString()}
|
{this.state.error.toString()}
|
||||||
</pre>
|
</pre>
|
||||||
|
|||||||
@@ -11,8 +11,7 @@ import { useSessionStore } from '@/stores/useSessionStore';
|
|||||||
import { useConfigStore } from '@/stores/useConfigStore';
|
import { useConfigStore } from '@/stores/useConfigStore';
|
||||||
import { useUIStore } from '@/stores/useUIStore';
|
import { useUIStore } from '@/stores/useUIStore';
|
||||||
import { useMessageQueueStore, type QueuedMessage } from '@/stores/messageQueueStore';
|
import { useMessageQueueStore, type QueuedMessage } from '@/stores/messageQueueStore';
|
||||||
import type { AttachedFile, EditPermissionMode } from '@/stores/types/sessionTypes';
|
import type { AttachedFile } from '@/stores/types/sessionTypes';
|
||||||
import { getEditModeColors } from '@/lib/permissions/editModeColors';
|
|
||||||
import { AttachedFilesList } from './FileAttachment';
|
import { AttachedFilesList } from './FileAttachment';
|
||||||
import { QueuedMessageChips } from './QueuedMessageChips';
|
import { QueuedMessageChips } from './QueuedMessageChips';
|
||||||
import { FileMentionAutocomplete, type FileMentionHandle } from './FileMentionAutocomplete';
|
import { FileMentionAutocomplete, type FileMentionHandle } from './FileMentionAutocomplete';
|
||||||
@@ -40,7 +39,7 @@ import {
|
|||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from '@/components/ui/dropdown-menu';
|
} from '@/components/ui/dropdown-menu';
|
||||||
import { useContextStore } from '@/stores/contextStore';
|
import { useThemeSystem } from '@/contexts/useThemeSystem';
|
||||||
|
|
||||||
const MAX_VISIBLE_TEXTAREA_LINES = 8;
|
const MAX_VISIBLE_TEXTAREA_LINES = 8;
|
||||||
const EMPTY_QUEUE: QueuedMessage[] = [];
|
const EMPTY_QUEUE: QueuedMessage[] = [];
|
||||||
@@ -52,53 +51,6 @@ interface ChatInputProps {
|
|||||||
|
|
||||||
const isPrimaryMode = (mode?: string) => mode === 'primary' || mode === 'all' || mode === undefined || mode === null;
|
const isPrimaryMode = (mode?: string) => mode === 'primary' || mode === 'all' || mode === undefined || mode === null;
|
||||||
|
|
||||||
type PermissionAction = 'allow' | 'ask' | 'deny';
|
|
||||||
type PermissionRule = { permission: string; pattern: string; action: PermissionAction };
|
|
||||||
|
|
||||||
const asPermissionRuleset = (value: unknown): PermissionRule[] | null => {
|
|
||||||
if (!Array.isArray(value)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const rules: PermissionRule[] = [];
|
|
||||||
for (const entry of value) {
|
|
||||||
if (!entry || typeof entry !== 'object') {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const candidate = entry as Partial<PermissionRule>;
|
|
||||||
if (typeof candidate.permission !== 'string' || typeof candidate.pattern !== 'string' || typeof candidate.action !== 'string') {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (candidate.action !== 'allow' && candidate.action !== 'ask' && candidate.action !== 'deny') {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
rules.push({ permission: candidate.permission, pattern: candidate.pattern, action: candidate.action });
|
|
||||||
}
|
|
||||||
return rules;
|
|
||||||
};
|
|
||||||
|
|
||||||
const resolveWildcardPermissionAction = (ruleset: unknown, permission: string): PermissionAction | undefined => {
|
|
||||||
const rules = asPermissionRuleset(ruleset);
|
|
||||||
if (!rules || rules.length === 0) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = rules.length - 1; i >= 0; i -= 1) {
|
|
||||||
const rule = rules[i];
|
|
||||||
if (rule.permission === permission && rule.pattern === '*') {
|
|
||||||
return rule.action;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = rules.length - 1; i >= 0; i -= 1) {
|
|
||||||
const rule = rules[i];
|
|
||||||
if (rule.permission === '*' && rule.pattern === '*') {
|
|
||||||
return rule.action;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ChatInput: React.FC<ChatInputProps> = ({ onOpenSettings, scrollToBottom }) => {
|
export const ChatInput: React.FC<ChatInputProps> = ({ onOpenSettings, scrollToBottom }) => {
|
||||||
const [message, setMessage] = React.useState('');
|
const [message, setMessage] = React.useState('');
|
||||||
const [isDragging, setIsDragging] = React.useState(false);
|
const [isDragging, setIsDragging] = React.useState(false);
|
||||||
@@ -140,6 +92,7 @@ export const ChatInput: React.FC<ChatInputProps> = ({ onOpenSettings, scrollToBo
|
|||||||
const agents = getVisibleAgents();
|
const agents = getVisibleAgents();
|
||||||
const { isMobile, inputBarOffset, isKeyboardOpen, setTimelineDialogOpen, cornerRadius } = useUIStore();
|
const { isMobile, inputBarOffset, isKeyboardOpen, setTimelineDialogOpen, cornerRadius } = useUIStore();
|
||||||
const { working } = useAssistantStatus();
|
const { working } = useAssistantStatus();
|
||||||
|
const { currentTheme } = useThemeSystem();
|
||||||
const [showAbortStatus, setShowAbortStatus] = React.useState(false);
|
const [showAbortStatus, setShowAbortStatus] = React.useState(false);
|
||||||
const abortTimeoutRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);
|
const abortTimeoutRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
const prevWasAbortedRef = React.useRef(false);
|
const prevWasAbortedRef = React.useRef(false);
|
||||||
@@ -270,79 +223,6 @@ export const ChatInput: React.FC<ChatInputProps> = ({ onOpenSettings, scrollToBo
|
|||||||
}
|
}
|
||||||
}, [pendingInputText, consumePendingInputText]);
|
}, [pendingInputText, consumePendingInputText]);
|
||||||
|
|
||||||
const currentAgent = React.useMemo(() => {
|
|
||||||
const selectedName = currentSessionId
|
|
||||||
? (useContextStore.getState().getSessionAgentSelection(currentSessionId) || currentAgentName)
|
|
||||||
: currentAgentName;
|
|
||||||
if (!selectedName) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
return agents.find((agent) => agent.name === selectedName);
|
|
||||||
}, [agents, currentAgentName, currentSessionId]);
|
|
||||||
|
|
||||||
const agentEditAction = React.useMemo<EditPermissionMode>(() => {
|
|
||||||
if (!currentAgent) {
|
|
||||||
return 'deny';
|
|
||||||
}
|
|
||||||
|
|
||||||
return resolveWildcardPermissionAction(currentAgent.permission, 'edit') ?? 'allow';
|
|
||||||
}, [currentAgent]);
|
|
||||||
|
|
||||||
const sessionEditMode = useContextStore(
|
|
||||||
React.useCallback((state) => {
|
|
||||||
if (!currentAgentName) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
const sessionId = currentSessionId ?? '__global__';
|
|
||||||
return state.getSessionAgentEditMode(sessionId, currentAgentName, 'ask');
|
|
||||||
}, [currentAgentName, currentSessionId])
|
|
||||||
);
|
|
||||||
|
|
||||||
const selectionContextReady = Boolean(currentSessionId && currentAgentName);
|
|
||||||
|
|
||||||
const effectiveEditPermission = React.useMemo<EditPermissionMode>(() => {
|
|
||||||
// Only show accent when edits are effectively allowed.
|
|
||||||
if (agentEditAction === 'allow') {
|
|
||||||
return 'allow';
|
|
||||||
}
|
|
||||||
if (agentEditAction !== 'ask') {
|
|
||||||
return 'ask';
|
|
||||||
}
|
|
||||||
|
|
||||||
const sessionMode = selectionContextReady ? (sessionEditMode ?? 'ask') : 'ask';
|
|
||||||
return (sessionMode === 'allow' || sessionMode === 'full') ? 'allow' : 'ask';
|
|
||||||
}, [agentEditAction, selectionContextReady, sessionEditMode]);
|
|
||||||
|
|
||||||
const chatInputAccent = React.useMemo(() => getEditModeColors(effectiveEditPermission), [effectiveEditPermission]);
|
|
||||||
|
|
||||||
// VS Code webviews tend to have stronger status border colors; in web/desktop themes the same
|
|
||||||
// border tokens can already be subtle, so avoid double-softening there.
|
|
||||||
const softenBorderColor = React.useCallback((color: string) => (
|
|
||||||
isVSCodeRuntime()
|
|
||||||
? `color-mix(in srgb, ${color} 55%, transparent)`
|
|
||||||
: color
|
|
||||||
), []);
|
|
||||||
|
|
||||||
const chatInputWrapperStyle = React.useMemo<React.CSSProperties | undefined>(() => {
|
|
||||||
// Keep border width stable so toggling modes doesn't shift layout.
|
|
||||||
const baseBorderWidth = isVSCodeRuntime() ? 1 : 2;
|
|
||||||
|
|
||||||
const baseStyle: React.CSSProperties = {
|
|
||||||
borderRadius: cornerRadius,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!chatInputAccent) {
|
|
||||||
return { ...baseStyle, borderWidth: baseBorderWidth };
|
|
||||||
}
|
|
||||||
|
|
||||||
const borderColor = chatInputAccent.border ?? chatInputAccent.text;
|
|
||||||
return {
|
|
||||||
...baseStyle,
|
|
||||||
borderColor: softenBorderColor(borderColor),
|
|
||||||
borderWidth: baseBorderWidth,
|
|
||||||
};
|
|
||||||
}, [chatInputAccent, softenBorderColor, cornerRadius]);
|
|
||||||
|
|
||||||
const hasContent = message.trim() || attachedFiles.length > 0;
|
const hasContent = message.trim() || attachedFiles.length > 0;
|
||||||
const hasQueuedMessages = queuedMessages.length > 0;
|
const hasQueuedMessages = queuedMessages.length > 0;
|
||||||
const canSend = hasContent || hasQueuedMessages;
|
const canSend = hasContent || hasQueuedMessages;
|
||||||
@@ -1378,7 +1258,7 @@ export const ChatInput: React.FC<ChatInputProps> = ({ onOpenSettings, scrollToBo
|
|||||||
<form
|
<form
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative pt-0 pb-2 md:pb-4",
|
"relative pt-0 pb-4",
|
||||||
isMobile && isKeyboardOpen ? "ios-keyboard-safe-area" : "bottom-safe-area"
|
isMobile && isKeyboardOpen ? "ios-keyboard-safe-area" : "bottom-safe-area"
|
||||||
)}
|
)}
|
||||||
data-keyboard-avoid="true"
|
data-keyboard-avoid="true"
|
||||||
@@ -1438,10 +1318,14 @@ export const ChatInput: React.FC<ChatInputProps> = ({ onOpenSettings, scrollToBo
|
|||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"border border-border/80 bg-input/10 dark:bg-input/30",
|
"flex flex-col relative overflow-visible",
|
||||||
"flex flex-col relative overflow-visible"
|
"border border-border/80",
|
||||||
|
"focus-within:ring-1 focus-within:ring-primary/50"
|
||||||
)}
|
)}
|
||||||
style={chatInputWrapperStyle}
|
style={{
|
||||||
|
borderRadius: cornerRadius,
|
||||||
|
backgroundColor: currentTheme?.colors?.surface?.subtle,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{stopButton}
|
{stopButton}
|
||||||
{}
|
{}
|
||||||
@@ -1493,12 +1377,11 @@ export const ChatInput: React.FC<ChatInputProps> = ({ onOpenSettings, scrollToBo
|
|||||||
? "# for agents; @ for files; / for commands"
|
? "# for agents; @ for files; / for commands"
|
||||||
: "Select or create a session to start chatting"}
|
: "Select or create a session to start chatting"}
|
||||||
disabled={!currentSessionId && !newSessionDraftOpen}
|
disabled={!currentSessionId && !newSessionDraftOpen}
|
||||||
|
outerClassName="focus-within:ring-0"
|
||||||
className={cn(
|
className={cn(
|
||||||
'min-h-[52px] resize-none border-0 px-3 shadow-none rounded-b-none appearance-none focus:shadow-none focus-visible:shadow-none focus-visible:border-transparent focus-visible:ring-0 focus-visible:ring-transparent hover:border-transparent bg-transparent',
|
'min-h-[52px] resize-none border-0 px-3 rounded-b-none appearance-none hover:border-transparent bg-transparent',
|
||||||
isMobile ? "py-2.5" : "pt-4 pb-2",
|
isMobile ? "py-2.5" : "pt-4 pb-2",
|
||||||
canAbort && 'pr-10',
|
canAbort && 'pr-10'
|
||||||
"focus-visible:outline-none focus-visible:ring-0"
|
|
||||||
)}
|
)}
|
||||||
style={{
|
style={{
|
||||||
flex: 'none',
|
flex: 'none',
|
||||||
|
|||||||
@@ -898,7 +898,7 @@ const ChatMessage: React.FC<ChatMessageProps> = ({
|
|||||||
displayParts.length === 0 ? null : (
|
displayParts.length === 0 ? null : (
|
||||||
<FadeInOnReveal>
|
<FadeInOnReveal>
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
<div className="max-w-[85%] rounded-2xl rounded-br-sm bg-primary/10 dark:bg-primary/10 px-5 py-3 shadow-sm border border-primary/5">
|
<div style={{ backgroundColor: 'var(--chat-user-message-bg)' }} className="max-w-[85%] rounded-2xl rounded-br-sm px-5 py-3 shadow-sm border border-primary/5">
|
||||||
<MessageBody
|
<MessageBody
|
||||||
messageId={message.info.id}
|
messageId={message.info.id}
|
||||||
parts={displayParts}
|
parts={displayParts}
|
||||||
@@ -932,7 +932,7 @@ const ChatMessage: React.FC<ChatMessageProps> = ({
|
|||||||
</FadeInOnReveal>
|
</FadeInOnReveal>
|
||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
<div className="relative pl-4 ml-1">
|
<div className="relative">
|
||||||
{shouldShowHeader && (
|
{shouldShowHeader && (
|
||||||
<MessageHeader
|
<MessageHeader
|
||||||
isUser={isUser}
|
isUser={isUser}
|
||||||
|
|||||||
@@ -232,7 +232,7 @@ export const CommandAutocomplete = React.forwardRef<CommandAutocompleteHandle, C
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
className="absolute z-[100] min-w-0 w-full max-w-[450px] max-h-64 bg-background border border-border rounded-xl shadow-none bottom-full mb-2 left-0 flex flex-col"
|
className="absolute z-[100] min-w-0 w-full max-w-[450px] max-h-64 bg-background border-2 border-border/60 rounded-xl shadow-md bottom-full mb-2 left-0 flex flex-col"
|
||||||
>
|
>
|
||||||
<ScrollableOverlay outerClassName="flex-1 min-h-0" className="px-0 pb-2" fillContainer={false}>
|
<ScrollableOverlay outerClassName="flex-1 min-h-0" className="px-0 pb-2" fillContainer={false}>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
@@ -251,7 +251,7 @@ export const CommandAutocomplete = React.forwardRef<CommandAutocompleteHandle, C
|
|||||||
ref={(el) => { itemRefs.current[index] = el; }}
|
ref={(el) => { itemRefs.current[index] = el; }}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex items-start gap-2 px-3 py-2 cursor-pointer rounded-lg",
|
"flex items-start gap-2 px-3 py-2 cursor-pointer rounded-lg",
|
||||||
index === selectedIndex && "bg-muted"
|
index === selectedIndex && "bg-interactive-selection"
|
||||||
)}
|
)}
|
||||||
onClick={() => onCommandSelect(command)}
|
onClick={() => onCommandSelect(command)}
|
||||||
onMouseEnter={() => setSelectedIndex(index)}
|
onMouseEnter={() => setSelectedIndex(index)}
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ export const DiffPreview: React.FC<DiffPreviewProps> = ({ diff, syntaxTheme, fil
|
|||||||
: {}
|
: {}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<span className="text-muted-foreground/60 w-8 flex-shrink-0 text-right pr-2 self-start select-none">
|
<span className="w-10 flex-shrink-0 text-right pr-3 select-none border-r mr-3 -my-0.5 py-0.5" style={{ color: 'var(--tools-edit-line-number)', borderColor: 'var(--tools-border)' }}>
|
||||||
{line.lineNumber || ''}
|
{line.lineNumber || ''}
|
||||||
</span>
|
</span>
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
@@ -94,7 +94,7 @@ export const WritePreview: React.FC<WritePreviewProps> = ({ content, syntaxTheme
|
|||||||
<div className="space-y-0">
|
<div className="space-y-0">
|
||||||
{lines.map((line, lineIdx) => (
|
{lines.map((line, lineIdx) => (
|
||||||
<div key={lineIdx} className="typography-code font-mono px-2 py-0.5 flex -mx-1">
|
<div key={lineIdx} className="typography-code font-mono px-2 py-0.5 flex -mx-1">
|
||||||
<span className="text-muted-foreground/60 w-8 flex-shrink-0 text-right pr-2 self-start select-none">
|
<span className="w-10 flex-shrink-0 text-right pr-3 select-none border-r mr-3 -my-0.5 py-0.5" style={{ color: 'var(--tools-edit-line-number)', borderColor: 'var(--tools-border)' }}>
|
||||||
{lineIdx + 1}
|
{lineIdx + 1}
|
||||||
</span>
|
</span>
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
|
|||||||
@@ -295,18 +295,18 @@ export const FileMentionAutocomplete = React.forwardRef<FileMentionHandle, FileM
|
|||||||
case 'tsx':
|
case 'tsx':
|
||||||
case 'js':
|
case 'js':
|
||||||
case 'jsx':
|
case 'jsx':
|
||||||
return <RiCodeLine className="h-3.5 w-3.5 text-blue-500" />;
|
return <RiCodeLine className="h-3.5 w-3.5 text-[var(--status-info)]" />;
|
||||||
case 'json':
|
case 'json':
|
||||||
return <RiCodeLine className="h-3.5 w-3.5 text-yellow-500" />;
|
return <RiCodeLine className="h-3.5 w-3.5 text-[var(--status-warning)]" />;
|
||||||
case 'md':
|
case 'md':
|
||||||
case 'mdx':
|
case 'mdx':
|
||||||
return <RiFileLine className="h-3.5 w-3.5 text-gray-500" />;
|
return <RiFileLine className="h-3.5 w-3.5 text-muted-foreground" />;
|
||||||
case 'png':
|
case 'png':
|
||||||
case 'jpg':
|
case 'jpg':
|
||||||
case 'jpeg':
|
case 'jpeg':
|
||||||
case 'gif':
|
case 'gif':
|
||||||
case 'svg':
|
case 'svg':
|
||||||
return <RiFileImageLine className="h-3.5 w-3.5 text-green-500" />;
|
return <RiFileImageLine className="h-3.5 w-3.5 text-[var(--status-success)]" />;
|
||||||
default:
|
default:
|
||||||
return <RiFilePdfLine className="h-3.5 w-3.5 text-muted-foreground" />;
|
return <RiFilePdfLine className="h-3.5 w-3.5 text-muted-foreground" />;
|
||||||
}
|
}
|
||||||
@@ -315,7 +315,7 @@ export const FileMentionAutocomplete = React.forwardRef<FileMentionHandle, FileM
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
className="absolute z-[100] min-w-0 w-full max-w-[520px] max-h-64 bg-background border border-border rounded-xl shadow-none bottom-full mb-2 left-0 flex flex-col"
|
className="absolute z-[100] min-w-0 w-full max-w-[520px] max-h-64 bg-background border-2 border-border/60 rounded-xl shadow-md bottom-full mb-2 left-0 flex flex-col"
|
||||||
>
|
>
|
||||||
<ScrollableOverlay outerClassName="flex-1 min-h-0" className="px-0">
|
<ScrollableOverlay outerClassName="flex-1 min-h-0" className="px-0">
|
||||||
{loading ? (
|
{loading ? (
|
||||||
@@ -334,10 +334,10 @@ export const FileMentionAutocomplete = React.forwardRef<FileMentionHandle, FileM
|
|||||||
const item = (
|
const item = (
|
||||||
<div
|
<div
|
||||||
ref={(el) => { itemRefs.current[index] = el; }}
|
ref={(el) => { itemRefs.current[index] = el; }}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex items-center gap-2 px-3 py-1.5 cursor-pointer typography-ui-label rounded-lg",
|
"flex items-center gap-2 px-3 py-1.5 cursor-pointer typography-ui-label rounded-lg",
|
||||||
isSelected && "bg-muted"
|
isSelected && "bg-interactive-selection"
|
||||||
)}
|
)}
|
||||||
onClick={() => handleFileSelect(file)}
|
onClick={() => handleFileSelect(file)}
|
||||||
onMouseEnter={() => setSelectedIndex(index)}
|
onMouseEnter={() => setSelectedIndex(index)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -5,8 +5,10 @@ import type { Part } from '@opencode-ai/sdk/v2';
|
|||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { RiFileCopyLine, RiCheckLine, RiDownloadLine } from '@remixicon/react';
|
import { RiFileCopyLine, RiCheckLine, RiDownloadLine } from '@remixicon/react';
|
||||||
|
|
||||||
import { flexokiStreamdownThemes } from '@/lib/shiki/flexokiThemes';
|
|
||||||
import { isVSCodeRuntime } from '@/lib/desktop';
|
import { isVSCodeRuntime } from '@/lib/desktop';
|
||||||
|
import { useOptionalThemeSystem } from '@/contexts/useThemeSystem';
|
||||||
|
import { getStreamdownThemePair } from '@/lib/shiki/appThemeRegistry';
|
||||||
|
import { getDefaultTheme } from '@/lib/theme/themes';
|
||||||
|
|
||||||
const withStableStringId = <T extends object>(value: T, id: string): T => {
|
const withStableStringId = <T extends object>(value: T, id: string): T => {
|
||||||
const existingPrimitive = (value as Record<symbol, unknown>)[Symbol.toPrimitive];
|
const existingPrimitive = (value as Record<symbol, unknown>)[Symbol.toPrimitive];
|
||||||
@@ -43,45 +45,75 @@ const withStableStringId = <T extends object>(value: T, id: string): T => {
|
|||||||
return value;
|
return value;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getMarkdownShikiThemes = (): readonly [string | object, string | object] => {
|
|
||||||
if (!isVSCodeRuntime() || typeof window === 'undefined') {
|
|
||||||
return flexokiStreamdownThemes;
|
|
||||||
}
|
|
||||||
|
|
||||||
const provided = window.__OPENCHAMBER_VSCODE_SHIKI_THEMES__;
|
|
||||||
if (provided?.light && provided?.dark) {
|
|
||||||
const light = withStableStringId(
|
|
||||||
{ ...(provided.light as Record<string, unknown>) },
|
|
||||||
`vscode-shiki-light:${String((provided.light as { name?: unknown })?.name ?? 'theme')}`,
|
|
||||||
);
|
|
||||||
const dark = withStableStringId(
|
|
||||||
{ ...(provided.dark as Record<string, unknown>) },
|
|
||||||
`vscode-shiki-dark:${String((provided.dark as { name?: unknown })?.name ?? 'theme')}`,
|
|
||||||
);
|
|
||||||
return [light, dark] as const;
|
|
||||||
}
|
|
||||||
|
|
||||||
return flexokiStreamdownThemes;
|
|
||||||
};
|
|
||||||
|
|
||||||
const useMarkdownShikiThemes = (): readonly [string | object, string | object] => {
|
const useMarkdownShikiThemes = (): readonly [string | object, string | object] => {
|
||||||
const [themes, setThemes] = React.useState(getMarkdownShikiThemes);
|
const themeSystem = useOptionalThemeSystem();
|
||||||
|
|
||||||
|
const isVSCode = isVSCodeRuntime() && typeof window !== 'undefined';
|
||||||
|
|
||||||
|
const fallbackLight = getDefaultTheme(false);
|
||||||
|
const fallbackDark = getDefaultTheme(true);
|
||||||
|
|
||||||
|
const lightThemeId = themeSystem?.lightThemeId ?? fallbackLight.metadata.id;
|
||||||
|
const darkThemeId = themeSystem?.darkThemeId ?? fallbackDark.metadata.id;
|
||||||
|
|
||||||
|
const lightTheme =
|
||||||
|
themeSystem?.availableThemes.find((theme) => theme.metadata.id === lightThemeId) ??
|
||||||
|
fallbackLight;
|
||||||
|
const darkTheme =
|
||||||
|
themeSystem?.availableThemes.find((theme) => theme.metadata.id === darkThemeId) ??
|
||||||
|
fallbackDark;
|
||||||
|
|
||||||
|
const fallbackThemes = React.useMemo(
|
||||||
|
() => getStreamdownThemePair(lightTheme, darkTheme),
|
||||||
|
[darkTheme, lightTheme],
|
||||||
|
);
|
||||||
|
|
||||||
|
const getThemes = React.useCallback((): readonly [string | object, string | object] => {
|
||||||
|
if (!isVSCode) {
|
||||||
|
return fallbackThemes;
|
||||||
|
}
|
||||||
|
|
||||||
|
const provided = window.__OPENCHAMBER_VSCODE_SHIKI_THEMES__;
|
||||||
|
if (provided?.light && provided?.dark) {
|
||||||
|
const light = withStableStringId(
|
||||||
|
{ ...(provided.light as Record<string, unknown>) },
|
||||||
|
`vscode-shiki-light:${String((provided.light as { name?: unknown })?.name ?? 'theme')}`,
|
||||||
|
);
|
||||||
|
const dark = withStableStringId(
|
||||||
|
{ ...(provided.dark as Record<string, unknown>) },
|
||||||
|
`vscode-shiki-dark:${String((provided.dark as { name?: unknown })?.name ?? 'theme')}`,
|
||||||
|
);
|
||||||
|
return [light, dark] as const;
|
||||||
|
}
|
||||||
|
|
||||||
|
return fallbackThemes;
|
||||||
|
}, [fallbackThemes, isVSCode]);
|
||||||
|
|
||||||
|
const [themes, setThemes] = React.useState(getThemes);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (!isVSCodeRuntime() || typeof window === 'undefined') return;
|
if (!isVSCode) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setThemes(getThemes());
|
||||||
|
}, [getThemes, isVSCode]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!isVSCode) return;
|
||||||
|
|
||||||
const handler = (event: Event) => {
|
const handler = (event: Event) => {
|
||||||
// Rely on the canonical `window.__OPENCHAMBER_VSCODE_SHIKI_THEMES__` that the webview updates
|
// Rely on the canonical `window.__OPENCHAMBER_VSCODE_SHIKI_THEMES__` that the webview updates
|
||||||
// before dispatching this event, so we always apply stable cache keys and avoid stale token reuse.
|
// before dispatching this event, so we always apply stable cache keys and avoid stale token reuse.
|
||||||
void event;
|
void event;
|
||||||
setThemes(getMarkdownShikiThemes());
|
setThemes(getThemes());
|
||||||
};
|
};
|
||||||
|
|
||||||
window.addEventListener('openchamber:vscode-shiki-themes', handler as EventListener);
|
window.addEventListener('openchamber:vscode-shiki-themes', handler as EventListener);
|
||||||
return () => window.removeEventListener('openchamber:vscode-shiki-themes', handler as EventListener);
|
return () => window.removeEventListener('openchamber:vscode-shiki-themes', handler as EventListener);
|
||||||
}, []);
|
}, [getThemes, isVSCode]);
|
||||||
|
|
||||||
return themes;
|
return isVSCode ? themes : fallbackThemes;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Table utility functions
|
// Table utility functions
|
||||||
@@ -208,7 +240,7 @@ const TableCopyButton: React.FC<{ tableRef: React.RefObject<HTMLDivElement | nul
|
|||||||
<div className="relative" ref={menuRef}>
|
<div className="relative" ref={menuRef}>
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowMenu(!showMenu)}
|
onClick={() => setShowMenu(!showMenu)}
|
||||||
className="p-1 rounded hover:bg-muted/60 text-muted-foreground hover:text-foreground transition-colors"
|
className="p-1 rounded hover:bg-interactive-hover/60 text-muted-foreground hover:text-foreground transition-colors"
|
||||||
title="Copy table"
|
title="Copy table"
|
||||||
>
|
>
|
||||||
{copied ? <RiCheckLine className="size-3.5" /> : <RiFileCopyLine className="size-3.5" />}
|
{copied ? <RiCheckLine className="size-3.5" /> : <RiFileCopyLine className="size-3.5" />}
|
||||||
@@ -216,13 +248,13 @@ const TableCopyButton: React.FC<{ tableRef: React.RefObject<HTMLDivElement | nul
|
|||||||
{showMenu && (
|
{showMenu && (
|
||||||
<div className="absolute top-full right-0 z-10 mt-1 min-w-[100px] overflow-hidden rounded-md border border-border bg-background shadow-lg">
|
<div className="absolute top-full right-0 z-10 mt-1 min-w-[100px] overflow-hidden rounded-md border border-border bg-background shadow-lg">
|
||||||
<button
|
<button
|
||||||
className="w-full px-3 py-1.5 text-left text-sm transition-colors hover:bg-muted/40"
|
className="w-full px-3 py-1.5 text-left text-sm transition-colors hover:bg-interactive-hover/40"
|
||||||
onClick={() => handleCopy('csv')}
|
onClick={() => handleCopy('csv')}
|
||||||
>
|
>
|
||||||
CSV
|
CSV
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className="w-full px-3 py-1.5 text-left text-sm transition-colors hover:bg-muted/40"
|
className="w-full px-3 py-1.5 text-left text-sm transition-colors hover:bg-interactive-hover/40"
|
||||||
onClick={() => handleCopy('tsv')}
|
onClick={() => handleCopy('tsv')}
|
||||||
>
|
>
|
||||||
TSV
|
TSV
|
||||||
@@ -268,7 +300,7 @@ const TableDownloadButton: React.FC<{ tableRef: React.RefObject<HTMLDivElement |
|
|||||||
<div className="relative" ref={menuRef}>
|
<div className="relative" ref={menuRef}>
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowMenu(!showMenu)}
|
onClick={() => setShowMenu(!showMenu)}
|
||||||
className="p-1 rounded hover:bg-muted/60 text-muted-foreground hover:text-foreground transition-colors"
|
className="p-1 rounded hover:bg-interactive-hover/60 text-muted-foreground hover:text-foreground transition-colors"
|
||||||
title="Download table"
|
title="Download table"
|
||||||
>
|
>
|
||||||
<RiDownloadLine className="size-3.5" />
|
<RiDownloadLine className="size-3.5" />
|
||||||
@@ -276,13 +308,13 @@ const TableDownloadButton: React.FC<{ tableRef: React.RefObject<HTMLDivElement |
|
|||||||
{showMenu && (
|
{showMenu && (
|
||||||
<div className="absolute top-full right-0 z-10 mt-1 min-w-[100px] overflow-hidden rounded-md border border-border bg-background shadow-lg">
|
<div className="absolute top-full right-0 z-10 mt-1 min-w-[100px] overflow-hidden rounded-md border border-border bg-background shadow-lg">
|
||||||
<button
|
<button
|
||||||
className="w-full px-3 py-1.5 text-left text-sm transition-colors hover:bg-muted/40"
|
className="w-full px-3 py-1.5 text-left text-sm transition-colors hover:bg-interactive-hover/40"
|
||||||
onClick={() => handleDownload('csv')}
|
onClick={() => handleDownload('csv')}
|
||||||
>
|
>
|
||||||
CSV
|
CSV
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className="w-full px-3 py-1.5 text-left text-sm transition-colors hover:bg-muted/40"
|
className="w-full px-3 py-1.5 text-left text-sm transition-colors hover:bg-interactive-hover/40"
|
||||||
onClick={() => handleDownload('markdown')}
|
onClick={() => handleDownload('markdown')}
|
||||||
>
|
>
|
||||||
Markdown
|
Markdown
|
||||||
@@ -395,7 +427,7 @@ const CodeBlockWrapper: React.FC<CodeBlockWrapperProps> = ({ children, className
|
|||||||
<div className="absolute top-1 right-2 opacity-0 group-hover:opacity-100 transition-opacity">
|
<div className="absolute top-1 right-2 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||||
<button
|
<button
|
||||||
onClick={handleCopy}
|
onClick={handleCopy}
|
||||||
className="p-1 rounded hover:bg-muted/60 text-muted-foreground hover:text-foreground transition-colors"
|
className="p-1 rounded hover:bg-interactive-hover/60 text-muted-foreground hover:text-foreground transition-colors"
|
||||||
title="Copy"
|
title="Copy"
|
||||||
>
|
>
|
||||||
{copied ? <RiCheckLine className="size-3.5" /> : <RiFileCopyLine className="size-3.5" />}
|
{copied ? <RiCheckLine className="size-3.5" /> : <RiFileCopyLine className="size-3.5" />}
|
||||||
|
|||||||
@@ -1371,7 +1371,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
|||||||
value={mobileModelQuery}
|
value={mobileModelQuery}
|
||||||
onChange={(event) => setMobileModelQuery(event.target.value)}
|
onChange={(event) => setMobileModelQuery(event.target.value)}
|
||||||
placeholder="Search providers or models"
|
placeholder="Search providers or models"
|
||||||
className="pl-7 h-9 rounded-xl border-border/40 bg-background/95 typography-meta"
|
className="pl-7 h-9 rounded-xl border-border/40 bg-[var(--surface-elevated)] typography-meta"
|
||||||
/>
|
/>
|
||||||
{mobileModelQuery && (
|
{mobileModelQuery && (
|
||||||
<button
|
<button
|
||||||
@@ -1394,7 +1394,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
|||||||
|
|
||||||
{/* Favorites Section for Mobile */}
|
{/* Favorites Section for Mobile */}
|
||||||
{!mobileModelQuery && favoriteModelsList.length > 0 && (
|
{!mobileModelQuery && favoriteModelsList.length > 0 && (
|
||||||
<div className="rounded-xl border border-border/40 bg-background/95 overflow-hidden">
|
<div className="rounded-xl border border-border/40 bg-[var(--surface-elevated)] overflow-hidden">
|
||||||
<div className="px-2 py-1.5 text-xs font-semibold text-muted-foreground uppercase tracking-wider">
|
<div className="px-2 py-1.5 text-xs font-semibold text-muted-foreground uppercase tracking-wider">
|
||||||
<RiStarFill className="h-3 w-3 inline-block mr-1.5 text-primary" />
|
<RiStarFill className="h-3 w-3 inline-block mr-1.5 text-primary" />
|
||||||
Favorites
|
Favorites
|
||||||
@@ -1413,7 +1413,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
|||||||
'flex w-full items-start gap-2 border-b border-border/30 px-2 py-1.5 text-left last:border-b-0',
|
'flex w-full items-start gap-2 border-b border-border/30 px-2 py-1.5 text-left last:border-b-0',
|
||||||
'focus:outline-none focus-visible:ring-1 focus-visible:ring-primary',
|
'focus:outline-none focus-visible:ring-1 focus-visible:ring-primary',
|
||||||
'first:rounded-t-xl last:rounded-b-xl transition-colors',
|
'first:rounded-t-xl last:rounded-b-xl transition-colors',
|
||||||
isSelected ? 'bg-primary/15 text-primary' : 'hover:bg-muted'
|
isSelected ? 'bg-interactive-selection/15 text-interactive-selection-foreground' : 'hover:bg-interactive-hover'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 min-w-0">
|
<div className="flex items-center gap-2 min-w-0">
|
||||||
@@ -1440,7 +1440,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
|||||||
|
|
||||||
{/* Recent Section for Mobile */}
|
{/* Recent Section for Mobile */}
|
||||||
{!mobileModelQuery && recentModelsList.length > 0 && (
|
{!mobileModelQuery && recentModelsList.length > 0 && (
|
||||||
<div className="rounded-xl border border-border/40 bg-background/95 overflow-hidden">
|
<div className="rounded-xl border border-border/40 bg-[var(--surface-elevated)] overflow-hidden">
|
||||||
<div className="px-2 py-1.5 text-xs font-semibold text-muted-foreground uppercase tracking-wider">
|
<div className="px-2 py-1.5 text-xs font-semibold text-muted-foreground uppercase tracking-wider">
|
||||||
<RiTimeLine className="h-3 w-3 inline-block mr-1.5" />
|
<RiTimeLine className="h-3 w-3 inline-block mr-1.5" />
|
||||||
Recent
|
Recent
|
||||||
@@ -1459,7 +1459,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
|||||||
'flex w-full items-start gap-2 border-b border-border/30 px-2 py-1.5 text-left last:border-b-0',
|
'flex w-full items-start gap-2 border-b border-border/30 px-2 py-1.5 text-left last:border-b-0',
|
||||||
'focus:outline-none focus-visible:ring-1 focus-visible:ring-primary',
|
'focus:outline-none focus-visible:ring-1 focus-visible:ring-primary',
|
||||||
'first:rounded-t-xl last:rounded-b-xl transition-colors',
|
'first:rounded-t-xl last:rounded-b-xl transition-colors',
|
||||||
isSelected ? 'bg-primary/15 text-primary' : 'hover:bg-muted'
|
isSelected ? 'bg-interactive-selection/15 text-interactive-selection-foreground' : 'hover:bg-interactive-hover'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 min-w-0">
|
<div className="flex items-center gap-2 min-w-0">
|
||||||
@@ -1492,8 +1492,8 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
|||||||
const isActiveProvider = provider.id === currentProviderId;
|
const isActiveProvider = provider.id === currentProviderId;
|
||||||
const isExpanded = expandedMobileProviders.has(provider.id) || normalizedQuery.length > 0;
|
const isExpanded = expandedMobileProviders.has(provider.id) || normalizedQuery.length > 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={provider.id} className="rounded-xl border border-border/40 bg-background/95 overflow-hidden">
|
<div key={provider.id} className="rounded-xl border border-border/40 bg-[var(--surface-elevated)] overflow-hidden">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => toggleMobileProviderExpansion(provider.id)}
|
onClick={() => toggleMobileProviderExpansion(provider.id)}
|
||||||
@@ -1533,9 +1533,9 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
|||||||
className={cn(
|
className={cn(
|
||||||
'flex w-full items-start gap-2 border-b border-border/30 px-2 py-1.5 last:border-b-0',
|
'flex w-full items-start gap-2 border-b border-border/30 px-2 py-1.5 last:border-b-0',
|
||||||
'rounded-lg transition-colors',
|
'rounded-lg transition-colors',
|
||||||
!isSelected && 'hover:bg-muted',
|
!isSelected && 'hover:bg-interactive-hover',
|
||||||
isSelected
|
isSelected
|
||||||
? 'bg-primary/15 text-primary'
|
? 'bg-interactive-selection/15 text-interactive-selection-foreground'
|
||||||
: ''
|
: ''
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -1584,9 +1584,9 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
|||||||
toggleFavoriteModel(provider.id as string, model.id as string);
|
toggleFavoriteModel(provider.id as string, model.id as string);
|
||||||
}}
|
}}
|
||||||
className={cn(
|
className={cn(
|
||||||
"model-favorite-button flex h-5 w-5 items-center justify-center hover:text-yellow-600 flex-shrink-0",
|
"model-favorite-button flex h-5 w-5 items-center justify-center hover:text-primary/80 flex-shrink-0",
|
||||||
isFavoriteModel(provider.id as string, model.id as string)
|
isFavoriteModel(provider.id as string, model.id as string)
|
||||||
? "text-yellow-500"
|
? "text-primary"
|
||||||
: "text-muted-foreground"
|
: "text-muted-foreground"
|
||||||
)}
|
)}
|
||||||
aria-label={isFavoriteModel(provider.id as string, model.id as string) ? "Unfavorite" : "Favorite"}
|
aria-label={isFavoriteModel(provider.id as string, model.id as string) ? "Unfavorite" : "Favorite"}
|
||||||
@@ -1857,7 +1857,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
|||||||
ref={(el) => { modelItemRefs.current[flatIndex] = el; }}
|
ref={(el) => { modelItemRefs.current[flatIndex] = el; }}
|
||||||
className={cn(
|
className={cn(
|
||||||
"typography-meta group flex items-center gap-2 px-2 py-1.5 rounded-md cursor-pointer",
|
"typography-meta group flex items-center gap-2 px-2 py-1.5 rounded-md cursor-pointer",
|
||||||
isHighlighted ? "bg-accent" : "hover:bg-accent/50"
|
isHighlighted ? "bg-interactive-selection" : "hover:bg-interactive-hover/50"
|
||||||
)}
|
)}
|
||||||
onClick={() => handleProviderAndModelChange(providerID, modelID)}
|
onClick={() => handleProviderAndModelChange(providerID, modelID)}
|
||||||
onMouseEnter={() => setModelSelectedIndex(flatIndex)}
|
onMouseEnter={() => setModelSelectedIndex(flatIndex)}
|
||||||
@@ -1901,8 +1901,8 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
|||||||
toggleFavoriteModel(providerID, modelID);
|
toggleFavoriteModel(providerID, modelID);
|
||||||
}}
|
}}
|
||||||
className={cn(
|
className={cn(
|
||||||
"model-favorite-button flex h-4 w-4 items-center justify-center hover:text-yellow-600",
|
"model-favorite-button flex h-4 w-4 items-center justify-center hover:text-primary/80",
|
||||||
isFavorite ? "text-yellow-500" : "text-muted-foreground"
|
isFavorite ? "text-primary" : "text-muted-foreground"
|
||||||
)}
|
)}
|
||||||
aria-label={isFavorite ? "Unfavorite" : "Favorite"}
|
aria-label={isFavorite ? "Unfavorite" : "Favorite"}
|
||||||
title={isFavorite ? "Remove from favorites" : "Add to favorites"}
|
title={isFavorite ? "Remove from favorites" : "Add to favorites"}
|
||||||
@@ -2021,7 +2021,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
|||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'model-controls__model-trigger flex items-center gap-1.5 cursor-pointer hover:opacity-70 min-w-0',
|
'model-controls__model-trigger flex items-center gap-1.5 cursor-pointer hover:bg-transparent hover:opacity-70 min-w-0',
|
||||||
buttonHeight
|
buttonHeight
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -2082,7 +2082,10 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
|||||||
{/* Favorites Section */}
|
{/* Favorites Section */}
|
||||||
{filteredFavorites.length > 0 && (
|
{filteredFavorites.length > 0 && (
|
||||||
<>
|
<>
|
||||||
<DropdownMenuLabel className="typography-micro font-semibold text-muted-foreground uppercase tracking-wider flex items-center gap-2 -mx-1 px-3 py-1.5 sticky top-0 z-10 bg-background border-b border-border/30">
|
<DropdownMenuLabel
|
||||||
|
style={{ backgroundColor: 'var(--surface-elevated)' }}
|
||||||
|
className="typography-micro font-semibold text-muted-foreground uppercase tracking-wider flex items-center gap-2 -mx-1 px-3 py-1.5 sticky top-0 z-10 border-b border-border/30"
|
||||||
|
>
|
||||||
<RiStarFill className="h-4 w-4 text-primary" />
|
<RiStarFill className="h-4 w-4 text-primary" />
|
||||||
Favorites
|
Favorites
|
||||||
</DropdownMenuLabel>
|
</DropdownMenuLabel>
|
||||||
@@ -2097,7 +2100,10 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
|||||||
{filteredRecents.length > 0 && (
|
{filteredRecents.length > 0 && (
|
||||||
<>
|
<>
|
||||||
{filteredFavorites.length > 0 && <DropdownMenuSeparator />}
|
{filteredFavorites.length > 0 && <DropdownMenuSeparator />}
|
||||||
<DropdownMenuLabel className="typography-micro font-semibold text-muted-foreground uppercase tracking-wider flex items-center gap-2 -mx-1 px-3 py-1.5 sticky top-0 z-10 bg-background border-b border-border/30">
|
<DropdownMenuLabel
|
||||||
|
style={{ backgroundColor: 'var(--surface-elevated)' }}
|
||||||
|
className="typography-micro font-semibold text-muted-foreground uppercase tracking-wider flex items-center gap-2 -mx-1 px-3 py-1.5 sticky top-0 z-10 border-b border-border/30"
|
||||||
|
>
|
||||||
<RiTimeLine className="h-4 w-4" />
|
<RiTimeLine className="h-4 w-4" />
|
||||||
Recent
|
Recent
|
||||||
</DropdownMenuLabel>
|
</DropdownMenuLabel>
|
||||||
@@ -2117,7 +2123,10 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
|||||||
{filteredProviders.map((provider, index) => (
|
{filteredProviders.map((provider, index) => (
|
||||||
<React.Fragment key={provider.id}>
|
<React.Fragment key={provider.id}>
|
||||||
{index > 0 && <DropdownMenuSeparator />}
|
{index > 0 && <DropdownMenuSeparator />}
|
||||||
<DropdownMenuLabel className="typography-micro font-semibold text-muted-foreground uppercase tracking-wider flex items-center gap-2 -mx-1 px-3 py-1.5 sticky top-0 z-10 bg-background border-b border-border/30">
|
<DropdownMenuLabel
|
||||||
|
style={{ backgroundColor: 'var(--surface-elevated)' }}
|
||||||
|
className="typography-micro font-semibold text-muted-foreground uppercase tracking-wider flex items-center gap-2 -mx-1 px-3 py-1.5 sticky top-0 z-10 border-b border-border/30"
|
||||||
|
>
|
||||||
<ProviderLogo
|
<ProviderLogo
|
||||||
providerId={provider.id}
|
providerId={provider.id}
|
||||||
className="h-4 w-4 flex-shrink-0"
|
className="h-4 w-4 flex-shrink-0"
|
||||||
@@ -2148,7 +2157,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
|||||||
onTouchCancel={handleLongPressEnd}
|
onTouchCancel={handleLongPressEnd}
|
||||||
className={cn(
|
className={cn(
|
||||||
'model-controls__model-trigger flex items-center gap-1.5 min-w-0 focus:outline-none',
|
'model-controls__model-trigger flex items-center gap-1.5 min-w-0 focus:outline-none',
|
||||||
'cursor-pointer hover:opacity-70',
|
'cursor-pointer hover:bg-transparent hover:opacity-70',
|
||||||
buttonHeight
|
buttonHeight
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -2317,7 +2326,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
|||||||
className={cn(
|
className={cn(
|
||||||
'model-controls__variant-trigger flex items-center gap-1.5 transition-opacity min-w-0 focus:outline-none',
|
'model-controls__variant-trigger flex items-center gap-1.5 transition-opacity min-w-0 focus:outline-none',
|
||||||
buttonHeight,
|
buttonHeight,
|
||||||
'cursor-pointer hover:opacity-70',
|
'cursor-pointer hover:bg-transparent hover:opacity-70',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<RiBrainAi3Line className={cn(controlIconSize, 'flex-shrink-0', colorClass)} />
|
<RiBrainAi3Line className={cn(controlIconSize, 'flex-shrink-0', colorClass)} />
|
||||||
@@ -2341,7 +2350,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
|||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'model-controls__variant-trigger flex items-center gap-1.5 transition-opacity cursor-pointer hover:opacity-70 min-w-0',
|
'model-controls__variant-trigger flex items-center gap-1.5 transition-colors cursor-pointer hover:bg-transparent hover:opacity-70 min-w-0',
|
||||||
buttonHeight,
|
buttonHeight,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -2403,7 +2412,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
|||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<div className={cn(
|
<div className={cn(
|
||||||
'flex items-center gap-1.5 transition-opacity cursor-pointer hover:opacity-70 min-w-0',
|
'flex items-center gap-1.5 transition-colors cursor-pointer hover:bg-transparent hover:opacity-70 min-w-0',
|
||||||
buttonHeight
|
buttonHeight
|
||||||
)}>
|
)}>
|
||||||
<RiAiAgentLine
|
<RiAiAgentLine
|
||||||
@@ -2485,9 +2494,9 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
|||||||
onTouchEnd={handleLongPressEnd}
|
onTouchEnd={handleLongPressEnd}
|
||||||
onTouchCancel={handleLongPressEnd}
|
onTouchCancel={handleLongPressEnd}
|
||||||
className={cn(
|
className={cn(
|
||||||
'model-controls__agent-trigger flex items-center gap-1.5 transition-opacity min-w-0 focus:outline-none',
|
'model-controls__agent-trigger flex items-center gap-1.5 transition-colors min-w-0 focus:outline-none',
|
||||||
buttonHeight,
|
buttonHeight,
|
||||||
'cursor-pointer hover:opacity-70',
|
'cursor-pointer hover:bg-transparent hover:opacity-70',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<RiAiAgentLine
|
<RiAiAgentLine
|
||||||
|
|||||||
@@ -313,7 +313,7 @@ export const PermissionCard: React.FC<PermissionCardProps> = ({
|
|||||||
<div className="px-2 py-1.5 border-b border-border/20 bg-muted/5">
|
<div className="px-2 py-1.5 border-b border-border/20 bg-muted/5">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<RiQuestionLine className="h-3.5 w-3.5 text-yellow-500" />
|
<RiQuestionLine className="h-3.5 w-3.5 text-[var(--status-warning)]" />
|
||||||
<span className="typography-meta font-medium text-muted-foreground">
|
<span className="typography-meta font-medium text-muted-foreground">
|
||||||
Permission Required
|
Permission Required
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { RiCheckLine, RiCheckboxCircleFill, RiCircleLine, RiCloseLine, RiEditLine, RiListCheck3, RiQuestionLine } from '@remixicon/react';
|
import { RiCheckLine, RiCloseLine, RiEditLine, RiListCheck3, RiQuestionLine } from '@remixicon/react';
|
||||||
|
import { Checkbox } from '@/components/ui/checkbox';
|
||||||
|
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import type { QuestionRequest } from '@/types/question';
|
import type { QuestionRequest } from '@/types/question';
|
||||||
@@ -191,8 +192,8 @@ export const QuestionCard: React.FC<QuestionCardProps> = ({ question }) => {
|
|||||||
className={cn(
|
className={cn(
|
||||||
'px-2 py-0.5 typography-meta font-medium rounded transition-colors flex items-center gap-1',
|
'px-2 py-0.5 typography-meta font-medium rounded transition-colors flex items-center gap-1',
|
||||||
isActive
|
isActive
|
||||||
? 'bg-muted/40 text-foreground'
|
? 'bg-interactive-selection/40 text-foreground'
|
||||||
: 'text-muted-foreground hover:text-foreground hover:bg-muted/20'
|
: 'text-muted-foreground hover:text-foreground hover:bg-interactive-hover/20'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{isSummary ? <RiListCheck3 className="h-3 w-3" /> : null}
|
{isSummary ? <RiListCheck3 className="h-3 w-3" /> : null}
|
||||||
@@ -214,7 +215,7 @@ export const QuestionCard: React.FC<QuestionCardProps> = ({ question }) => {
|
|||||||
key={index}
|
key={index}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setActiveTab(String(index))}
|
onClick={() => setActiveTab(String(index))}
|
||||||
className="w-full text-left rounded px-1.5 py-1 hover:bg-muted/20 transition-colors"
|
className="w-full text-left rounded px-1.5 py-1 hover:bg-interactive-hover/20 transition-colors"
|
||||||
>
|
>
|
||||||
<div className="typography-micro text-muted-foreground">{q.header || `Question ${index + 1}`}</div>
|
<div className="typography-micro text-muted-foreground">{q.header || `Question ${index + 1}`}</div>
|
||||||
<div className={cn(
|
<div className={cn(
|
||||||
@@ -248,18 +249,18 @@ export const QuestionCard: React.FC<QuestionCardProps> = ({ question }) => {
|
|||||||
disabled={isResponding}
|
disabled={isResponding}
|
||||||
className={cn(
|
className={cn(
|
||||||
'w-full px-1.5 py-1 text-left rounded transition-colors',
|
'w-full px-1.5 py-1 text-left rounded transition-colors',
|
||||||
'hover:bg-muted/30',
|
'hover:bg-interactive-hover/30',
|
||||||
selected ? 'bg-muted/20' : null,
|
selected ? 'bg-interactive-selection/20' : null,
|
||||||
isResponding ? 'opacity-60 cursor-not-allowed' : null
|
isResponding ? 'opacity-60 cursor-not-allowed' : null
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex items-start gap-2">
|
<div className="flex items-start gap-2">
|
||||||
<div className="mt-0.5 shrink-0">
|
<div className="mt-0.5 shrink-0">
|
||||||
{selected ? (
|
<Checkbox
|
||||||
<RiCheckboxCircleFill className="h-3.5 w-3.5 text-primary" />
|
checked={selected}
|
||||||
) : (
|
onChange={() => handleToggleOption(option.label)}
|
||||||
<RiCircleLine className="h-3.5 w-3.5 text-muted-foreground/50" />
|
disabled={isResponding}
|
||||||
)}
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
@@ -290,8 +291,8 @@ export const QuestionCard: React.FC<QuestionCardProps> = ({ question }) => {
|
|||||||
disabled={isResponding}
|
disabled={isResponding}
|
||||||
className={cn(
|
className={cn(
|
||||||
'w-full px-1.5 py-1 text-left rounded transition-colors',
|
'w-full px-1.5 py-1 text-left rounded transition-colors',
|
||||||
'hover:bg-muted/30',
|
'hover:bg-interactive-hover/30',
|
||||||
isCustomActive ? 'bg-muted/20' : null,
|
isCustomActive ? 'bg-interactive-selection/20' : null,
|
||||||
isResponding ? 'opacity-60 cursor-not-allowed' : null
|
isResponding ? 'opacity-60 cursor-not-allowed' : null
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -269,18 +269,18 @@ export const ServerFilePicker: React.FC<ServerFilePickerProps> = ({
|
|||||||
case 'css':
|
case 'css':
|
||||||
case 'scss':
|
case 'scss':
|
||||||
case 'less':
|
case 'less':
|
||||||
return <RiCodeLine className="h-3.5 w-3.5 text-blue-500" />;
|
return <RiCodeLine className="h-3.5 w-3.5 text-[var(--status-info)]" />;
|
||||||
case 'json':
|
case 'json':
|
||||||
return <RiCodeLine className="h-3.5 w-3.5 text-yellow-500" />;
|
return <RiCodeLine className="h-3.5 w-3.5 text-[var(--status-warning)]" />;
|
||||||
case 'md':
|
case 'md':
|
||||||
case 'mdx':
|
case 'mdx':
|
||||||
return <RiFileTextLine className="h-3.5 w-3.5 text-gray-500" />;
|
return <RiFileTextLine className="h-3.5 w-3.5 text-muted-foreground" />;
|
||||||
case 'png':
|
case 'png':
|
||||||
case 'jpg':
|
case 'jpg':
|
||||||
case 'jpeg':
|
case 'jpeg':
|
||||||
case 'gif':
|
case 'gif':
|
||||||
case 'svg':
|
case 'svg':
|
||||||
return <RiFileImageLine className="h-3.5 w-3.5 text-green-500" />;
|
return <RiFileImageLine className="h-3.5 w-3.5 text-[var(--status-success)]" />;
|
||||||
default:
|
default:
|
||||||
return <RiFileTextLine className="h-3.5 w-3.5 text-muted-foreground" />;
|
return <RiFileTextLine className="h-3.5 w-3.5 text-muted-foreground" />;
|
||||||
}
|
}
|
||||||
@@ -381,7 +381,7 @@ export const ServerFilePicker: React.FC<ServerFilePickerProps> = ({
|
|||||||
const row = (
|
const row = (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex w-full items-center justify-start gap-1 px-2 py-1.5 rounded hover:bg-muted cursor-pointer typography-ui-label text-foreground text-left",
|
"flex w-full items-center justify-start gap-1 px-2 py-1.5 rounded hover:bg-interactive-hover cursor-pointer typography-ui-label text-foreground text-left",
|
||||||
file.type === 'file' && selectedFiles.has(file.path) && "bg-primary/10"
|
file.type === 'file' && selectedFiles.has(file.path) && "bg-primary/10"
|
||||||
)}
|
)}
|
||||||
style={{ paddingLeft: `${level * 12}px` }}
|
style={{ paddingLeft: `${level * 12}px` }}
|
||||||
@@ -514,7 +514,7 @@ export const ServerFilePicker: React.FC<ServerFilePickerProps> = ({
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
setSearchQuery('');
|
setSearchQuery('');
|
||||||
}}
|
}}
|
||||||
className="absolute right-2 top-1/2 -translate-y-1/2 p-0.5 hover:bg-muted rounded"
|
className="absolute right-2 top-1/2 -translate-y-1/2 p-0.5 hover:bg-interactive-hover rounded"
|
||||||
>
|
>
|
||||||
<RiCloseLine className="h-3 w-3"/>
|
<RiCloseLine className="h-3 w-3"/>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -114,10 +114,10 @@ export const SkillAutocomplete = React.forwardRef<SkillAutocompleteHandle, Skill
|
|||||||
ref={(el) => {
|
ref={(el) => {
|
||||||
itemRefs.current[index] = el;
|
itemRefs.current[index] = el;
|
||||||
}}
|
}}
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex items-start gap-2 px-3 py-1.5 cursor-pointer rounded-lg typography-ui-label',
|
'flex items-start gap-2 px-3 py-1.5 cursor-pointer rounded-lg typography-ui-label',
|
||||||
index === selectedIndex && 'bg-muted'
|
index === selectedIndex && 'bg-interactive-selection'
|
||||||
)}
|
)}
|
||||||
onClick={() => onSkillSelect(skill.name)}
|
onClick={() => onSkillSelect(skill.name)}
|
||||||
onMouseEnter={() => setSelectedIndex(index)}
|
onMouseEnter={() => setSelectedIndex(index)}
|
||||||
>
|
>
|
||||||
@@ -146,7 +146,7 @@ export const SkillAutocomplete = React.forwardRef<SkillAutocompleteHandle, Skill
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
className="absolute z-[100] min-w-0 w-full max-w-[360px] max-h-60 bg-background border border-border rounded-xl shadow-none bottom-full mb-2 left-0 flex flex-col"
|
className="absolute z-[100] min-w-0 w-full max-w-[360px] max-h-60 bg-background border-2 border-border/60 rounded-xl shadow-md bottom-full mb-2 left-0 flex flex-col"
|
||||||
>
|
>
|
||||||
<ScrollableOverlay outerClassName="flex-1 min-h-0" className="px-0 pb-2" fillContainer={false}>
|
<ScrollableOverlay outerClassName="flex-1 min-h-0" className="px-0 pb-2" fillContainer={false}>
|
||||||
{filteredSkills.length ? (
|
{filteredSkills.length ? (
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export const StatusChip: React.FC<StatusChipProps> = ({ onClick, className }) =>
|
|||||||
'inline-flex min-w-0 items-center justify-center',
|
'inline-flex min-w-0 items-center justify-center',
|
||||||
'rounded-lg border border-border/50 px-1.5',
|
'rounded-lg border border-border/50 px-1.5',
|
||||||
'typography-meta font-medium text-foreground/80',
|
'typography-meta font-medium text-foreground/80',
|
||||||
'focus:outline-none',
|
'focus:outline-none hover:bg-[var(--interactive-hover)]',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
style={{
|
style={{
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ export const TimelineDialog: React.FC<TimelineDialogProps> = ({ open, onOpenChan
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={message.info.id}
|
key={message.info.id}
|
||||||
className="group flex items-center gap-2 py-1.5 hover:bg-muted/30 rounded transition-colors cursor-pointer"
|
className="group flex items-center gap-2 py-1.5 hover:bg-interactive-hover/30 rounded transition-colors cursor-pointer"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onScrollToMessage?.(message.info.id);
|
onScrollToMessage?.(message.info.id);
|
||||||
onOpenChange(false);
|
onOpenChange(false);
|
||||||
|
|||||||
@@ -323,7 +323,7 @@ export const UnifiedControlsDrawer: React.FC<UnifiedControlsDrawerProps> = ({
|
|||||||
'inline-flex items-center rounded-full border px-2.5 py-1 typography-meta font-medium',
|
'inline-flex items-center rounded-full border px-2.5 py-1 typography-meta font-medium',
|
||||||
isSelected
|
isSelected
|
||||||
? 'border-primary/30 bg-primary/10 text-foreground'
|
? 'border-primary/30 bg-primary/10 text-foreground'
|
||||||
: 'border-border/40 text-muted-foreground hover:bg-muted/50'
|
: 'border-border/40 text-muted-foreground hover:bg-interactive-hover/50'
|
||||||
)}
|
)}
|
||||||
aria-pressed={isSelected}
|
aria-pressed={isSelected}
|
||||||
>
|
>
|
||||||
@@ -335,7 +335,7 @@ export const UnifiedControlsDrawer: React.FC<UnifiedControlsDrawerProps> = ({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onOpenEffort}
|
onClick={onOpenEffort}
|
||||||
className="inline-flex items-center rounded-full border border-border/40 px-2.5 py-1 typography-meta font-medium text-muted-foreground hover:bg-muted/50"
|
className="inline-flex items-center rounded-full border border-border/40 px-2.5 py-1 typography-meta font-medium text-muted-foreground hover:bg-interactive-hover/50"
|
||||||
aria-label="More effort options"
|
aria-label="More effort options"
|
||||||
>
|
>
|
||||||
...
|
...
|
||||||
|
|||||||
@@ -209,7 +209,7 @@ const ToolOutputDialog: React.FC<ToolOutputDialogProps> = ({ popup, onOpenChange
|
|||||||
: {}),
|
: {}),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span className="text-muted-foreground/60 w-10 flex-shrink-0 text-right pr-3 self-start select-none">
|
<span className="w-12 flex-shrink-0 text-right pr-3 select-none border-r mr-3 -my-0.5 py-0.5" style={{ color: 'var(--tools-edit-line-number)', borderColor: 'var(--tools-border)' }}>
|
||||||
{line.lineNumber || ''}
|
{line.lineNumber || ''}
|
||||||
</span>
|
</span>
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
@@ -282,7 +282,7 @@ const ToolOutputDialog: React.FC<ToolOutputDialogProps> = ({ popup, onOpenChange
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<span className="text-muted-foreground/60 w-10 flex-shrink-0 text-right pr-3 self-start select-none">
|
<span className="w-12 flex-shrink-0 text-right pr-3 select-none border-r mr-3 -my-0.5 py-0.5" style={{ color: 'var(--tools-edit-line-number)', borderColor: 'var(--tools-border)' }}>
|
||||||
{line.leftLine.lineNumber || ''}
|
{line.leftLine.lineNumber || ''}
|
||||||
</span>
|
</span>
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
@@ -336,7 +336,7 @@ const ToolOutputDialog: React.FC<ToolOutputDialogProps> = ({ popup, onOpenChange
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<span className="text-muted-foreground/60 w-10 flex-shrink-0 text-right pr-3 self-start select-none">
|
<span className="w-12 flex-shrink-0 text-right pr-3 select-none border-r mr-3 -my-0.5 py-0.5" style={{ color: 'var(--tools-edit-line-number)', borderColor: 'var(--tools-border)' }}>
|
||||||
{line.rightLine.lineNumber || ''}
|
{line.rightLine.lineNumber || ''}
|
||||||
</span>
|
</span>
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
@@ -500,12 +500,12 @@ const ToolOutputDialog: React.FC<ToolOutputDialogProps> = ({ popup, onOpenChange
|
|||||||
|
|
||||||
const shouldShowLineNumber = !isInfo && (limit === undefined || idx < limit);
|
const shouldShowLineNumber = !isInfo && (limit === undefined || idx < limit);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={idx} className={`typography-code font-mono flex ${isInfo ? 'text-muted-foreground/70 italic' : ''}`}>
|
<div key={idx} className={`typography-code font-mono flex ${isInfo ? 'text-muted-foreground/70 italic' : ''}`}>
|
||||||
<span className="text-muted-foreground/60 w-10 flex-shrink-0 text-right pr-4 self-start select-none">
|
<span className="w-12 flex-shrink-0 text-right pr-3 select-none border-r mr-3 -my-0.5 py-0.5" style={{ color: 'var(--tools-edit-line-number)', borderColor: 'var(--tools-border)' }}>
|
||||||
{shouldShowLineNumber ? lineNumber : ''}
|
{shouldShowLineNumber ? lineNumber : ''}
|
||||||
</span>
|
</span>
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
{isInfo ? (
|
{isInfo ? (
|
||||||
<div className="whitespace-pre-wrap break-words">{line}</div>
|
<div className="whitespace-pre-wrap break-words">{line}</div>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -159,6 +159,7 @@ const ProgressiveGroup: React.FC<ProgressiveGroupProps> = ({
|
|||||||
isExpanded && 'opacity-0',
|
isExpanded && 'opacity-0',
|
||||||
!isExpanded && !isMobile && 'group-hover/tool:opacity-0'
|
!isExpanded && !isMobile && 'group-hover/tool:opacity-0'
|
||||||
)}
|
)}
|
||||||
|
style={{ color: 'var(--tools-icon)' }}
|
||||||
>
|
>
|
||||||
<RiStackLine className="h-3.5 w-3.5" />
|
<RiStackLine className="h-3.5 w-3.5" />
|
||||||
</div>
|
</div>
|
||||||
@@ -178,10 +179,10 @@ const ProgressiveGroup: React.FC<ProgressiveGroupProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<RiStackLine className="h-3.5 w-3.5" />
|
<RiStackLine className="h-3.5 w-3.5" style={{ color: 'var(--tools-icon)' }} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<span className="typography-meta font-medium">Activity</span>
|
<span className="typography-meta font-medium" style={{ color: 'var(--tools-title)' }}>Activity</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{diffStats && (diffStats.additions > 0 || diffStats.deletions > 0) && (
|
{diffStats && (diffStats.additions > 0 || diffStats.deletions > 0) && (
|
||||||
@@ -201,11 +202,13 @@ const ProgressiveGroup: React.FC<ProgressiveGroupProps> = ({
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'relative pr-2 pb-1 pt-1 pl-[1.4375rem]',
|
'relative pr-2 pb-1 pt-1 pl-[1.4375rem]'
|
||||||
'before:absolute before:left-[0.4375rem] before:w-px before:bg-border/80 before:content-[""]',
|
|
||||||
'before:top-[-0.25rem] before:bottom-0'
|
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
<div
|
||||||
|
className="absolute left-[0.4375rem] w-px top-[-0.25rem] bottom-0"
|
||||||
|
style={{ backgroundColor: 'var(--tools-border)', borderWidth: '0', width: '1px' }}
|
||||||
|
></div>
|
||||||
{!isExpanded && hiddenCount > 0 && (
|
{!isExpanded && hiddenCount > 0 && (
|
||||||
<div
|
<div
|
||||||
className="typography-micro text-muted-foreground/70 mb-1 cursor-pointer hover:text-muted-foreground"
|
className="typography-micro text-muted-foreground/70 mb-1 cursor-pointer hover:text-muted-foreground"
|
||||||
|
|||||||
@@ -256,7 +256,8 @@ const ToolScrollableSection: React.FC<ToolScrollableSectionProps> = ({
|
|||||||
}) => (
|
}) => (
|
||||||
<ScrollableOverlay
|
<ScrollableOverlay
|
||||||
outerClassName={cn('w-full min-w-0 flex-none overflow-hidden', maxHeightClass, outerClassName)}
|
outerClassName={cn('w-full min-w-0 flex-none overflow-hidden', maxHeightClass, outerClassName)}
|
||||||
className={cn('tool-output-surface p-2 rounded-xl w-full min-w-0 border border-border/20 bg-transparent', className)}
|
className={cn('tool-output-surface p-2 rounded-xl w-full min-w-0 bg-transparent', className)}
|
||||||
|
style={{ borderWidth: '1px', borderColor: 'var(--tools-border)' }}
|
||||||
disableHorizontal={disableHorizontal}
|
disableHorizontal={disableHorizontal}
|
||||||
>
|
>
|
||||||
<div className="w-full min-w-0">
|
<div className="w-full min-w-0">
|
||||||
@@ -410,8 +411,8 @@ interface DiffPreviewProps {
|
|||||||
const DiffPreview: React.FC<DiffPreviewProps> = React.memo(({ diff, syntaxTheme, input }) => (
|
const DiffPreview: React.FC<DiffPreviewProps> = React.memo(({ diff, syntaxTheme, input }) => (
|
||||||
<div className="typography-code px-1 pb-1 pt-0 space-y-0">
|
<div className="typography-code px-1 pb-1 pt-0 space-y-0">
|
||||||
{parseDiffToUnified(diff).map((hunk, hunkIdx) => (
|
{parseDiffToUnified(diff).map((hunk, hunkIdx) => (
|
||||||
<div key={hunkIdx} className="-mx-1 px-1 border-b border-border/20 last:border-b-0">
|
<div key={hunkIdx} className="-mx-1 px-1 last:border-b-0" style={{ borderBottomWidth: '1px', borderBottomColor: 'var(--tools-border)' }}>
|
||||||
<div className="bg-muted/20 px-2 py-1 typography-meta font-medium text-muted-foreground border-b border-border/10 break-words -mx-1">
|
<div className="bg-muted/20 px-2 py-1 typography-meta font-medium text-muted-foreground break-words -mx-1" style={{ borderBottomWidth: '1px', borderBottomColor: 'var(--tools-border)' }}>
|
||||||
{`${hunk.file} (line ${hunk.oldStart})`}
|
{`${hunk.file} (line ${hunk.oldStart})`}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -433,7 +434,7 @@ const DiffPreview: React.FC<DiffPreviewProps> = React.memo(({ diff, syntaxTheme,
|
|||||||
: {}
|
: {}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<span className="text-muted-foreground/60 w-8 flex-shrink-0 text-right pr-2 self-start select-none">
|
<span className="w-10 flex-shrink-0 text-right pr-3 select-none border-r mr-3 -my-0.5 py-0.5" style={{ color: 'var(--tools-edit-line-number)', borderColor: 'var(--tools-border)' }}>
|
||||||
{line.lineNumber || ''}
|
{line.lineNumber || ''}
|
||||||
</span>
|
</span>
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
@@ -454,9 +455,15 @@ const DiffPreview: React.FC<DiffPreviewProps> = React.memo(({ diff, syntaxTheme,
|
|||||||
whiteSpace: 'pre-wrap',
|
whiteSpace: 'pre-wrap',
|
||||||
wordBreak: 'break-all',
|
wordBreak: 'break-all',
|
||||||
overflowWrap: 'anywhere',
|
overflowWrap: 'anywhere',
|
||||||
|
color: line.type === 'removed' ? 'var(--tools-edit-removed)' : line.type === 'added' ? 'var(--tools-edit-added)' : 'inherit',
|
||||||
}}
|
}}
|
||||||
codeTagProps={{
|
codeTagProps={{
|
||||||
style: { background: 'transparent', backgroundColor: 'transparent', fontSize: 'inherit' },
|
style: {
|
||||||
|
background: 'transparent',
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
fontSize: 'inherit',
|
||||||
|
color: line.type === 'removed' ? 'var(--tools-edit-removed)' : line.type === 'added' ? 'var(--tools-edit-added)' : 'inherit',
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{line.content}
|
{line.content}
|
||||||
@@ -491,13 +498,13 @@ const WriteInputPreview: React.FC<WriteInputPreviewProps> = React.memo(({ conten
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full min-w-0">
|
<div className="w-full min-w-0">
|
||||||
<div className="bg-muted/20 px-2 py-1 typography-meta font-medium text-muted-foreground border border-border/10 rounded-lg mb-1">
|
<div className="bg-muted/20 px-2 py-1 typography-meta font-medium text-muted-foreground rounded-lg mb-1" style={{ borderWidth: '1px', borderColor: 'var(--tools-border)' }}>
|
||||||
{`${displayPath} (${headerLineLabel})`}
|
{`${displayPath} (${headerLineLabel})`}
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-0">
|
<div className="space-y-0">
|
||||||
{lines.map((line, lineIdx) => (
|
{lines.map((line, lineIdx) => (
|
||||||
<div key={lineIdx} className="typography-code font-mono px-2 py-0.5 flex -mx-1">
|
<div key={lineIdx} className="typography-code font-mono px-2 py-0.5 flex -mx-1">
|
||||||
<span className="text-muted-foreground/60 w-8 flex-shrink-0 text-right pr-2 self-start select-none">
|
<span className="w-10 flex-shrink-0 text-right pr-3 select-none border-r mr-3 -my-0.5 py-0.5" style={{ color: 'var(--tools-edit-line-number)', borderColor: 'var(--tools-border)' }}>
|
||||||
{lineIdx + 1}
|
{lineIdx + 1}
|
||||||
</span>
|
</span>
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
@@ -560,10 +567,10 @@ const ImagePreview: React.FC<ImagePreviewProps> = React.memo(({ content, filePat
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full min-w-0">
|
<div className="w-full min-w-0">
|
||||||
<div className="bg-muted/20 px-2 py-1 typography-meta font-medium text-muted-foreground border border-border/10 rounded-lg mb-2">
|
<div className="bg-muted/20 px-2 py-1 typography-meta font-medium text-muted-foreground rounded-lg mb-2" style={{ borderWidth: '1px', borderColor: 'var(--tools-border)' }}>
|
||||||
{displayPath}
|
{displayPath}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-center p-4 bg-muted/10 rounded-lg border border-border/10">
|
<div className="flex justify-center p-4 bg-muted/10 rounded-lg" style={{ borderWidth: '1px', borderColor: 'var(--tools-border)' }}>
|
||||||
<img
|
<img
|
||||||
src={imageSrc}
|
src={imageSrc}
|
||||||
alt={displayPath}
|
alt={displayPath}
|
||||||
@@ -815,7 +822,7 @@ const ToolExpandedContent: React.FC<ToolExpandedContentProps> = React.memo(({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={idx} className={cn('typography-code font-mono flex w-full min-w-0', isInfo && 'text-muted-foreground/70 italic')}>
|
<div key={idx} className={cn('typography-code font-mono flex w-full min-w-0', isInfo && 'text-muted-foreground/70 italic')}>
|
||||||
<span className="text-muted-foreground/60 w-8 flex-shrink-0 text-right pr-3 self-start select-none">
|
<span className="w-10 flex-shrink-0 text-right pr-3 select-none border-r mr-3 -my-0.5 py-0.5" style={{ color: 'var(--tools-edit-line-number)', borderColor: 'var(--tools-border)' }}>
|
||||||
{shouldShowLineNumber ? lineNumber : ''}
|
{shouldShowLineNumber ? lineNumber : ''}
|
||||||
</span>
|
</span>
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
@@ -892,12 +899,18 @@ const ToolExpandedContent: React.FC<ToolExpandedContentProps> = React.memo(({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'relative pr-2 pb-2 pt-2 space-y-2 pl-[1.4375rem]',
|
'relative pr-2 pb-2 pt-2 space-y-2 pl-[1.4375rem]'
|
||||||
'before:absolute before:left-[0.4375rem] before:w-px before:bg-border/80 before:content-[""]',
|
|
||||||
hasPrevTool ? 'before:top-[-0.45rem]' : 'before:top-[-0.25rem]',
|
|
||||||
hasNextTool ? 'before:bottom-[-0.6rem]' : 'before:bottom-0'
|
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
<div
|
||||||
|
className="absolute left-[0.4375rem] w-px"
|
||||||
|
style={{
|
||||||
|
backgroundColor: 'var(--tools-border)',
|
||||||
|
top: hasPrevTool ? '-0.45rem' : '-0.25rem',
|
||||||
|
bottom: hasNextTool ? '-0.6rem' : '0',
|
||||||
|
width: '1px'
|
||||||
|
}}
|
||||||
|
></div>
|
||||||
{(part.tool === 'todowrite' || part.tool === 'todoread' || part.tool === 'question') ? (
|
{(part.tool === 'todowrite' || part.tool === 'todoread' || part.tool === 'question') ? (
|
||||||
renderResultContent()
|
renderResultContent()
|
||||||
) : (
|
) : (
|
||||||
@@ -1109,7 +1122,7 @@ const ToolPart: React.FC<ToolPartProps> = ({ part, isExpanded, onToggle, syntaxT
|
|||||||
isExpanded && 'opacity-0',
|
isExpanded && 'opacity-0',
|
||||||
!isExpanded && !isMobile && 'group-hover/tool:opacity-0'
|
!isExpanded && !isMobile && 'group-hover/tool:opacity-0'
|
||||||
)}
|
)}
|
||||||
style={!isTaskTool && isError ? { color: 'var(--status-error)' } : {}}
|
style={!isTaskTool && isError ? { color: 'var(--status-error)' } : { color: 'var(--tools-icon)' }}
|
||||||
>
|
>
|
||||||
{getToolIcon(part.tool)}
|
{getToolIcon(part.tool)}
|
||||||
</div>
|
</div>
|
||||||
@@ -1127,13 +1140,13 @@ const ToolPart: React.FC<ToolPartProps> = ({ part, isExpanded, onToggle, syntaxT
|
|||||||
</div>
|
</div>
|
||||||
<span
|
<span
|
||||||
className="typography-meta font-medium"
|
className="typography-meta font-medium"
|
||||||
style={!isTaskTool && isError ? { color: 'var(--status-error)' } : {}}
|
style={!isTaskTool && isError ? { color: 'var(--status-error)' } : { color: 'var(--tools-title)' }}
|
||||||
>
|
>
|
||||||
{displayName}
|
{displayName}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-1 flex-1 min-w-0 typography-meta text-muted-foreground/70">
|
<div className="flex items-center gap-1 flex-1 min-w-0 typography-meta" style={{ color: 'var(--tools-description)' }}>
|
||||||
{description && (
|
{description && (
|
||||||
<span className={cn("truncate", isMobile && "max-w-[120px]")}>
|
<span className={cn("truncate", isMobile && "max-w-[120px]")}>
|
||||||
{description}
|
{description}
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import type { SVGProps } from 'react';
|
||||||
|
|
||||||
|
interface DiffIconProps extends Omit<SVGProps<SVGSVGElement>, 'children'> {
|
||||||
|
size?: number | string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Git merge/branch icon for the Diff tab.
|
||||||
|
*/
|
||||||
|
export function DiffIcon({ size, className, style, ...props }: DiffIconProps) {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 256 256"
|
||||||
|
fill="currentColor"
|
||||||
|
className={className}
|
||||||
|
style={{
|
||||||
|
width: typeof size === 'number' ? `${size}px` : size,
|
||||||
|
height: typeof size === 'number' ? `${size}px` : size,
|
||||||
|
...style,
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path d="M112,148a12,12,0,0,0-12,12v19L69.17,148.2A4,4,0,0,1,68,145.37V97.94a36,36,0,1,0-24,0v47.43a27.81,27.81,0,0,0,8.2,19.8L83,196H64a12,12,0,0,0,0,24h48a12,12,0,0,0,12-12V160A12,12,0,0,0,112,148ZM56,52A12,12,0,1,1,44,64,12,12,0,0,1,56,52ZM212,158.06V110.63a27.81,27.81,0,0,0-8.2-19.8L173,60h19a12,12,0,0,0,0-24H144a12,12,0,0,0-12,12V96a12,12,0,0,0,24,0V77l30.83,30.83a4,4,0,0,1,1.17,2.83v47.43a36,36,0,1,0,24,0ZM200,204a12,12,0,1,1,12-12A12,12,0,0,1,200,204Z" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -13,7 +13,8 @@ import {
|
|||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from '@/components/ui/dropdown-menu';
|
} from '@/components/ui/dropdown-menu';
|
||||||
|
|
||||||
import { RiArrowLeftSLine, RiChat4Line, RiCheckLine, RiCodeLine, RiCommandLine, RiFileTextLine, RiFolder6Line, RiGitBranchLine, RiGithubFill, RiLayoutLeftLine, RiPlayListAddLine, RiQuestionLine, RiSettings3Line, RiTerminalBoxLine, type RemixiconComponentType } from '@remixicon/react';
|
import { RiArrowLeftSLine, RiChat4Line, RiCheckLine, RiCommandLine, RiFileTextLine, RiFolder6Line, RiGitBranchLine, RiGithubFill, RiLayoutLeftLine, RiPlayListAddLine, RiQuestionLine, RiSettings3Line, RiTerminalBoxLine, type RemixiconComponentType } from '@remixicon/react';
|
||||||
|
import { DiffIcon } from '@/components/icons/DiffIcon';
|
||||||
import { useUIStore, type MainTab } from '@/stores/useUIStore';
|
import { useUIStore, type MainTab } from '@/stores/useUIStore';
|
||||||
import { useUpdateStore } from '@/stores/useUpdateStore';
|
import { useUpdateStore } from '@/stores/useUpdateStore';
|
||||||
import { useConfigStore } from '@/stores/useConfigStore';
|
import { useConfigStore } from '@/stores/useConfigStore';
|
||||||
@@ -64,7 +65,7 @@ const resolveTilde = (path: string, homeDir: string | null): string => {
|
|||||||
interface TabConfig {
|
interface TabConfig {
|
||||||
id: MainTab;
|
id: MainTab;
|
||||||
label: string;
|
label: string;
|
||||||
icon: RemixiconComponentType;
|
icon: RemixiconComponentType | 'diff';
|
||||||
badge?: number;
|
badge?: number;
|
||||||
showDot?: boolean;
|
showDot?: boolean;
|
||||||
}
|
}
|
||||||
@@ -304,7 +305,7 @@ export const Header: React.FC = () => {
|
|||||||
setSettingsDialogOpen(true);
|
setSettingsDialogOpen(true);
|
||||||
}, [blurActiveElement, isMobile, setSessionSwitcherOpen, setSettingsDialogOpen]);
|
}, [blurActiveElement, isMobile, setSessionSwitcherOpen, setSettingsDialogOpen]);
|
||||||
|
|
||||||
const headerIconButtonClass = 'app-region-no-drag inline-flex h-9 w-9 items-center justify-center gap-2 p-2 rounded-md typography-ui-label font-medium text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary disabled:pointer-events-none disabled:opacity-50 hover:text-foreground hover:bg-secondary/50 transition-colors';
|
const headerIconButtonClass = 'app-region-no-drag inline-flex h-9 w-9 items-center justify-center gap-2 p-2 rounded-md typography-ui-label font-medium text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary disabled:pointer-events-none disabled:opacity-50 hover:text-foreground hover:bg-interactive-hover transition-colors';
|
||||||
|
|
||||||
const desktopPaddingClass = React.useMemo(() => {
|
const desktopPaddingClass = React.useMemo(() => {
|
||||||
if (isDesktopApp && isMacPlatform) {
|
if (isDesktopApp && isMacPlatform) {
|
||||||
@@ -415,7 +416,7 @@ export const Header: React.FC = () => {
|
|||||||
{
|
{
|
||||||
id: 'diff',
|
id: 'diff',
|
||||||
label: 'Diff',
|
label: 'Diff',
|
||||||
icon: RiCodeLine,
|
icon: 'diff',
|
||||||
badge: !isMobile && diffFileCount > 0 ? diffFileCount : undefined,
|
badge: !isMobile && diffFileCount > 0 ? diffFileCount : undefined,
|
||||||
},
|
},
|
||||||
{ id: 'files', label: 'Files', icon: RiFolder6Line },
|
{ id: 'files', label: 'Files', icon: RiFolder6Line },
|
||||||
@@ -447,18 +448,34 @@ export const Header: React.FC = () => {
|
|||||||
|
|
||||||
const renderTab = (tab: TabConfig) => {
|
const renderTab = (tab: TabConfig) => {
|
||||||
const isActive = activeMainTab === tab.id;
|
const isActive = activeMainTab === tab.id;
|
||||||
const Icon = tab.icon;
|
const isDiffTab = tab.icon === 'diff';
|
||||||
|
const Icon = isDiffTab ? null : (tab.icon as RemixiconComponentType);
|
||||||
const isChatTab = tab.id === 'chat';
|
const isChatTab = tab.id === 'chat';
|
||||||
|
const showContextTooltip = isChatTab && !isMobile && contextUsage && contextUsage.totalTokens > 0;
|
||||||
|
|
||||||
return (
|
const renderIcon = (iconSize: number) => {
|
||||||
|
if (isDiffTab) {
|
||||||
|
return <DiffIcon size={iconSize} />;
|
||||||
|
}
|
||||||
|
return Icon ? <Icon size={iconSize} /> : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatTokens = (tokens: number) => {
|
||||||
|
if (tokens >= 1_000_000) return `${(tokens / 1_000_000).toFixed(1)}M`;
|
||||||
|
if (tokens >= 1_000) return `${(tokens / 1_000).toFixed(1)}K`;
|
||||||
|
return tokens.toFixed(1).replace(/\.0$/, '');
|
||||||
|
};
|
||||||
|
|
||||||
|
const tabButton = (
|
||||||
<button
|
<button
|
||||||
key={tab.id}
|
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setActiveMainTab(tab.id)}
|
onClick={() => setActiveMainTab(tab.id)}
|
||||||
onMouseDown={isActive ? handleActiveTabDragStart : undefined}
|
onMouseDown={isActive ? handleActiveTabDragStart : undefined}
|
||||||
className={cn(
|
className={cn(
|
||||||
'relative flex h-8 items-center gap-2 px-3 rounded-md typography-ui-label font-medium transition-colors',
|
'relative flex h-8 items-center gap-2 px-3 rounded-md typography-ui-label font-medium transition-colors',
|
||||||
isActive ? 'app-region-drag bg-secondary text-foreground shadow-sm' : 'app-region-no-drag text-muted-foreground hover:bg-secondary/50 hover:text-foreground',
|
isActive
|
||||||
|
? 'app-region-drag bg-interactive-selection text-interactive-selection-foreground shadow-sm'
|
||||||
|
: 'app-region-no-drag text-muted-foreground hover:bg-interactive-hover/50 hover:text-foreground',
|
||||||
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary',
|
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary',
|
||||||
isChatTab && !isMobile && 'min-w-[100px] justify-center'
|
isChatTab && !isMobile && 'min-w-[100px] justify-center'
|
||||||
)}
|
)}
|
||||||
@@ -467,33 +484,57 @@ export const Header: React.FC = () => {
|
|||||||
role="tab"
|
role="tab"
|
||||||
>
|
>
|
||||||
{isMobile ? (
|
{isMobile ? (
|
||||||
<Icon size={20} />
|
renderIcon(20)
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Icon size={16} />
|
{renderIcon(16)}
|
||||||
<span className="header-tab-label">{tab.label}</span>
|
<span className="header-tab-label">{tab.label}</span>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isChatTab && !isMobile && contextUsage && contextUsage.totalTokens > 0 && (
|
{showContextTooltip && (
|
||||||
<span className="ml-1">
|
<span className="header-tab-badge">
|
||||||
<ContextUsageDisplay
|
<div className={cn(
|
||||||
totalTokens={contextUsage.totalTokens}
|
'app-region-no-drag flex items-center gap-1.5 text-muted-foreground/60 select-none typography-micro',
|
||||||
percentage={contextUsage.percentage}
|
)}>
|
||||||
contextLimit={contextUsage.contextLimit}
|
<span className={cn(
|
||||||
outputLimit={contextUsage.outputLimit ?? 0}
|
'font-medium',
|
||||||
size="compact"
|
contextUsage.percentage >= 90 ? 'text-status-error' :
|
||||||
/>
|
contextUsage.percentage >= 75 ? 'text-status-warning' : 'text-status-success'
|
||||||
|
)}>
|
||||||
|
{Math.min(contextUsage.percentage, 999).toFixed(1)}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{tab.badge !== undefined && tab.badge > 0 && (
|
{tab.badge !== undefined && tab.badge > 0 && (
|
||||||
<span className="ml-1 flex h-4 min-w-4 items-center justify-center rounded-full bg-primary/10 px-1 text-[10px] font-bold text-primary">
|
<span className="header-tab-badge typography-micro text-status-info font-medium">
|
||||||
{tab.badge}
|
{tab.badge}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (showContextTooltip) {
|
||||||
|
const safeOutputLimit = typeof contextUsage.outputLimit === 'number' ? Math.max(contextUsage.outputLimit, 0) : 0;
|
||||||
|
return (
|
||||||
|
<Tooltip key={tab.id} delayDuration={1000}>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
{tabButton}
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<p className="typography-micro leading-tight">Used tokens: {formatTokens(contextUsage.totalTokens)}</p>
|
||||||
|
<p className="typography-micro leading-tight">Context limit: {formatTokens(contextUsage.contextLimit)}</p>
|
||||||
|
<p className="typography-micro leading-tight">Output limit: {formatTokens(safeOutputLimit)}</p>
|
||||||
|
</div>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <React.Fragment key={tab.id}>{tabButton}</React.Fragment>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderDesktop = () => (
|
const renderDesktop = () => (
|
||||||
@@ -662,7 +703,7 @@ export const Header: React.FC = () => {
|
|||||||
{isSessionSwitcherOpen ? (
|
{isSessionSwitcherOpen ? (
|
||||||
<button
|
<button
|
||||||
onClick={() => setSessionSwitcherOpen(false)}
|
onClick={() => setSessionSwitcherOpen(false)}
|
||||||
className="app-region-no-drag h-9 w-9 p-2 text-muted-foreground hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary rounded-md active:bg-secondary"
|
className="app-region-no-drag h-9 w-9 p-2 text-muted-foreground hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary rounded-md active:bg-interactive-active"
|
||||||
aria-label="Back"
|
aria-label="Back"
|
||||||
>
|
>
|
||||||
<RiArrowLeftSLine className="h-5 w-5" />
|
<RiArrowLeftSLine className="h-5 w-5" />
|
||||||
@@ -670,7 +711,7 @@ export const Header: React.FC = () => {
|
|||||||
) : (
|
) : (
|
||||||
<button
|
<button
|
||||||
onClick={handleOpenSessionSwitcher}
|
onClick={handleOpenSessionSwitcher}
|
||||||
className="app-region-no-drag h-9 w-9 p-2 text-muted-foreground hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary rounded-md active:bg-secondary"
|
className="app-region-no-drag h-9 w-9 p-2 text-muted-foreground hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary rounded-md active:bg-interactive-active"
|
||||||
aria-label="Open sessions"
|
aria-label="Open sessions"
|
||||||
>
|
>
|
||||||
<RiPlayListAddLine className="h-5 w-5" />
|
<RiPlayListAddLine className="h-5 w-5" />
|
||||||
@@ -697,7 +738,8 @@ export const Header: React.FC = () => {
|
|||||||
<div className="flex items-center gap-0.5" role="tablist" aria-label="Main navigation">
|
<div className="flex items-center gap-0.5" role="tablist" aria-label="Main navigation">
|
||||||
{tabs.map((tab) => {
|
{tabs.map((tab) => {
|
||||||
const isActive = activeMainTab === tab.id;
|
const isActive = activeMainTab === tab.id;
|
||||||
const Icon = tab.icon;
|
const isDiffTab = tab.icon === 'diff';
|
||||||
|
const Icon = isDiffTab ? null : (tab.icon as RemixiconComponentType);
|
||||||
return (
|
return (
|
||||||
<Tooltip key={tab.id} delayDuration={500}>
|
<Tooltip key={tab.id} delayDuration={500}>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
@@ -715,10 +757,14 @@ export const Header: React.FC = () => {
|
|||||||
className={cn(
|
className={cn(
|
||||||
headerIconButtonClass,
|
headerIconButtonClass,
|
||||||
'relative',
|
'relative',
|
||||||
isActive && 'text-foreground bg-secondary'
|
isActive && 'bg-interactive-selection text-interactive-selection-foreground'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Icon className="h-5 w-5" />
|
{isDiffTab ? (
|
||||||
|
<DiffIcon className="h-5 w-5" />
|
||||||
|
) : Icon ? (
|
||||||
|
<Icon className="h-5 w-5" />
|
||||||
|
) : null}
|
||||||
{tab.badge !== undefined && tab.badge > 0 && (
|
{tab.badge !== undefined && tab.badge > 0 && (
|
||||||
<span className="absolute -top-1 -right-1 text-[10px] font-semibold text-primary">
|
<span className="absolute -top-1 -right-1 text-[10px] font-semibold text-primary">
|
||||||
{tab.badge}
|
{tab.badge}
|
||||||
|
|||||||
@@ -182,14 +182,14 @@ export const Sidebar: React.FC<SidebarProps> = ({ isOpen, isMobile, children })
|
|||||||
<div className="flex-1 overflow-hidden">
|
<div className="flex-1 overflow-hidden">
|
||||||
<ErrorBoundary>{children}</ErrorBoundary>
|
<ErrorBoundary>{children}</ErrorBoundary>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-shrink-0 border-t border-border h-12 px-2 bg-sidebar-accent/10">
|
<div className="flex-shrink-0 border-t border-border h-12 px-2 bg-sidebar">
|
||||||
<div className="flex h-full items-center justify-between gap-2">
|
<div className="flex h-full items-center justify-between gap-2">
|
||||||
<button
|
<button
|
||||||
onClick={() => setSettingsDialogOpen(true)}
|
onClick={() => setSettingsDialogOpen(true)}
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex h-8 items-center gap-2 rounded-md px-2',
|
'flex h-8 items-center gap-2 rounded-md px-2',
|
||||||
'text-sm font-semibold text-sidebar-foreground/90',
|
'text-sm font-semibold text-sidebar-foreground/90',
|
||||||
'hover:text-sidebar-foreground hover:bg-sidebar-accent',
|
'hover:text-sidebar-foreground hover:bg-interactive-hover',
|
||||||
'transition-all duration-200'
|
'transition-all duration-200'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -219,7 +219,7 @@ export const Sidebar: React.FC<SidebarProps> = ({ isOpen, isMobile, children })
|
|||||||
className={cn(
|
className={cn(
|
||||||
'flex h-8 w-8 items-center justify-center rounded-md',
|
'flex h-8 w-8 items-center justify-center rounded-md',
|
||||||
'text-sidebar-foreground/70',
|
'text-sidebar-foreground/70',
|
||||||
'hover:text-sidebar-foreground hover:bg-sidebar-accent',
|
'hover:text-sidebar-foreground hover:bg-interactive-hover',
|
||||||
'transition-all duration-200'
|
'transition-all duration-200'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -127,7 +127,7 @@ export const McpDropdown: React.FC<McpDropdownProps> = ({ headerIconButtonClass
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={serverName}
|
key={serverName}
|
||||||
className="flex items-center justify-between gap-2 px-2 py-1.5 rounded-lg hover:bg-muted/50"
|
className="flex items-center justify-between gap-2 px-2 py-1.5 rounded-lg hover:bg-interactive-hover/50"
|
||||||
>
|
>
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<div className="flex items-center gap-2 min-w-0">
|
<div className="flex items-center gap-2 min-w-0">
|
||||||
@@ -235,7 +235,7 @@ export const McpDropdown: React.FC<McpDropdownProps> = ({ headerIconButtonClass
|
|||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="p-1 rounded-md text-muted-foreground hover:text-foreground hover:bg-muted transition-colors"
|
className="p-1 rounded-md text-muted-foreground hover:text-foreground hover:bg-interactive-hover transition-colors"
|
||||||
disabled={isSpinning}
|
disabled={isSpinning}
|
||||||
onClick={handleRefresh}
|
onClick={handleRefresh}
|
||||||
aria-label="Refresh"
|
aria-label="Refresh"
|
||||||
@@ -281,7 +281,7 @@ export const McpDropdown: React.FC<McpDropdownProps> = ({ headerIconButtonClass
|
|||||||
<span className="typography-ui-label font-semibold">MCP Servers</span>
|
<span className="typography-ui-label font-semibold">MCP Servers</span>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="p-1 rounded-md text-muted-foreground hover:text-foreground hover:bg-muted transition-colors"
|
className="p-1 rounded-md text-muted-foreground hover:text-foreground hover:bg-interactive-hover transition-colors"
|
||||||
disabled={isSpinning}
|
disabled={isSpinning}
|
||||||
onClick={handleRefresh}
|
onClick={handleRefresh}
|
||||||
aria-label="Refresh"
|
aria-label="Refresh"
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ export const ModelChip: React.FC<{
|
|||||||
const label = totalSameModel > 1 ? `${displayName} (${instanceIndex})` : displayName;
|
const label = totalSameModel > 1 ? `${displayName} (${instanceIndex})` : displayName;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn('flex items-center gap-1.5 px-2 rounded-md bg-accent/50 border border-border/30', CHIP_HEIGHT_CLASS)}>
|
<div className={cn('flex items-center gap-1.5 px-2 rounded-md bg-interactive-selection/20 border border-border/30', CHIP_HEIGHT_CLASS)}>
|
||||||
<ProviderLogo providerId={model.providerID} className="h-3.5 w-3.5" />
|
<ProviderLogo providerId={model.providerID} className="h-3.5 w-3.5" />
|
||||||
<span className="typography-meta font-medium truncate max-w-[140px]">
|
<span className="typography-meta font-medium truncate max-w-[140px]">
|
||||||
{label}
|
{label}
|
||||||
@@ -273,7 +273,7 @@ export const ModelMultiSelect: React.FC<ModelMultiSelectProps> = ({
|
|||||||
onMouseEnter={() => setSelectedIndex(flatIndex)}
|
onMouseEnter={() => setSelectedIndex(flatIndex)}
|
||||||
className={cn(
|
className={cn(
|
||||||
'w-full text-left px-2 py-1.5 rounded-md typography-meta transition-colors flex items-center gap-2',
|
'w-full text-left px-2 py-1.5 rounded-md typography-meta transition-colors flex items-center gap-2',
|
||||||
isHighlighted ? 'bg-accent' : 'hover:bg-accent/50'
|
isHighlighted ? 'bg-interactive-selection' : 'hover:bg-interactive-hover/50'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-1.5 flex-1 min-w-0">
|
<div className="flex items-center gap-1.5 flex-1 min-w-0">
|
||||||
@@ -379,7 +379,7 @@ export const ModelMultiSelect: React.FC<ModelMultiSelectProps> = ({
|
|||||||
let currentFlatIndex = 0;
|
let currentFlatIndex = 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="absolute bottom-full left-0 mb-1 z-50 border border-border/30 rounded-xl overflow-hidden bg-background shadow-lg w-[min(380px,calc(100vw-2rem))] flex flex-col">
|
<div style={{ backgroundColor: 'var(--surface-elevated)' }} className="absolute bottom-full left-0 mb-1 z-50 border border-border/30 rounded-xl overflow-hidden shadow-lg w-[min(380px,calc(100vw-2rem))] flex flex-col">
|
||||||
{/* Search input */}
|
{/* Search input */}
|
||||||
<div className="p-2 border-b border-border/40">
|
<div className="p-2 border-b border-border/40">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
@@ -411,7 +411,10 @@ export const ModelMultiSelect: React.FC<ModelMultiSelectProps> = ({
|
|||||||
{/* Favorites Section */}
|
{/* Favorites Section */}
|
||||||
{filteredFavorites.length > 0 && (
|
{filteredFavorites.length > 0 && (
|
||||||
<>
|
<>
|
||||||
<div className="typography-micro font-semibold text-muted-foreground uppercase tracking-wider flex items-center gap-2 -mx-1 px-3 py-1.5 sticky top-0 z-10 bg-background border-b border-border/30">
|
<div
|
||||||
|
style={{ backgroundColor: 'var(--surface-elevated)' }}
|
||||||
|
className="typography-micro font-semibold text-muted-foreground uppercase tracking-wider flex items-center gap-2 -mx-1 px-3 py-1.5 sticky top-0 z-10 border-b border-border/30"
|
||||||
|
>
|
||||||
<RiStarFill className="h-4 w-4 text-primary" />
|
<RiStarFill className="h-4 w-4 text-primary" />
|
||||||
Favorites
|
Favorites
|
||||||
</div>
|
</div>
|
||||||
@@ -426,7 +429,10 @@ export const ModelMultiSelect: React.FC<ModelMultiSelectProps> = ({
|
|||||||
{filteredRecents.length > 0 && (
|
{filteredRecents.length > 0 && (
|
||||||
<>
|
<>
|
||||||
{filteredFavorites.length > 0 && <div className="h-px bg-border/40 my-1" />}
|
{filteredFavorites.length > 0 && <div className="h-px bg-border/40 my-1" />}
|
||||||
<div className="typography-micro font-semibold text-muted-foreground uppercase tracking-wider flex items-center gap-2 -mx-1 px-3 py-1.5 sticky top-0 z-10 bg-background border-b border-border/30">
|
<div
|
||||||
|
style={{ backgroundColor: 'var(--surface-elevated)' }}
|
||||||
|
className="typography-micro font-semibold text-muted-foreground uppercase tracking-wider flex items-center gap-2 -mx-1 px-3 py-1.5 sticky top-0 z-10 border-b border-border/30"
|
||||||
|
>
|
||||||
<RiTimeLine className="h-4 w-4" />
|
<RiTimeLine className="h-4 w-4" />
|
||||||
Recent
|
Recent
|
||||||
</div>
|
</div>
|
||||||
@@ -446,7 +452,7 @@ export const ModelMultiSelect: React.FC<ModelMultiSelectProps> = ({
|
|||||||
{filteredProviders.map((provider, index) => (
|
{filteredProviders.map((provider, index) => (
|
||||||
<React.Fragment key={provider.id}>
|
<React.Fragment key={provider.id}>
|
||||||
{index > 0 && <div className="h-px bg-border/40 my-1" />}
|
{index > 0 && <div className="h-px bg-border/40 my-1" />}
|
||||||
<div className="typography-micro font-semibold text-muted-foreground uppercase tracking-wider flex items-center gap-2 -mx-1 px-3 py-1.5 sticky top-0 z-10 bg-background border-b border-border/30">
|
<div style={{ backgroundColor: 'var(--surface-elevated)' }} className="typography-micro font-semibold text-muted-foreground uppercase tracking-wider flex items-center gap-2 -mx-1 px-3 py-1.5 sticky top-0 z-10 border-b border-border/30">
|
||||||
<ProviderLogo
|
<ProviderLogo
|
||||||
providerId={provider.id}
|
providerId={provider.id}
|
||||||
className="h-4 w-4 flex-shrink-0"
|
className="h-4 w-4 flex-shrink-0"
|
||||||
@@ -507,7 +513,7 @@ export const ModelMultiSelect: React.FC<ModelMultiSelectProps> = ({
|
|||||||
onUpdate(index, { ...model, variant: nextVariant });
|
onUpdate(index, { ...model, variant: nextVariant });
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SelectTrigger size="chip" className="px-2 gap-1.5 rounded-md bg-accent/50 border-border/30 hover:bg-accent/60 typography-meta font-medium text-foreground">
|
<SelectTrigger size="chip" className="px-2 gap-1.5 rounded-md bg-interactive-selection/20 border-border/30 hover:bg-interactive-hover/30 typography-meta font-medium text-foreground">
|
||||||
<RiBrainAi3Line
|
<RiBrainAi3Line
|
||||||
className={cn(
|
className={cn(
|
||||||
'h-3.5 w-3.5 flex-shrink-0',
|
'h-3.5 w-3.5 flex-shrink-0',
|
||||||
|
|||||||
@@ -334,7 +334,7 @@ export const MultiRunLauncher: React.FC<MultiRunLauncherProps> = ({
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={onCancel}
|
onClick={onCancel}
|
||||||
aria-label="Close (Esc)"
|
aria-label="Close (Esc)"
|
||||||
className="inline-flex h-9 w-9 items-center justify-center p-2 text-muted-foreground hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary app-region-no-drag"
|
className="inline-flex h-9 w-9 items-center justify-center p-2 rounded-lg text-muted-foreground hover:text-foreground hover:bg-interactive-hover/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary app-region-no-drag"
|
||||||
>
|
>
|
||||||
<RiCloseLine className="h-5 w-5" />
|
<RiCloseLine className="h-5 w-5" />
|
||||||
</button>
|
</button>
|
||||||
@@ -399,7 +399,7 @@ export const MultiRunLauncher: React.FC<MultiRunLauncherProps> = ({
|
|||||||
|
|
||||||
{/* Setup commands collapsible */}
|
{/* Setup commands collapsible */}
|
||||||
<Collapsible open={isSetupCommandsOpen} onOpenChange={setIsSetupCommandsOpen}>
|
<Collapsible open={isSetupCommandsOpen} onOpenChange={setIsSetupCommandsOpen}>
|
||||||
<CollapsibleTrigger className="w-full flex items-center justify-between py-1 hover:opacity-80 transition-opacity">
|
<CollapsibleTrigger className="w-full flex items-center justify-between py-1 hover:bg-[var(--interactive-hover)] rounded-md px-1 -mx-1 transition-colors">
|
||||||
<p className="typography-ui-label font-medium text-foreground">
|
<p className="typography-ui-label font-medium text-foreground">
|
||||||
Setup commands
|
Setup commands
|
||||||
{setupCommands.filter(cmd => cmd.trim()).length > 0 && (
|
{setupCommands.filter(cmd => cmd.trim()).length > 0 && (
|
||||||
|
|||||||
@@ -6,30 +6,12 @@ interface ThemeProviderProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const ThemeProvider: React.FC<ThemeProviderProps> = ({ children }) => {
|
export const ThemeProvider: React.FC<ThemeProviderProps> = ({ children }) => {
|
||||||
const { theme, applyTheme, fontSize, applyTypography, padding, applyPadding } = useUIStore();
|
const { fontSize, applyTypography, padding, applyPadding } = useUIStore();
|
||||||
|
|
||||||
React.useLayoutEffect(() => {
|
React.useLayoutEffect(() => {
|
||||||
applyTheme();
|
|
||||||
applyTypography();
|
applyTypography();
|
||||||
applyPadding();
|
applyPadding();
|
||||||
}, [theme, applyTheme, fontSize, applyTypography, padding, applyPadding]);
|
}, [fontSize, applyTypography, padding, applyPadding]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
|
|
||||||
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
|
||||||
|
|
||||||
const handleChange = () => {
|
|
||||||
if (theme === 'system') {
|
|
||||||
applyTheme();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
mediaQuery.addEventListener('change', handleChange);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
mediaQuery.removeEventListener('change', handleChange);
|
|
||||||
};
|
|
||||||
}, [theme, applyTheme]);
|
|
||||||
|
|
||||||
return <>{children}</>;
|
return <>{children}</>;
|
||||||
};
|
};
|
||||||
@@ -756,7 +756,7 @@ export const AgentsPage: React.FC = () => {
|
|||||||
const newValue = Math.max(0, current - 0.1);
|
const newValue = Math.max(0, current - 0.1);
|
||||||
setTemperature(parseFloat(newValue.toFixed(1)));
|
setTemperature(parseFloat(newValue.toFixed(1)));
|
||||||
}}
|
}}
|
||||||
className="absolute left-2 top-1/2 -translate-y-1/2 flex items-center justify-center h-6 w-6 rounded hover:bg-accent text-muted-foreground hover:text-foreground"
|
className="absolute left-2 top-1/2 -translate-y-1/2 flex items-center justify-center h-6 w-6 rounded hover:bg-interactive-hover text-muted-foreground hover:text-foreground"
|
||||||
>
|
>
|
||||||
<RiSubtractLine className="h-3.5 w-3.5" />
|
<RiSubtractLine className="h-3.5 w-3.5" />
|
||||||
</button>
|
</button>
|
||||||
@@ -795,7 +795,7 @@ export const AgentsPage: React.FC = () => {
|
|||||||
const newValue = Math.min(2, current + 0.1);
|
const newValue = Math.min(2, current + 0.1);
|
||||||
setTemperature(parseFloat(newValue.toFixed(1)));
|
setTemperature(parseFloat(newValue.toFixed(1)));
|
||||||
}}
|
}}
|
||||||
className="absolute right-2 top-1/2 -translate-y-1/2 flex items-center justify-center h-6 w-6 rounded hover:bg-accent text-muted-foreground hover:text-foreground"
|
className="absolute right-2 top-1/2 -translate-y-1/2 flex items-center justify-center h-6 w-6 rounded hover:bg-interactive-hover text-muted-foreground hover:text-foreground"
|
||||||
>
|
>
|
||||||
<RiAddLine className="h-3.5 w-3.5" />
|
<RiAddLine className="h-3.5 w-3.5" />
|
||||||
</button>
|
</button>
|
||||||
@@ -824,7 +824,7 @@ export const AgentsPage: React.FC = () => {
|
|||||||
const newValue = Math.max(0, current - 0.1);
|
const newValue = Math.max(0, current - 0.1);
|
||||||
setTopP(parseFloat(newValue.toFixed(1)));
|
setTopP(parseFloat(newValue.toFixed(1)));
|
||||||
}}
|
}}
|
||||||
className="absolute left-2 top-1/2 -translate-y-1/2 flex items-center justify-center h-6 w-6 rounded hover:bg-accent text-muted-foreground hover:text-foreground"
|
className="absolute left-2 top-1/2 -translate-y-1/2 flex items-center justify-center h-6 w-6 rounded hover:bg-interactive-hover text-muted-foreground hover:text-foreground"
|
||||||
>
|
>
|
||||||
<RiSubtractLine className="h-3.5 w-3.5" />
|
<RiSubtractLine className="h-3.5 w-3.5" />
|
||||||
</button>
|
</button>
|
||||||
@@ -863,7 +863,7 @@ export const AgentsPage: React.FC = () => {
|
|||||||
const newValue = Math.min(1, current + 0.1);
|
const newValue = Math.min(1, current + 0.1);
|
||||||
setTopP(parseFloat(newValue.toFixed(1)));
|
setTopP(parseFloat(newValue.toFixed(1)));
|
||||||
}}
|
}}
|
||||||
className="absolute right-2 top-1/2 -translate-y-1/2 flex items-center justify-center h-6 w-6 rounded hover:bg-accent text-muted-foreground hover:text-foreground"
|
className="absolute right-2 top-1/2 -translate-y-1/2 flex items-center justify-center h-6 w-6 rounded hover:bg-interactive-hover text-muted-foreground hover:text-foreground"
|
||||||
>
|
>
|
||||||
<RiAddLine className="h-3.5 w-3.5" />
|
<RiAddLine className="h-3.5 w-3.5" />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -405,7 +405,7 @@ export const AgentsSidebar: React.FC<AgentsSidebarProps> = ({ onItemSelect }) =>
|
|||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={() => setRenameDialogAgent(null)}
|
onClick={() => setRenameDialogAgent(null)}
|
||||||
className="text-foreground hover:bg-muted hover:text-foreground"
|
className="text-foreground hover:bg-interactive-hover hover:text-foreground"
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
@@ -446,7 +446,7 @@ const AgentListItem: React.FC<AgentListItemProps> = ({
|
|||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'group relative flex items-center rounded-md px-1.5 py-1 transition-all duration-200',
|
'group relative flex items-center rounded-md px-1.5 py-1 transition-all duration-200',
|
||||||
isSelected ? 'dark:bg-accent/80 bg-primary/12' : 'hover:dark:bg-accent/40 hover:bg-primary/6'
|
isSelected ? 'bg-interactive-selection' : 'hover:bg-interactive-hover'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex min-w-0 flex-1 items-center">
|
<div className="flex min-w-0 flex-1 items-center">
|
||||||
|
|||||||
@@ -161,7 +161,7 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
|
|||||||
ref={(el) => { itemRefs.current[flatIndex] = el; }}
|
ref={(el) => { itemRefs.current[flatIndex] = el; }}
|
||||||
className={cn(
|
className={cn(
|
||||||
"typography-meta group flex items-center gap-2 px-2 py-1.5 rounded-md cursor-pointer",
|
"typography-meta group flex items-center gap-2 px-2 py-1.5 rounded-md cursor-pointer",
|
||||||
isHighlighted ? "bg-accent" : "hover:bg-accent/50"
|
isHighlighted ? "bg-interactive-selection" : "hover:bg-interactive-hover/50"
|
||||||
)}
|
)}
|
||||||
onClick={() => handleProviderAndModelChange(provID, modID)}
|
onClick={() => handleProviderAndModelChange(provID, modID)}
|
||||||
onMouseEnter={() => setSelectedIndex(flatIndex)}
|
onMouseEnter={() => setSelectedIndex(flatIndex)}
|
||||||
@@ -190,8 +190,8 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
|
|||||||
toggleFavoriteModel(provID, modID);
|
toggleFavoriteModel(provID, modID);
|
||||||
}}
|
}}
|
||||||
className={cn(
|
className={cn(
|
||||||
"model-favorite-button flex h-4 w-4 items-center justify-center hover:text-yellow-600",
|
"model-favorite-button flex h-4 w-4 items-center justify-center hover:text-primary/80",
|
||||||
isFavorite ? "text-yellow-500" : "text-muted-foreground"
|
isFavorite ? "text-primary" : "text-muted-foreground"
|
||||||
)}
|
)}
|
||||||
aria-label={isFavorite ? "Unfavorite" : "Favorite"}
|
aria-label={isFavorite ? "Unfavorite" : "Favorite"}
|
||||||
title={isFavorite ? "Remove from favorites" : "Add to favorites"}
|
title={isFavorite ? "Remove from favorites" : "Add to favorites"}
|
||||||
@@ -253,7 +253,7 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
|
|||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
{/* Favorites Section for Mobile */}
|
{/* Favorites Section for Mobile */}
|
||||||
{favoriteModelsList.length > 0 && (
|
{favoriteModelsList.length > 0 && (
|
||||||
<div className="rounded-xl border border-border/40 bg-background/95 mb-2">
|
<div className="rounded-xl border border-border/40 bg-[var(--surface-elevated)] mb-2">
|
||||||
<div className="px-2 py-1.5 text-xs font-semibold text-muted-foreground uppercase tracking-wider">
|
<div className="px-2 py-1.5 text-xs font-semibold text-muted-foreground uppercase tracking-wider">
|
||||||
Favorites
|
Favorites
|
||||||
</div>
|
</div>
|
||||||
@@ -293,7 +293,7 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
toggleFavoriteModel(providerID, modelID);
|
toggleFavoriteModel(providerID, modelID);
|
||||||
}}
|
}}
|
||||||
className="model-favorite-button flex h-8 w-8 items-center justify-center text-yellow-500 hover:text-yellow-600 active:scale-95 touch-manipulation"
|
className="model-favorite-button flex h-8 w-8 items-center justify-center text-primary hover:text-primary/80 active:scale-95 touch-manipulation"
|
||||||
aria-label="Unfavorite"
|
aria-label="Unfavorite"
|
||||||
>
|
>
|
||||||
<RiStarFill className="h-4 w-4" />
|
<RiStarFill className="h-4 w-4" />
|
||||||
@@ -307,7 +307,7 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
|
|||||||
|
|
||||||
{/* Recents Section for Mobile */}
|
{/* Recents Section for Mobile */}
|
||||||
{recentModelsList.length > 0 && (
|
{recentModelsList.length > 0 && (
|
||||||
<div className="rounded-xl border border-border/40 bg-background/95 mb-2">
|
<div className="rounded-xl border border-border/40 bg-[var(--surface-elevated)] mb-2">
|
||||||
<div className="px-2 py-1.5 text-xs font-semibold text-muted-foreground uppercase tracking-wider">
|
<div className="px-2 py-1.5 text-xs font-semibold text-muted-foreground uppercase tracking-wider">
|
||||||
Recents
|
Recents
|
||||||
</div>
|
</div>
|
||||||
@@ -347,7 +347,7 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
toggleFavoriteModel(providerID, modelID);
|
toggleFavoriteModel(providerID, modelID);
|
||||||
}}
|
}}
|
||||||
className="model-favorite-button flex h-8 w-8 items-center justify-center text-muted-foreground/50 hover:text-yellow-600 active:scale-95 touch-manipulation"
|
className="model-favorite-button flex h-8 w-8 items-center justify-center text-muted-foreground/50 hover:text-primary/80 active:scale-95 touch-manipulation"
|
||||||
aria-label="Favorite"
|
aria-label="Favorite"
|
||||||
>
|
>
|
||||||
<RiStarLine className="h-4 w-4" />
|
<RiStarLine className="h-4 w-4" />
|
||||||
@@ -367,7 +367,7 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
|
|||||||
const isExpanded = expandedMobileProviders.has(provider.id);
|
const isExpanded = expandedMobileProviders.has(provider.id);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={provider.id} className="rounded-xl border border-border/40 bg-background/95">
|
<div key={provider.id} className="rounded-xl border border-border/40 bg-[var(--surface-elevated)]">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="flex w-full items-center justify-between gap-1.5 px-2 py-1.5 text-left"
|
className="flex w-full items-center justify-between gap-1.5 px-2 py-1.5 text-left"
|
||||||
@@ -425,9 +425,9 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
|
|||||||
toggleFavoriteModel(provider.id as string, modelItem.id as string);
|
toggleFavoriteModel(provider.id as string, modelItem.id as string);
|
||||||
}}
|
}}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex h-8 w-8 items-center justify-center active:scale-95 touch-manipulation",
|
"flex h-8 w-8 items-center justify-center active:scale-95 touch-manipulation hover:text-primary/80",
|
||||||
isFavoriteModel(provider.id as string, modelItem.id as string)
|
isFavoriteModel(provider.id as string, modelItem.id as string)
|
||||||
? "text-yellow-500"
|
? "text-primary"
|
||||||
: "text-muted-foreground/50"
|
: "text-muted-foreground/50"
|
||||||
)}
|
)}
|
||||||
aria-label={isFavoriteModel(provider.id as string, modelItem.id as string) ? "Unfavorite" : "Favorite"}
|
aria-label={isFavoriteModel(provider.id as string, modelItem.id as string) ? "Unfavorite" : "Favorite"}
|
||||||
@@ -454,7 +454,7 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="flex w-full items-center justify-between rounded-lg border border-border/40 bg-background/95 px-2 py-1.5 text-left"
|
className="flex w-full items-center justify-between rounded-lg border border-border/40 bg-[var(--surface-elevated)] px-2 py-1.5 text-left"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
handleProviderAndModelChange('', '');
|
handleProviderAndModelChange('', '');
|
||||||
closeMobilePanel();
|
closeMobilePanel();
|
||||||
@@ -474,7 +474,7 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => setIsMobilePanelOpen(true)}
|
onClick={() => setIsMobilePanelOpen(true)}
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex w-full items-center justify-between gap-2 rounded-lg border border-border/40 bg-background/95 px-2 py-1.5 text-left',
|
'flex w-full items-center justify-between gap-2 rounded-lg border border-border/40 bg-[var(--surface-elevated)] px-2 py-1.5 text-left',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -497,7 +497,7 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
|
|||||||
<DropdownMenu open={isDropdownOpen} onOpenChange={setIsDropdownOpen}>
|
<DropdownMenu open={isDropdownOpen} onOpenChange={setIsDropdownOpen}>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<div className={cn(
|
<div className={cn(
|
||||||
'flex items-center gap-2 px-2 rounded-lg bg-accent/20 border border-border/20 cursor-pointer hover:bg-accent/30 h-6 w-fit',
|
'flex items-center gap-2 px-2 rounded-lg bg-interactive-selection/20 border border-border/20 cursor-pointer hover:bg-interactive-hover/30 h-6 w-fit',
|
||||||
className
|
className
|
||||||
)}>
|
)}>
|
||||||
{providerId ? (
|
{providerId ? (
|
||||||
@@ -596,7 +596,7 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
|
|||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"typography-meta flex items-center gap-2 px-2 py-1.5 rounded-md cursor-pointer",
|
"typography-meta flex items-center gap-2 px-2 py-1.5 rounded-md cursor-pointer",
|
||||||
"hover:bg-accent/50"
|
"hover:bg-interactive-hover/50"
|
||||||
)}
|
)}
|
||||||
onClick={() => handleProviderAndModelChange('', '')}
|
onClick={() => handleProviderAndModelChange('', '')}
|
||||||
>
|
>
|
||||||
@@ -618,7 +618,7 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
|
|||||||
{/* Favorites Section */}
|
{/* Favorites Section */}
|
||||||
{filteredFavorites.length > 0 && (
|
{filteredFavorites.length > 0 && (
|
||||||
<>
|
<>
|
||||||
<DropdownMenuLabel className="typography-micro font-semibold text-muted-foreground uppercase tracking-wider flex items-center gap-2 -mx-1 px-3 py-1.5 sticky top-0 z-10 bg-background border-b border-border/30">
|
<DropdownMenuLabel style={{ backgroundColor: 'var(--surface-elevated)' }} className="typography-micro font-semibold text-muted-foreground uppercase tracking-wider flex items-center gap-2 -mx-1 px-3 py-1.5 sticky top-0 z-10 border-b border-border/30">
|
||||||
<RiStarFill className="h-4 w-4 text-primary" />
|
<RiStarFill className="h-4 w-4 text-primary" />
|
||||||
Favorites
|
Favorites
|
||||||
</DropdownMenuLabel>
|
</DropdownMenuLabel>
|
||||||
@@ -633,7 +633,7 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
|
|||||||
{filteredRecents.length > 0 && (
|
{filteredRecents.length > 0 && (
|
||||||
<>
|
<>
|
||||||
{filteredFavorites.length > 0 && <DropdownMenuSeparator />}
|
{filteredFavorites.length > 0 && <DropdownMenuSeparator />}
|
||||||
<DropdownMenuLabel className="typography-micro font-semibold text-muted-foreground uppercase tracking-wider flex items-center gap-2 -mx-1 px-3 py-1.5 sticky top-0 z-10 bg-background border-b border-border/30">
|
<DropdownMenuLabel style={{ backgroundColor: 'var(--surface-elevated)' }} className="typography-micro font-semibold text-muted-foreground uppercase tracking-wider flex items-center gap-2 -mx-1 px-3 py-1.5 sticky top-0 z-10 border-b border-border/30">
|
||||||
<RiTimeLine className="h-4 w-4" />
|
<RiTimeLine className="h-4 w-4" />
|
||||||
Recent
|
Recent
|
||||||
</DropdownMenuLabel>
|
</DropdownMenuLabel>
|
||||||
@@ -653,7 +653,7 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
|
|||||||
{filteredProviders.map((provider, index) => (
|
{filteredProviders.map((provider, index) => (
|
||||||
<React.Fragment key={provider.id}>
|
<React.Fragment key={provider.id}>
|
||||||
{index > 0 && <DropdownMenuSeparator />}
|
{index > 0 && <DropdownMenuSeparator />}
|
||||||
<DropdownMenuLabel className="typography-micro font-semibold text-muted-foreground uppercase tracking-wider flex items-center gap-2 -mx-1 px-3 py-1.5 sticky top-0 z-10 bg-background border-b border-border/30">
|
<DropdownMenuLabel style={{ backgroundColor: 'var(--surface-elevated)' }} className="typography-micro font-semibold text-muted-foreground uppercase tracking-wider flex items-center gap-2 -mx-1 px-3 py-1.5 sticky top-0 z-10 border-b border-border/30">
|
||||||
<ProviderLogo
|
<ProviderLogo
|
||||||
providerId={provider.id}
|
providerId={provider.id}
|
||||||
className="h-4 w-4 flex-shrink-0"
|
className="h-4 w-4 flex-shrink-0"
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ export const AgentSelector: React.FC<AgentSelectorProps> = ({
|
|||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<div className={cn(
|
<div className={cn(
|
||||||
'flex items-center gap-2 px-2 rounded-lg bg-accent/20 border border-border/20 cursor-pointer hover:bg-accent/30 h-6 w-fit',
|
'flex items-center gap-2 px-2 rounded-lg bg-interactive-selection/20 border border-border/20 cursor-pointer hover:bg-interactive-hover/30 h-6 w-fit',
|
||||||
className
|
className
|
||||||
)}>
|
)}>
|
||||||
<RiRobot2Line className="h-3 w-3 flex-shrink-0 text-muted-foreground" />
|
<RiRobot2Line className="h-3 w-3 flex-shrink-0 text-muted-foreground" />
|
||||||
|
|||||||
@@ -2,10 +2,10 @@ import React from 'react';
|
|||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Textarea } from '@/components/ui/textarea';
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
|
import { Checkbox } from '@/components/ui/checkbox';
|
||||||
import { toast } from '@/components/ui';
|
import { toast } from '@/components/ui';
|
||||||
import { useCommandsStore, type CommandConfig, type CommandScope } from '@/stores/useCommandsStore';
|
import { useCommandsStore, type CommandConfig, type CommandScope } from '@/stores/useCommandsStore';
|
||||||
import { RiCheckLine, RiInformationLine, RiSaveLine, RiTerminalBoxLine, RiUser3Line, RiFolderLine } from '@remixicon/react';
|
import { RiInformationLine, RiSaveLine, RiTerminalBoxLine, RiUser3Line, RiFolderLine } from '@remixicon/react';
|
||||||
import { cn } from '@/lib/utils';
|
|
||||||
import { ModelSelector } from '../agents/ModelSelector';
|
import { ModelSelector } from '../agents/ModelSelector';
|
||||||
import { AgentSelector } from './AgentSelector';
|
import { AgentSelector } from './AgentSelector';
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
||||||
@@ -309,22 +309,10 @@ export const CommandsPage: React.FC = () => {
|
|||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="typography-ui-label font-medium text-foreground flex items-center gap-2 cursor-pointer">
|
<label className="typography-ui-label font-medium text-foreground flex items-center gap-2 cursor-pointer">
|
||||||
<div className="relative">
|
<Checkbox
|
||||||
<input
|
checked={subtask}
|
||||||
type="checkbox"
|
onChange={(checked) => setSubtask(checked)}
|
||||||
checked={subtask}
|
/>
|
||||||
onChange={(e) => setSubtask(e.target.checked)}
|
|
||||||
className="sr-only"
|
|
||||||
/>
|
|
||||||
<div className={cn(
|
|
||||||
"w-5 h-5 rounded border-2 flex items-center justify-center",
|
|
||||||
subtask
|
|
||||||
? "bg-primary border-primary"
|
|
||||||
: "bg-background border-border hover:border-primary/50"
|
|
||||||
)}>
|
|
||||||
{subtask && <RiCheckLine className="w-3 h-3 text-primary-foreground" />}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
Force Subagent Invocation
|
Force Subagent Invocation
|
||||||
</label>
|
</label>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
|
|||||||
@@ -302,7 +302,7 @@ export const CommandsSidebar: React.FC<CommandsSidebarProps> = ({ onItemSelect }
|
|||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={() => setRenameDialogCommand(null)}
|
onClick={() => setRenameDialogCommand(null)}
|
||||||
className="text-foreground hover:bg-muted hover:text-foreground"
|
className="text-foreground hover:bg-interactive-hover hover:text-foreground"
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
@@ -339,7 +339,7 @@ const CommandListItem: React.FC<CommandListItemProps> = ({
|
|||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'group relative flex items-center rounded-md px-1.5 py-1 transition-all duration-200',
|
'group relative flex items-center rounded-md px-1.5 py-1 transition-all duration-200',
|
||||||
isSelected ? 'dark:bg-accent/80 bg-primary/12' : 'hover:dark:bg-accent/40 hover:bg-primary/6'
|
isSelected ? 'bg-interactive-selection' : 'hover:bg-interactive-hover'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex min-w-0 flex-1 items-center">
|
<div className="flex min-w-0 flex-1 items-center">
|
||||||
|
|||||||
@@ -261,7 +261,7 @@ const ProfileListItem: React.FC<ProfileListItemProps> = ({
|
|||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'group relative flex items-center rounded-md px-1.5 py-1 transition-all duration-200',
|
'group relative flex items-center rounded-md px-1.5 py-1 transition-all duration-200',
|
||||||
isSelected ? 'dark:bg-accent/80 bg-primary/12' : 'hover:dark:bg-accent/40 hover:bg-primary/6'
|
isSelected ? 'bg-interactive-selection' : 'hover:bg-interactive-hover'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex min-w-0 flex-1 items-center">
|
<div className="flex min-w-0 flex-1 items-center">
|
||||||
@@ -357,7 +357,7 @@ const DiscoveredCredentialItem: React.FC<DiscoveredCredentialItemProps> = ({
|
|||||||
const isRepoSpecific = credential.host.includes('/');
|
const isRepoSpecific = credential.host.includes('/');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="group relative flex items-center rounded-md px-1.5 py-1 transition-all duration-200 hover:dark:bg-accent/40 hover:bg-primary/6">
|
<div className="group relative flex items-center rounded-md px-1.5 py-1 transition-all duration-200 hover:bg-interactive-hover">
|
||||||
<div className="flex min-w-0 flex-1 items-center">
|
<div className="flex min-w-0 flex-1 items-center">
|
||||||
<div className="flex min-w-0 flex-1 flex-col gap-0">
|
<div className="flex min-w-0 flex-1 flex-col gap-0">
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip
|
|||||||
import { ModelSelector } from '@/components/sections/agents/ModelSelector';
|
import { ModelSelector } from '@/components/sections/agents/ModelSelector';
|
||||||
import { AgentSelector } from '@/components/sections/commands/AgentSelector';
|
import { AgentSelector } from '@/components/sections/commands/AgentSelector';
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||||
|
import { Checkbox } from '@/components/ui/checkbox';
|
||||||
import { updateDesktopSettings } from '@/lib/persistence';
|
import { updateDesktopSettings } from '@/lib/persistence';
|
||||||
import { getDesktopSettings, isDesktopRuntime, isVSCodeRuntime } from '@/lib/desktop';
|
import { getDesktopSettings, isDesktopRuntime, isVSCodeRuntime } from '@/lib/desktop';
|
||||||
import { useConfigStore } from '@/stores/useConfigStore';
|
import { useConfigStore } from '@/stores/useConfigStore';
|
||||||
@@ -317,17 +318,15 @@ export const DefaultsSettings: React.FC = () => {
|
|||||||
{!isVSCode && (
|
{!isVSCode && (
|
||||||
<div className="pt-2">
|
<div className="pt-2">
|
||||||
<label className="flex items-center gap-2 cursor-pointer">
|
<label className="flex items-center gap-2 cursor-pointer">
|
||||||
<input
|
<Checkbox
|
||||||
type="checkbox"
|
|
||||||
className="h-3.5 w-3.5 accent-primary"
|
|
||||||
checked={settingsAutoCreateWorktree}
|
checked={settingsAutoCreateWorktree}
|
||||||
onChange={handleAutoWorktreeChange}
|
onChange={(checked) => handleAutoWorktreeChange({ target: { checked } } as React.ChangeEvent<HTMLInputElement>)}
|
||||||
/>
|
/>
|
||||||
<span className="typography-ui-label text-foreground">
|
<span className="typography-ui-label text-foreground">
|
||||||
Always create worktree for new sessions
|
Always create worktree for new sessions
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
<p className="typography-meta text-muted-foreground pl-5.5 mt-1">
|
<p className="typography-meta text-muted-foreground pl-5 mt-1">
|
||||||
{settingsAutoCreateWorktree
|
{settingsAutoCreateWorktree
|
||||||
? `New session (Worktree): ${getModifierLabel()} + N • New session (Standard): Shift + ${getModifierLabel()} + N`
|
? `New session (Worktree): ${getModifierLabel()} + N • New session (Standard): Shift + ${getModifierLabel()} + N`
|
||||||
: `New session (Standard): ${getModifierLabel()} + N • New session (Worktree): Shift + ${getModifierLabel()} + N`}
|
: `New session (Standard): ${getModifierLabel()} + N • New session (Worktree): Shift + ${getModifierLabel()} + N`}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { RiInformationLine } from '@remixicon/react';
|
import { RiInformationLine } from '@remixicon/react';
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
||||||
|
import { Checkbox } from '@/components/ui/checkbox';
|
||||||
import { updateDesktopSettings } from '@/lib/persistence';
|
import { updateDesktopSettings } from '@/lib/persistence';
|
||||||
import { isDesktopRuntime, getDesktopSettings } from '@/lib/desktop';
|
import { isDesktopRuntime, getDesktopSettings } from '@/lib/desktop';
|
||||||
import { useConfigStore } from '@/stores/useConfigStore';
|
import { useConfigStore } from '@/stores/useConfigStore';
|
||||||
@@ -105,11 +106,9 @@ export const GitSettings: React.FC = () => {
|
|||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<label className="flex items-center gap-2 cursor-pointer">
|
<label className="flex items-center gap-2 cursor-pointer">
|
||||||
<input
|
<Checkbox
|
||||||
type="checkbox"
|
|
||||||
className="h-3.5 w-3.5 accent-primary"
|
|
||||||
checked={settingsGitmojiEnabled}
|
checked={settingsGitmojiEnabled}
|
||||||
onChange={handleGitmojiChange}
|
onChange={(checked) => handleGitmojiChange({ target: { checked } } as React.ChangeEvent<HTMLInputElement>)}
|
||||||
/>
|
/>
|
||||||
<span className="typography-ui-label text-foreground">Enable gitmoji picker</span>
|
<span className="typography-ui-label text-foreground">Enable gitmoji picker</span>
|
||||||
</label>
|
</label>
|
||||||
@@ -128,11 +127,9 @@ export const GitSettings: React.FC = () => {
|
|||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<label className="flex items-center gap-2 cursor-pointer">
|
<label className="flex items-center gap-2 cursor-pointer">
|
||||||
<input
|
<Checkbox
|
||||||
type="checkbox"
|
|
||||||
className="h-3.5 w-3.5 accent-primary"
|
|
||||||
checked={showGitignored}
|
checked={showGitignored}
|
||||||
onChange={(event) => setFilesViewShowGitignored(event.target.checked)}
|
onChange={setFilesViewShowGitignored}
|
||||||
/>
|
/>
|
||||||
<span className="typography-ui-label text-foreground">Display gitignored files</span>
|
<span className="typography-ui-label text-foreground">Display gitignored files</span>
|
||||||
</label>
|
</label>
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ export const OpenChamberSidebar: React.FC<OpenChamberSidebarProps> = ({
|
|||||||
key={group.id}
|
key={group.id}
|
||||||
className={cn(
|
className={cn(
|
||||||
'group relative rounded-md px-1.5 py-1 transition-all duration-200',
|
'group relative rounded-md px-1.5 py-1 transition-all duration-200',
|
||||||
isSelected ? 'dark:bg-accent/80 bg-primary/12' : 'hover:dark:bg-accent/40 hover:bg-primary/6'
|
isSelected ? 'bg-interactive-selection' : 'hover:bg-interactive-hover'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -9,6 +9,14 @@ import { cn, getModifierLabel } from '@/lib/utils';
|
|||||||
import { ButtonSmall } from '@/components/ui/button-small';
|
import { ButtonSmall } from '@/components/ui/button-small';
|
||||||
import { NumberInput } from '@/components/ui/number-input';
|
import { NumberInput } from '@/components/ui/number-input';
|
||||||
import { Switch } from '@/components/ui/switch';
|
import { Switch } from '@/components/ui/switch';
|
||||||
|
import { Checkbox } from '@/components/ui/checkbox';
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '@/components/ui/select';
|
||||||
import { isVSCodeRuntime } from '@/lib/desktop';
|
import { isVSCodeRuntime } from '@/lib/desktop';
|
||||||
import { useDeviceInfo } from '@/lib/device';
|
import { useDeviceInfo } from '@/lib/device';
|
||||||
import {
|
import {
|
||||||
@@ -109,8 +117,46 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
|
|||||||
const {
|
const {
|
||||||
themeMode,
|
themeMode,
|
||||||
setThemeMode,
|
setThemeMode,
|
||||||
|
availableThemes,
|
||||||
|
customThemesLoading,
|
||||||
|
reloadCustomThemes,
|
||||||
|
lightThemeId,
|
||||||
|
darkThemeId,
|
||||||
|
setLightThemePreference,
|
||||||
|
setDarkThemePreference,
|
||||||
} = useThemeSystem();
|
} = useThemeSystem();
|
||||||
|
|
||||||
|
const [themesReloading, setThemesReloading] = React.useState(false);
|
||||||
|
|
||||||
|
const lightThemes = React.useMemo(
|
||||||
|
() => availableThemes
|
||||||
|
.filter((theme) => theme.metadata.variant === 'light')
|
||||||
|
.sort((a, b) => a.metadata.name.localeCompare(b.metadata.name)),
|
||||||
|
[availableThemes],
|
||||||
|
);
|
||||||
|
|
||||||
|
const darkThemes = React.useMemo(
|
||||||
|
() => availableThemes
|
||||||
|
.filter((theme) => theme.metadata.variant === 'dark')
|
||||||
|
.sort((a, b) => a.metadata.name.localeCompare(b.metadata.name)),
|
||||||
|
[availableThemes],
|
||||||
|
);
|
||||||
|
|
||||||
|
const selectedLightTheme = React.useMemo(
|
||||||
|
() => lightThemes.find((theme) => theme.metadata.id === lightThemeId) ?? lightThemes[0],
|
||||||
|
[lightThemes, lightThemeId],
|
||||||
|
);
|
||||||
|
|
||||||
|
const selectedDarkTheme = React.useMemo(
|
||||||
|
() => darkThemes.find((theme) => theme.metadata.id === darkThemeId) ?? darkThemes[0],
|
||||||
|
[darkThemes, darkThemeId],
|
||||||
|
);
|
||||||
|
|
||||||
|
const formatThemeLabel = React.useCallback((themeName: string, variant: 'light' | 'dark') => {
|
||||||
|
const suffix = variant === 'dark' ? ' Dark' : ' Light';
|
||||||
|
return themeName.endsWith(suffix) ? themeName.slice(0, -suffix.length) : themeName;
|
||||||
|
}, []);
|
||||||
|
|
||||||
const shouldShow = (setting: VisibleSetting): boolean => {
|
const shouldShow = (setting: VisibleSetting): boolean => {
|
||||||
if (!visibleSettings) return true;
|
if (!visibleSettings) return true;
|
||||||
return visibleSettings.includes(setting);
|
return visibleSettings.includes(setting);
|
||||||
@@ -138,6 +184,62 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
|
|||||||
</ButtonSmall>
|
</ButtonSmall>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-10">
|
||||||
|
<div className="flex flex-col gap-1.5">
|
||||||
|
<h4 className="typography-ui-label font-medium text-foreground">Light Theme</h4>
|
||||||
|
<Select value={selectedLightTheme?.metadata.id ?? ''} onValueChange={setLightThemePreference}>
|
||||||
|
<SelectTrigger aria-label="Select light theme" className="min-w-32">
|
||||||
|
<SelectValue placeholder="Select theme" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent className="max-h-64 min-w-40">
|
||||||
|
{lightThemes.map((theme) => (
|
||||||
|
<SelectItem key={theme.metadata.id} value={theme.metadata.id}>
|
||||||
|
{formatThemeLabel(theme.metadata.name, 'light')}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-1.5">
|
||||||
|
<h4 className="typography-ui-label font-medium text-foreground">Dark Theme</h4>
|
||||||
|
<Select value={selectedDarkTheme?.metadata.id ?? ''} onValueChange={setDarkThemePreference}>
|
||||||
|
<SelectTrigger aria-label="Select dark theme" className="min-w-32">
|
||||||
|
<SelectValue placeholder="Select theme" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent className="max-h-64 min-w-40">
|
||||||
|
{darkThemes.map((theme) => (
|
||||||
|
<SelectItem key={theme.metadata.id} value={theme.metadata.id}>
|
||||||
|
{formatThemeLabel(theme.metadata.name, 'dark')}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
disabled={customThemesLoading || themesReloading}
|
||||||
|
onClick={async () => {
|
||||||
|
setThemesReloading(true);
|
||||||
|
try {
|
||||||
|
await reloadCustomThemes();
|
||||||
|
} finally {
|
||||||
|
setThemesReloading(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="typography-ui-label text-muted-foreground hover:text-foreground hover:underline underline-offset-2 disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-1.5"
|
||||||
|
>
|
||||||
|
<RiRestartLine className={cn('h-3.5 w-3.5', themesReloading && 'animate-spin')} />
|
||||||
|
Reload custom themes
|
||||||
|
</button>
|
||||||
|
<p className="typography-meta text-muted-foreground/70">
|
||||||
|
Import themes from ~/.config/openchamber/themes/
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -172,7 +274,7 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={() => setFontSize(100)}
|
onClick={() => setFontSize(100)}
|
||||||
disabled={fontSize === 100}
|
disabled={fontSize === 100}
|
||||||
className="h-8 w-8 px-0 border border-border bg-background hover:bg-accent disabled:opacity-100 disabled:bg-background"
|
className="h-8 w-8 px-0 border border-border bg-background hover:bg-interactive-hover disabled:opacity-100 disabled:bg-background"
|
||||||
aria-label="Reset font size"
|
aria-label="Reset font size"
|
||||||
title="Reset"
|
title="Reset"
|
||||||
>
|
>
|
||||||
@@ -213,7 +315,7 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={() => setPadding(100)}
|
onClick={() => setPadding(100)}
|
||||||
disabled={padding === 100}
|
disabled={padding === 100}
|
||||||
className="h-8 w-8 px-0 border border-border bg-background hover:bg-accent disabled:opacity-100 disabled:bg-background"
|
className="h-8 w-8 px-0 border border-border bg-background hover:bg-interactive-hover disabled:opacity-100 disabled:bg-background"
|
||||||
aria-label="Reset spacing"
|
aria-label="Reset spacing"
|
||||||
title="Reset"
|
title="Reset"
|
||||||
>
|
>
|
||||||
@@ -244,7 +346,7 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={() => setPadding(100)}
|
onClick={() => setPadding(100)}
|
||||||
disabled={padding === 100}
|
disabled={padding === 100}
|
||||||
className="h-8 w-8 px-0 border border-border bg-background hover:bg-accent disabled:opacity-100 disabled:bg-background"
|
className="h-8 w-8 px-0 border border-border bg-background hover:bg-interactive-hover disabled:opacity-100 disabled:bg-background"
|
||||||
aria-label="Reset spacing"
|
aria-label="Reset spacing"
|
||||||
title="Reset"
|
title="Reset"
|
||||||
>
|
>
|
||||||
@@ -304,7 +406,7 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={() => setCornerRadius(12)}
|
onClick={() => setCornerRadius(12)}
|
||||||
disabled={cornerRadius === 12}
|
disabled={cornerRadius === 12}
|
||||||
className="h-8 w-8 px-0 border border-border bg-background hover:bg-accent disabled:opacity-100 disabled:bg-background"
|
className="h-8 w-8 px-0 border border-border bg-background hover:bg-interactive-hover disabled:opacity-100 disabled:bg-background"
|
||||||
aria-label="Reset corner radius"
|
aria-label="Reset corner radius"
|
||||||
title="Reset"
|
title="Reset"
|
||||||
>
|
>
|
||||||
@@ -336,7 +438,7 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={() => setCornerRadius(12)}
|
onClick={() => setCornerRadius(12)}
|
||||||
disabled={cornerRadius === 12}
|
disabled={cornerRadius === 12}
|
||||||
className="h-8 w-8 px-0 border border-border bg-background hover:bg-accent disabled:opacity-100 disabled:bg-background"
|
className="h-8 w-8 px-0 border border-border bg-background hover:bg-interactive-hover disabled:opacity-100 disabled:bg-background"
|
||||||
aria-label="Reset corner radius"
|
aria-label="Reset corner radius"
|
||||||
title="Reset"
|
title="Reset"
|
||||||
>
|
>
|
||||||
@@ -380,7 +482,7 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={() => setInputBarOffset(0)}
|
onClick={() => setInputBarOffset(0)}
|
||||||
disabled={inputBarOffset === 0}
|
disabled={inputBarOffset === 0}
|
||||||
className="h-8 w-8 px-0 border border-border bg-background hover:bg-accent disabled:opacity-100 disabled:bg-background"
|
className="h-8 w-8 px-0 border border-border bg-background hover:bg-interactive-hover disabled:opacity-100 disabled:bg-background"
|
||||||
aria-label="Reset input bar offset"
|
aria-label="Reset input bar offset"
|
||||||
title="Reset"
|
title="Reset"
|
||||||
>
|
>
|
||||||
@@ -412,7 +514,7 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={() => setInputBarOffset(0)}
|
onClick={() => setInputBarOffset(0)}
|
||||||
disabled={inputBarOffset === 0}
|
disabled={inputBarOffset === 0}
|
||||||
className="h-8 w-8 px-0 border border-border bg-background hover:bg-accent disabled:opacity-100 disabled:bg-background"
|
className="h-8 w-8 px-0 border border-border bg-background hover:bg-interactive-hover disabled:opacity-100 disabled:bg-background"
|
||||||
aria-label="Reset input bar offset"
|
aria-label="Reset input bar offset"
|
||||||
title="Reset"
|
title="Reset"
|
||||||
>
|
>
|
||||||
@@ -540,11 +642,9 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
|
|||||||
|
|
||||||
{shouldShow('reasoning') && (
|
{shouldShow('reasoning') && (
|
||||||
<label className="flex items-center gap-2 cursor-pointer">
|
<label className="flex items-center gap-2 cursor-pointer">
|
||||||
<input
|
<Checkbox
|
||||||
type="checkbox"
|
|
||||||
className="h-3.5 w-3.5 accent-primary"
|
|
||||||
checked={showReasoningTraces}
|
checked={showReasoningTraces}
|
||||||
onChange={(event) => setShowReasoningTraces(event.target.checked)}
|
onChange={setShowReasoningTraces}
|
||||||
/>
|
/>
|
||||||
<span className="typography-ui-header font-semibold text-foreground">
|
<span className="typography-ui-header font-semibold text-foreground">
|
||||||
Show thinking / reasoning traces
|
Show thinking / reasoning traces
|
||||||
@@ -554,11 +654,9 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
|
|||||||
|
|
||||||
{shouldShow('textJustificationActivity') && (
|
{shouldShow('textJustificationActivity') && (
|
||||||
<label className="flex items-center gap-2 cursor-pointer">
|
<label className="flex items-center gap-2 cursor-pointer">
|
||||||
<input
|
<Checkbox
|
||||||
type="checkbox"
|
|
||||||
className="h-3.5 w-3.5 accent-primary"
|
|
||||||
checked={showTextJustificationActivity}
|
checked={showTextJustificationActivity}
|
||||||
onChange={(event) => setShowTextJustificationActivity(event.target.checked)}
|
onChange={setShowTextJustificationActivity}
|
||||||
/>
|
/>
|
||||||
<span className="typography-ui-header font-semibold text-foreground">
|
<span className="typography-ui-header font-semibold text-foreground">
|
||||||
Show text justification in activity
|
Show text justification in activity
|
||||||
@@ -569,11 +667,9 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
|
|||||||
{shouldShow('queueMode') && (
|
{shouldShow('queueMode') && (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="flex items-center gap-2 cursor-pointer">
|
<label className="flex items-center gap-2 cursor-pointer">
|
||||||
<input
|
<Checkbox
|
||||||
type="checkbox"
|
|
||||||
className="h-3.5 w-3.5 accent-primary"
|
|
||||||
checked={queueModeEnabled}
|
checked={queueModeEnabled}
|
||||||
onChange={(event) => setQueueMode(event.target.checked)}
|
onChange={setQueueMode}
|
||||||
/>
|
/>
|
||||||
<span className="typography-ui-header font-semibold text-foreground">
|
<span className="typography-ui-header font-semibold text-foreground">
|
||||||
Queue messages by default
|
Queue messages by default
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { RiInformationLine } from '@remixicon/react';
|
|||||||
import { NumberInput } from '@/components/ui/number-input';
|
import { NumberInput } from '@/components/ui/number-input';
|
||||||
import { ButtonSmall } from '@/components/ui/button-small';
|
import { ButtonSmall } from '@/components/ui/button-small';
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
||||||
|
import { Checkbox } from '@/components/ui/checkbox';
|
||||||
import { useDeviceInfo } from '@/lib/device';
|
import { useDeviceInfo } from '@/lib/device';
|
||||||
import { useUIStore } from '@/stores/useUIStore';
|
import { useUIStore } from '@/stores/useUIStore';
|
||||||
import { useSessionAutoCleanup } from '@/hooks/useSessionAutoCleanup';
|
import { useSessionAutoCleanup } from '@/hooks/useSessionAutoCleanup';
|
||||||
@@ -60,11 +61,9 @@ export const SessionRetentionSettings: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<label className="flex items-center gap-2 cursor-pointer">
|
<label className="flex items-center gap-2 cursor-pointer">
|
||||||
<input
|
<Checkbox
|
||||||
type="checkbox"
|
|
||||||
className="h-3.5 w-3.5 accent-primary"
|
|
||||||
checked={autoDeleteEnabled}
|
checked={autoDeleteEnabled}
|
||||||
onChange={(event) => setAutoDeleteEnabled(event.target.checked)}
|
onChange={setAutoDeleteEnabled}
|
||||||
/>
|
/>
|
||||||
<span className="typography-ui-header font-semibold text-foreground">Enable auto-cleanup</span>
|
<span className="typography-ui-header font-semibold text-foreground">Enable auto-cleanup</span>
|
||||||
</label>
|
</label>
|
||||||
|
|||||||
@@ -542,7 +542,7 @@ export const ProvidersPage: React.FC = () => {
|
|||||||
type="button"
|
type="button"
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex w-fit items-center justify-between gap-2 rounded-lg border border-input bg-transparent px-3 py-2 typography-ui-label",
|
"flex w-fit items-center justify-between gap-2 rounded-lg border border-input bg-transparent px-3 py-2 typography-ui-label",
|
||||||
"hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
"hover:bg-interactive-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<span className={candidateProviderId ? "text-foreground" : "text-muted-foreground"}>
|
<span className={candidateProviderId ? "text-foreground" : "text-muted-foreground"}>
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ export const ProvidersSidebar: React.FC<ProvidersSidebarProps> = ({ onItemSelect
|
|||||||
key={provider.id}
|
key={provider.id}
|
||||||
className={cn(
|
className={cn(
|
||||||
'group relative flex items-center rounded-md px-1.5 py-1 transition-all duration-200',
|
'group relative flex items-center rounded-md px-1.5 py-1 transition-all duration-200',
|
||||||
isSelected ? 'dark:bg-accent/80 bg-primary/12' : 'hover:dark:bg-accent/40 hover:bg-primary/6'
|
isSelected ? 'bg-interactive-selection' : 'hover:bg-interactive-hover'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -70,8 +70,8 @@ export const SettingsSidebarItem: React.FC<SettingsSidebarItemProps> = ({
|
|||||||
className={cn(
|
className={cn(
|
||||||
'group relative flex items-center rounded-md px-1.5 py-1 transition-all duration-200',
|
'group relative flex items-center rounded-md px-1.5 py-1 transition-all duration-200',
|
||||||
selected
|
selected
|
||||||
? 'dark:bg-accent/80 bg-primary/12'
|
? 'bg-interactive-selection'
|
||||||
: 'hover:dark:bg-accent/40 hover:bg-primary/6',
|
: 'hover:bg-interactive-hover',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -509,7 +509,7 @@ export const SkillsPage: React.FC = () => {
|
|||||||
{filesToShow.map((file) => (
|
{filesToShow.map((file) => (
|
||||||
<div
|
<div
|
||||||
key={file.path}
|
key={file.path}
|
||||||
className="flex items-center justify-between px-3 py-2 rounded-lg border bg-muted/30 hover:bg-muted/50 cursor-pointer transition-colors"
|
className="flex items-center justify-between px-3 py-2 rounded-lg border bg-muted/30 hover:bg-interactive-hover cursor-pointer transition-colors"
|
||||||
onClick={() => handleEditFile(file.path)}
|
onClick={() => handleEditFile(file.path)}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 min-w-0">
|
<div className="flex items-center gap-2 min-w-0">
|
||||||
@@ -608,7 +608,7 @@ export const SkillsPage: React.FC = () => {
|
|||||||
setIsFileDialogOpen(false);
|
setIsFileDialogOpen(false);
|
||||||
setEditingFilePath(null);
|
setEditingFilePath(null);
|
||||||
}}
|
}}
|
||||||
className="text-foreground hover:bg-muted hover:text-foreground"
|
className="text-foreground hover:bg-interactive-hover hover:text-foreground"
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -294,7 +294,7 @@ export const SkillsSidebar: React.FC<SkillsSidebarProps> = ({ onItemSelect }) =>
|
|||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={() => setRenameDialogSkill(null)}
|
onClick={() => setRenameDialogSkill(null)}
|
||||||
className="text-foreground hover:bg-muted hover:text-foreground"
|
className="text-foreground hover:bg-interactive-hover hover:text-foreground"
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
@@ -329,7 +329,7 @@ const SkillListItem: React.FC<SkillListItemProps> = ({
|
|||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'group relative flex items-center rounded-md px-1.5 py-1 transition-all duration-200',
|
'group relative flex items-center rounded-md px-1.5 py-1 transition-all duration-200',
|
||||||
isSelected ? 'dark:bg-accent/80 bg-primary/12' : 'hover:dark:bg-accent/40 hover:bg-primary/6'
|
isSelected ? 'bg-interactive-selection' : 'hover:bg-interactive-hover'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex min-w-0 flex-1 items-center">
|
<div className="flex min-w-0 flex-1 items-center">
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { Button } from '@/components/ui/button';
|
|||||||
import { ButtonLarge } from '@/components/ui/button-large';
|
import { ButtonLarge } from '@/components/ui/button-large';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay';
|
import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay';
|
||||||
|
import { Checkbox } from '@/components/ui/checkbox';
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
@@ -362,16 +363,16 @@ export const InstallFromRepoDialog: React.FC<InstallFromRepoDialogProps> = ({ op
|
|||||||
key={item.skillDir}
|
key={item.skillDir}
|
||||||
className={
|
className={
|
||||||
'flex items-start gap-3 rounded-lg border bg-muted/10 px-3 py-2 cursor-pointer transition-colors ' +
|
'flex items-start gap-3 rounded-lg border bg-muted/10 px-3 py-2 cursor-pointer transition-colors ' +
|
||||||
(disabled ? 'opacity-60 cursor-not-allowed' : 'hover:bg-muted/20')
|
(disabled ? 'opacity-60 cursor-not-allowed' : 'hover:bg-interactive-hover/20')
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<input
|
<div className="mt-1">
|
||||||
type="checkbox"
|
<Checkbox
|
||||||
className="mt-1 h-4 w-4"
|
checked={checked}
|
||||||
checked={checked}
|
disabled={disabled}
|
||||||
disabled={disabled}
|
onChange={(newChecked) => setSelected((prev) => ({ ...prev, [item.skillDir]: newChecked }))}
|
||||||
onChange={(e) => setSelected((prev) => ({ ...prev, [item.skillDir]: e.target.checked }))}
|
/>
|
||||||
/>
|
</div>
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="typography-ui-label truncate">{item.skillName}</div>
|
<div className="typography-ui-label truncate">{item.skillName}</div>
|
||||||
|
|||||||
@@ -236,7 +236,7 @@ export function BranchPickerDialog({ open, onOpenChange, project }: BranchPicker
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={branchName}
|
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" />
|
<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"
|
type="button"
|
||||||
onClick={() => beginRename(branchName)}
|
onClick={() => beginRename(branchName)}
|
||||||
disabled={disableRename}
|
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"
|
aria-label="Rename"
|
||||||
>
|
>
|
||||||
<RiPencilLine className="h-4 w-4" />
|
<RiPencilLine className="h-4 w-4" />
|
||||||
@@ -371,7 +371,7 @@ export function BranchPickerDialog({ open, onOpenChange, project }: BranchPicker
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => void commitRename(branchName)}
|
onClick={() => void commitRename(branchName)}
|
||||||
disabled={isRenaming}
|
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"
|
aria-label="Confirm rename"
|
||||||
>
|
>
|
||||||
{isRenaming ? (
|
{isRenaming ? (
|
||||||
@@ -383,7 +383,7 @@ export function BranchPickerDialog({ open, onOpenChange, project }: BranchPicker
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={cancelRename}
|
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"
|
aria-label="Cancel rename"
|
||||||
>
|
>
|
||||||
<RiCloseLine className="h-4 w-4" />
|
<RiCloseLine className="h-4 w-4" />
|
||||||
@@ -420,7 +420,7 @@ export function BranchPickerDialog({ open, onOpenChange, project }: BranchPicker
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={cancelDelete}
|
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"
|
aria-label="Cancel delete"
|
||||||
>
|
>
|
||||||
<RiCloseLine className="h-4 w-4" />
|
<RiCloseLine className="h-4 w-4" />
|
||||||
|
|||||||
@@ -292,7 +292,7 @@ export const DirectoryAutocomplete = React.forwardRef<DirectoryAutocompleteHandl
|
|||||||
ref={(el) => { itemRefs.current[index] = el; }}
|
ref={(el) => { itemRefs.current[index] = el; }}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex items-center gap-2 px-3 py-1.5 cursor-pointer typography-ui-label",
|
"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(); }}
|
onClick={() => { handleSelectSuggestion(entry); onClose(); }}
|
||||||
onMouseEnter={() => setSelectedIndex(index)}
|
onMouseEnter={() => setSelectedIndex(index)}
|
||||||
|
|||||||
@@ -208,7 +208,7 @@ export const DirectoryExplorerDialog: React.FC<DirectoryExplorerDialogProps> = (
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={toggleShowHidden}
|
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 ? (
|
{showHidden ? (
|
||||||
<RiCheckboxLine className="h-4 w-4 text-primary" />
|
<RiCheckboxLine className="h-4 w-4 text-primary" />
|
||||||
|
|||||||
@@ -626,7 +626,7 @@ export const DirectoryTree: React.FC<DirectoryTreeProps> = ({
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
toggleExpanded(item);
|
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 ? (
|
{isExpanded ? (
|
||||||
<RiArrowDownSLine className={isMobile ? "h-3.5 w-3.5" : "h-3 w-3"} />
|
<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);
|
startCreatingDirectory(item);
|
||||||
}}
|
}}
|
||||||
className={cn(
|
className={cn(
|
||||||
"hover:bg-accent rounded transition-opacity",
|
"hover:bg-interactive-hover rounded transition-opacity",
|
||||||
isMobile ? "p-1.5" : "p-1",
|
isMobile ? "p-1.5" : "p-1",
|
||||||
alwaysShowActions ? "opacity-60" : "opacity-0 group-hover:opacity-100"
|
alwaysShowActions ? "opacity-60" : "opacity-0 group-hover:opacity-100"
|
||||||
)}
|
)}
|
||||||
@@ -696,7 +696,7 @@ export const DirectoryTree: React.FC<DirectoryTreeProps> = ({
|
|||||||
togglePin(item.path);
|
togglePin(item.path);
|
||||||
}}
|
}}
|
||||||
className={cn(
|
className={cn(
|
||||||
"hover:bg-accent rounded transition-opacity",
|
"hover:bg-interactive-hover rounded transition-opacity",
|
||||||
isMobile ? "p-1.5" : "p-1",
|
isMobile ? "p-1.5" : "p-1",
|
||||||
alwaysShowActions ? "opacity-60" : "opacity-0 group-hover:opacity-100"
|
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',
|
isMobile ? 'px-1.5 py-1' : 'px-2 py-1.5',
|
||||||
isSelected
|
isSelected
|
||||||
? 'bg-primary/10 text-primary'
|
? '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` }}
|
style={{ paddingLeft: `${level * (isMobile ? 12 : 14) + (isMobile ? 4 : 6)}px` }}
|
||||||
>
|
>
|
||||||
@@ -751,7 +751,7 @@ export const DirectoryTree: React.FC<DirectoryTreeProps> = ({
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onBlur={createDirectory}
|
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"
|
placeholder="new_directory"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
@@ -760,7 +760,7 @@ export const DirectoryTree: React.FC<DirectoryTreeProps> = ({
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
createDirectory();
|
createDirectory();
|
||||||
}}
|
}}
|
||||||
className="p-1 hover:bg-accent rounded"
|
className="p-1 hover:bg-interactive-hover rounded"
|
||||||
title="Create directory"
|
title="Create directory"
|
||||||
>
|
>
|
||||||
<RiCheckLine className="h-3 w-3 text-green-600" />
|
<RiCheckLine className="h-3 w-3 text-green-600" />
|
||||||
@@ -771,7 +771,7 @@ export const DirectoryTree: React.FC<DirectoryTreeProps> = ({
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
cancelCreatingDirectory();
|
cancelCreatingDirectory();
|
||||||
}}
|
}}
|
||||||
className="p-1 hover:bg-accent rounded"
|
className="p-1 hover:bg-interactive-hover rounded"
|
||||||
title="Cancel"
|
title="Cancel"
|
||||||
>
|
>
|
||||||
<RiCloseLine className="h-3 w-3 text-muted-foreground" />
|
<RiCloseLine className="h-3 w-3 text-muted-foreground" />
|
||||||
@@ -790,7 +790,7 @@ export const DirectoryTree: React.FC<DirectoryTreeProps> = ({
|
|||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex items-center gap-1 cursor-pointer group',
|
'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` }}
|
style={{ paddingLeft: `${level * 12 + 8}px` }}
|
||||||
onSelect={(e) => {
|
onSelect={(e) => {
|
||||||
@@ -803,7 +803,7 @@ export const DirectoryTree: React.FC<DirectoryTreeProps> = ({
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
toggleExpanded(item);
|
toggleExpanded(item);
|
||||||
}}
|
}}
|
||||||
className="p-0.5 hover:bg-accent rounded"
|
className="p-0.5 hover:bg-interactive-hover rounded"
|
||||||
>
|
>
|
||||||
{isExpanded ? (
|
{isExpanded ? (
|
||||||
<RiArrowDownSLine className="h-3 w-3" />
|
<RiArrowDownSLine className="h-3 w-3" />
|
||||||
@@ -840,7 +840,7 @@ export const DirectoryTree: React.FC<DirectoryTreeProps> = ({
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onBlur={createDirectory}
|
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"
|
placeholder="new_directory"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
@@ -849,7 +849,7 @@ export const DirectoryTree: React.FC<DirectoryTreeProps> = ({
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
createDirectory();
|
createDirectory();
|
||||||
}}
|
}}
|
||||||
className="p-1 hover:bg-accent rounded"
|
className="p-1 hover:bg-interactive-hover rounded"
|
||||||
title="Create directory"
|
title="Create directory"
|
||||||
>
|
>
|
||||||
<RiCheckLine className="h-3 w-3 text-green-600" />
|
<RiCheckLine className="h-3 w-3 text-green-600" />
|
||||||
@@ -860,7 +860,7 @@ export const DirectoryTree: React.FC<DirectoryTreeProps> = ({
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
cancelCreatingDirectory();
|
cancelCreatingDirectory();
|
||||||
}}
|
}}
|
||||||
className="p-1 hover:bg-accent rounded"
|
className="p-1 hover:bg-interactive-hover rounded"
|
||||||
title="Cancel"
|
title="Cancel"
|
||||||
>
|
>
|
||||||
<RiCloseLine className="h-3 w-3 text-muted-foreground" />
|
<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',
|
isMobile ? 'px-1.5 py-1' : 'px-2 py-1.5',
|
||||||
isSelected
|
isSelected
|
||||||
? 'bg-primary/10'
|
? 'bg-primary/10'
|
||||||
: 'hover:bg-accent/50'
|
: 'hover:bg-interactive-hover/50'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
@@ -923,7 +923,7 @@ export const DirectoryTree: React.FC<DirectoryTreeProps> = ({
|
|||||||
<button
|
<button
|
||||||
onClick={() => togglePin(path)}
|
onClick={() => togglePin(path)}
|
||||||
className={cn(
|
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"
|
isMobile ? "p-1.5 opacity-60" : "p-1 opacity-0 group-hover:opacity-100"
|
||||||
)}
|
)}
|
||||||
title="Unpin directory"
|
title="Unpin directory"
|
||||||
@@ -946,7 +946,7 @@ export const DirectoryTree: React.FC<DirectoryTreeProps> = ({
|
|||||||
}}
|
}}
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex items-start gap-2 cursor-pointer group py-2',
|
'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" />
|
<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();
|
e.preventDefault();
|
||||||
togglePin(path);
|
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"
|
title="Unpin directory"
|
||||||
>
|
>
|
||||||
<RiPushpin2Line className="h-3 w-3 text-primary" />
|
<RiPushpin2Line className="h-3 w-3 text-primary" />
|
||||||
@@ -985,7 +985,7 @@ export const DirectoryTree: React.FC<DirectoryTreeProps> = ({
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => setIsPinnedExpanded(prev => !prev)}
|
onClick={() => setIsPinnedExpanded(prev => !prev)}
|
||||||
className={cn(
|
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"
|
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 ? (
|
{directNumber && projectDirectory && github && connected ? (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'group flex items-center gap-2 py-1.5 hover:bg-muted/30 rounded transition-colors cursor-pointer',
|
'group flex items-center gap-2 py-1.5 hover:bg-interactive-hover/30 rounded transition-colors cursor-pointer',
|
||||||
startingIssueNumber === directNumber && 'bg-muted/30'
|
startingIssueNumber === directNumber && 'bg-interactive-selection/30'
|
||||||
)}
|
)}
|
||||||
onClick={() => void startSession(directNumber)}
|
onClick={() => void startSession(directNumber)}
|
||||||
>
|
>
|
||||||
@@ -530,8 +530,8 @@ Do not implement changes until I confirm; end with: “Next actions: <1 sentence
|
|||||||
<div
|
<div
|
||||||
key={issue.number}
|
key={issue.number}
|
||||||
className={cn(
|
className={cn(
|
||||||
'group flex items-center gap-2 py-1.5 hover:bg-muted/30 rounded transition-colors cursor-pointer',
|
'group flex items-center gap-2 py-1.5 hover:bg-interactive-hover/30 rounded transition-colors cursor-pointer',
|
||||||
startingIssueNumber === issue.number && 'bg-muted/30'
|
startingIssueNumber === issue.number && 'bg-interactive-selection/30'
|
||||||
)}
|
)}
|
||||||
onClick={() => void startSession(issue.number)}
|
onClick={() => void startSession(issue.number)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -646,8 +646,8 @@ Nice-to-have:
|
|||||||
{directNumber && projectDirectory && github && connected ? (
|
{directNumber && projectDirectory && github && connected ? (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'group flex items-center gap-2 py-1.5 hover:bg-muted/30 rounded transition-colors cursor-pointer',
|
'group flex items-center gap-2 py-1.5 hover:bg-interactive-hover/30 rounded transition-colors cursor-pointer',
|
||||||
startingNumber === directNumber && 'bg-muted/30'
|
startingNumber === directNumber && 'bg-interactive-selection/30'
|
||||||
)}
|
)}
|
||||||
onClick={() => void startSession(directNumber)}
|
onClick={() => void startSession(directNumber)}
|
||||||
>
|
>
|
||||||
@@ -675,10 +675,10 @@ Nice-to-have:
|
|||||||
key={pr.number}
|
key={pr.number}
|
||||||
className={cn(
|
className={cn(
|
||||||
'group flex items-start gap-2 py-1.5 rounded transition-colors',
|
'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
|
disabledByWorktree
|
||||||
? 'opacity-50 cursor-not-allowed'
|
? 'opacity-50 cursor-not-allowed'
|
||||||
: 'hover:bg-muted/30 cursor-pointer'
|
: 'hover:bg-interactive-hover/30 cursor-pointer'
|
||||||
)}
|
)}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (disabledByWorktree) return;
|
if (disabledByWorktree) return;
|
||||||
|
|||||||
@@ -491,7 +491,7 @@ export const SessionDialogs: React.FC = () => {
|
|||||||
{targetWorktree ? formatPathForDisplay(targetWorktree.path, homeDirectory) : 'Worktree path unavailable.'}
|
{targetWorktree ? formatPathForDisplay(targetWorktree.path, homeDirectory) : 'Worktree path unavailable.'}
|
||||||
</p>
|
</p>
|
||||||
{hasDirtyWorktrees && (
|
{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>
|
</div>
|
||||||
|
|||||||
@@ -210,7 +210,7 @@ const SortableProjectItem: React.FC<SortableProjectItemProps> = ({
|
|||||||
? isStuck ? 'var(--sidebar-stuck-bg)' : 'transparent'
|
? isStuck ? 'var(--sidebar-stuck-bg)' : 'transparent'
|
||||||
: undefined,
|
: undefined,
|
||||||
borderColor: isHovered
|
borderColor: isHovered
|
||||||
? 'var(--color-border)'
|
? 'var(--color-border-hover)'
|
||||||
: isCollapsed
|
: isCollapsed
|
||||||
? 'color-mix(in srgb, var(--color-border) 35%, transparent)'
|
? 'color-mix(in srgb, var(--color-border) 35%, transparent)'
|
||||||
: 'var(--color-border)'
|
: 'var(--color-border)'
|
||||||
@@ -320,7 +320,7 @@ const SortableProjectItem: React.FC<SortableProjectItemProps> = ({
|
|||||||
onNewWorktreeSession();
|
onNewWorktreeSession();
|
||||||
}}
|
}}
|
||||||
className={cn(
|
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',
|
mobileVariant ? 'opacity-70' : 'opacity-100',
|
||||||
)}
|
)}
|
||||||
aria-label="New session in worktree"
|
aria-label="New session in worktree"
|
||||||
@@ -342,7 +342,7 @@ const SortableProjectItem: React.FC<SortableProjectItemProps> = ({
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
onNewSession();
|
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"
|
aria-label="New session"
|
||||||
>
|
>
|
||||||
<RiAddLine className="h-4 w-4" />
|
<RiAddLine className="h-4 w-4" />
|
||||||
@@ -1133,7 +1133,7 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
|
|||||||
key={session.id}
|
key={session.id}
|
||||||
className={cn(
|
className={cn(
|
||||||
'group relative flex items-center rounded-md px-1.5 py-1',
|
'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]',
|
depth > 0 && 'pl-[20px]',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -1209,7 +1209,7 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
|
|||||||
const streamingIndicator = (() => {
|
const streamingIndicator = (() => {
|
||||||
if (!memoryState) return null;
|
if (!memoryState) return null;
|
||||||
if (memoryState.isZombie) {
|
if (memoryState.isZombie) {
|
||||||
return <RiErrorWarningLine className="h-4 w-4 text-warning" />;
|
return <RiErrorWarningLine className="h-4 w-4 text-status-warning" />;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
})();
|
})();
|
||||||
@@ -1219,7 +1219,7 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
|
|||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'group relative flex items-center rounded-md px-1.5 py-1',
|
'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' : '',
|
isMissingDirectory ? 'opacity-75' : '',
|
||||||
depth > 0 && 'pl-[20px]',
|
depth > 0 && 'pl-[20px]',
|
||||||
)}
|
)}
|
||||||
@@ -1314,7 +1314,7 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
|
|||||||
</span>
|
</span>
|
||||||
) : null}
|
) : null}
|
||||||
{isMissingDirectory ? (
|
{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" />
|
<RiErrorWarningLine className="h-3 w-3" />
|
||||||
Missing
|
Missing
|
||||||
</span>
|
</span>
|
||||||
@@ -1570,7 +1570,7 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={handleOpenDirectoryDialog}
|
onClick={handleOpenDirectoryDialog}
|
||||||
className={cn(
|
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',
|
!isDesktopRuntime && 'bg-sidebar/60 hover:bg-sidebar',
|
||||||
)}
|
)}
|
||||||
aria-label="Add project"
|
aria-label="Add project"
|
||||||
|
|||||||
@@ -145,6 +145,7 @@ export function CodeMirrorEditor({ value, onChange, extensions, className, readO
|
|||||||
'h-full w-full',
|
'h-full w-full',
|
||||||
'[&_.cm-editor]:h-full [&_.cm-editor]:w-full',
|
'[&_.cm-editor]:h-full [&_.cm-editor]:w-full',
|
||||||
'[&_.cm-scroller]:font-mono [&_.cm-scroller]:text-[var(--text-code)] [&_.cm-scroller]:leading-6',
|
'[&_.cm-scroller]:font-mono [&_.cm-scroller]:text-[var(--text-code)] [&_.cm-scroller]:leading-6',
|
||||||
|
'[&_.cm-lineNumbers]:text-[var(--tools-edit-line-number)]',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ interface ContextUsageDisplayProps {
|
|||||||
outputLimit?: number;
|
outputLimit?: number;
|
||||||
size?: 'default' | 'compact';
|
size?: 'default' | 'compact';
|
||||||
isMobile?: boolean;
|
isMobile?: boolean;
|
||||||
|
hideIcon?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ContextUsageDisplay: React.FC<ContextUsageDisplayProps> = ({
|
export const ContextUsageDisplay: React.FC<ContextUsageDisplayProps> = ({
|
||||||
@@ -20,6 +21,7 @@ export const ContextUsageDisplay: React.FC<ContextUsageDisplayProps> = ({
|
|||||||
outputLimit,
|
outputLimit,
|
||||||
size = 'default',
|
size = 'default',
|
||||||
isMobile = false,
|
isMobile = false,
|
||||||
|
hideIcon = false,
|
||||||
}) => {
|
}) => {
|
||||||
const [mobileTooltipOpen, setMobileTooltipOpen] = React.useState(false);
|
const [mobileTooltipOpen, setMobileTooltipOpen] = React.useState(false);
|
||||||
const longPressTimerRef = React.useRef<NodeJS.Timeout | undefined>(undefined);
|
const longPressTimerRef = React.useRef<NodeJS.Timeout | undefined>(undefined);
|
||||||
@@ -80,7 +82,7 @@ export const ContextUsageDisplay: React.FC<ContextUsageDisplayProps> = ({
|
|||||||
onTouchStart={isMobile ? handleLongPressStart : undefined}
|
onTouchStart={isMobile ? handleLongPressStart : undefined}
|
||||||
onTouchEnd={isMobile ? handleLongPressEnd : undefined}
|
onTouchEnd={isMobile ? handleLongPressEnd : undefined}
|
||||||
>
|
>
|
||||||
{!isMobile && <RiDonutChartLine className="h-4 w-4 flex-shrink-0" />}
|
{!isMobile && !hideIcon && <RiDonutChartLine className="h-4 w-4 flex-shrink-0" />}
|
||||||
<span className={cn(getPercentageColor(percentage), 'font-medium')}>
|
<span className={cn(getPercentageColor(percentage), 'font-medium')}>
|
||||||
{Math.min(percentage, 999).toFixed(1)}%
|
{Math.min(percentage, 999).toFixed(1)}%
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ export class ErrorBoundary extends React.Component<ErrorBoundaryProps, ErrorBoun
|
|||||||
|
|
||||||
{this.state.error && (
|
{this.state.error && (
|
||||||
<details className="text-xs font-mono bg-muted p-3 rounded">
|
<details className="text-xs font-mono bg-muted p-3 rounded">
|
||||||
<summary className="cursor-pointer hover:bg-muted/80">Error details</summary>
|
<summary className="cursor-pointer hover:bg-interactive-hover/80">Error details</summary>
|
||||||
<pre className="mt-2 overflow-x-auto">
|
<pre className="mt-2 overflow-x-auto">
|
||||||
{this.state.error.toString()}
|
{this.state.error.toString()}
|
||||||
</pre>
|
</pre>
|
||||||
|
|||||||
@@ -134,12 +134,12 @@ export const MemoryDebugPanel: React.FC<MemoryDebugPanelProps> = ({ onClose }) =
|
|||||||
<RiPulseLine className="h-3 w-3 text-primary animate-pulse" />
|
<RiPulseLine className="h-3 w-3 text-primary animate-pulse" />
|
||||||
)}
|
)}
|
||||||
{stat.isZombie && (
|
{stat.isZombie && (
|
||||||
<span className="text-warning">!</span>
|
<span className="text-status-warning">!</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className={`font-mono ${
|
<span className={`font-mono ${
|
||||||
stat.messageCount > MEMORY_LIMITS.VIEWPORT_MESSAGES ? 'text-warning' : ''
|
stat.messageCount > MEMORY_LIMITS.VIEWPORT_MESSAGES ? 'text-status-warning' : ''
|
||||||
}`}>
|
}`}>
|
||||||
{stat.messageCount} msgs
|
{stat.messageCount} msgs
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ export const MobileOverlayPanel: React.FC<MobileOverlayPanelProps> = ({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
className="flex h-8 w-8 items-center justify-center rounded-lg text-muted-foreground hover:bg-accent"
|
className="flex h-8 w-8 items-center justify-center rounded-lg text-muted-foreground hover:bg-interactive-hover"
|
||||||
>
|
>
|
||||||
<RiCloseLine className="h-4 w-4" />
|
<RiCloseLine className="h-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -78,10 +78,37 @@ export const OpenChamberLogo: React.FC<OpenChamberLogoProps> = ({
|
|||||||
isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||||
}
|
}
|
||||||
|
|
||||||
const strokeColor = isDark ? 'white' : 'black';
|
const strokeColor = useMemo(() => {
|
||||||
const fillColor = isDark ? 'rgba(255,255,255,0.15)' : 'rgba(0,0,0,0.15)';
|
if (typeof window !== 'undefined') {
|
||||||
const logoFillColor = isDark ? 'white' : 'black';
|
const fromVars = getComputedStyle(document.documentElement).getPropertyValue('--splash-stroke').trim();
|
||||||
const cellHighlightColor = isDark ? 'rgba(255,255,255,0.35)' : 'rgba(0,0,0,0.4)';
|
if (fromVars) {
|
||||||
|
return fromVars;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return isDark ? 'white' : 'black';
|
||||||
|
}, [isDark]);
|
||||||
|
|
||||||
|
const fillColor = useMemo(() => {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
const fromVars = getComputedStyle(document.documentElement).getPropertyValue('--splash-face-fill').trim();
|
||||||
|
if (fromVars) {
|
||||||
|
return fromVars;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return isDark ? 'rgba(255,255,255,0.15)' : 'rgba(0,0,0,0.15)';
|
||||||
|
}, [isDark]);
|
||||||
|
|
||||||
|
const cellHighlightColor = useMemo(() => {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
const fromVars = getComputedStyle(document.documentElement).getPropertyValue('--splash-cell-fill').trim();
|
||||||
|
if (fromVars) {
|
||||||
|
return fromVars;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return isDark ? 'rgba(255,255,255,0.35)' : 'rgba(0,0,0,0.4)';
|
||||||
|
}, [isDark]);
|
||||||
|
|
||||||
|
const logoFillColor = strokeColor;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -360,7 +360,7 @@ export const UpdateDialog: React.FC<UpdateDialogProps> = ({
|
|||||||
onClick={handleCopyCommand}
|
onClick={handleCopyCommand}
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex items-center justify-center p-2 rounded-md',
|
'flex items-center justify-center p-2 rounded-md',
|
||||||
'text-muted-foreground hover:text-foreground hover:bg-accent',
|
'text-muted-foreground hover:text-foreground hover:bg-interactive-hover',
|
||||||
'transition-colors',
|
'transition-colors',
|
||||||
copied && 'text-primary'
|
copied && 'text-primary'
|
||||||
)}
|
)}
|
||||||
@@ -406,7 +406,7 @@ export const UpdateDialog: React.FC<UpdateDialogProps> = ({
|
|||||||
className={cn(
|
className={cn(
|
||||||
'flex items-center justify-center gap-1.5 px-3 py-1.5 rounded-md',
|
'flex items-center justify-center gap-1.5 px-3 py-1.5 rounded-md',
|
||||||
'text-sm text-muted-foreground',
|
'text-sm text-muted-foreground',
|
||||||
'hover:text-foreground hover:bg-accent',
|
'hover:text-foreground hover:bg-interactive-hover',
|
||||||
'transition-colors'
|
'transition-colors'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ export function AnimatedTabs<T extends string>({
|
|||||||
animate ? '[transition:clip-path_200ms_ease]' : null
|
animate ? '[transition:clip-path_200ms_ease]' : null
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex h-9 items-center gap-1 rounded-lg bg-accent px-1.5 text-accent-foreground">
|
<div className="flex h-9 items-center gap-1 rounded-lg bg-interactive-selection px-1.5 text-interactive-selection-foreground">
|
||||||
{tabs.map((tab) => {
|
{tabs.map((tab) => {
|
||||||
const Icon = tab.icon;
|
const Icon = tab.icon;
|
||||||
return (
|
return (
|
||||||
@@ -101,7 +101,7 @@ export function AnimatedTabs<T extends string>({
|
|||||||
className={cn(
|
className={cn(
|
||||||
'flex h-7 flex-1 items-center justify-center gap-1.25 rounded-lg px-2.5 text-sm font-semibold transition-colors duration-150',
|
'flex h-7 flex-1 items-center justify-center gap-1.25 rounded-lg px-2.5 text-sm font-semibold transition-colors duration-150',
|
||||||
isActive ? 'text-accent-foreground' : 'text-muted-foreground',
|
isActive ? 'text-accent-foreground' : 'text-muted-foreground',
|
||||||
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-1 focus-visible:ring-offset-background'
|
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--interactive-focus-ring)] focus-visible:ring-offset-1 focus-visible:ring-offset-background'
|
||||||
)}
|
)}
|
||||||
aria-pressed={isActive}
|
aria-pressed={isActive}
|
||||||
aria-disabled={!isInteractive}
|
aria-disabled={!isInteractive}
|
||||||
|
|||||||
@@ -15,11 +15,11 @@ const buttonVariants = cva(
|
|||||||
destructive:
|
destructive:
|
||||||
"bg-destructive text-white shadow-none hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
"bg-destructive text-white shadow-none hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||||
outline:
|
outline:
|
||||||
"border bg-background shadow-none hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
|
"border bg-background shadow-none hover:bg-interactive-hover hover:text-foreground",
|
||||||
secondary:
|
secondary:
|
||||||
"bg-secondary text-secondary-foreground shadow-none hover:bg-secondary/80",
|
"bg-interactive-hover text-foreground shadow-none hover:bg-interactive-active",
|
||||||
ghost:
|
ghost:
|
||||||
"text-foreground hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
"text-foreground hover:bg-interactive-hover hover:text-foreground",
|
||||||
link: "text-primary underline-offset-4 hover:underline",
|
link: "text-primary underline-offset-4 hover:underline",
|
||||||
},
|
},
|
||||||
size: {
|
size: {
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { RiCheckboxBlankLine, RiCheckboxLine } from '@remixicon/react';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
|
interface CheckboxProps {
|
||||||
|
checked: boolean;
|
||||||
|
onChange: (checked: boolean) => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
ariaLabel?: string;
|
||||||
|
className?: string;
|
||||||
|
iconClassName?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Checkbox = React.memo<CheckboxProps>(function Checkbox({
|
||||||
|
checked,
|
||||||
|
onChange,
|
||||||
|
disabled = false,
|
||||||
|
ariaLabel,
|
||||||
|
className,
|
||||||
|
iconClassName,
|
||||||
|
}) {
|
||||||
|
const handleClick = React.useCallback(
|
||||||
|
(event: React.MouseEvent) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
if (!disabled) {
|
||||||
|
onChange(!checked);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[checked, disabled, onChange]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleKeyDown = React.useCallback(
|
||||||
|
(event: React.KeyboardEvent) => {
|
||||||
|
if (event.key === ' ' || event.key === 'Enter') {
|
||||||
|
event.preventDefault();
|
||||||
|
if (!disabled) {
|
||||||
|
onChange(!checked);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[checked, disabled, onChange]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleClick}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
disabled={disabled}
|
||||||
|
aria-pressed={checked}
|
||||||
|
aria-label={ariaLabel}
|
||||||
|
className={cn(
|
||||||
|
'flex size-5 shrink-0 items-center justify-center rounded',
|
||||||
|
'text-muted-foreground hover:text-foreground',
|
||||||
|
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary',
|
||||||
|
disabled && 'cursor-not-allowed opacity-50',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{checked ? (
|
||||||
|
<RiCheckboxLine className={cn('size-4 text-primary', iconClassName)} />
|
||||||
|
) : (
|
||||||
|
<RiCheckboxBlankLine className={cn('size-4', iconClassName)} />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -11,7 +11,7 @@ const CollapsibleTrigger = ({
|
|||||||
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleTrigger>) => (
|
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleTrigger>) => (
|
||||||
<CollapsiblePrimitive.CollapsibleTrigger
|
<CollapsiblePrimitive.CollapsibleTrigger
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex w-full items-center justify-between rounded-lg px-2 py-1.5 text-left text-foreground hover:bg-accent/60 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary",
|
"flex w-full items-center justify-between rounded-lg px-2 py-1.5 text-left text-foreground hover:bg-interactive-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -21,8 +21,12 @@ function Command({
|
|||||||
return (
|
return (
|
||||||
<CommandPrimitive
|
<CommandPrimitive
|
||||||
data-slot="command"
|
data-slot="command"
|
||||||
|
style={{
|
||||||
|
backgroundColor: 'var(--surface-elevated)',
|
||||||
|
color: 'var(--surface-elevated-foreground)',
|
||||||
|
}}
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-background text-foreground flex h-full w-full flex-col overflow-hidden rounded-xl",
|
"flex h-full w-full flex-col overflow-hidden rounded-xl",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@@ -154,7 +158,7 @@ function CommandItem({
|
|||||||
<CommandPrimitive.Item
|
<CommandPrimitive.Item
|
||||||
data-slot="command-item"
|
data-slot="command-item"
|
||||||
className={cn(
|
className={cn(
|
||||||
"data-[selected=true]:bg-muted hover:bg-muted [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-lg px-2 py-1.5 typography-meta outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
"data-[selected=true]:bg-interactive-selection data-[selected=true]:text-interactive-selection-foreground data-[highlighted]:bg-interactive-hover hover:bg-interactive-hover [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-lg px-2 py-1.5 typography-meta outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ function DialogContent({
|
|||||||
{showCloseButton && (
|
{showCloseButton && (
|
||||||
<DialogPrimitive.Close
|
<DialogPrimitive.Close
|
||||||
data-slot="dialog-close"
|
data-slot="dialog-close"
|
||||||
className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-2 right-2 rounded-lg opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none text-muted-foreground hover:text-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
|
className="ring-offset-background focus:ring-ring data-[state=open]:bg-interactive-active data-[state=open]:text-foreground absolute top-2 right-2 rounded-lg opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none text-muted-foreground hover:text-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
|
||||||
>
|
>
|
||||||
<RiCloseLine/>
|
<RiCloseLine/>
|
||||||
<span className="sr-only">Close</span>
|
<span className="sr-only">Close</span>
|
||||||
|
|||||||
@@ -39,8 +39,12 @@ function DropdownMenuContent({
|
|||||||
<DropdownMenuPrimitive.Content
|
<DropdownMenuPrimitive.Content
|
||||||
data-slot="dropdown-menu-content"
|
data-slot="dropdown-menu-content"
|
||||||
sideOffset={sideOffset}
|
sideOffset={sideOffset}
|
||||||
|
style={{
|
||||||
|
backgroundColor: 'var(--surface-elevated)',
|
||||||
|
color: 'var(--surface-elevated-foreground)',
|
||||||
|
}}
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-background text-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 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 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-visible rounded-xl border p-1 shadow-none",
|
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 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 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-visible rounded-xl border-2 border-border/60 p-1 shadow-md",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@@ -72,7 +76,7 @@ function DropdownMenuItem({
|
|||||||
data-inset={inset}
|
data-inset={inset}
|
||||||
data-variant={variant}
|
data-variant={variant}
|
||||||
className={cn(
|
className={cn(
|
||||||
"hover:bg-muted data-[variant=destructive]:text-destructive data-[variant=destructive]:hover:bg-destructive/10 dark:data-[variant=destructive]:hover:bg-destructive/20 data-[variant=destructive]:hover:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-lg px-2 py-1 typography-ui-label outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5",
|
"data-[highlighted]:bg-interactive-hover hover:bg-interactive-hover data-[variant=destructive]:text-destructive data-[variant=destructive]:hover:bg-destructive/10 dark:data-[variant=destructive]:hover:bg-destructive/20 data-[variant=destructive]:hover:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-lg px-2 py-1 typography-ui-label outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@@ -90,7 +94,7 @@ function DropdownMenuCheckboxItem({
|
|||||||
<DropdownMenuPrimitive.CheckboxItem
|
<DropdownMenuPrimitive.CheckboxItem
|
||||||
data-slot="dropdown-menu-checkbox-item"
|
data-slot="dropdown-menu-checkbox-item"
|
||||||
className={cn(
|
className={cn(
|
||||||
"hover:bg-muted relative flex cursor-default items-center gap-2 rounded-lg py-1 px-2 typography-ui-label outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5",
|
"data-[highlighted]:bg-interactive-hover hover:bg-interactive-hover data-[state=checked]:bg-interactive-selection data-[state=checked]:text-interactive-selection-foreground relative flex cursor-default items-center gap-2 rounded-lg py-1 px-2 typography-ui-label outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
checked={checked}
|
checked={checked}
|
||||||
@@ -126,7 +130,7 @@ function DropdownMenuRadioItem({
|
|||||||
<DropdownMenuPrimitive.RadioItem
|
<DropdownMenuPrimitive.RadioItem
|
||||||
data-slot="dropdown-menu-radio-item"
|
data-slot="dropdown-menu-radio-item"
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative flex cursor-default items-start gap-2 rounded-lg py-1 pl-2 pr-8 typography-ui-label outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 hover:bg-muted [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5",
|
"data-[highlighted]:bg-interactive-hover hover:bg-interactive-hover data-[state=checked]:bg-interactive-selection data-[state=checked]:text-interactive-selection-foreground relative flex cursor-default items-start gap-2 rounded-lg py-1 pl-2 pr-8 typography-ui-label outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@@ -209,7 +213,7 @@ function DropdownMenuSubTrigger({
|
|||||||
data-slot="dropdown-menu-sub-trigger"
|
data-slot="dropdown-menu-sub-trigger"
|
||||||
data-inset={inset}
|
data-inset={inset}
|
||||||
className={cn(
|
className={cn(
|
||||||
"hover:bg-muted flex cursor-default items-center rounded-lg px-2 py-1 typography-ui-label outline-hidden select-none data-[inset]:pl-8",
|
"data-[highlighted]:bg-interactive-hover hover:bg-interactive-hover flex cursor-default items-center rounded-lg px-2 py-1 typography-ui-label outline-hidden select-none data-[inset]:pl-8",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@@ -227,8 +231,12 @@ function DropdownMenuSubContent({
|
|||||||
return (
|
return (
|
||||||
<DropdownMenuPrimitive.SubContent
|
<DropdownMenuPrimitive.SubContent
|
||||||
data-slot="dropdown-menu-sub-content"
|
data-slot="dropdown-menu-sub-content"
|
||||||
|
style={{
|
||||||
|
backgroundColor: 'var(--surface-elevated)',
|
||||||
|
color: 'var(--surface-elevated-foreground)',
|
||||||
|
}}
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-background text-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 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 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-visible rounded-xl border p-1 shadow-none",
|
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 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 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-visible rounded-xl border-2 border-border/60 p-1 shadow-md",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -8,9 +8,10 @@ function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
|||||||
type={type}
|
type={type}
|
||||||
data-slot="input"
|
data-slot="input"
|
||||||
className={cn(
|
className={cn(
|
||||||
"text-foreground border-input file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 appearance-none hover:border-input focus:border-ring flex h-9 w-full min-w-0 rounded-lg border bg-transparent px-3 py-1 typography-markdown shadow-none transition-[color,box-shadow,border-color] outline-none focus-visible:outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:typography-ui-label file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:typography-ui-label",
|
"text-foreground border border-border/80 file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 appearance-none flex h-9 w-full min-w-0 rounded-lg bg-transparent px-3 py-1 typography-markdown outline-none focus-visible:outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:typography-ui-label file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:typography-ui-label",
|
||||||
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
"hover:border-input",
|
||||||
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
"focus:ring-1 focus:ring-primary/50 focus:border-primary/70",
|
||||||
|
"aria-invalid:border-destructive aria-invalid:ring-destructive",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
|
|||||||
@@ -166,7 +166,7 @@ const NumberInput = React.forwardRef<HTMLInputElement, NumberInputProps>(
|
|||||||
onClick={handleIncrement}
|
onClick={handleIncrement}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex flex-1 items-center justify-center",
|
"flex flex-1 items-center justify-center",
|
||||||
"text-muted-foreground hover:bg-accent hover:text-foreground",
|
"text-muted-foreground hover:bg-interactive-hover hover:text-foreground",
|
||||||
"disabled:pointer-events-none disabled:opacity-50"
|
"disabled:pointer-events-none disabled:opacity-50"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -179,7 +179,7 @@ const NumberInput = React.forwardRef<HTMLInputElement, NumberInputProps>(
|
|||||||
onClick={handleDecrement}
|
onClick={handleDecrement}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex flex-1 items-center justify-center border-t border-border",
|
"flex flex-1 items-center justify-center border-t border-border",
|
||||||
"text-muted-foreground hover:bg-accent hover:text-foreground",
|
"text-muted-foreground hover:bg-interactive-hover hover:text-foreground",
|
||||||
"disabled:pointer-events-none disabled:opacity-50"
|
"disabled:pointer-events-none disabled:opacity-50"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ function SelectTrigger({
|
|||||||
data-slot="select-trigger"
|
data-slot="select-trigger"
|
||||||
data-size={size}
|
data-size={size}
|
||||||
className={cn(
|
className={cn(
|
||||||
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-lg border bg-transparent px-2 py-2 typography-ui-label whitespace-nowrap shadow-none outline-none focus-visible:outline-none hover:bg-muted data-[state=open]:bg-muted focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-6 data-[size=sm]:h-6 data-[size=lg]:h-8 data-[size=lg]:py-1.5 data-[size=chip]:h-7 data-[size=chip]:py-1 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive flex w-fit items-center justify-between gap-2 rounded-lg border bg-transparent px-2 py-2 typography-ui-label whitespace-nowrap shadow-none outline-none focus-visible:outline-none hover:bg-interactive-hover data-[state=open]:bg-interactive-active focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-6 data-[size=sm]:h-6 data-[size=lg]:h-8 data-[size=lg]:py-1.5 data-[size=chip]:h-7 data-[size=chip]:py-1 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@@ -64,8 +64,12 @@ function SelectContent({
|
|||||||
<SelectPrimitive.Portal>
|
<SelectPrimitive.Portal>
|
||||||
<SelectPrimitive.Content
|
<SelectPrimitive.Content
|
||||||
data-slot="select-content"
|
data-slot="select-content"
|
||||||
|
style={{
|
||||||
|
backgroundColor: 'var(--surface-elevated)',
|
||||||
|
color: 'var(--surface-elevated-foreground)',
|
||||||
|
}}
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-background text-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 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 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden rounded-xl border shadow-none transform-gpu will-change-transform",
|
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 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 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden rounded-xl border-2 border-border/60 shadow-md transform-gpu will-change-transform",
|
||||||
position === "popper" &&
|
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",
|
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||||
fitContent && "w-max min-w-0",
|
fitContent && "w-max min-w-0",
|
||||||
@@ -117,7 +121,7 @@ function SelectItem({
|
|||||||
<SelectPrimitive.Item
|
<SelectPrimitive.Item
|
||||||
data-slot="select-item"
|
data-slot="select-item"
|
||||||
className={cn(
|
className={cn(
|
||||||
"hover:bg-muted [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-lg py-1.5 pr-8 pl-2 typography-ui-label outline-none select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
|
"data-[highlighted]:bg-interactive-hover hover:bg-interactive-hover data-[state=checked]:bg-interactive-selection data-[state=checked]:text-interactive-selection-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-lg py-1.5 pr-8 pl-2 typography-ui-label outline-none select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ const Switch = React.forwardRef<
|
|||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<SwitchPrimitives.Root
|
<SwitchPrimitives.Root
|
||||||
className={cn(
|
className={cn(
|
||||||
'peer inline-flex shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-muted-foreground/30',
|
'peer inline-flex shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--interactive-focus-ring)] focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-[var(--interactive-border)]',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
style={{ width: '36px', height: '20px', minWidth: '36px', minHeight: '20px' }}
|
style={{ width: '36px', height: '20px', minWidth: '36px', minHeight: '20px' }}
|
||||||
|
|||||||
@@ -3,17 +3,19 @@ import * as React from "react"
|
|||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
import { ScrollableOverlay } from "./ScrollableOverlay"
|
import { ScrollableOverlay } from "./ScrollableOverlay"
|
||||||
|
|
||||||
const Textarea = React.forwardRef<HTMLTextAreaElement, React.ComponentProps<"textarea">>(
|
const Textarea = React.forwardRef<HTMLTextAreaElement, React.ComponentProps<"textarea"> & { outerClassName?: string }>(
|
||||||
({ className, ...props }, ref) => {
|
({ className, outerClassName, ...props }, ref) => {
|
||||||
return (
|
return (
|
||||||
<ScrollableOverlay
|
<ScrollableOverlay
|
||||||
as="textarea"
|
as="textarea"
|
||||||
ref={ref as React.Ref<HTMLTextAreaElement>}
|
ref={ref as React.Ref<HTMLTextAreaElement>}
|
||||||
disableHorizontal
|
disableHorizontal
|
||||||
fillContainer={false}
|
fillContainer={false}
|
||||||
outerClassName="w-full"
|
outerClassName={cn("w-full rounded-lg focus-within:ring-1 focus-within:ring-primary/50", outerClassName)}
|
||||||
className={cn(
|
className={cn(
|
||||||
"text-foreground border-input placeholder:text-muted-foreground appearance-none hover:border-input focus:border-ring focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-lg border bg-transparent px-3 py-2 typography-markdown shadow-none transition-[color,box-shadow,border-color] outline-none focus-visible:outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:typography-ui-label",
|
"text-foreground border border-border/80 placeholder:text-muted-foreground appearance-none dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-lg bg-transparent px-3 py-2 typography-markdown outline-none focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50 md:typography-ui-label",
|
||||||
|
"hover:border-input aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||||
|
"focus:border-primary/70",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
|
|||||||
@@ -6,13 +6,13 @@ import { cva, type VariantProps } from "class-variance-authority"
|
|||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
const toggleVariants = cva(
|
const toggleVariants = cva(
|
||||||
"inline-flex items-center justify-center gap-2 rounded-xl typography-ui-label font-medium hover:bg-muted hover:text-muted-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] outline-none transition-[color,box-shadow] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive whitespace-nowrap",
|
"inline-flex items-center justify-center gap-2 rounded-xl typography-ui-label font-medium hover:bg-interactive-hover hover:text-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-interactive-selection data-[state=on]:text-interactive-selection-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] outline-none transition-[color,box-shadow] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive whitespace-nowrap",
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
default: "bg-transparent",
|
default: "bg-transparent",
|
||||||
outline:
|
outline:
|
||||||
"border border-input bg-transparent shadow-none hover:bg-accent hover:text-accent-foreground",
|
"border border-input bg-transparent shadow-none hover:bg-interactive-hover hover:text-foreground",
|
||||||
},
|
},
|
||||||
size: {
|
size: {
|
||||||
default: "h-9 px-2 min-w-9",
|
default: "h-9 px-2 min-w-9",
|
||||||
|
|||||||
@@ -140,7 +140,7 @@ const FileSelector = React.memo<FileSelectorProps>(({
|
|||||||
return (
|
return (
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<button className="flex h-8 items-center gap-2 rounded-lg border border-input bg-transparent px-2 typography-ui-label text-foreground outline-none hover:bg-accent hover:text-accent-foreground focus-visible:ring-2 focus-visible:ring-ring">
|
<button className="flex h-8 items-center gap-2 rounded-lg border border-input bg-transparent px-2 typography-ui-label text-foreground outline-none hover:bg-interactive-hover hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring">
|
||||||
{selectedFileEntry ? (
|
{selectedFileEntry ? (
|
||||||
<div className="flex min-w-0 items-center gap-3">
|
<div className="flex min-w-0 items-center gap-3">
|
||||||
<span className="min-w-0 flex-1 truncate typography-meta">
|
<span className="min-w-0 flex-1 truncate typography-meta">
|
||||||
@@ -210,7 +210,7 @@ const DiffViewModeSelector = React.memo<DiffViewModeSelectorProps>(({ mode, onMo
|
|||||||
return (
|
return (
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<button className="flex h-8 items-center gap-2 rounded-lg border border-input bg-transparent px-2 typography-ui-label text-foreground outline-none hover:bg-accent hover:text-accent-foreground focus-visible:ring-2 focus-visible:ring-ring">
|
<button className="flex h-8 items-center gap-2 rounded-lg border border-input bg-transparent px-2 typography-ui-label text-foreground outline-none hover:bg-interactive-hover hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring">
|
||||||
<span className="min-w-0 truncate typography-meta">
|
<span className="min-w-0 truncate typography-meta">
|
||||||
{currentOption.label}
|
{currentOption.label}
|
||||||
</span>
|
</span>
|
||||||
@@ -268,8 +268,8 @@ const FileList = React.memo<FileListProps>(({
|
|||||||
className={cn(
|
className={cn(
|
||||||
'flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left transition-colors',
|
'flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left transition-colors',
|
||||||
isActive
|
isActive
|
||||||
? 'bg-accent/70 text-foreground'
|
? 'bg-interactive-selection text-interactive-selection-foreground'
|
||||||
: 'text-muted-foreground hover:bg-accent/40 hover:text-foreground'
|
: 'text-muted-foreground hover:bg-interactive-hover hover:text-foreground'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
@@ -700,13 +700,13 @@ const MultiFileDiffEntry = React.memo<MultiFileDiffEntryProps>(({
|
|||||||
'bg-background hover:bg-background',
|
'bg-background hover:bg-background',
|
||||||
isExpanded ? 'rounded-b-none' : 'rounded-b-xl',
|
isExpanded ? 'rounded-b-none' : 'rounded-b-xl',
|
||||||
isSelected
|
isSelected
|
||||||
? 'text-primary'
|
? 'text-foreground'
|
||||||
: 'text-muted-foreground hover:text-foreground'
|
: 'text-muted-foreground hover:text-foreground'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className={cn(
|
<div className={cn(
|
||||||
'absolute inset-0 pointer-events-none transition-colors',
|
'absolute inset-0 pointer-events-none transition-colors',
|
||||||
isSelected ? 'bg-primary/10' : 'group-hover:bg-accent/40'
|
isSelected ? 'bg-interactive-selection' : 'group-hover:bg-interactive-hover'
|
||||||
)} />
|
)} />
|
||||||
<div className="relative flex min-w-0 flex-1 items-center gap-2">
|
<div className="relative flex min-w-0 flex-1 items-center gap-2">
|
||||||
<span className="flex size-5 items-center justify-center opacity-70 group-hover:opacity-100 transition-opacity">
|
<span className="flex size-5 items-center justify-center opacity-70 group-hover:opacity-100 transition-opacity">
|
||||||
|
|||||||
@@ -226,13 +226,13 @@ const getFileIcon = (extension?: string): React.ReactNode => {
|
|||||||
const ext = extension?.toLowerCase();
|
const ext = extension?.toLowerCase();
|
||||||
|
|
||||||
if (ext && CODE_EXTENSIONS.has(ext)) {
|
if (ext && CODE_EXTENSIONS.has(ext)) {
|
||||||
return <RiCodeLine className="h-4 w-4 flex-shrink-0 text-blue-500" />;
|
return <RiCodeLine className="h-4 w-4 flex-shrink-0 text-[var(--status-info)]" />;
|
||||||
}
|
}
|
||||||
if (ext && DATA_EXTENSIONS.has(ext)) {
|
if (ext && DATA_EXTENSIONS.has(ext)) {
|
||||||
return <RiCodeLine className="h-4 w-4 flex-shrink-0 text-yellow-500" />;
|
return <RiCodeLine className="h-4 w-4 flex-shrink-0 text-[var(--status-warning)]" />;
|
||||||
}
|
}
|
||||||
if (ext && IMAGE_EXTENSIONS.has(ext)) {
|
if (ext && IMAGE_EXTENSIONS.has(ext)) {
|
||||||
return <RiFileImageLine className="h-4 w-4 flex-shrink-0 text-green-500" />;
|
return <RiFileImageLine className="h-4 w-4 flex-shrink-0 text-[var(--status-success)]" />;
|
||||||
}
|
}
|
||||||
if (ext && DOCUMENT_EXTENSIONS.has(ext)) {
|
if (ext && DOCUMENT_EXTENSIONS.has(ext)) {
|
||||||
return <RiFileTextLine className="h-4 w-4 flex-shrink-0 text-muted-foreground" />;
|
return <RiFileTextLine className="h-4 w-4 flex-shrink-0 text-muted-foreground" />;
|
||||||
@@ -1152,7 +1152,7 @@ export const FilesView: React.FC = () => {
|
|||||||
}}
|
}}
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex w-full items-center gap-1.5 rounded-md px-2 py-1 text-left text-foreground transition-colors pr-8 select-none',
|
'flex w-full items-center gap-1.5 rounded-md px-2 py-1 text-left text-foreground transition-colors pr-8 select-none',
|
||||||
isActive ? 'bg-accent/70' : 'hover:bg-accent/40'
|
isActive ? 'bg-interactive-selection/70' : 'hover:bg-interactive-hover/40'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{isDir ? (
|
{isDir ? (
|
||||||
@@ -1398,7 +1398,12 @@ export const FilesView: React.FC = () => {
|
|||||||
className="flex flex-col items-center gap-2 px-4"
|
className="flex flex-col items-center gap-2 px-4"
|
||||||
style={{ width: 'min(100vw - 1rem, 42rem)' }}
|
style={{ width: 'min(100vw - 1rem, 42rem)' }}
|
||||||
>
|
>
|
||||||
<div className="w-full rounded-xl border bg-background flex flex-col relative shadow-lg" style={{ borderColor: 'var(--primary)' }}>
|
<div
|
||||||
|
className="w-full rounded-xl flex flex-col relative shadow-lg border border-border/80 focus-within:border-primary/70 focus-within:ring-1 focus-within:ring-primary/50"
|
||||||
|
style={{
|
||||||
|
backgroundColor: currentTheme?.colors?.surface?.subtle,
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Textarea
|
<Textarea
|
||||||
value={commentText}
|
value={commentText}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
@@ -1410,7 +1415,8 @@ export const FilesView: React.FC = () => {
|
|||||||
textarea.style.height = `${Math.min(textarea.scrollHeight, maxHeight)}px`;
|
textarea.style.height = `${Math.min(textarea.scrollHeight, maxHeight)}px`;
|
||||||
}}
|
}}
|
||||||
placeholder="Type your comment..."
|
placeholder="Type your comment..."
|
||||||
className="min-h-[28px] max-h-[108px] resize-none border-0 px-3 pt-2 pb-1 shadow-none rounded-none appearance-none focus:shadow-none focus-visible:shadow-none focus-visible:border-transparent focus-visible:ring-0 focus-visible:ring-transparent hover:border-transparent bg-transparent dark:bg-transparent focus-visible:outline-none overflow-y-auto"
|
outerClassName="focus-within:ring-0"
|
||||||
|
className="min-h-[28px] max-h-[108px] resize-none border-0 px-3 pt-2 pb-1 rounded-none appearance-none hover:border-transparent bg-transparent dark:bg-transparent overflow-y-auto focus:ring-0 focus:shadow-none"
|
||||||
autoFocus={!isMobile}
|
autoFocus={!isMobile}
|
||||||
rows={1}
|
rows={1}
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
@@ -1777,7 +1783,7 @@ export const FilesView: React.FC = () => {
|
|||||||
) : selectedFile && isMarkdownFile(selectedFile.path) && getMdViewMode() === 'preview' ? (
|
) : selectedFile && isMarkdownFile(selectedFile.path) && getMdViewMode() === 'preview' ? (
|
||||||
<div className="h-full overflow-auto p-3">
|
<div className="h-full overflow-auto p-3">
|
||||||
{fileContent.length > 500 * 1024 && (
|
{fileContent.length > 500 * 1024 && (
|
||||||
<div className="mb-3 rounded-md border border-warning/20 bg-warning/10 px-3 py-2 text-sm text-warning">
|
<div className="mb-3 rounded-md border border-status-warning/20 bg-status-warning/10 px-3 py-2 text-sm text-status-warning">
|
||||||
⚠️ This file is large ({Math.round(fileContent.length / 1024)}KB). Preview may be limited.
|
⚠️ This file is large ({Math.round(fileContent.length / 1024)}KB). Preview may be limited.
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -1973,7 +1979,7 @@ export const FilesView: React.FC = () => {
|
|||||||
onClick={() => void handleSelectFile(node)}
|
onClick={() => void handleSelectFile(node)}
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex w-full items-center gap-1.5 rounded-md px-2 py-1 text-left text-foreground transition-colors',
|
'flex w-full items-center gap-1.5 rounded-md px-2 py-1 text-left text-foreground transition-colors',
|
||||||
isActive ? 'bg-accent/70' : 'hover:bg-accent/40'
|
isActive ? 'bg-interactive-selection/70' : 'hover:bg-interactive-hover/40'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{getFileIcon(node.extension)}
|
{getFileIcon(node.extension)}
|
||||||
@@ -2156,7 +2162,7 @@ export const FilesView: React.FC = () => {
|
|||||||
) : isMarkdownFile(selectedFile.path) && getMdViewMode() === 'preview' ? (
|
) : isMarkdownFile(selectedFile.path) && getMdViewMode() === 'preview' ? (
|
||||||
<div className="h-full overflow-auto p-4">
|
<div className="h-full overflow-auto p-4">
|
||||||
{fileContent.length > 500 * 1024 && (
|
{fileContent.length > 500 * 1024 && (
|
||||||
<div className="mb-3 rounded-md border border-warning/20 bg-warning/10 px-3 py-2 text-sm text-warning">
|
<div className="mb-3 rounded-md border border-status-warning/20 bg-status-warning/10 px-3 py-2 text-sm text-status-warning">
|
||||||
This file is large ({Math.round(fileContent.length / 1024)}KB). Preview may be limited.
|
This file is large ({Math.round(fileContent.length / 1024)}KB). Preview may be limited.
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -2200,7 +2206,7 @@ export const FilesView: React.FC = () => {
|
|||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-1 min-h-0 min-w-0 gap-3 px-3 pb-3 pt-2">
|
<div className="flex flex-1 min-h-0 min-w-0 gap-3 px-3 pb-3 pt-2">
|
||||||
{screenWidth >= 1024 && (
|
{screenWidth >= 700 && (
|
||||||
<div className="w-72 flex-shrink-0 min-h-0 overflow-hidden">
|
<div className="w-72 flex-shrink-0 min-h-0 overflow-hidden">
|
||||||
{treePanel}
|
{treePanel}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import { RiSendPlane2Line } from '@remixicon/react';
|
|||||||
|
|
||||||
import { useOptionalThemeSystem } from '@/contexts/useThemeSystem';
|
import { useOptionalThemeSystem } from '@/contexts/useThemeSystem';
|
||||||
import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay';
|
import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay';
|
||||||
import { ensureFlexokiThemesRegistered } from '@/lib/shiki/registerFlexokiThemes';
|
import { ensurePierreThemeRegistered, getResolvedShikiTheme } from '@/lib/shiki/appThemeRegistry';
|
||||||
import { flexokiThemeNames } from '@/lib/shiki/flexokiThemes';
|
import { getDefaultTheme } from '@/lib/theme/themes';
|
||||||
|
|
||||||
import { toast } from '@/components/ui';
|
import { toast } from '@/components/ui';
|
||||||
import { Textarea } from '@/components/ui/textarea';
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
@@ -65,38 +65,38 @@ const WEBKIT_SCROLL_FIX_CSS = `
|
|||||||
touch-action: manipulation;
|
touch-action: manipulation;
|
||||||
}
|
}
|
||||||
/* Reduce hunk separator height */
|
/* Reduce hunk separator height */
|
||||||
[data-separator-content] {
|
// [data-separator-content] {
|
||||||
height: 24px !important;
|
// height: 24px !important;
|
||||||
}
|
// }
|
||||||
[data-expand-button] {
|
// [data-expand-button] {
|
||||||
height: 24px !important;
|
// height: 24px !important;
|
||||||
width: 24px !important;
|
// width: 24px !important;
|
||||||
}
|
// }
|
||||||
[data-separator-multi-button] {
|
// [data-separator-multi-button] {
|
||||||
row-gap: 0 !important;
|
// row-gap: 0 !important;
|
||||||
}
|
// }
|
||||||
[data-expand-up] {
|
// [data-expand-up] {
|
||||||
height: 12px !important;
|
// height: 12px !important;
|
||||||
min-height: 12px !important;
|
// min-height: 12px !important;
|
||||||
max-height: 12px !important;
|
// max-height: 12px !important;
|
||||||
margin: 0 !important;
|
// margin: 0 !important;
|
||||||
margin-top: 3px !important;
|
// margin-top: 3px !important;
|
||||||
padding: 0 !important;
|
// padding: 0 !important;
|
||||||
border-radius: 4px 4px 0 0 !important;
|
// border-radius: 4px 4px 0 0 !important;
|
||||||
}
|
// }
|
||||||
[data-expand-down] {
|
// [data-expand-down] {
|
||||||
height: 12px !important;
|
// height: 12px !important;
|
||||||
min-height: 12px !important;
|
// min-height: 12px !important;
|
||||||
max-height: 12px !important;
|
// max-height: 12px !important;
|
||||||
margin: 0 !important;
|
// margin: 0 !important;
|
||||||
margin-top: -3px !important;
|
// margin-top: -3px !important;
|
||||||
padding: 0 !important;
|
// padding: 0 !important;
|
||||||
border-radius: 0 0 4px 4px !important;
|
// border-radius: 0 0 4px 4px !important;
|
||||||
}
|
// }
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// Fast cache key - use length + samples instead of full hash
|
// Fast cache key - use length + samples instead of full hash
|
||||||
function getCacheKey(fileName: string, original: string, modified: string): string {
|
function getCacheKey(fileName: string, original: string, modified: string, themeKey: string): string {
|
||||||
// Sample a few characters instead of hashing entire content
|
// Sample a few characters instead of hashing entire content
|
||||||
const sampleOriginal = original.length > 100
|
const sampleOriginal = original.length > 100
|
||||||
? `${original.slice(0, 50)}${original.slice(-50)}`
|
? `${original.slice(0, 50)}${original.slice(-50)}`
|
||||||
@@ -104,7 +104,7 @@ function getCacheKey(fileName: string, original: string, modified: string): stri
|
|||||||
const sampleModified = modified.length > 100
|
const sampleModified = modified.length > 100
|
||||||
? `${modified.slice(0, 50)}${modified.slice(-50)}`
|
? `${modified.slice(0, 50)}${modified.slice(-50)}`
|
||||||
: modified;
|
: modified;
|
||||||
return `${fileName}:${original.length}:${modified.length}:${sampleOriginal.length}:${sampleModified.length}`;
|
return `${themeKey}::${fileName}:${original.length}:${modified.length}:${sampleOriginal.length}:${sampleModified.length}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const extractSelectedCode = (original: string, modified: string, range: SelectedLineRange): string => {
|
const extractSelectedCode = (original: string, modified: string, range: SelectedLineRange): string => {
|
||||||
@@ -137,24 +137,42 @@ export const PierreDiffViewer: React.FC<PierreDiffViewerProps> = ({
|
|||||||
const themeSystem = useOptionalThemeSystem();
|
const themeSystem = useOptionalThemeSystem();
|
||||||
const isDark = themeSystem?.currentTheme?.metadata?.variant === 'dark';
|
const isDark = themeSystem?.currentTheme?.metadata?.variant === 'dark';
|
||||||
|
|
||||||
|
const fallbackLight = getDefaultTheme(false);
|
||||||
|
const fallbackDark = getDefaultTheme(true);
|
||||||
|
|
||||||
|
const lightThemeId = themeSystem?.lightThemeId ?? fallbackLight.metadata.id;
|
||||||
|
const darkThemeId = themeSystem?.darkThemeId ?? fallbackDark.metadata.id;
|
||||||
|
|
||||||
|
const lightTheme =
|
||||||
|
themeSystem?.availableThemes.find((theme) => theme.metadata.id === lightThemeId) ??
|
||||||
|
fallbackLight;
|
||||||
|
const darkTheme =
|
||||||
|
themeSystem?.availableThemes.find((theme) => theme.metadata.id === darkThemeId) ??
|
||||||
|
fallbackDark;
|
||||||
|
|
||||||
const setActiveMainTab = useUIStore(state => state.setActiveMainTab);
|
const setActiveMainTab = useUIStore(state => state.setActiveMainTab);
|
||||||
|
|
||||||
const [selection, setSelection] = useState<SelectedLineRange | null>(null);
|
const [selection, setSelection] = useState<SelectedLineRange | null>(null);
|
||||||
const [commentText, setCommentText] = useState('');
|
const [commentText, setCommentText] = useState('');
|
||||||
const commentContainerRef = useRef<HTMLDivElement>(null);
|
const commentContainerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
// Calculate initial center synchronously to avoid flicker
|
// Calculate initial center and width synchronously to avoid flicker
|
||||||
const getMainContentCenter = useCallback(() => {
|
const getMainContentMetrics = useCallback(() => {
|
||||||
if (isMobile) return '50%';
|
if (isMobile) return { center: '50%', width: '100vw' };
|
||||||
const mainContent = document.querySelector('main.flex-1');
|
const mainContent = document.querySelector('main.flex-1');
|
||||||
if (mainContent) {
|
if (mainContent) {
|
||||||
const rect = mainContent.getBoundingClientRect();
|
const rect = mainContent.getBoundingClientRect();
|
||||||
return `${rect.left + rect.width / 2}px`;
|
return {
|
||||||
|
center: `${rect.left + rect.width / 2}px`,
|
||||||
|
width: `${rect.width}px`
|
||||||
|
};
|
||||||
}
|
}
|
||||||
return '50%';
|
return { center: '50%', width: '100vw' };
|
||||||
}, [isMobile]);
|
}, [isMobile]);
|
||||||
|
|
||||||
const [mainContentCenter, setMainContentCenter] = useState<string>(getMainContentCenter);
|
const [mainContentMetrics, setMainContentMetrics] = useState(getMainContentMetrics);
|
||||||
|
const mainContentCenter = mainContentMetrics.center;
|
||||||
|
const mainContentWidth = mainContentMetrics.width;
|
||||||
|
|
||||||
const sendMessage = useSessionStore(state => state.sendMessage);
|
const sendMessage = useSessionStore(state => state.sendMessage);
|
||||||
const currentSessionId = useSessionStore(state => state.currentSessionId);
|
const currentSessionId = useSessionStore(state => state.currentSessionId);
|
||||||
@@ -163,17 +181,17 @@ export const PierreDiffViewer: React.FC<PierreDiffViewerProps> = ({
|
|||||||
const getAgentModelForSession = useContextStore(state => state.getAgentModelForSession);
|
const getAgentModelForSession = useContextStore(state => state.getAgentModelForSession);
|
||||||
const getAgentModelVariantForSession = useContextStore(state => state.getAgentModelVariantForSession);
|
const getAgentModelVariantForSession = useContextStore(state => state.getAgentModelVariantForSession);
|
||||||
|
|
||||||
// Update main content center on resize
|
// Update main content metrics on resize
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isMobile) return;
|
if (isMobile) return;
|
||||||
|
|
||||||
const updateCenter = () => {
|
const updateMetrics = () => {
|
||||||
setMainContentCenter(getMainContentCenter());
|
setMainContentMetrics(getMainContentMetrics());
|
||||||
};
|
};
|
||||||
|
|
||||||
window.addEventListener('resize', updateCenter);
|
window.addEventListener('resize', updateMetrics);
|
||||||
return () => window.removeEventListener('resize', updateCenter);
|
return () => window.removeEventListener('resize', updateMetrics);
|
||||||
}, [isMobile, getMainContentCenter]);
|
}, [isMobile, getMainContentMetrics]);
|
||||||
|
|
||||||
const handleSelectionChange = useCallback((range: SelectedLineRange | null) => {
|
const handleSelectionChange = useCallback((range: SelectedLineRange | null) => {
|
||||||
// On mobile: implement "tap to extend" behavior
|
// On mobile: implement "tap to extend" behavior
|
||||||
@@ -291,7 +309,89 @@ export const PierreDiffViewer: React.FC<PierreDiffViewerProps> = ({
|
|||||||
});
|
});
|
||||||
}, [selection, commentText, original, modified, fileName, language, sendMessage, currentSessionId, currentProviderId, currentModelId, currentAgentName, currentVariant, setActiveMainTab, getSessionAgentSelection, getAgentModelForSession, getAgentModelVariantForSession]);
|
}, [selection, commentText, original, modified, fileName, language, sendMessage, currentSessionId, currentProviderId, currentModelId, currentAgentName, currentVariant, setActiveMainTab, getSessionAgentSelection, getAgentModelForSession, getAgentModelVariantForSession]);
|
||||||
|
|
||||||
ensureFlexokiThemesRegistered();
|
ensurePierreThemeRegistered(lightTheme);
|
||||||
|
ensurePierreThemeRegistered(darkTheme);
|
||||||
|
|
||||||
|
const diffThemeKey = `${lightTheme.metadata.id}:${darkTheme.metadata.id}:${isDark ? 'dark' : 'light'}`;
|
||||||
|
|
||||||
|
const diffRootRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
|
const lightResolvedTheme = useMemo(() => getResolvedShikiTheme(lightTheme), [lightTheme]);
|
||||||
|
const darkResolvedTheme = useMemo(() => getResolvedShikiTheme(darkTheme), [darkTheme]);
|
||||||
|
|
||||||
|
// Fast-path: update base diff theme vars immediately.
|
||||||
|
// Without this, already-mounted diffs can keep old bg/bars until async highlight completes.
|
||||||
|
React.useLayoutEffect(() => {
|
||||||
|
const root = diffRootRef.current;
|
||||||
|
if (!root) return;
|
||||||
|
|
||||||
|
const container = root.querySelector('diffs-container') as HTMLElement | null;
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
const currentResolved = isDark ? darkResolvedTheme : lightResolvedTheme;
|
||||||
|
|
||||||
|
const getColor = (
|
||||||
|
resolved: typeof currentResolved,
|
||||||
|
key: string,
|
||||||
|
): string | undefined => {
|
||||||
|
const colors = resolved.colors as Record<string, string> | undefined;
|
||||||
|
return colors?.[key];
|
||||||
|
};
|
||||||
|
|
||||||
|
const lightAdd = getColor(lightResolvedTheme, 'terminal.ansiGreen');
|
||||||
|
const lightDel = getColor(lightResolvedTheme, 'terminal.ansiRed');
|
||||||
|
const lightMod = getColor(lightResolvedTheme, 'terminal.ansiBlue');
|
||||||
|
|
||||||
|
const darkAdd = getColor(darkResolvedTheme, 'terminal.ansiGreen');
|
||||||
|
const darkDel = getColor(darkResolvedTheme, 'terminal.ansiRed');
|
||||||
|
const darkMod = getColor(darkResolvedTheme, 'terminal.ansiBlue');
|
||||||
|
|
||||||
|
// Apply on host; vars inherit into shadow root.
|
||||||
|
container.style.setProperty('--shiki-light', lightResolvedTheme.fg);
|
||||||
|
container.style.setProperty('--shiki-light-bg', lightResolvedTheme.bg);
|
||||||
|
if (lightAdd) container.style.setProperty('--shiki-light-addition-color', lightAdd);
|
||||||
|
if (lightDel) container.style.setProperty('--shiki-light-deletion-color', lightDel);
|
||||||
|
if (lightMod) container.style.setProperty('--shiki-light-modified-color', lightMod);
|
||||||
|
|
||||||
|
container.style.setProperty('--shiki-dark', darkResolvedTheme.fg);
|
||||||
|
container.style.setProperty('--shiki-dark-bg', darkResolvedTheme.bg);
|
||||||
|
if (darkAdd) container.style.setProperty('--shiki-dark-addition-color', darkAdd);
|
||||||
|
if (darkDel) container.style.setProperty('--shiki-dark-deletion-color', darkDel);
|
||||||
|
if (darkMod) container.style.setProperty('--shiki-dark-modified-color', darkMod);
|
||||||
|
|
||||||
|
container.style.setProperty('--diffs-bg', currentResolved.bg);
|
||||||
|
container.style.setProperty('--diffs-fg', currentResolved.fg);
|
||||||
|
|
||||||
|
const currentAdd = isDark ? darkAdd : lightAdd;
|
||||||
|
const currentDel = isDark ? darkDel : lightDel;
|
||||||
|
const currentMod = isDark ? darkMod : lightMod;
|
||||||
|
if (currentAdd) container.style.setProperty('--diffs-addition-color-override', currentAdd);
|
||||||
|
if (currentDel) container.style.setProperty('--diffs-deletion-color-override', currentDel);
|
||||||
|
if (currentMod) container.style.setProperty('--diffs-modified-color-override', currentMod);
|
||||||
|
|
||||||
|
// Pierre also inlines theme styles on <pre> inside shadow root.
|
||||||
|
// Patch it too so already-expanded diffs switch instantly.
|
||||||
|
const pre = container.shadowRoot?.querySelector('pre') as HTMLPreElement | null;
|
||||||
|
if (pre) {
|
||||||
|
pre.style.setProperty('--shiki-light', lightResolvedTheme.fg);
|
||||||
|
pre.style.setProperty('--shiki-light-bg', lightResolvedTheme.bg);
|
||||||
|
if (lightAdd) pre.style.setProperty('--shiki-light-addition-color', lightAdd);
|
||||||
|
if (lightDel) pre.style.setProperty('--shiki-light-deletion-color', lightDel);
|
||||||
|
if (lightMod) pre.style.setProperty('--shiki-light-modified-color', lightMod);
|
||||||
|
|
||||||
|
pre.style.setProperty('--shiki-dark', darkResolvedTheme.fg);
|
||||||
|
pre.style.setProperty('--shiki-dark-bg', darkResolvedTheme.bg);
|
||||||
|
if (darkAdd) pre.style.setProperty('--shiki-dark-addition-color', darkAdd);
|
||||||
|
if (darkDel) pre.style.setProperty('--shiki-dark-deletion-color', darkDel);
|
||||||
|
if (darkMod) pre.style.setProperty('--shiki-dark-modified-color', darkMod);
|
||||||
|
|
||||||
|
pre.style.setProperty('--diffs-bg', currentResolved.bg);
|
||||||
|
pre.style.setProperty('--diffs-fg', currentResolved.fg);
|
||||||
|
if (currentAdd) pre.style.setProperty('--diffs-addition-color-override', currentAdd);
|
||||||
|
if (currentDel) pre.style.setProperty('--diffs-deletion-color-override', currentDel);
|
||||||
|
if (currentMod) pre.style.setProperty('--diffs-modified-color-override', currentMod);
|
||||||
|
}
|
||||||
|
}, [darkResolvedTheme, diffThemeKey, isDark, lightResolvedTheme]);
|
||||||
|
|
||||||
// Cache the last computed diff to avoid recomputing on every render
|
// Cache the last computed diff to avoid recomputing on every render
|
||||||
const diffCacheRef = useRef<{
|
const diffCacheRef = useRef<{
|
||||||
@@ -301,7 +401,7 @@ export const PierreDiffViewer: React.FC<PierreDiffViewerProps> = ({
|
|||||||
|
|
||||||
// Pre-parse the diff with cacheKey for worker pool caching
|
// Pre-parse the diff with cacheKey for worker pool caching
|
||||||
const fileDiff = useMemo(() => {
|
const fileDiff = useMemo(() => {
|
||||||
const cacheKey = getCacheKey(fileName, original, modified);
|
const cacheKey = getCacheKey(fileName, original, modified, diffThemeKey);
|
||||||
|
|
||||||
// Return cached diff if inputs haven't changed
|
// Return cached diff if inputs haven't changed
|
||||||
if (diffCacheRef.current?.key === cacheKey) {
|
if (diffCacheRef.current?.key === cacheKey) {
|
||||||
@@ -328,12 +428,12 @@ export const PierreDiffViewer: React.FC<PierreDiffViewerProps> = ({
|
|||||||
diffCacheRef.current = { key: cacheKey, fileDiff: diff };
|
diffCacheRef.current = { key: cacheKey, fileDiff: diff };
|
||||||
|
|
||||||
return diff;
|
return diff;
|
||||||
}, [fileName, original, modified, language]);
|
}, [diffThemeKey, fileName, original, modified, language]);
|
||||||
|
|
||||||
const options = useMemo(() => ({
|
const options = useMemo(() => ({
|
||||||
theme: {
|
theme: {
|
||||||
dark: flexokiThemeNames.dark,
|
dark: darkTheme.metadata.id,
|
||||||
light: flexokiThemeNames.light,
|
light: lightTheme.metadata.id,
|
||||||
},
|
},
|
||||||
themeType: isDark ? ('dark' as const) : ('light' as const),
|
themeType: isDark ? ('dark' as const) : ('light' as const),
|
||||||
diffStyle: renderSideBySide ? ('split' as const) : ('unified' as const),
|
diffStyle: renderSideBySide ? ('split' as const) : ('unified' as const),
|
||||||
@@ -346,7 +446,7 @@ export const PierreDiffViewer: React.FC<PierreDiffViewerProps> = ({
|
|||||||
enableHoverUtility: false,
|
enableHoverUtility: false,
|
||||||
onLineSelected: handleSelectionChange,
|
onLineSelected: handleSelectionChange,
|
||||||
unsafeCSS: WEBKIT_SCROLL_FIX_CSS,
|
unsafeCSS: WEBKIT_SCROLL_FIX_CSS,
|
||||||
}), [isDark, renderSideBySide, wrapLines, handleSelectionChange]);
|
}), [darkTheme.metadata.id, isDark, lightTheme.metadata.id, renderSideBySide, wrapLines, handleSelectionChange]);
|
||||||
|
|
||||||
if (typeof window === 'undefined') {
|
if (typeof window === 'undefined') {
|
||||||
return null;
|
return null;
|
||||||
@@ -358,9 +458,14 @@ export const PierreDiffViewer: React.FC<PierreDiffViewerProps> = ({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="flex flex-col items-center gap-2 px-4"
|
className="flex flex-col items-center gap-2 px-4"
|
||||||
style={{ width: 'min(100vw - 1rem, 42rem)' }}
|
style={{ width: `min(calc(${mainContentWidth} - 2rem), 42rem)` }}
|
||||||
>
|
>
|
||||||
<div className="w-full rounded-xl border bg-sidebar flex flex-col relative shadow-lg" style={{ borderColor: 'var(--primary)' }}>
|
<div
|
||||||
|
className="w-full rounded-xl flex flex-col relative shadow-lg border border-border/80 focus-within:border-primary/70 focus-within:ring-1 focus-within:ring-primary/50"
|
||||||
|
style={{
|
||||||
|
backgroundColor: themeSystem?.currentTheme?.colors?.surface?.subtle,
|
||||||
|
}}
|
||||||
|
>
|
||||||
{/* Textarea - auto-grows from 1 line to max 5 lines */}
|
{/* Textarea - auto-grows from 1 line to max 5 lines */}
|
||||||
<Textarea
|
<Textarea
|
||||||
value={commentText}
|
value={commentText}
|
||||||
@@ -374,7 +479,8 @@ export const PierreDiffViewer: React.FC<PierreDiffViewerProps> = ({
|
|||||||
textarea.style.height = `${Math.min(textarea.scrollHeight, maxHeight)}px`;
|
textarea.style.height = `${Math.min(textarea.scrollHeight, maxHeight)}px`;
|
||||||
}}
|
}}
|
||||||
placeholder="Type your comment..."
|
placeholder="Type your comment..."
|
||||||
className="min-h-[28px] max-h-[108px] resize-none border-0 px-3 pt-2 pb-1 shadow-none rounded-none appearance-none focus:shadow-none focus-visible:shadow-none focus-visible:border-transparent focus-visible:ring-0 focus-visible:ring-transparent hover:border-transparent bg-transparent dark:bg-transparent focus-visible:outline-none overflow-y-auto"
|
outerClassName="focus-within:ring-0"
|
||||||
|
className="min-h-[28px] max-h-[108px] resize-none border-0 px-3 pt-2 pb-1 rounded-none appearance-none hover:border-transparent bg-transparent dark:bg-transparent overflow-y-auto focus:ring-0 focus:shadow-none"
|
||||||
autoFocus={!isMobile}
|
autoFocus={!isMobile}
|
||||||
rows={1}
|
rows={1}
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
@@ -450,19 +556,22 @@ export const PierreDiffViewer: React.FC<PierreDiffViewerProps> = ({
|
|||||||
disableHorizontal={false}
|
disableHorizontal={false}
|
||||||
fillContainer={true}
|
fillContainer={true}
|
||||||
>
|
>
|
||||||
<FileDiff
|
<div ref={diffRootRef} className="size-full">
|
||||||
fileDiff={fileDiff}
|
<FileDiff
|
||||||
options={options}
|
key={diffThemeKey}
|
||||||
selectedLines={selection}
|
fileDiff={fileDiff}
|
||||||
/>
|
options={options}
|
||||||
|
selectedLines={selection}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</ScrollableOverlay>
|
</ScrollableOverlay>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Render Input In-Flow at the bottom */}
|
{/* Render Input overlay at the bottom */}
|
||||||
{selection && (
|
{selection && (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"pointer-events-auto relative pb-2 transition-none z-50 flex justify-center",
|
"pointer-events-auto absolute bottom-0 left-0 right-0 pb-2 transition-none z-50 flex justify-center w-full",
|
||||||
isMobile && isKeyboardOpen ? "ios-keyboard-safe-area" : "bottom-safe-area"
|
isMobile && isKeyboardOpen ? "ios-keyboard-safe-area" : "bottom-safe-area"
|
||||||
)}
|
)}
|
||||||
style={{
|
style={{
|
||||||
@@ -485,8 +594,9 @@ export const PierreDiffViewer: React.FC<PierreDiffViewerProps> = ({
|
|||||||
// Use simple div with overflow-x-auto to avoid nested ScrollableOverlay issues in Chrome
|
// Use simple div with overflow-x-auto to avoid nested ScrollableOverlay issues in Chrome
|
||||||
return (
|
return (
|
||||||
<div className={cn("relative", "w-full")}>
|
<div className={cn("relative", "w-full")}>
|
||||||
<div className="pierre-diff-wrapper w-full overflow-x-auto overflow-y-visible">
|
<div ref={diffRootRef} className="pierre-diff-wrapper w-full overflow-x-auto overflow-y-visible">
|
||||||
<FileDiff
|
<FileDiff
|
||||||
|
key={diffThemeKey}
|
||||||
fileDiff={fileDiff}
|
fileDiff={fileDiff}
|
||||||
options={options}
|
options={options}
|
||||||
selectedLines={selection}
|
selectedLines={selection}
|
||||||
|
|||||||
@@ -389,7 +389,7 @@ export const SettingsView: React.FC<SettingsViewProps> = ({ onClose, forceMobile
|
|||||||
className={cn(
|
className={cn(
|
||||||
'relative flex h-9 w-9 items-center justify-center rounded-md transition-colors',
|
'relative flex h-9 w-9 items-center justify-center rounded-md transition-colors',
|
||||||
'hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary',
|
'hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary',
|
||||||
isActive ? 'bg-secondary text-foreground shadow-sm' : 'text-muted-foreground'
|
isActive ? 'bg-interactive-selection text-interactive-selection-foreground shadow-sm' : 'text-muted-foreground hover:bg-interactive-hover/50'
|
||||||
)}
|
)}
|
||||||
aria-pressed={isActive}
|
aria-pressed={isActive}
|
||||||
aria-label={label}
|
aria-label={label}
|
||||||
@@ -412,7 +412,7 @@ export const SettingsView: React.FC<SettingsViewProps> = ({ onClose, forceMobile
|
|||||||
onMouseDown={isActive ? handleActiveTabDragStart : undefined}
|
onMouseDown={isActive ? handleActiveTabDragStart : undefined}
|
||||||
className={cn(
|
className={cn(
|
||||||
'relative flex h-8 items-center gap-2 px-3 rounded-md typography-ui-label font-medium transition-colors',
|
'relative flex h-8 items-center gap-2 px-3 rounded-md typography-ui-label font-medium transition-colors',
|
||||||
isActive ? 'app-region-drag bg-secondary text-foreground shadow-sm' : 'app-region-no-drag text-muted-foreground hover:bg-secondary/50 hover:text-foreground',
|
isActive ? 'app-region-drag bg-interactive-selection text-interactive-selection-foreground shadow-sm' : 'app-region-no-drag text-muted-foreground hover:bg-interactive-hover/50 hover:text-foreground',
|
||||||
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary'
|
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary'
|
||||||
)}
|
)}
|
||||||
aria-pressed={isActive}
|
aria-pressed={isActive}
|
||||||
@@ -436,7 +436,7 @@ export const SettingsView: React.FC<SettingsViewProps> = ({ onClose, forceMobile
|
|||||||
type="button"
|
type="button"
|
||||||
aria-label="Switch project"
|
aria-label="Switch project"
|
||||||
title={activeProjectLabel}
|
title={activeProjectLabel}
|
||||||
className="inline-flex h-9 w-9 items-center justify-center p-2 text-muted-foreground hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary"
|
className="inline-flex h-9 w-9 items-center justify-center p-2 rounded-lg text-muted-foreground hover:text-foreground hover:bg-interactive-hover/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary border border-[var(--interactive-border)]"
|
||||||
>
|
>
|
||||||
<RiFolderLine className="h-5 w-5" />
|
<RiFolderLine className="h-5 w-5" />
|
||||||
</button>
|
</button>
|
||||||
@@ -446,7 +446,7 @@ export const SettingsView: React.FC<SettingsViewProps> = ({ onClose, forceMobile
|
|||||||
aria-label="Switch project"
|
aria-label="Switch project"
|
||||||
title={activeProjectLabel}
|
title={activeProjectLabel}
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex h-9 max-w-[18rem] items-center gap-1.5 bg-transparent px-2 text-foreground outline-none hover:text-foreground/80 focus-visible:ring-2 focus-visible:ring-ring',
|
'flex h-9 max-w-[18rem] items-center gap-1.5 bg-transparent px-2 rounded-lg text-foreground outline-none hover:bg-interactive-hover/50 focus-visible:ring-2 focus-visible:ring-ring border border-[var(--interactive-border)]',
|
||||||
!isMobile && 'app-region-no-drag'
|
!isMobile && 'app-region-no-drag'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -487,7 +487,7 @@ export const SettingsView: React.FC<SettingsViewProps> = ({ onClose, forceMobile
|
|||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
aria-label="Close settings"
|
aria-label="Close settings"
|
||||||
className={cn(
|
className={cn(
|
||||||
'inline-flex h-9 w-9 items-center justify-center p-2 text-muted-foreground hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary',
|
'inline-flex h-9 w-9 items-center justify-center p-2 rounded-lg text-muted-foreground hover:text-foreground hover:bg-interactive-hover/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary',
|
||||||
!isMobile && 'app-region-no-drag'
|
!isMobile && 'app-region-no-drag'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import { BranchSelector, useBranchOptions } from '@/components/multirun/BranchSe
|
|||||||
import { AgentSelector } from '@/components/multirun/AgentSelector';
|
import { AgentSelector } from '@/components/multirun/AgentSelector';
|
||||||
import { isIMECompositionEvent } from '@/lib/ime';
|
import { isIMECompositionEvent } from '@/lib/ime';
|
||||||
import { getWorktreeSetupCommands } from '@/lib/openchamberConfig';
|
import { getWorktreeSetupCommands } from '@/lib/openchamberConfig';
|
||||||
|
import { useThemeSystem } from '@/contexts/useThemeSystem';
|
||||||
import type { ProjectRef } from '@/lib/openchamberConfig';
|
import type { ProjectRef } from '@/lib/openchamberConfig';
|
||||||
import type { CreateMultiRunParams, MultiRunFileAttachment } from '@/types/multirun';
|
import type { CreateMultiRunParams, MultiRunFileAttachment } from '@/types/multirun';
|
||||||
|
|
||||||
@@ -66,6 +67,7 @@ export const AgentManagerEmptyState: React.FC<AgentManagerEmptyStateProps> = ({
|
|||||||
const fileInputRef = React.useRef<HTMLInputElement>(null);
|
const fileInputRef = React.useRef<HTMLInputElement>(null);
|
||||||
const textareaRef = React.useRef<HTMLTextAreaElement>(null);
|
const textareaRef = React.useRef<HTMLTextAreaElement>(null);
|
||||||
|
|
||||||
|
const { currentTheme } = useThemeSystem();
|
||||||
const currentDirectory = useDirectoryStore((state) => state.currentDirectory ?? null);
|
const currentDirectory = useDirectoryStore((state) => state.currentDirectory ?? null);
|
||||||
const { isGitRepository, isLoading: isLoadingBranches } = useBranchOptions(currentDirectory);
|
const { isGitRepository, isLoading: isLoadingBranches } = useBranchOptions(currentDirectory);
|
||||||
|
|
||||||
@@ -312,7 +314,7 @@ export const AgentManagerEmptyState: React.FC<AgentManagerEmptyStateProps> = ({
|
|||||||
|
|
||||||
{/* Setup commands collapsible */}
|
{/* Setup commands collapsible */}
|
||||||
<Collapsible open={isSetupCommandsOpen} onOpenChange={setIsSetupCommandsOpen}>
|
<Collapsible open={isSetupCommandsOpen} onOpenChange={setIsSetupCommandsOpen}>
|
||||||
<CollapsibleTrigger className="w-full flex items-center justify-between py-1 hover:opacity-80 transition-opacity">
|
<CollapsibleTrigger className="w-full flex items-center justify-between py-1 hover:bg-[var(--interactive-hover)] rounded-md px-1 -mx-1 transition-colors">
|
||||||
<p className="typography-ui-label font-medium text-foreground">
|
<p className="typography-ui-label font-medium text-foreground">
|
||||||
Setup commands
|
Setup commands
|
||||||
{setupCommands.filter(cmd => cmd.trim()).length > 0 && (
|
{setupCommands.filter(cmd => cmd.trim()).length > 0 && (
|
||||||
@@ -409,7 +411,10 @@ export const AgentManagerEmptyState: React.FC<AgentManagerEmptyStateProps> = ({
|
|||||||
<label htmlFor="prompt" className="typography-ui-label font-medium text-foreground">
|
<label htmlFor="prompt" className="typography-ui-label font-medium text-foreground">
|
||||||
Prompt
|
Prompt
|
||||||
</label>
|
</label>
|
||||||
<div className="rounded-xl border border-border/60 bg-input/10 dark:bg-input/30 overflow-hidden">
|
<div
|
||||||
|
className="rounded-xl border border-border/80 overflow-hidden focus-within:ring-1 focus-within:ring-primary/50"
|
||||||
|
style={{ backgroundColor: currentTheme?.colors?.surface?.subtle }}
|
||||||
|
>
|
||||||
{/* Text Area */}
|
{/* Text Area */}
|
||||||
<Textarea
|
<Textarea
|
||||||
ref={textareaRef}
|
ref={textareaRef}
|
||||||
@@ -418,7 +423,7 @@ export const AgentManagerEmptyState: React.FC<AgentManagerEmptyStateProps> = ({
|
|||||||
onChange={(e) => setPrompt(e.target.value)}
|
onChange={(e) => setPrompt(e.target.value)}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
placeholder="Ask anything..."
|
placeholder="Ask anything..."
|
||||||
className="min-h-[100px] max-h-[300px] resize-none border-0 bg-transparent px-4 py-3 typography-markdown focus-visible:ring-0 focus-visible:ring-offset-0"
|
className="min-h-[100px] max-h-[300px] resize-none border-0 bg-transparent dark:bg-transparent px-4 py-3 typography-markdown focus-visible:ring-0 focus-visible:ring-offset-0"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Attached Files Display */}
|
{/* Attached Files Display */}
|
||||||
@@ -450,7 +455,7 @@ export const AgentManagerEmptyState: React.FC<AgentManagerEmptyStateProps> = ({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Footer Controls */}
|
{/* Footer Controls */}
|
||||||
<div className="flex items-center justify-between px-3 py-2 border-t border-border/40">
|
<div className="flex items-center justify-between px-3 py-2 border-t border-border/40 bg-transparent">
|
||||||
{/* Left Controls - Attachments */}
|
{/* Left Controls - Attachments */}
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<input
|
<input
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ const AgentGroupItem: React.FC<AgentGroupItemProps> = ({ group, isSelected, onSe
|
|||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'group relative flex items-center rounded-md px-1.5 py-1.5 cursor-pointer',
|
'group relative flex items-center rounded-md px-1.5 py-1.5 cursor-pointer',
|
||||||
isSelected ? 'dark:bg-accent/80 bg-primary/12' : 'hover:dark:bg-accent/40 hover:bg-primary/6',
|
isSelected ? 'bg-interactive-selection' : 'hover:bg-interactive-hover',
|
||||||
)}
|
)}
|
||||||
onClick={onSelect}
|
onClick={onSelect}
|
||||||
>
|
>
|
||||||
@@ -192,7 +192,7 @@ export const AgentManagerSidebar: React.FC<AgentManagerSidebarProps> = ({
|
|||||||
const remainingCount = filteredGroups.length - MAX_VISIBLE;
|
const remainingCount = filteredGroups.length - MAX_VISIBLE;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn('flex h-full flex-col bg-background/50 dark:bg-neutral-900/80 text-foreground border-r border-border/30', className)}>
|
<div className={cn('flex h-full flex-col text-foreground border-r border-border/30', className)}>
|
||||||
{/* Search Input */}
|
{/* Search Input */}
|
||||||
<div className="px-2.5 pt-3 pb-2">
|
<div className="px-2.5 pt-3 pb-2">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
|
|||||||
@@ -33,13 +33,13 @@ function formatCommitDate(date: string) {
|
|||||||
function getChangeTypeColor(changeType: string) {
|
function getChangeTypeColor(changeType: string) {
|
||||||
switch (changeType) {
|
switch (changeType) {
|
||||||
case 'A':
|
case 'A':
|
||||||
return 'text-emerald-500';
|
return 'text-[var(--status-success)]';
|
||||||
case 'D':
|
case 'D':
|
||||||
return 'text-red-500';
|
return 'text-[var(--status-error)]';
|
||||||
case 'M':
|
case 'M':
|
||||||
return 'text-amber-500';
|
return 'text-[var(--status-warning)]';
|
||||||
case 'R':
|
case 'R':
|
||||||
return 'text-blue-500';
|
return 'text-[var(--status-info)]';
|
||||||
default:
|
default:
|
||||||
return 'text-muted-foreground';
|
return 'text-muted-foreground';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import type { SupportedLanguages } from '@pierre/diffs';
|
|||||||
|
|
||||||
import { useOptionalThemeSystem } from './useThemeSystem';
|
import { useOptionalThemeSystem } from './useThemeSystem';
|
||||||
import { workerFactory } from '@/lib/diff/workerFactory';
|
import { workerFactory } from '@/lib/diff/workerFactory';
|
||||||
import { ensureFlexokiThemesRegistered } from '@/lib/shiki/registerFlexokiThemes';
|
import { ensurePierreThemeRegistered } from '@/lib/shiki/appThemeRegistry';
|
||||||
import { flexokiThemeNames } from '@/lib/shiki/flexokiThemes';
|
import { getDefaultTheme } from '@/lib/theme/themes';
|
||||||
import { useGitStore } from '@/stores/useGitStore';
|
import { useGitStore } from '@/stores/useGitStore';
|
||||||
import { getLanguageFromExtension } from '@/lib/toolHelpers';
|
import { getLanguageFromExtension } from '@/lib/toolHelpers';
|
||||||
|
|
||||||
@@ -31,14 +31,14 @@ const PRELOAD_LANGS: SupportedLanguages[] = [
|
|||||||
const WARMUP_MAX_FILES = 10;
|
const WARMUP_MAX_FILES = 10;
|
||||||
|
|
||||||
// Matches cache key logic in `packages/ui/src/components/views/PierreDiffViewer.tsx`
|
// Matches cache key logic in `packages/ui/src/components/views/PierreDiffViewer.tsx`
|
||||||
function getPierreCacheKey(fileName: string, original: string, modified: string): string {
|
function getPierreCacheKey(fileName: string, original: string, modified: string, themeKey: string): string {
|
||||||
const sampleOriginal = original.length > 100
|
const sampleOriginal = original.length > 100
|
||||||
? `${original.slice(0, 50)}${original.slice(-50)}`
|
? `${original.slice(0, 50)}${original.slice(-50)}`
|
||||||
: original;
|
: original;
|
||||||
const sampleModified = modified.length > 100
|
const sampleModified = modified.length > 100
|
||||||
? `${modified.slice(0, 50)}${modified.slice(-50)}`
|
? `${modified.slice(0, 50)}${modified.slice(-50)}`
|
||||||
: modified;
|
: modified;
|
||||||
return `${fileName}:${original.length}:${modified.length}:${sampleOriginal.length}:${sampleModified.length}`;
|
return `${themeKey}::${fileName}:${original.length}:${modified.length}:${sampleOriginal.length}:${sampleModified.length}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DiffWorkerProviderProps {
|
interface DiffWorkerProviderProps {
|
||||||
@@ -57,7 +57,11 @@ function scheduleWarmupWork(cb: (deadline?: IdleDeadlineLike) => void): () => vo
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Component that warms up the worker pool and precomputes diff ASTs
|
// Component that warms up the worker pool and precomputes diff ASTs
|
||||||
const WorkerPoolWarmup: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
const WorkerPoolWarmup: React.FC<{
|
||||||
|
children: React.ReactNode;
|
||||||
|
themeKey: string;
|
||||||
|
renderTheme: { light: string; dark: string };
|
||||||
|
}> = ({ children, themeKey, renderTheme }) => {
|
||||||
const workerPool = useWorkerPool();
|
const workerPool = useWorkerPool();
|
||||||
const activeDirectory = useGitStore((state) => state.activeDirectory);
|
const activeDirectory = useGitStore((state) => state.activeDirectory);
|
||||||
const lastStatusChange = useGitStore((state) => {
|
const lastStatusChange = useGitStore((state) => {
|
||||||
@@ -72,6 +76,20 @@ const WorkerPoolWarmup: React.FC<{ children: React.ReactNode }> = ({ children })
|
|||||||
const didDummyWarmupRef = useRef(false);
|
const didDummyWarmupRef = useRef(false);
|
||||||
const warmedStatusRef = useRef(new Map<string, number>());
|
const warmedStatusRef = useRef(new Map<string, number>());
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
warmedStatusRef.current.clear();
|
||||||
|
}, [themeKey]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!workerPool) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Important: WorkerPoolContextProvider uses a singleton and does not react to
|
||||||
|
// prop changes. Update the worker pool render options explicitly.
|
||||||
|
void workerPool.setRenderOptions({ theme: renderTheme });
|
||||||
|
}, [renderTheme, workerPool]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!workerPool || didDummyWarmupRef.current) return;
|
if (!workerPool || didDummyWarmupRef.current) return;
|
||||||
|
|
||||||
@@ -128,7 +146,7 @@ const WorkerPoolWarmup: React.FC<{ children: React.ReactNode }> = ({ children })
|
|||||||
index += 1;
|
index += 1;
|
||||||
|
|
||||||
const language = getLanguageFromExtension(filePath) || 'text';
|
const language = getLanguageFromExtension(filePath) || 'text';
|
||||||
const cacheKey = getPierreCacheKey(filePath, diff.original, diff.modified);
|
const cacheKey = getPierreCacheKey(filePath, diff.original, diff.modified, themeKey);
|
||||||
|
|
||||||
const oldFile: FileContents = {
|
const oldFile: FileContents = {
|
||||||
name: filePath,
|
name: filePath,
|
||||||
@@ -171,7 +189,7 @@ const WorkerPoolWarmup: React.FC<{ children: React.ReactNode }> = ({ children })
|
|||||||
cancelled = true;
|
cancelled = true;
|
||||||
cancelScheduled?.();
|
cancelScheduled?.();
|
||||||
};
|
};
|
||||||
}, [workerPool, activeDirectory, lastStatusChange, diffCacheSize]);
|
}, [workerPool, activeDirectory, lastStatusChange, diffCacheSize, themeKey]);
|
||||||
|
|
||||||
return <>{children}</>;
|
return <>{children}</>;
|
||||||
};
|
};
|
||||||
@@ -180,16 +198,40 @@ export const DiffWorkerProvider: React.FC<DiffWorkerProviderProps> = ({ children
|
|||||||
const themeSystem = useOptionalThemeSystem();
|
const themeSystem = useOptionalThemeSystem();
|
||||||
const isDark = themeSystem?.currentTheme?.metadata?.variant === 'dark';
|
const isDark = themeSystem?.currentTheme?.metadata?.variant === 'dark';
|
||||||
|
|
||||||
ensureFlexokiThemesRegistered();
|
const fallbackLight = getDefaultTheme(false);
|
||||||
|
const fallbackDark = getDefaultTheme(true);
|
||||||
|
|
||||||
|
const lightThemeId = themeSystem?.lightThemeId ?? fallbackLight.metadata.id;
|
||||||
|
const darkThemeId = themeSystem?.darkThemeId ?? fallbackDark.metadata.id;
|
||||||
|
|
||||||
|
const lightTheme =
|
||||||
|
themeSystem?.availableThemes.find((theme) => theme.metadata.id === lightThemeId) ??
|
||||||
|
fallbackLight;
|
||||||
|
const darkTheme =
|
||||||
|
themeSystem?.availableThemes.find((theme) => theme.metadata.id === darkThemeId) ??
|
||||||
|
fallbackDark;
|
||||||
|
|
||||||
|
ensurePierreThemeRegistered(lightTheme);
|
||||||
|
ensurePierreThemeRegistered(darkTheme);
|
||||||
|
|
||||||
const highlighterOptions = useMemo(() => ({
|
const highlighterOptions = useMemo(() => ({
|
||||||
theme: {
|
theme: {
|
||||||
dark: flexokiThemeNames.dark,
|
dark: darkTheme.metadata.id,
|
||||||
light: flexokiThemeNames.light,
|
light: lightTheme.metadata.id,
|
||||||
},
|
},
|
||||||
themeType: isDark ? ('dark' as const) : ('light' as const),
|
themeType: isDark ? ('dark' as const) : ('light' as const),
|
||||||
langs: PRELOAD_LANGS,
|
langs: PRELOAD_LANGS,
|
||||||
}), [isDark]);
|
}), [darkTheme.metadata.id, isDark, lightTheme.metadata.id]);
|
||||||
|
|
||||||
|
const workerThemeKey = `${lightTheme.metadata.id}:${darkTheme.metadata.id}:${isDark ? 'dark' : 'light'}`;
|
||||||
|
|
||||||
|
const renderTheme = useMemo(
|
||||||
|
() => ({
|
||||||
|
light: lightTheme.metadata.id,
|
||||||
|
dark: darkTheme.metadata.id,
|
||||||
|
}),
|
||||||
|
[darkTheme.metadata.id, lightTheme.metadata.id],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<WorkerPoolContextProvider
|
<WorkerPoolContextProvider
|
||||||
@@ -200,7 +242,10 @@ export const DiffWorkerProvider: React.FC<DiffWorkerProviderProps> = ({ children
|
|||||||
}}
|
}}
|
||||||
highlighterOptions={highlighterOptions}
|
highlighterOptions={highlighterOptions}
|
||||||
>
|
>
|
||||||
<WorkerPoolWarmup>
|
<WorkerPoolWarmup
|
||||||
|
themeKey={workerThemeKey}
|
||||||
|
renderTheme={renderTheme}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</WorkerPoolWarmup>
|
</WorkerPoolWarmup>
|
||||||
</WorkerPoolContextProvider>
|
</WorkerPoolContextProvider>
|
||||||
|
|||||||
@@ -6,18 +6,18 @@ import React, {
|
|||||||
} from 'react';
|
} from 'react';
|
||||||
import type { Theme, ThemeMode } from '@/types/theme';
|
import type { Theme, ThemeMode } from '@/types/theme';
|
||||||
import type { DesktopSettings } from '@/lib/desktop';
|
import type { DesktopSettings } from '@/lib/desktop';
|
||||||
import { isVSCodeRuntime } from '@/lib/desktop';
|
import { isDesktopRuntime, isVSCodeRuntime } from '@/lib/desktop';
|
||||||
import { CSSVariableGenerator } from '@/lib/theme/cssGenerator';
|
import { CSSVariableGenerator } from '@/lib/theme/cssGenerator';
|
||||||
import { updateDesktopSettings } from '@/lib/persistence';
|
import { updateDesktopSettings } from '@/lib/persistence';
|
||||||
import {
|
import {
|
||||||
themes,
|
themes,
|
||||||
getThemeById,
|
getThemeById,
|
||||||
flexokiLightTheme,
|
getDefaultTheme,
|
||||||
flexokiDarkTheme,
|
DEFAULT_LIGHT_THEME_ID,
|
||||||
|
DEFAULT_DARK_THEME_ID,
|
||||||
} from '@/lib/theme/themes';
|
} from '@/lib/theme/themes';
|
||||||
import { ThemeSystemContext, type ThemeContextValue } from './theme-system-context';
|
import { ThemeSystemContext, type ThemeContextValue } from './theme-system-context';
|
||||||
import type { VSCodeThemePayload } from '@/lib/theme/vscode/adapter';
|
import type { VSCodeThemePayload } from '@/lib/theme/vscode/adapter';
|
||||||
import { useUIStore } from '@/stores/useUIStore';
|
|
||||||
|
|
||||||
type ThemePreferences = {
|
type ThemePreferences = {
|
||||||
themeMode: ThemeMode;
|
themeMode: ThemeMode;
|
||||||
@@ -25,8 +25,8 @@ type ThemePreferences = {
|
|||||||
darkThemeId: string;
|
darkThemeId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const DEFAULT_LIGHT_ID = flexokiLightTheme.metadata.id;
|
const DEFAULT_LIGHT_ID = DEFAULT_LIGHT_THEME_ID;
|
||||||
const DEFAULT_DARK_ID = flexokiDarkTheme.metadata.id;
|
const DEFAULT_DARK_ID = DEFAULT_DARK_THEME_ID;
|
||||||
|
|
||||||
const getSystemPreference = (): boolean => {
|
const getSystemPreference = (): boolean => {
|
||||||
if (typeof window === 'undefined') {
|
if (typeof window === 'undefined') {
|
||||||
@@ -36,38 +36,83 @@ const getSystemPreference = (): boolean => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const fallbackThemeForVariant = (variant: 'light' | 'dark'): Theme =>
|
const fallbackThemeForVariant = (variant: 'light' | 'dark'): Theme =>
|
||||||
variant === 'dark' ? flexokiDarkTheme : flexokiLightTheme;
|
getDefaultTheme(variant === 'dark');
|
||||||
|
|
||||||
const findFallbackThemeId = (variant: 'light' | 'dark'): string => {
|
|
||||||
const fallback = themes.find((candidate) => candidate.metadata.variant === variant);
|
|
||||||
return (fallback ?? fallbackThemeForVariant(variant)).metadata.id;
|
|
||||||
};
|
|
||||||
|
|
||||||
const ensureThemeById = (themeId: string, variant: 'light' | 'dark'): Theme => {
|
|
||||||
const theme = getThemeById(themeId);
|
|
||||||
if (theme && theme.metadata.variant === variant) {
|
|
||||||
return theme;
|
|
||||||
}
|
|
||||||
const fallback = themes.find((candidate) => candidate.metadata.variant === variant);
|
|
||||||
return fallback ?? fallbackThemeForVariant(variant);
|
|
||||||
};
|
|
||||||
|
|
||||||
const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? React.useLayoutEffect : React.useEffect;
|
const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? React.useLayoutEffect : React.useEffect;
|
||||||
|
|
||||||
const validateThemeId = (themeId: string | null, variant: 'light' | 'dark'): string => {
|
const isNonEmptyString = (value: unknown): value is string =>
|
||||||
if (!themeId) {
|
typeof value === 'string' && value.trim().length > 0;
|
||||||
return variant === 'light' ? DEFAULT_LIGHT_ID : DEFAULT_DARK_ID;
|
|
||||||
|
const getNested = (value: unknown, path: string[]): unknown =>
|
||||||
|
path.reduce<unknown>((acc, key) => (acc && typeof acc === 'object' ? (acc as Record<string, unknown>)[key] : undefined), value);
|
||||||
|
|
||||||
|
const isValidCustomTheme = (value: unknown): value is Theme => {
|
||||||
|
if (!value || typeof value !== 'object') {
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
const theme = getThemeById(themeId);
|
|
||||||
if (theme && theme.metadata.variant === variant) {
|
const requiredPaths = [
|
||||||
return theme.metadata.id;
|
['metadata', 'id'],
|
||||||
|
['metadata', 'name'],
|
||||||
|
['metadata', 'variant'],
|
||||||
|
['colors', 'primary', 'base'],
|
||||||
|
['colors', 'primary', 'foreground'],
|
||||||
|
['colors', 'surface', 'background'],
|
||||||
|
['colors', 'surface', 'foreground'],
|
||||||
|
['colors', 'surface', 'muted'],
|
||||||
|
['colors', 'surface', 'mutedForeground'],
|
||||||
|
['colors', 'surface', 'elevated'],
|
||||||
|
['colors', 'surface', 'elevatedForeground'],
|
||||||
|
['colors', 'surface', 'subtle'],
|
||||||
|
['colors', 'interactive', 'border'],
|
||||||
|
['colors', 'interactive', 'selection'],
|
||||||
|
['colors', 'interactive', 'selectionForeground'],
|
||||||
|
['colors', 'interactive', 'focusRing'],
|
||||||
|
['colors', 'interactive', 'hover'],
|
||||||
|
['colors', 'status', 'error'],
|
||||||
|
['colors', 'status', 'errorForeground'],
|
||||||
|
['colors', 'status', 'errorBackground'],
|
||||||
|
['colors', 'status', 'errorBorder'],
|
||||||
|
['colors', 'status', 'warning'],
|
||||||
|
['colors', 'status', 'warningForeground'],
|
||||||
|
['colors', 'status', 'warningBackground'],
|
||||||
|
['colors', 'status', 'warningBorder'],
|
||||||
|
['colors', 'status', 'success'],
|
||||||
|
['colors', 'status', 'successForeground'],
|
||||||
|
['colors', 'status', 'successBackground'],
|
||||||
|
['colors', 'status', 'successBorder'],
|
||||||
|
['colors', 'status', 'info'],
|
||||||
|
['colors', 'status', 'infoForeground'],
|
||||||
|
['colors', 'status', 'infoBackground'],
|
||||||
|
['colors', 'status', 'infoBorder'],
|
||||||
|
['colors', 'syntax', 'base', 'background'],
|
||||||
|
['colors', 'syntax', 'base', 'foreground'],
|
||||||
|
['colors', 'syntax', 'base', 'keyword'],
|
||||||
|
['colors', 'syntax', 'base', 'string'],
|
||||||
|
['colors', 'syntax', 'base', 'number'],
|
||||||
|
['colors', 'syntax', 'base', 'function'],
|
||||||
|
['colors', 'syntax', 'base', 'variable'],
|
||||||
|
['colors', 'syntax', 'base', 'type'],
|
||||||
|
['colors', 'syntax', 'base', 'comment'],
|
||||||
|
['colors', 'syntax', 'base', 'operator'],
|
||||||
|
['colors', 'syntax', 'highlights', 'diffAdded'],
|
||||||
|
['colors', 'syntax', 'highlights', 'diffRemoved'],
|
||||||
|
['colors', 'syntax', 'highlights', 'lineNumber'],
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const path of requiredPaths) {
|
||||||
|
if (!isNonEmptyString(getNested(value, path))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return findFallbackThemeId(variant);
|
|
||||||
|
const variant = getNested(value, ['metadata', 'variant']);
|
||||||
|
return variant === 'light' || variant === 'dark';
|
||||||
};
|
};
|
||||||
|
|
||||||
const buildInitialPreferences = (defaultThemeId?: string): ThemePreferences => {
|
const buildInitialPreferences = (defaultThemeId?: string): ThemePreferences => {
|
||||||
let lightThemeId = DEFAULT_LIGHT_ID;
|
let lightThemeId: string = DEFAULT_LIGHT_ID;
|
||||||
let darkThemeId = DEFAULT_DARK_ID;
|
let darkThemeId: string = DEFAULT_DARK_ID;
|
||||||
let themeMode: ThemeMode = 'system';
|
let themeMode: ThemeMode = 'system';
|
||||||
|
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
@@ -99,8 +144,13 @@ const buildInitialPreferences = (defaultThemeId?: string): ThemePreferences => {
|
|||||||
themeMode = legacyVariant;
|
themeMode = legacyVariant;
|
||||||
}
|
}
|
||||||
|
|
||||||
lightThemeId = validateThemeId(storedLightId, 'light');
|
if (typeof storedLightId === 'string' && storedLightId.trim().length > 0) {
|
||||||
darkThemeId = validateThemeId(storedDarkId, 'dark');
|
lightThemeId = storedLightId.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof storedDarkId === 'string' && storedDarkId.trim().length > 0) {
|
||||||
|
darkThemeId = storedDarkId.trim();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (defaultThemeId) {
|
if (defaultThemeId) {
|
||||||
@@ -130,6 +180,8 @@ export function ThemeSystemProvider({ children, defaultThemeId }: ThemeSystemPro
|
|||||||
const cssGenerator = useMemo(() => new CSSVariableGenerator(), []);
|
const cssGenerator = useMemo(() => new CSSVariableGenerator(), []);
|
||||||
const [preferences, setPreferences] = useState<ThemePreferences>(() => buildInitialPreferences(defaultThemeId));
|
const [preferences, setPreferences] = useState<ThemePreferences>(() => buildInitialPreferences(defaultThemeId));
|
||||||
const [systemPrefersDark, setSystemPrefersDark] = useState<boolean>(() => getSystemPreference());
|
const [systemPrefersDark, setSystemPrefersDark] = useState<boolean>(() => getSystemPreference());
|
||||||
|
const [customThemes, setCustomThemes] = useState<Theme[]>([]);
|
||||||
|
const [customThemesLoading, setCustomThemesLoading] = useState(false);
|
||||||
const [vscodeTheme, setVSCodeTheme] = useState<Theme | null>(() => {
|
const [vscodeTheme, setVSCodeTheme] = useState<Theme | null>(() => {
|
||||||
if (typeof window === 'undefined' || !isVSCodeRuntime()) {
|
if (typeof window === 'undefined' || !isVSCodeRuntime()) {
|
||||||
return null;
|
return null;
|
||||||
@@ -138,6 +190,47 @@ export function ThemeSystemProvider({ children, defaultThemeId }: ThemeSystemPro
|
|||||||
return existing || null;
|
return existing || null;
|
||||||
});
|
});
|
||||||
const isVSCode = useMemo(() => isVSCodeRuntime(), []);
|
const isVSCode = useMemo(() => isVSCodeRuntime(), []);
|
||||||
|
const isDesktop = useMemo(() => isDesktopRuntime(), []);
|
||||||
|
|
||||||
|
const availableThemes = useMemo(() => {
|
||||||
|
const merged: Theme[] = [];
|
||||||
|
const seen = new Set<string>();
|
||||||
|
|
||||||
|
const add = (theme: Theme) => {
|
||||||
|
const id = theme.metadata.id;
|
||||||
|
if (seen.has(id)) return;
|
||||||
|
seen.add(id);
|
||||||
|
merged.push(theme);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isVSCode && vscodeTheme) {
|
||||||
|
add(vscodeTheme);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom themes first so they can override built-ins with the same id.
|
||||||
|
customThemes.forEach(add);
|
||||||
|
themes.forEach(add);
|
||||||
|
|
||||||
|
return merged;
|
||||||
|
}, [customThemes, isVSCode, vscodeTheme]);
|
||||||
|
|
||||||
|
const getThemeByIdFromAvailable = useCallback(
|
||||||
|
(themeId: string): Theme | undefined => availableThemes.find((theme) => theme.metadata.id === themeId),
|
||||||
|
[availableThemes],
|
||||||
|
);
|
||||||
|
|
||||||
|
const ensureThemeById = useCallback(
|
||||||
|
(themeId: string, variant: 'light' | 'dark'): Theme => {
|
||||||
|
const theme = getThemeByIdFromAvailable(themeId);
|
||||||
|
if (theme && theme.metadata.variant === variant) {
|
||||||
|
return theme;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fallback = availableThemes.find((candidate) => candidate.metadata.variant === variant);
|
||||||
|
return fallback ?? fallbackThemeForVariant(variant);
|
||||||
|
},
|
||||||
|
[availableThemes, getThemeByIdFromAvailable],
|
||||||
|
);
|
||||||
|
|
||||||
const currentTheme = useMemo(() => {
|
const currentTheme = useMemo(() => {
|
||||||
if (isVSCode && vscodeTheme) {
|
if (isVSCode && vscodeTheme) {
|
||||||
@@ -152,12 +245,46 @@ export function ThemeSystemProvider({ children, defaultThemeId }: ThemeSystemPro
|
|||||||
return systemPrefersDark
|
return systemPrefersDark
|
||||||
? ensureThemeById(preferences.darkThemeId, 'dark')
|
? ensureThemeById(preferences.darkThemeId, 'dark')
|
||||||
: ensureThemeById(preferences.lightThemeId, 'light');
|
: ensureThemeById(preferences.lightThemeId, 'light');
|
||||||
}, [isVSCode, preferences, systemPrefersDark, vscodeTheme]);
|
}, [ensureThemeById, isVSCode, preferences, systemPrefersDark, vscodeTheme]);
|
||||||
|
|
||||||
const availableThemes = useMemo(
|
const reloadCustomThemes = useCallback(async () => {
|
||||||
() => (isVSCode && vscodeTheme ? [vscodeTheme, ...themes] : themes),
|
if (typeof window === 'undefined' || isVSCode) {
|
||||||
[isVSCode, vscodeTheme],
|
return;
|
||||||
);
|
}
|
||||||
|
|
||||||
|
setCustomThemesLoading(true);
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/config/themes', {
|
||||||
|
method: 'GET',
|
||||||
|
credentials: isDesktop ? 'omit' : 'include',
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status === 401) {
|
||||||
|
// UI auth gate will handle prompting; avoid noisy retries here.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = await res.json();
|
||||||
|
const incoming = Array.isArray(payload?.themes) ? payload.themes : [];
|
||||||
|
const normalized = incoming.filter(isValidCustomTheme);
|
||||||
|
setCustomThemes(normalized);
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
} finally {
|
||||||
|
setCustomThemesLoading(false);
|
||||||
|
}
|
||||||
|
}, [isDesktop, isVSCode]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
void reloadCustomThemes();
|
||||||
|
}, [reloadCustomThemes]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isVSCode) {
|
if (!isVSCode) {
|
||||||
@@ -166,11 +293,6 @@ export function ThemeSystemProvider({ children, defaultThemeId }: ThemeSystemPro
|
|||||||
|
|
||||||
const applyVSCodeTheme = (theme: Theme) => {
|
const applyVSCodeTheme = (theme: Theme) => {
|
||||||
setVSCodeTheme(theme);
|
setVSCodeTheme(theme);
|
||||||
const variant: ThemeMode = theme.metadata.variant === 'dark' ? 'dark' : 'light';
|
|
||||||
const uiStore = useUIStore.getState();
|
|
||||||
if (uiStore.theme !== variant) {
|
|
||||||
uiStore.setTheme(variant);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleThemeEvent = (event: Event) => {
|
const handleThemeEvent = (event: Event) => {
|
||||||
@@ -270,7 +392,17 @@ export function ThemeSystemProvider({ children, defaultThemeId }: ThemeSystemPro
|
|||||||
'selectedThemeVariant',
|
'selectedThemeVariant',
|
||||||
currentTheme.metadata.variant === 'light' ? 'light' : 'dark',
|
currentTheme.metadata.variant === 'light' ? 'light' : 'dark',
|
||||||
);
|
);
|
||||||
}, [preferences, currentTheme]);
|
|
||||||
|
// Splash screen (packages/web/index.html) runs before the theme CSS vars load.
|
||||||
|
// Persist just enough to theme it on next boot.
|
||||||
|
const lightTheme = ensureThemeById(preferences.lightThemeId, 'light');
|
||||||
|
const darkTheme = ensureThemeById(preferences.darkThemeId, 'dark');
|
||||||
|
|
||||||
|
localStorage.setItem('splashBgLight', lightTheme.colors.surface.background);
|
||||||
|
localStorage.setItem('splashFgLight', lightTheme.colors.surface.foreground);
|
||||||
|
localStorage.setItem('splashBgDark', darkTheme.colors.surface.background);
|
||||||
|
localStorage.setItem('splashFgDark', darkTheme.colors.surface.foreground);
|
||||||
|
}, [preferences, currentTheme, ensureThemeById]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
void updateDesktopSettings({
|
void updateDesktopSettings({
|
||||||
@@ -304,12 +436,12 @@ export function ThemeSystemProvider({ children, defaultThemeId }: ThemeSystemPro
|
|||||||
|
|
||||||
let nextLight = prev.lightThemeId;
|
let nextLight = prev.lightThemeId;
|
||||||
if (typeof detail.lightThemeId === 'string' && detail.lightThemeId.length > 0) {
|
if (typeof detail.lightThemeId === 'string' && detail.lightThemeId.length > 0) {
|
||||||
nextLight = validateThemeId(detail.lightThemeId, 'light');
|
nextLight = detail.lightThemeId.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
let nextDark = prev.darkThemeId;
|
let nextDark = prev.darkThemeId;
|
||||||
if (typeof detail.darkThemeId === 'string' && detail.darkThemeId.length > 0) {
|
if (typeof detail.darkThemeId === 'string' && detail.darkThemeId.length > 0) {
|
||||||
nextDark = validateThemeId(detail.darkThemeId, 'dark');
|
nextDark = detail.darkThemeId.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
const same =
|
const same =
|
||||||
@@ -458,6 +590,8 @@ export function ThemeSystemProvider({ children, defaultThemeId }: ThemeSystemPro
|
|||||||
currentTheme,
|
currentTheme,
|
||||||
availableThemes,
|
availableThemes,
|
||||||
setTheme,
|
setTheme,
|
||||||
|
customThemesLoading,
|
||||||
|
reloadCustomThemes,
|
||||||
isSystemPreference: preferences.themeMode === 'system',
|
isSystemPreference: preferences.themeMode === 'system',
|
||||||
setSystemPreference: setSystemPreferenceHandler,
|
setSystemPreference: setSystemPreferenceHandler,
|
||||||
themeMode: preferences.themeMode,
|
themeMode: preferences.themeMode,
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ export interface ThemeContextValue {
|
|||||||
currentTheme: Theme;
|
currentTheme: Theme;
|
||||||
availableThemes: Theme[];
|
availableThemes: Theme[];
|
||||||
setTheme: (themeId: string) => void;
|
setTheme: (themeId: string) => void;
|
||||||
|
customThemesLoading: boolean;
|
||||||
|
reloadCustomThemes: () => Promise<void>;
|
||||||
isSystemPreference: boolean;
|
isSystemPreference: boolean;
|
||||||
setSystemPreference: (use: boolean) => void;
|
setSystemPreference: (use: boolean) => void;
|
||||||
themeMode: ThemeMode;
|
themeMode: ThemeMode;
|
||||||
|
|||||||
@@ -15,9 +15,9 @@ textarea[data-chat-input="true"] {
|
|||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
textarea[data-chat-input="true"]:hover,
|
||||||
textarea[data-chat-input="true"]:focus,
|
textarea[data-chat-input="true"]:focus,
|
||||||
textarea[data-chat-input="true"]:focus-visible,
|
textarea[data-chat-input="true"]:focus-visible {
|
||||||
textarea[data-chat-input="true"]:hover {
|
|
||||||
outline: none;
|
outline: none;
|
||||||
border: none;
|
border: none;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
@@ -331,6 +331,7 @@ html:not(.dark) .chat-scroll {
|
|||||||
.overlay-scrollbar-target {
|
.overlay-scrollbar-target {
|
||||||
scrollbar-width: none !important;
|
scrollbar-width: none !important;
|
||||||
-ms-overflow-style: none !important;
|
-ms-overflow-style: none !important;
|
||||||
|
scrollbar-gutter: auto !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.overlay-scrollbar-target::-webkit-scrollbar {
|
.overlay-scrollbar-target::-webkit-scrollbar {
|
||||||
@@ -444,6 +445,9 @@ html:not(.dark) .chat-scroll {
|
|||||||
:root:not(.mobile-pointer):not(.vscode-runtime) .header-tab-label {
|
:root:not(.mobile-pointer):not(.vscode-runtime) .header-tab-label {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
:root:not(.mobile-pointer):not(.vscode-runtime) .header-tab-badge {
|
||||||
|
margin-left: -0.125rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ============================================
|
/* ============================================
|
||||||
@@ -570,6 +574,64 @@ html:not(.dark) .chat-scroll {
|
|||||||
font-size: var(--text-code);
|
font-size: var(--text-code);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Override Streamdown's hardcoded bg-muted for inline code - use theme colors instead */
|
||||||
|
.streamdown-content code[data-streamdown="inline-code"] {
|
||||||
|
background-color: var(--markdown-inline-code-bg, var(--surface-muted)) !important;
|
||||||
|
color: var(--markdown-inline-code, var(--foreground)) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Markdown headings - use theme colors */
|
||||||
|
.streamdown-content h1 {
|
||||||
|
color: var(--markdown-heading1, var(--primary));
|
||||||
|
}
|
||||||
|
|
||||||
|
.streamdown-content h2 {
|
||||||
|
color: var(--markdown-heading2, var(--primary));
|
||||||
|
}
|
||||||
|
|
||||||
|
.streamdown-content h3 {
|
||||||
|
color: var(--markdown-heading3, var(--primary));
|
||||||
|
}
|
||||||
|
|
||||||
|
.streamdown-content h4,
|
||||||
|
.streamdown-content h5,
|
||||||
|
.streamdown-content h6 {
|
||||||
|
color: var(--markdown-heading4, var(--foreground));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Markdown links - use theme colors */
|
||||||
|
.streamdown-content a {
|
||||||
|
color: var(--markdown-link, var(--primary));
|
||||||
|
}
|
||||||
|
|
||||||
|
.streamdown-content a:hover {
|
||||||
|
color: var(--markdown-link-hover, var(--primary));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Markdown blockquote - use theme colors */
|
||||||
|
.streamdown-content blockquote {
|
||||||
|
color: var(--markdown-blockquote, var(--muted-foreground));
|
||||||
|
border-left-color: var(--markdown-blockquote-border, var(--border));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Markdown horizontal rule - use theme colors */
|
||||||
|
.streamdown-content hr {
|
||||||
|
border-color: var(--markdown-hr, var(--border));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Markdown bold/italic/strikethrough - use theme colors */
|
||||||
|
.streamdown-content strong {
|
||||||
|
color: var(--markdown-bold, var(--foreground));
|
||||||
|
}
|
||||||
|
|
||||||
|
.streamdown-content em {
|
||||||
|
color: var(--markdown-italic, var(--foreground));
|
||||||
|
}
|
||||||
|
|
||||||
|
.streamdown-content del {
|
||||||
|
color: var(--markdown-strikethrough, var(--muted-foreground));
|
||||||
|
}
|
||||||
|
|
||||||
/* Streamdown code blocks: ensure light/dark Shiki vars work even when Tailwind doesn't scan Streamdown's internal class names. */
|
/* Streamdown code blocks: ensure light/dark Shiki vars work even when Tailwind doesn't scan Streamdown's internal class names. */
|
||||||
.streamdown-content [data-streamdown="code-block-body"] {
|
.streamdown-content [data-streamdown="code-block-body"] {
|
||||||
font-size: var(--text-code);
|
font-size: var(--text-code);
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
import { registerCustomTheme } from '@pierre/diffs';
|
||||||
|
|
||||||
|
import type { Theme } from '@/types/theme';
|
||||||
|
import type { VSCodeTextMateTheme, VSCodeTokenColorRule } from './vscodeTextMateTheme';
|
||||||
|
import { buildTextMateThemeFromAppTheme } from './textMateThemeFromAppTheme';
|
||||||
|
|
||||||
|
export type ShikiThemeRegistrationResolvedLike = VSCodeTextMateTheme & {
|
||||||
|
settings: VSCodeTokenColorRule[];
|
||||||
|
fg: string;
|
||||||
|
bg: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isHex8 = (value: string): boolean => /^#[0-9a-fA-F]{8}$/.test(value);
|
||||||
|
|
||||||
|
const stripAlpha = (value: string): string => {
|
||||||
|
if (isHex8(value)) {
|
||||||
|
return value.slice(0, 7);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
|
||||||
|
function withStableStringId<T extends object>(value: T, id: string): T {
|
||||||
|
Object.defineProperty(value, 'toString', {
|
||||||
|
value: () => id,
|
||||||
|
enumerable: false,
|
||||||
|
configurable: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.defineProperty(value, Symbol.toPrimitive, {
|
||||||
|
value: () => id,
|
||||||
|
enumerable: false,
|
||||||
|
configurable: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolvedThemeCache = new Map<string, ShikiThemeRegistrationResolvedLike>();
|
||||||
|
const registeredPierreThemes = new Set<string>();
|
||||||
|
|
||||||
|
const toResolvedTheme = (raw: VSCodeTextMateTheme, id: string): ShikiThemeRegistrationResolvedLike => {
|
||||||
|
const bgRaw = raw.colors?.['editor.background'];
|
||||||
|
const fgRaw = raw.colors?.['editor.foreground'];
|
||||||
|
|
||||||
|
const bg = bgRaw ? stripAlpha(bgRaw) : undefined;
|
||||||
|
const fg = fgRaw ? stripAlpha(fgRaw) : undefined;
|
||||||
|
|
||||||
|
if (!bg || !fg) {
|
||||||
|
throw new Error(`Theme "${id}" is missing editor.background/editor.foreground`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const settings = raw.tokenColors ?? [];
|
||||||
|
|
||||||
|
return withStableStringId(
|
||||||
|
{
|
||||||
|
...raw,
|
||||||
|
name: id,
|
||||||
|
fg,
|
||||||
|
bg,
|
||||||
|
settings,
|
||||||
|
},
|
||||||
|
id,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildTextMateTheme = (theme: Theme): VSCodeTextMateTheme => {
|
||||||
|
return buildTextMateThemeFromAppTheme(theme);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getResolvedShikiTheme = (theme: Theme): ShikiThemeRegistrationResolvedLike => {
|
||||||
|
const cached = resolvedThemeCache.get(theme.metadata.id);
|
||||||
|
if (cached) {
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
const raw = buildTextMateTheme(theme);
|
||||||
|
const resolved = toResolvedTheme(raw, theme.metadata.id);
|
||||||
|
resolvedThemeCache.set(theme.metadata.id, resolved);
|
||||||
|
return resolved;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ensurePierreThemeRegistered = (theme: Theme): void => {
|
||||||
|
const id = theme.metadata.id;
|
||||||
|
if (registeredPierreThemes.has(id)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolved = getResolvedShikiTheme(theme);
|
||||||
|
registerCustomTheme(id, async () => resolved);
|
||||||
|
registeredPierreThemes.add(id);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getStreamdownThemePair = (light: Theme, dark: Theme) => {
|
||||||
|
return [getResolvedShikiTheme(light), getResolvedShikiTheme(dark)] as const;
|
||||||
|
};
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
import flexokiDarkRawJson from './themes/flexoki-dark.json';
|
|
||||||
import flexokiLightRawJson from './themes/flexoki-light.json';
|
|
||||||
|
|
||||||
export const FLEXOKI_SHIKI_DARK_THEME_NAME = 'flexoki-dark';
|
|
||||||
export const FLEXOKI_SHIKI_LIGHT_THEME_NAME = 'flexoki-light';
|
|
||||||
|
|
||||||
type VSCodeTokenColorRule = {
|
|
||||||
name?: string;
|
|
||||||
scope?: string | string[];
|
|
||||||
settings: Record<string, string | undefined>;
|
|
||||||
};
|
|
||||||
|
|
||||||
type VSCodeTextMateTheme = {
|
|
||||||
name: string;
|
|
||||||
type: 'dark' | 'light';
|
|
||||||
colors?: Record<string, string>;
|
|
||||||
tokenColors?: VSCodeTokenColorRule[];
|
|
||||||
semanticHighlighting?: boolean;
|
|
||||||
semanticTokenColors?: Record<string, string>;
|
|
||||||
};
|
|
||||||
|
|
||||||
type ShikiThemeRegistrationResolvedLike = VSCodeTextMateTheme & {
|
|
||||||
settings: VSCodeTokenColorRule[];
|
|
||||||
fg: string;
|
|
||||||
bg: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const flexokiDarkRaw = flexokiDarkRawJson as VSCodeTextMateTheme;
|
|
||||||
const flexokiLightRaw = flexokiLightRawJson as VSCodeTextMateTheme;
|
|
||||||
|
|
||||||
function withStableStringId<T extends object>(value: T, id: string): T {
|
|
||||||
Object.defineProperty(value, 'toString', {
|
|
||||||
value: () => id,
|
|
||||||
enumerable: false,
|
|
||||||
configurable: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
Object.defineProperty(value, Symbol.toPrimitive, {
|
|
||||||
value: () => id,
|
|
||||||
enumerable: false,
|
|
||||||
configurable: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
function toResolvedTheme(
|
|
||||||
raw: VSCodeTextMateTheme,
|
|
||||||
name: string,
|
|
||||||
type: VSCodeTextMateTheme['type']
|
|
||||||
): ShikiThemeRegistrationResolvedLike {
|
|
||||||
const bg = raw.colors?.['editor.background'];
|
|
||||||
const fg = raw.colors?.['editor.foreground'];
|
|
||||||
|
|
||||||
if (!bg || !fg) {
|
|
||||||
throw new Error(
|
|
||||||
`Flexoki Shiki theme "${name}" is missing editor.background/editor.foreground`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const settings = raw.tokenColors ?? [];
|
|
||||||
|
|
||||||
return {
|
|
||||||
...raw,
|
|
||||||
name,
|
|
||||||
type,
|
|
||||||
fg,
|
|
||||||
bg,
|
|
||||||
settings,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export const flexokiDarkTheme = toResolvedTheme(
|
|
||||||
flexokiDarkRaw,
|
|
||||||
FLEXOKI_SHIKI_DARK_THEME_NAME,
|
|
||||||
'dark'
|
|
||||||
);
|
|
||||||
|
|
||||||
export const flexokiLightTheme = toResolvedTheme(
|
|
||||||
flexokiLightRaw,
|
|
||||||
FLEXOKI_SHIKI_LIGHT_THEME_NAME,
|
|
||||||
'light'
|
|
||||||
);
|
|
||||||
|
|
||||||
withStableStringId(flexokiDarkTheme, FLEXOKI_SHIKI_DARK_THEME_NAME);
|
|
||||||
withStableStringId(flexokiLightTheme, FLEXOKI_SHIKI_LIGHT_THEME_NAME);
|
|
||||||
|
|
||||||
export const flexokiThemeNames = {
|
|
||||||
dark: FLEXOKI_SHIKI_DARK_THEME_NAME,
|
|
||||||
light: FLEXOKI_SHIKI_LIGHT_THEME_NAME,
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
export const flexokiStreamdownThemes = [
|
|
||||||
flexokiLightTheme,
|
|
||||||
flexokiDarkTheme,
|
|
||||||
] as const;
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
import { registerCustomTheme } from '@pierre/diffs';
|
|
||||||
|
|
||||||
import { flexokiDarkTheme, flexokiLightTheme, flexokiThemeNames } from './flexokiThemes';
|
|
||||||
|
|
||||||
let hasRegistered = false;
|
|
||||||
|
|
||||||
export function ensureFlexokiThemesRegistered(): void {
|
|
||||||
if (hasRegistered) return;
|
|
||||||
|
|
||||||
registerCustomTheme(flexokiThemeNames.dark, async () => flexokiDarkTheme);
|
|
||||||
registerCustomTheme(flexokiThemeNames.light, async () => flexokiLightTheme);
|
|
||||||
|
|
||||||
hasRegistered = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -0,0 +1,482 @@
|
|||||||
|
import type { Theme } from '@/types/theme';
|
||||||
|
|
||||||
|
import type { VSCodeTextMateTheme, VSCodeTokenColorRule } from './vscodeTextMateTheme';
|
||||||
|
|
||||||
|
const isHex6 = (value: string): boolean => /^#[0-9a-fA-F]{6}$/.test(value);
|
||||||
|
const isHex8 = (value: string): boolean => /^#[0-9a-fA-F]{8}$/.test(value);
|
||||||
|
|
||||||
|
const addAlpha = (value: string, alphaHex: string): string => {
|
||||||
|
if (!/^[0-9a-fA-F]{2}$/.test(alphaHex)) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isHex6(value)) {
|
||||||
|
return `${value}${alphaHex}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isHex8(value)) {
|
||||||
|
return `${value.slice(0, 7)}${alphaHex}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
|
||||||
|
const pick = (value: string | undefined, fallback: string): string => {
|
||||||
|
if (typeof value === 'string' && value.trim().length > 0) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
return fallback;
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildTokenColors = (theme: Theme): VSCodeTokenColorRule[] => {
|
||||||
|
const base = theme.colors.syntax.base;
|
||||||
|
const tokens = theme.colors.syntax.tokens ?? {};
|
||||||
|
|
||||||
|
const t = (key: string, fallback: string): string => pick(tokens[key], fallback);
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: 'plain',
|
||||||
|
scope: ['source', 'support.type.property-name.css'],
|
||||||
|
settings: { foreground: base.foreground },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'classes',
|
||||||
|
scope: ['entity.name.type.class'],
|
||||||
|
settings: { foreground: t('className', t('class', base.function)) },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'interfaces',
|
||||||
|
scope: ['entity.name.type.interface', 'entity.name.type'],
|
||||||
|
settings: { foreground: t('interface', base.type) },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'structs',
|
||||||
|
scope: ['entity.name.type.struct'],
|
||||||
|
settings: { foreground: t('struct', base.function) },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'enums',
|
||||||
|
scope: ['entity.name.type.enum'],
|
||||||
|
settings: { foreground: t('enum', base.function) },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'keys',
|
||||||
|
scope: ['meta.object-literal.key', 'support.type.property-name'],
|
||||||
|
settings: { foreground: t('key', base.function) },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'methods',
|
||||||
|
scope: ['entity.name.function.method', 'meta.function.method'],
|
||||||
|
settings: { foreground: t('method', theme.colors.status.success) },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'functions',
|
||||||
|
scope: ['entity.name.function', 'support.function', 'meta.function-call.generic'],
|
||||||
|
settings: { foreground: base.function, fontStyle: 'bold' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'variables',
|
||||||
|
scope: ['variable', 'meta.variable', 'variable.other.object.property'],
|
||||||
|
settings: { foreground: base.variable },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'variablesOther',
|
||||||
|
scope: ['variable.other.object', 'variable.other.readwrite.alias'],
|
||||||
|
settings: { foreground: t('variableOther', t('method', theme.colors.status.success)) },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'globalVariables',
|
||||||
|
scope: ['variable.other.global', 'variable.language.this'],
|
||||||
|
settings: { foreground: t('variableGlobal', base.number) },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'localVariables',
|
||||||
|
scope: ['variable.other.local'],
|
||||||
|
settings: { foreground: t('variableLocal', theme.colors.surface.elevated) },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'parameters',
|
||||||
|
scope: ['variable.parameter', 'meta.parameter'],
|
||||||
|
settings: { foreground: t('parameter', base.foreground) },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'properties',
|
||||||
|
scope: ['variable.other.property', 'meta.property'],
|
||||||
|
settings: { foreground: t('variableProperty', theme.colors.status.info) },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'strings',
|
||||||
|
scope: ['string', 'string.other.link', 'markup.inline.raw.string.markdown'],
|
||||||
|
settings: { foreground: base.string },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'stringEscapeSequences',
|
||||||
|
scope: ['constant.character.escape', 'constant.other.placeholder'],
|
||||||
|
settings: { foreground: t('stringEscape', base.foreground) },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'keywords',
|
||||||
|
scope: ['keyword'],
|
||||||
|
settings: { foreground: base.keyword },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'keywordsControl',
|
||||||
|
scope: ['keyword.control.import', 'keyword.control.from', 'keyword.import'],
|
||||||
|
settings: { foreground: t('keywordImport', base.operator) },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'storageModifiers',
|
||||||
|
scope: ['storage.modifier', 'keyword.modifier', 'storage.type'],
|
||||||
|
settings: { foreground: t('storageModifier', base.keyword) },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'comments',
|
||||||
|
scope: ['comment', 'punctuation.definition.comment'],
|
||||||
|
settings: { foreground: base.comment },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'docComments',
|
||||||
|
scope: ['comment.documentation', 'comment.line.documentation'],
|
||||||
|
settings: { foreground: t('commentDoc', theme.colors.surface.mutedForeground) },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'numbers',
|
||||||
|
scope: ['constant.numeric'],
|
||||||
|
settings: { foreground: base.number },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'booleans',
|
||||||
|
scope: ['constant.language.boolean', 'constant.language.json'],
|
||||||
|
settings: { foreground: t('boolean', base.type) },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'operators',
|
||||||
|
scope: ['keyword.operator'],
|
||||||
|
settings: { foreground: base.operator },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'macros',
|
||||||
|
scope: ['entity.name.function.preprocessor', 'meta.preprocessor'],
|
||||||
|
settings: { foreground: t('macro', base.keyword) },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'preprocessor',
|
||||||
|
scope: ['meta.preprocessor'],
|
||||||
|
settings: { foreground: t('preprocessor', t('label', base.number)) },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'urls',
|
||||||
|
scope: ['markup.underline.link'],
|
||||||
|
settings: { foreground: t('url', theme.colors.status.info) },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'tags',
|
||||||
|
scope: ['entity.name.tag'],
|
||||||
|
settings: { foreground: t('tag', base.keyword) },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'jsxTags',
|
||||||
|
scope: ['support.class.component'],
|
||||||
|
settings: { foreground: t('jsxTag', t('label', base.number)) },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'attributes',
|
||||||
|
scope: ['entity.other.attribute-name', 'meta.attribute'],
|
||||||
|
settings: { foreground: t('tagAttribute', base.type) },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'types',
|
||||||
|
scope: ['support.type'],
|
||||||
|
settings: { foreground: base.type },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'constants',
|
||||||
|
scope: ['variable.other.constant', 'variable.readonly'],
|
||||||
|
settings: { foreground: t('constant', base.foreground) },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'labels',
|
||||||
|
scope: ['entity.name.label', 'punctuation.definition.label'],
|
||||||
|
settings: { foreground: t('label', t('variableGlobal', base.number)) },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'namespaces',
|
||||||
|
scope: ['entity.name.namespace', 'storage.modifier.namespace', 'markup.bold.markdown'],
|
||||||
|
settings: { foreground: t('namespace', base.type) },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'modules',
|
||||||
|
scope: ['entity.name.module', 'storage.modifier.module'],
|
||||||
|
settings: { foreground: t('module', base.operator) },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'typeParameters',
|
||||||
|
scope: ['variable.type.parameter', 'variable.parameter.type'],
|
||||||
|
settings: { foreground: t('typeParameter', base.function) },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'exceptions',
|
||||||
|
scope: ['keyword.control.exception', 'keyword.control.trycatch'],
|
||||||
|
settings: { foreground: t('exception', t('label', base.number)) },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'decorators',
|
||||||
|
scope: ['meta.decorator', 'punctuation.decorator', 'entity.name.function.decorator'],
|
||||||
|
settings: { foreground: t('decorator', base.type) },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'calls',
|
||||||
|
scope: ['variable.function'],
|
||||||
|
settings: { foreground: base.foreground },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'punctuation',
|
||||||
|
scope: [
|
||||||
|
'punctuation',
|
||||||
|
'punctuation.terminator',
|
||||||
|
'punctuation.definition.tag',
|
||||||
|
'punctuation.separator',
|
||||||
|
'punctuation.definition.string',
|
||||||
|
'punctuation.section.block',
|
||||||
|
],
|
||||||
|
settings: { foreground: t('punctuation', base.comment) },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'yellow',
|
||||||
|
scope: [
|
||||||
|
'storage.type.numeric.go',
|
||||||
|
'storage.type.byte.go',
|
||||||
|
'storage.type.boolean.go',
|
||||||
|
'storage.type.string.go',
|
||||||
|
'storage.type.uintptr.go',
|
||||||
|
'storage.type.error.go',
|
||||||
|
'storage.type.rune.go',
|
||||||
|
'constant.language.go',
|
||||||
|
'support.class.dart',
|
||||||
|
'keyword.other.documentation',
|
||||||
|
'storage.modifier.import.java',
|
||||||
|
'punctuation.definition.list.begin.markdown',
|
||||||
|
'punctuation.definition.quote.begin.markdown',
|
||||||
|
'meta.separator.markdown',
|
||||||
|
'entity.name.section.markdown',
|
||||||
|
],
|
||||||
|
settings: { foreground: base.type },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'green',
|
||||||
|
scope: [],
|
||||||
|
settings: { foreground: t('method', theme.colors.status.success) },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'cyan',
|
||||||
|
scope: [
|
||||||
|
'markup.italic.markdown',
|
||||||
|
'support.type.python',
|
||||||
|
'variable.legacy.builtin.python',
|
||||||
|
'support.constant.property-value.css',
|
||||||
|
'storage.modifier.attribute.swift',
|
||||||
|
],
|
||||||
|
settings: { foreground: base.string },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'blue',
|
||||||
|
scope: [],
|
||||||
|
settings: { foreground: base.keyword },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'purple',
|
||||||
|
scope: ['keyword.channel.go', 'keyword.other.platform.os.swift'],
|
||||||
|
settings: { foreground: base.number },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'magenta',
|
||||||
|
scope: ['punctuation.definition.heading.markdown'],
|
||||||
|
settings: { foreground: t('label', t('variableGlobal', base.number)) },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'red',
|
||||||
|
scope: [],
|
||||||
|
settings: { foreground: base.operator },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'orange',
|
||||||
|
scope: [],
|
||||||
|
settings: { foreground: base.function },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildColors = (theme: Theme): Record<string, string> => {
|
||||||
|
const s = theme.colors.surface;
|
||||||
|
const i = theme.colors.interactive;
|
||||||
|
const st = theme.colors.status;
|
||||||
|
const base = theme.colors.syntax.base;
|
||||||
|
const hl = theme.colors.syntax.highlights ?? {};
|
||||||
|
|
||||||
|
const diffAddedBg = pick(hl.diffAddedBackground, st.successBackground);
|
||||||
|
const diffRemovedBg = pick(hl.diffRemovedBackground, st.errorBackground);
|
||||||
|
|
||||||
|
return {
|
||||||
|
'editor.background': s.background,
|
||||||
|
'editor.foreground': s.foreground,
|
||||||
|
'editor.hoverHighlightBackground': pick(i.hover, s.subtle),
|
||||||
|
'editor.lineHighlightBackground': s.muted,
|
||||||
|
'editor.selectionBackground': i.selection,
|
||||||
|
'editor.selectionHighlightBackground': i.selection,
|
||||||
|
'editor.findMatchBackground': base.type,
|
||||||
|
'editor.findMatchHighlightBackground': addAlpha(base.type, 'cc'),
|
||||||
|
'editor.findRangeHighlightBackground': s.muted,
|
||||||
|
'editor.inactiveSelectionBackground': s.elevated,
|
||||||
|
'editor.lineHighlightBorder': s.elevated,
|
||||||
|
'editor.rangeHighlightBackground': pick(i.active, i.borderHover),
|
||||||
|
'notifications.background': s.elevated,
|
||||||
|
'editorInlayHint.typeBackground': s.subtle,
|
||||||
|
'editorInlayHint.typeForeground': s.foreground,
|
||||||
|
'editorWhitespace.foreground': i.borderHover,
|
||||||
|
'editorIndentGuide.background1': s.subtle,
|
||||||
|
'editorHoverWidget.background': s.elevated,
|
||||||
|
'editorLineNumber.activeForeground': pick(hl.lineNumberActive, s.foreground),
|
||||||
|
'editorLineNumber.foreground': pick(hl.lineNumber, s.mutedForeground),
|
||||||
|
'editorGutter.background': s.background,
|
||||||
|
'editorGutter.modifiedBackground': st.info,
|
||||||
|
'editorGutter.addedBackground': st.success,
|
||||||
|
'editorGutter.deletedBackground': st.error,
|
||||||
|
'editorBracketMatch.background': s.elevated,
|
||||||
|
'editorBracketMatch.border': s.subtle,
|
||||||
|
'editorError.foreground': st.error,
|
||||||
|
'editorWarning.foreground': st.warning,
|
||||||
|
'editorInfo.foreground': st.info,
|
||||||
|
'diffEditor.insertedTextBackground': diffAddedBg,
|
||||||
|
'diffEditor.removedTextBackground': diffRemovedBg,
|
||||||
|
'editorGroupHeader.tabsBackground': s.background,
|
||||||
|
'editorGroup.border': i.border,
|
||||||
|
'tab.activeBackground': s.background,
|
||||||
|
'tab.inactiveBackground': s.muted,
|
||||||
|
'tab.inactiveForeground': s.mutedForeground,
|
||||||
|
'tab.activeForeground': s.foreground,
|
||||||
|
'tab.hoverBackground': s.subtle,
|
||||||
|
'tab.unfocusedHoverBackground': s.subtle,
|
||||||
|
'tab.border': i.border,
|
||||||
|
'tab.activeModifiedBorder': base.type,
|
||||||
|
'tab.inactiveModifiedBorder': st.info,
|
||||||
|
'tab.unfocusedActiveModifiedBorder': base.type,
|
||||||
|
'tab.unfocusedInactiveModifiedBorder': st.info,
|
||||||
|
'editorWidget.background': s.muted,
|
||||||
|
'editorWidget.border': i.border,
|
||||||
|
'editorSuggestWidget.background': s.background,
|
||||||
|
'editorSuggestWidget.border': i.border,
|
||||||
|
'editorSuggestWidget.foreground': s.foreground,
|
||||||
|
'editorSuggestWidget.highlightForeground': s.mutedForeground,
|
||||||
|
'editorSuggestWidget.selectedBackground': s.subtle,
|
||||||
|
'peekView.border': i.border,
|
||||||
|
'peekViewEditor.background': s.background,
|
||||||
|
'peekViewEditor.matchHighlightBackground': s.subtle,
|
||||||
|
'peekViewResult.background': s.muted,
|
||||||
|
'peekViewResult.fileForeground': s.foreground,
|
||||||
|
'peekViewResult.lineForeground': s.mutedForeground,
|
||||||
|
'peekViewResult.matchHighlightBackground': s.subtle,
|
||||||
|
'peekViewResult.selectionBackground': s.elevated,
|
||||||
|
'peekViewResult.selectionForeground': s.mutedForeground,
|
||||||
|
'peekViewTitle.background': s.subtle,
|
||||||
|
'peekViewTitleDescription.foreground': s.mutedForeground,
|
||||||
|
'peekViewTitleLabel.foreground': s.foreground,
|
||||||
|
'merge.currentHeaderBackground': st.success,
|
||||||
|
'merge.currentContentBackground': pick(hl.diffAdded, st.success),
|
||||||
|
'merge.incomingHeaderBackground': st.info,
|
||||||
|
'merge.incomingContentBackground': pick(hl.diffModified, st.info),
|
||||||
|
'merge.border': i.border,
|
||||||
|
'merge.commonContentBackground': s.subtle,
|
||||||
|
'merge.commonHeaderBackground': s.muted,
|
||||||
|
'panel.background': s.background,
|
||||||
|
'panel.border': i.border,
|
||||||
|
'panelTitle.activeBorder': i.borderHover,
|
||||||
|
'panelTitle.activeForeground': s.foreground,
|
||||||
|
'panelTitle.inactiveForeground': s.mutedForeground,
|
||||||
|
'statusBar.background': s.background,
|
||||||
|
'statusBar.foreground': s.foreground,
|
||||||
|
'statusBar.border': i.border,
|
||||||
|
'statusBar.debuggingBackground': st.error,
|
||||||
|
'statusBar.debuggingForeground': st.errorForeground,
|
||||||
|
'statusBar.noFolderBackground': s.subtle,
|
||||||
|
'statusBar.noFolderForeground': s.mutedForeground,
|
||||||
|
'titleBar.activeBackground': s.background,
|
||||||
|
'titleBar.activeForeground': s.foreground,
|
||||||
|
'titleBar.inactiveBackground': s.muted,
|
||||||
|
'titleBar.inactiveForeground': s.mutedForeground,
|
||||||
|
'titleBar.border': i.border,
|
||||||
|
'menu.foreground': s.foreground,
|
||||||
|
'menu.background': s.background,
|
||||||
|
'menu.selectionForeground': s.foreground,
|
||||||
|
'menu.selectionBackground': s.subtle,
|
||||||
|
'menu.border': i.border,
|
||||||
|
'editorInlayHint.foreground': s.mutedForeground,
|
||||||
|
'editorInlayHint.background': s.subtle,
|
||||||
|
'terminal.foreground': s.foreground,
|
||||||
|
'terminal.background': s.background,
|
||||||
|
'terminalCursor.foreground': s.foreground,
|
||||||
|
'terminalCursor.background': s.background,
|
||||||
|
'terminal.ansiRed': pick(hl.diffRemoved, st.error),
|
||||||
|
'terminal.ansiGreen': pick(hl.diffAdded, st.success),
|
||||||
|
'terminal.ansiYellow': st.warning,
|
||||||
|
'terminal.ansiBlue': pick(hl.diffModified, st.info),
|
||||||
|
'terminal.ansiMagenta': base.keyword,
|
||||||
|
'terminal.ansiCyan': base.type,
|
||||||
|
'activityBar.background': s.background,
|
||||||
|
'activityBar.foreground': s.foreground,
|
||||||
|
'activityBar.inactiveForeground': s.mutedForeground,
|
||||||
|
'activityBar.activeBorder': s.foreground,
|
||||||
|
'activityBar.border': i.border,
|
||||||
|
'sideBar.background': s.background,
|
||||||
|
'sideBar.foreground': s.foreground,
|
||||||
|
'sideBar.border': i.border,
|
||||||
|
'sideBarTitle.foreground': s.foreground,
|
||||||
|
'sideBarSectionHeader.background': s.muted,
|
||||||
|
'sideBarSectionHeader.foreground': s.foreground,
|
||||||
|
'sideBarSectionHeader.border': i.border,
|
||||||
|
'sideBar.activeBackground': s.subtle,
|
||||||
|
'sideBar.activeForeground': s.foreground,
|
||||||
|
'sideBar.hoverBackground': s.muted,
|
||||||
|
'sideBar.hoverForeground': s.foreground,
|
||||||
|
'list.warningForeground': st.warning,
|
||||||
|
'list.errorForeground': st.error,
|
||||||
|
'list.inactiveSelectionBackground': s.subtle,
|
||||||
|
'list.activeSelectionBackground': s.elevated,
|
||||||
|
'list.inactiveSelectionForeground': s.foreground,
|
||||||
|
'list.activeSelectionForeground': s.foreground,
|
||||||
|
'list.hoverForeground': s.foreground,
|
||||||
|
'list.hoverBackground': s.muted,
|
||||||
|
'input.background': s.muted,
|
||||||
|
'input.foreground': s.foreground,
|
||||||
|
'input.border': i.border,
|
||||||
|
'input.placeholderForeground': s.mutedForeground,
|
||||||
|
'inputOption.activeBorder': i.border,
|
||||||
|
'inputOption.activeBackground': s.elevated,
|
||||||
|
'inputOption.activeForeground': s.foreground,
|
||||||
|
'inputValidation.infoBackground': st.infoBackground,
|
||||||
|
'inputValidation.infoBorder': st.infoBorder,
|
||||||
|
'inputValidation.warningBackground': st.warningBackground,
|
||||||
|
'inputValidation.warningBorder': st.warningBorder,
|
||||||
|
'inputValidation.errorBackground': st.errorBackground,
|
||||||
|
'inputValidation.errorBorder': st.errorBorder,
|
||||||
|
'dropdown.background': s.muted,
|
||||||
|
'dropdown.foreground': s.foreground,
|
||||||
|
'dropdown.border': i.border,
|
||||||
|
'dropdown.listBackground': s.background,
|
||||||
|
'badge.background': theme.colors.primary.base,
|
||||||
|
'activityBarBadge.background': theme.colors.primary.base,
|
||||||
|
'button.background': theme.colors.primary.base,
|
||||||
|
'button.foreground': pick(theme.colors.primary.foreground, s.background),
|
||||||
|
'badge.foreground': pick(theme.colors.primary.foreground, s.background),
|
||||||
|
'activityBarBadge.foreground': pick(theme.colors.primary.foreground, s.background),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function buildTextMateThemeFromAppTheme(theme: Theme): VSCodeTextMateTheme {
|
||||||
|
return {
|
||||||
|
name: theme.metadata.name,
|
||||||
|
type: theme.metadata.variant,
|
||||||
|
colors: buildColors(theme),
|
||||||
|
tokenColors: buildTokenColors(theme),
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,547 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "Flexoki",
|
|
||||||
"type": "dark",
|
|
||||||
"colors": {
|
|
||||||
"editor.background": "#100F0F",
|
|
||||||
"editor.foreground": "#CECDC3",
|
|
||||||
"editor.hoverHighlightBackground": "#343331",
|
|
||||||
"editor.lineHighlightBackground": "#1C1B1A",
|
|
||||||
"editor.selectionBackground": "#CECDC333",
|
|
||||||
"editor.selectionHighlightBackground": "#CECDC333",
|
|
||||||
"editor.findMatchBackground": "#AD8301",
|
|
||||||
"editor.findMatchHighlightBackground": "#AD8301cc",
|
|
||||||
"editor.findRangeHighlightBackground": "#1C1B1A",
|
|
||||||
"editor.inactiveSelectionBackground": "#282726",
|
|
||||||
"editor.lineHighlightBorder": "#282726",
|
|
||||||
"editor.rangeHighlightBackground": "#403E3C",
|
|
||||||
"notifications.background": "#282726",
|
|
||||||
"editorInlayHint.typeBackground": "#343331",
|
|
||||||
"editorInlayHint.typeForeground": "#CECDC3",
|
|
||||||
"editorWhitespace.foreground": "#403E3C",
|
|
||||||
"editorIndentGuide.background1": "#343331",
|
|
||||||
"editorHoverWidget.background": "#282726",
|
|
||||||
"editorLineNumber.activeForeground": "#CECDC3",
|
|
||||||
"editorLineNumber.foreground": "#403E3C",
|
|
||||||
"editorGutter.background": "#100F0F",
|
|
||||||
"editorGutter.modifiedBackground": "#3AA99F",
|
|
||||||
"editorGutter.addedBackground": "#879A39",
|
|
||||||
"editorGutter.deletedBackground": "#D14D41",
|
|
||||||
"editorBracketMatch.background": "#282726",
|
|
||||||
"editorBracketMatch.border": "#343331",
|
|
||||||
"editorError.foreground": "#D14D41",
|
|
||||||
"editorWarning.foreground": "#DA702C",
|
|
||||||
"editorInfo.foreground": "#4385BE",
|
|
||||||
"diffEditor.insertedTextBackground": "#66800B99",
|
|
||||||
"diffEditor.removedTextBackground": "#AF302999",
|
|
||||||
"editorGroupHeader.tabsBackground": "#100F0F",
|
|
||||||
"editorGroup.border": "#343331",
|
|
||||||
"tab.activeBackground": "#100F0F",
|
|
||||||
"tab.inactiveBackground": "#1C1B1A",
|
|
||||||
"tab.inactiveForeground": "#878580",
|
|
||||||
"tab.activeForeground": "#CECDC3",
|
|
||||||
"tab.hoverBackground": "#343331",
|
|
||||||
"tab.unfocusedHoverBackground": "#343331",
|
|
||||||
"tab.border": "#343331",
|
|
||||||
"tab.activeModifiedBorder": "#D0A215",
|
|
||||||
"tab.inactiveModifiedBorder": "#4385BE",
|
|
||||||
"tab.unfocusedActiveModifiedBorder": "#AD8301",
|
|
||||||
"tab.unfocusedInactiveModifiedBorder": "#205EA6",
|
|
||||||
"editorWidget.background": "#1C1B1A",
|
|
||||||
"editorWidget.border": "#343331",
|
|
||||||
"editorSuggestWidget.background": "#100F0F",
|
|
||||||
"editorSuggestWidget.border": "#343331",
|
|
||||||
"editorSuggestWidget.foreground": "#CECDC3",
|
|
||||||
"editorSuggestWidget.highlightForeground": "#878580",
|
|
||||||
"editorSuggestWidget.selectedBackground": "#343331",
|
|
||||||
"peekView.border": "#343331",
|
|
||||||
"peekViewEditor.background": "#100F0F",
|
|
||||||
"peekViewEditor.matchHighlightBackground": "#403E3C",
|
|
||||||
"peekViewResult.background": "#1C1B1A",
|
|
||||||
"peekViewResult.fileForeground": "#CECDC3",
|
|
||||||
"peekViewResult.lineForeground": "#878580",
|
|
||||||
"peekViewResult.matchHighlightBackground": "#403E3C",
|
|
||||||
"peekViewResult.selectionBackground": "#282726",
|
|
||||||
"peekViewResult.selectionForeground": "#575653",
|
|
||||||
"peekViewTitle.background": "#343331",
|
|
||||||
"peekViewTitleDescription.foreground": "#878580",
|
|
||||||
"peekViewTitleLabel.foreground": "#CECDC3",
|
|
||||||
"merge.currentHeaderBackground": "#879A39",
|
|
||||||
"merge.currentContentBackground": "#66800B",
|
|
||||||
"merge.incomingHeaderBackground": "#3AA99F",
|
|
||||||
"merge.incomingContentBackground": "#24837B",
|
|
||||||
"merge.border": "#343331",
|
|
||||||
"merge.commonContentBackground": "#403E3C",
|
|
||||||
"merge.commonHeaderBackground": "#343331",
|
|
||||||
"panel.background": "#100F0F",
|
|
||||||
"panel.border": "#343331",
|
|
||||||
"panelTitle.activeBorder": "#403E3C",
|
|
||||||
"panelTitle.activeForeground": "#CECDC3",
|
|
||||||
"panelTitle.inactiveForeground": "#878580",
|
|
||||||
"statusBar.background": "#100F0F",
|
|
||||||
"statusBar.foreground": "#CECDC3",
|
|
||||||
"statusBar.border": "#343331",
|
|
||||||
"statusBar.debuggingBackground": "#D14D41",
|
|
||||||
"statusBar.debuggingForeground": "#CECDC3",
|
|
||||||
"statusBar.noFolderBackground": "#403E3C",
|
|
||||||
"statusBar.noFolderForeground": "#575653",
|
|
||||||
"titleBar.activeBackground": "#100F0F",
|
|
||||||
"titleBar.activeForeground": "#CECDC3",
|
|
||||||
"titleBar.inactiveBackground": "#1C1B1A",
|
|
||||||
"titleBar.inactiveForeground": "#878580",
|
|
||||||
"titleBar.border": "#343331",
|
|
||||||
"menu.foreground": "#CECDC3",
|
|
||||||
"menu.background": "#100F0F",
|
|
||||||
"menu.selectionForeground": "#CECDC3",
|
|
||||||
"menu.selectionBackground": "#343331",
|
|
||||||
"menu.border": "#343331",
|
|
||||||
"editorInlayHint.foreground": "#878580",
|
|
||||||
"editorInlayHint.background": "#343331",
|
|
||||||
"terminal.foreground": "#CECDC3",
|
|
||||||
"terminal.background": "#100F0F",
|
|
||||||
"terminalCursor.foreground": "#CECDC3",
|
|
||||||
"terminalCursor.background": "#100F0F",
|
|
||||||
"terminal.ansiRed": "#D14D41",
|
|
||||||
"terminal.ansiGreen": "#879A39",
|
|
||||||
"terminal.ansiYellow": "#D0A215",
|
|
||||||
"terminal.ansiBlue": "#4385BE",
|
|
||||||
"terminal.ansiMagenta": "#3AA99F",
|
|
||||||
"terminal.ansiCyan": "#3AA99F",
|
|
||||||
"activityBar.background": "#100F0F",
|
|
||||||
"activityBar.foreground": "#CECDC3",
|
|
||||||
"activityBar.inactiveForeground": "#878580",
|
|
||||||
"activityBar.activeBorder": "#CECDC3",
|
|
||||||
"activityBar.border": "#343331",
|
|
||||||
"sideBar.background": "#100F0F",
|
|
||||||
"sideBar.foreground": "#CECDC3",
|
|
||||||
"sideBar.border": "#343331",
|
|
||||||
"sideBarTitle.foreground": "#CECDC3",
|
|
||||||
"sideBarSectionHeader.background": "#1C1B1A",
|
|
||||||
"sideBarSectionHeader.foreground": "#CECDC3",
|
|
||||||
"sideBarSectionHeader.border": "#343331",
|
|
||||||
"sideBar.activeBackground": "#403E3C",
|
|
||||||
"sideBar.activeForeground": "#CECDC3",
|
|
||||||
"sideBar.hoverBackground": "#343331",
|
|
||||||
"sideBar.hoverForeground": "#878580",
|
|
||||||
"sideBar.folderIcon.foreground": "#879A39",
|
|
||||||
"sideBar.fileIcon.foreground": "#4385BE",
|
|
||||||
"list.warningForeground": "#DA702C",
|
|
||||||
"list.errorForeground": "#D14D41",
|
|
||||||
"list.inactiveSelectionBackground": "#343331",
|
|
||||||
"list.activeSelectionBackground": "#403E3C",
|
|
||||||
"list.inactiveSelectionForeground": "#CECDC3",
|
|
||||||
"list.activeSelectionForeground": "#CECDC3",
|
|
||||||
"list.hoverForeground": "#CECDC3",
|
|
||||||
"list.hoverBackground": "#343331",
|
|
||||||
"input.background": "#1C1B1A",
|
|
||||||
"input.foreground": "#CECDC3",
|
|
||||||
"input.border": "#343331",
|
|
||||||
"input.placeholderForeground": "#878580",
|
|
||||||
"inputOption.activeBorder": "#343331",
|
|
||||||
"inputOption.activeBackground": "#282726",
|
|
||||||
"inputOption.activeForeground": "#CECDC3",
|
|
||||||
"inputValidation.infoBackground": "#3AA99F",
|
|
||||||
"inputValidation.infoBorder": "#24837B",
|
|
||||||
"inputValidation.warningBackground": "#DA702C",
|
|
||||||
"inputValidation.warningBorder": "#BC5215",
|
|
||||||
"inputValidation.errorBackground": "#D14D41",
|
|
||||||
"inputValidation.errorBorder": "#AF3029",
|
|
||||||
"dropdown.background": "#1C1B1A",
|
|
||||||
"dropdown.foreground": "#CECDC3",
|
|
||||||
"dropdown.border": "#343331",
|
|
||||||
"dropdown.listBackground": "#100F0F",
|
|
||||||
"badge.background": "#3AA99F",
|
|
||||||
"activityBarBadge.background": "#3AA99F",
|
|
||||||
"button.background": "#3AA99F",
|
|
||||||
"button.foreground": "#100F0F",
|
|
||||||
"badge.foreground": "#100F0F",
|
|
||||||
"activityBarBadge.foreground": "#100F0F"
|
|
||||||
},
|
|
||||||
"tokenColors": [
|
|
||||||
{
|
|
||||||
"name": "plain",
|
|
||||||
"scope": ["source", "support.type.property-name.css"],
|
|
||||||
"settings": {
|
|
||||||
"foreground": "#CECDC3"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "classes",
|
|
||||||
"scope": ["entity.name.type.class"],
|
|
||||||
"settings": {
|
|
||||||
"foreground": "#DA702C"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "interfaces",
|
|
||||||
"scope": ["entity.name.type.interface", "entity.name.type"],
|
|
||||||
"settings": {
|
|
||||||
"foreground": "#D0A215"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "structs",
|
|
||||||
"scope": ["entity.name.type.struct"],
|
|
||||||
"settings": {
|
|
||||||
"foreground": "#DA702C"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "enums",
|
|
||||||
"scope": ["entity.name.type.enum"],
|
|
||||||
"settings": {
|
|
||||||
"foreground": "#DA702C"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "keys",
|
|
||||||
"scope": ["meta.object-literal.key", "support.type.property-name"],
|
|
||||||
"settings": {
|
|
||||||
"foreground": "#DA702C"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "methods",
|
|
||||||
"scope": ["entity.name.function.method", "meta.function.method"],
|
|
||||||
"settings": {
|
|
||||||
"foreground": "#879A39"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "functions",
|
|
||||||
"scope": [
|
|
||||||
"entity.name.function",
|
|
||||||
"support.function",
|
|
||||||
"meta.function-call.generic"
|
|
||||||
],
|
|
||||||
"settings": {
|
|
||||||
"foreground": "#DA702C",
|
|
||||||
"fontStyle": "bold"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "variables",
|
|
||||||
"scope": ["variable", "meta.variable", "variable.other.object.property"],
|
|
||||||
"settings": {
|
|
||||||
"foreground": "#CECDC3"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "variablesOther",
|
|
||||||
"scope": ["variable.other.object", "variable.other.readwrite.alias"],
|
|
||||||
"settings": {
|
|
||||||
"foreground": "#879A39"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "globalVariables",
|
|
||||||
"scope": ["variable.other.global", "variable.language.this"],
|
|
||||||
"settings": {
|
|
||||||
"foreground": "#CE5D97"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "localVariables",
|
|
||||||
"scope": ["variable.other.local"],
|
|
||||||
"settings": {
|
|
||||||
"foreground": "#282726"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "parameters",
|
|
||||||
"scope": ["variable.parameter", "meta.parameter"],
|
|
||||||
"settings": {
|
|
||||||
"foreground": "#CECDC3"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "properties",
|
|
||||||
"scope": ["variable.other.property", "meta.property"],
|
|
||||||
"settings": {
|
|
||||||
"foreground": "#4385BE"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "strings",
|
|
||||||
"scope": [
|
|
||||||
"string",
|
|
||||||
"string.other.link",
|
|
||||||
"markup.inline.raw.string.markdown"
|
|
||||||
],
|
|
||||||
"settings": {
|
|
||||||
"foreground": "#3AA99F"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "stringEscapeSequences",
|
|
||||||
"scope": ["constant.character.escape", "constant.other.placeholder"],
|
|
||||||
"settings": {
|
|
||||||
"foreground": "#CECDC3"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "keywords",
|
|
||||||
"scope": ["keyword"],
|
|
||||||
"settings": {
|
|
||||||
"foreground": "#879A39"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "keywordsControl",
|
|
||||||
"scope": [
|
|
||||||
"keyword.control.import",
|
|
||||||
"keyword.control.from",
|
|
||||||
"keyword.import"
|
|
||||||
],
|
|
||||||
"settings": {
|
|
||||||
"foreground": "#D14D41"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "storageModifiers",
|
|
||||||
"scope": ["storage.modifier", "keyword.modifier", "storage.type"],
|
|
||||||
"settings": {
|
|
||||||
"foreground": "#4385BE"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "comments",
|
|
||||||
"scope": ["comment", "punctuation.definition.comment"],
|
|
||||||
"settings": {
|
|
||||||
"foreground": "#878580"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "docComments",
|
|
||||||
"scope": ["comment.documentation", "comment.line.documentation"],
|
|
||||||
"settings": {
|
|
||||||
"foreground": "#575653"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "numbers",
|
|
||||||
"scope": ["constant.numeric"],
|
|
||||||
"settings": {
|
|
||||||
"foreground": "#8B7EC8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "booleans",
|
|
||||||
"scope": ["constant.language.boolean", "constant.language.json"],
|
|
||||||
"settings": {
|
|
||||||
"foreground": "#D0A215"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "operators",
|
|
||||||
"scope": ["keyword.operator"],
|
|
||||||
"settings": {
|
|
||||||
"foreground": "#D14D41"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "macros",
|
|
||||||
"scope": ["entity.name.function.preprocessor", "meta.preprocessor"],
|
|
||||||
"settings": {
|
|
||||||
"foreground": "#4385BE"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "preprocessor",
|
|
||||||
"scope": ["meta.preprocessor"],
|
|
||||||
"settings": {
|
|
||||||
"foreground": "#CE5D97"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "urls",
|
|
||||||
"scope": ["markup.underline.link"],
|
|
||||||
"settings": {
|
|
||||||
"foreground": "#4385BE"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "tags",
|
|
||||||
"scope": ["entity.name.tag"],
|
|
||||||
"settings": {
|
|
||||||
"foreground": "#4385BE"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "jsxTags",
|
|
||||||
"scope": ["support.class.component"],
|
|
||||||
"settings": {
|
|
||||||
"foreground": "#CE5D97"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "attributes",
|
|
||||||
"scope": ["entity.other.attribute-name", "meta.attribute"],
|
|
||||||
"settings": {
|
|
||||||
"foreground": "#D0A215"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "types",
|
|
||||||
"scope": ["support.type"],
|
|
||||||
"settings": {
|
|
||||||
"foreground": "#D0A215"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "constants",
|
|
||||||
"scope": ["variable.other.constant", "variable.readonly"],
|
|
||||||
"settings": {
|
|
||||||
"foreground": "#CECDC3"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "labels",
|
|
||||||
"scope": ["entity.name.label", "punctuation.definition.label"],
|
|
||||||
"settings": {
|
|
||||||
"foreground": "#CE5D97"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "namespaces",
|
|
||||||
"scope": [
|
|
||||||
"entity.name.namespace",
|
|
||||||
"storage.modifier.namespace",
|
|
||||||
"markup.bold.markdown"
|
|
||||||
],
|
|
||||||
"settings": {
|
|
||||||
"foreground": "#D0A215"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "modules",
|
|
||||||
"scope": ["entity.name.module", "storage.modifier.module"],
|
|
||||||
"settings": {
|
|
||||||
"foreground": "#D14D41"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "typeParameters",
|
|
||||||
"scope": ["variable.type.parameter", "variable.parameter.type"],
|
|
||||||
"settings": {
|
|
||||||
"foreground": "#DA702C"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "exceptions",
|
|
||||||
"scope": ["keyword.control.exception", "keyword.control.trycatch"],
|
|
||||||
"settings": {
|
|
||||||
"foreground": "#CE5D97"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "decorators",
|
|
||||||
"scope": [
|
|
||||||
"meta.decorator",
|
|
||||||
"punctuation.decorator",
|
|
||||||
"entity.name.function.decorator"
|
|
||||||
],
|
|
||||||
"settings": {
|
|
||||||
"foreground": "#D0A215"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "calls",
|
|
||||||
"scope": ["variable.function"],
|
|
||||||
"settings": {
|
|
||||||
"foreground": "#CECDC3"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "punctuation",
|
|
||||||
"scope": [
|
|
||||||
"punctuation",
|
|
||||||
"punctuation.terminator",
|
|
||||||
"punctuation.definition.tag",
|
|
||||||
"punctuation.separator",
|
|
||||||
"punctuation.definition.string",
|
|
||||||
"punctuation.section.block"
|
|
||||||
],
|
|
||||||
"settings": {
|
|
||||||
"foreground": "#878580"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "yellow",
|
|
||||||
"scope": [
|
|
||||||
"storage.type.numeric.go",
|
|
||||||
"storage.type.byte.go",
|
|
||||||
"storage.type.boolean.go",
|
|
||||||
"storage.type.string.go",
|
|
||||||
"storage.type.uintptr.go",
|
|
||||||
"storage.type.error.go",
|
|
||||||
"storage.type.rune.go",
|
|
||||||
"constant.language.go",
|
|
||||||
"support.class.dart",
|
|
||||||
"keyword.other.documentation",
|
|
||||||
"storage.modifier.import.java",
|
|
||||||
"punctuation.definition.list.begin.markdown",
|
|
||||||
"punctuation.definition.quote.begin.markdown",
|
|
||||||
"meta.separator.markdown",
|
|
||||||
"entity.name.section.markdown"
|
|
||||||
],
|
|
||||||
"settings": {
|
|
||||||
"foreground": "#D0A215"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "green",
|
|
||||||
"scope": [],
|
|
||||||
"settings": {
|
|
||||||
"foreground": "#879A39"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "cyan",
|
|
||||||
"scope": [
|
|
||||||
"markup.italic.markdown",
|
|
||||||
"support.type.python",
|
|
||||||
"variable.legacy.builtin.python",
|
|
||||||
"support.constant.property-value.css",
|
|
||||||
"storage.modifier.attribute.swift"
|
|
||||||
],
|
|
||||||
"settings": {
|
|
||||||
"foreground": "#3AA99F"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "blue",
|
|
||||||
"scope": [],
|
|
||||||
"settings": {
|
|
||||||
"foreground": "#4385BE"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "purple",
|
|
||||||
"scope": ["keyword.channel.go", "keyword.other.platform.os.swift"],
|
|
||||||
"settings": {
|
|
||||||
"foreground": "#8B7EC8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "magenta",
|
|
||||||
"scope": ["punctuation.definition.heading.markdown"],
|
|
||||||
"settings": {
|
|
||||||
"foreground": "#CE5D97"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "red",
|
|
||||||
"scope": [],
|
|
||||||
"settings": {
|
|
||||||
"foreground": "#D14D41"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "orange",
|
|
||||||
"scope": [],
|
|
||||||
"settings": {
|
|
||||||
"foreground": "#DA702C"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,547 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "Flexoki",
|
|
||||||
"type": "light",
|
|
||||||
"colors": {
|
|
||||||
"editor.background": "#FFFCF0",
|
|
||||||
"editor.foreground": "#100F0F",
|
|
||||||
"editor.hoverHighlightBackground": "#DAD8CE",
|
|
||||||
"editor.lineHighlightBackground": "#F2F0E5",
|
|
||||||
"editor.selectionBackground": "#100F0F44",
|
|
||||||
"editor.selectionHighlightBackground": "#100F0F44",
|
|
||||||
"editor.findMatchBackground": "#D0A215",
|
|
||||||
"editor.findMatchHighlightBackground": "#D0A215cc",
|
|
||||||
"editor.findRangeHighlightBackground": "#F2F0E5",
|
|
||||||
"editor.inactiveSelectionBackground": "#E6E4D9",
|
|
||||||
"editor.lineHighlightBorder": "#E6E4D9",
|
|
||||||
"editor.rangeHighlightBackground": "#CECDC3",
|
|
||||||
"notifications.background": "#E6E4D9",
|
|
||||||
"editorInlayHint.typeBackground": "#DAD8CE",
|
|
||||||
"editorInlayHint.typeForeground": "#100F0F",
|
|
||||||
"editorWhitespace.foreground": "#CECDC3",
|
|
||||||
"editorIndentGuide.background1": "#DAD8CE",
|
|
||||||
"editorHoverWidget.background": "#E6E4D9",
|
|
||||||
"editorLineNumber.activeForeground": "#100F0F",
|
|
||||||
"editorLineNumber.foreground": "#CECDC3",
|
|
||||||
"editorGutter.background": "#FFFCF0",
|
|
||||||
"editorGutter.modifiedBackground": "#24837B",
|
|
||||||
"editorGutter.addedBackground": "#66800B",
|
|
||||||
"editorGutter.deletedBackground": "#AF3029",
|
|
||||||
"editorBracketMatch.background": "#E6E4D9",
|
|
||||||
"editorBracketMatch.border": "#DAD8CE",
|
|
||||||
"editorError.foreground": "#AF3029",
|
|
||||||
"editorWarning.foreground": "#BC5215",
|
|
||||||
"editorInfo.foreground": "#205EA6",
|
|
||||||
"diffEditor.insertedTextBackground": "#879A3999",
|
|
||||||
"diffEditor.removedTextBackground": "#D14D4199",
|
|
||||||
"editorGroupHeader.tabsBackground": "#FFFCF0",
|
|
||||||
"editorGroup.border": "#DAD8CE",
|
|
||||||
"tab.activeBackground": "#FFFCF0",
|
|
||||||
"tab.inactiveBackground": "#F2F0E5",
|
|
||||||
"tab.inactiveForeground": "#6F6E69",
|
|
||||||
"tab.activeForeground": "#100F0F",
|
|
||||||
"tab.hoverBackground": "#DAD8CE",
|
|
||||||
"tab.unfocusedHoverBackground": "#DAD8CE",
|
|
||||||
"tab.border": "#DAD8CE",
|
|
||||||
"tab.activeModifiedBorder": "#AD8301",
|
|
||||||
"tab.inactiveModifiedBorder": "#205EA6",
|
|
||||||
"tab.unfocusedActiveModifiedBorder": "#D0A215",
|
|
||||||
"tab.unfocusedInactiveModifiedBorder": "#4385BE",
|
|
||||||
"editorWidget.background": "#F2F0E5",
|
|
||||||
"editorWidget.border": "#DAD8CE",
|
|
||||||
"editorSuggestWidget.background": "#FFFCF0",
|
|
||||||
"editorSuggestWidget.border": "#DAD8CE",
|
|
||||||
"editorSuggestWidget.foreground": "#100F0F",
|
|
||||||
"editorSuggestWidget.highlightForeground": "#6F6E69",
|
|
||||||
"editorSuggestWidget.selectedBackground": "#DAD8CE",
|
|
||||||
"peekView.border": "#DAD8CE",
|
|
||||||
"peekViewEditor.background": "#FFFCF0",
|
|
||||||
"peekViewEditor.matchHighlightBackground": "#CECDC3",
|
|
||||||
"peekViewResult.background": "#F2F0E5",
|
|
||||||
"peekViewResult.fileForeground": "#100F0F",
|
|
||||||
"peekViewResult.lineForeground": "#6F6E69",
|
|
||||||
"peekViewResult.matchHighlightBackground": "#CECDC3",
|
|
||||||
"peekViewResult.selectionBackground": "#E6E4D9",
|
|
||||||
"peekViewResult.selectionForeground": "#B7B5AC",
|
|
||||||
"peekViewTitle.background": "#DAD8CE",
|
|
||||||
"peekViewTitleDescription.foreground": "#6F6E69",
|
|
||||||
"peekViewTitleLabel.foreground": "#100F0F",
|
|
||||||
"merge.currentHeaderBackground": "#66800B",
|
|
||||||
"merge.currentContentBackground": "#879A39",
|
|
||||||
"merge.incomingHeaderBackground": "#24837B",
|
|
||||||
"merge.incomingContentBackground": "#3AA99F",
|
|
||||||
"merge.border": "#DAD8CE",
|
|
||||||
"merge.commonContentBackground": "#CECDC3",
|
|
||||||
"merge.commonHeaderBackground": "#DAD8CE",
|
|
||||||
"panel.background": "#FFFCF0",
|
|
||||||
"panel.border": "#DAD8CE",
|
|
||||||
"panelTitle.activeBorder": "#CECDC3",
|
|
||||||
"panelTitle.activeForeground": "#100F0F",
|
|
||||||
"panelTitle.inactiveForeground": "#6F6E69",
|
|
||||||
"statusBar.background": "#FFFCF0",
|
|
||||||
"statusBar.foreground": "#100F0F",
|
|
||||||
"statusBar.border": "#DAD8CE",
|
|
||||||
"statusBar.debuggingBackground": "#AF3029",
|
|
||||||
"statusBar.debuggingForeground": "#100F0F",
|
|
||||||
"statusBar.noFolderBackground": "#CECDC3",
|
|
||||||
"statusBar.noFolderForeground": "#B7B5AC",
|
|
||||||
"titleBar.activeBackground": "#FFFCF0",
|
|
||||||
"titleBar.activeForeground": "#100F0F",
|
|
||||||
"titleBar.inactiveBackground": "#F2F0E5",
|
|
||||||
"titleBar.inactiveForeground": "#6F6E69",
|
|
||||||
"titleBar.border": "#DAD8CE",
|
|
||||||
"menu.foreground": "#100F0F",
|
|
||||||
"menu.background": "#FFFCF0",
|
|
||||||
"menu.selectionForeground": "#100F0F",
|
|
||||||
"menu.selectionBackground": "#DAD8CE",
|
|
||||||
"menu.border": "#DAD8CE",
|
|
||||||
"editorInlayHint.foreground": "#6F6E69",
|
|
||||||
"editorInlayHint.background": "#DAD8CE",
|
|
||||||
"terminal.foreground": "#100F0F",
|
|
||||||
"terminal.background": "#FFFCF0",
|
|
||||||
"terminalCursor.foreground": "#100F0F",
|
|
||||||
"terminalCursor.background": "#FFFCF0",
|
|
||||||
"terminal.ansiRed": "#AF3029",
|
|
||||||
"terminal.ansiGreen": "#66800B",
|
|
||||||
"terminal.ansiYellow": "#AD8301",
|
|
||||||
"terminal.ansiBlue": "#205EA6",
|
|
||||||
"terminal.ansiMagenta": "#24837B",
|
|
||||||
"terminal.ansiCyan": "#24837B",
|
|
||||||
"activityBar.background": "#FFFCF0",
|
|
||||||
"activityBar.foreground": "#100F0F",
|
|
||||||
"activityBar.inactiveForeground": "#6F6E69",
|
|
||||||
"activityBar.activeBorder": "#100F0F",
|
|
||||||
"activityBar.border": "#DAD8CE",
|
|
||||||
"sideBar.background": "#FFFCF0",
|
|
||||||
"sideBar.foreground": "#100F0F",
|
|
||||||
"sideBar.border": "#DAD8CE",
|
|
||||||
"sideBarTitle.foreground": "#100F0F",
|
|
||||||
"sideBarSectionHeader.background": "#F2F0E5",
|
|
||||||
"sideBarSectionHeader.foreground": "#100F0F",
|
|
||||||
"sideBarSectionHeader.border": "#DAD8CE",
|
|
||||||
"sideBar.activeBackground": "#CECDC3",
|
|
||||||
"sideBar.activeForeground": "#100F0F",
|
|
||||||
"sideBar.hoverBackground": "#DAD8CE",
|
|
||||||
"sideBar.hoverForeground": "#6F6E69",
|
|
||||||
"sideBar.folderIcon.foreground": "#66800B",
|
|
||||||
"sideBar.fileIcon.foreground": "#205EA6",
|
|
||||||
"list.warningForeground": "#BC5215",
|
|
||||||
"list.errorForeground": "#AF3029",
|
|
||||||
"list.inactiveSelectionBackground": "#DAD8CE",
|
|
||||||
"list.activeSelectionBackground": "#CECDC3",
|
|
||||||
"list.inactiveSelectionForeground": "#100F0F",
|
|
||||||
"list.activeSelectionForeground": "#100F0F",
|
|
||||||
"list.hoverForeground": "#100F0F",
|
|
||||||
"list.hoverBackground": "#DAD8CE",
|
|
||||||
"input.background": "#F2F0E5",
|
|
||||||
"input.foreground": "#100F0F",
|
|
||||||
"input.border": "#DAD8CE",
|
|
||||||
"input.placeholderForeground": "#6F6E69",
|
|
||||||
"inputOption.activeBorder": "#DAD8CE",
|
|
||||||
"inputOption.activeBackground": "#E6E4D9",
|
|
||||||
"inputOption.activeForeground": "#100F0F",
|
|
||||||
"inputValidation.infoBackground": "#24837B",
|
|
||||||
"inputValidation.infoBorder": "#3AA99F",
|
|
||||||
"inputValidation.warningBackground": "#BC5215",
|
|
||||||
"inputValidation.warningBorder": "#DA702C",
|
|
||||||
"inputValidation.errorBackground": "#AF3029",
|
|
||||||
"inputValidation.errorBorder": "#D14D41",
|
|
||||||
"dropdown.background": "#F2F0E5",
|
|
||||||
"dropdown.foreground": "#100F0F",
|
|
||||||
"dropdown.border": "#DAD8CE",
|
|
||||||
"dropdown.listBackground": "#FFFCF0",
|
|
||||||
"badge.background": "#24837B",
|
|
||||||
"activityBarBadge.background": "#24837B",
|
|
||||||
"button.background": "#24837B",
|
|
||||||
"button.foreground": "#FFFCF0",
|
|
||||||
"badge.foreground": "#FFFCF0",
|
|
||||||
"activityBarBadge.foreground": "#FFFCF0"
|
|
||||||
},
|
|
||||||
"tokenColors": [
|
|
||||||
{
|
|
||||||
"name": "plain",
|
|
||||||
"scope": ["source", "support.type.property-name.css"],
|
|
||||||
"settings": {
|
|
||||||
"foreground": "#100F0F"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "classes",
|
|
||||||
"scope": ["entity.name.type.class"],
|
|
||||||
"settings": {
|
|
||||||
"foreground": "#BC5215"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "interfaces",
|
|
||||||
"scope": ["entity.name.type.interface", "entity.name.type"],
|
|
||||||
"settings": {
|
|
||||||
"foreground": "#AD8301"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "structs",
|
|
||||||
"scope": ["entity.name.type.struct"],
|
|
||||||
"settings": {
|
|
||||||
"foreground": "#BC5215"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "enums",
|
|
||||||
"scope": ["entity.name.type.enum"],
|
|
||||||
"settings": {
|
|
||||||
"foreground": "#BC5215"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "keys",
|
|
||||||
"scope": ["meta.object-literal.key", "support.type.property-name"],
|
|
||||||
"settings": {
|
|
||||||
"foreground": "#BC5215"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "methods",
|
|
||||||
"scope": ["entity.name.function.method", "meta.function.method"],
|
|
||||||
"settings": {
|
|
||||||
"foreground": "#66800B"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "functions",
|
|
||||||
"scope": [
|
|
||||||
"entity.name.function",
|
|
||||||
"support.function",
|
|
||||||
"meta.function-call.generic"
|
|
||||||
],
|
|
||||||
"settings": {
|
|
||||||
"foreground": "#BC5215",
|
|
||||||
"fontStyle": "bold"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "variables",
|
|
||||||
"scope": ["variable", "meta.variable", "variable.other.object.property"],
|
|
||||||
"settings": {
|
|
||||||
"foreground": "#100F0F"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "variablesOther",
|
|
||||||
"scope": ["variable.other.object", "variable.other.readwrite.alias"],
|
|
||||||
"settings": {
|
|
||||||
"foreground": "#66800B"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "globalVariables",
|
|
||||||
"scope": ["variable.other.global", "variable.language.this"],
|
|
||||||
"settings": {
|
|
||||||
"foreground": "#A02F6F"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "localVariables",
|
|
||||||
"scope": ["variable.other.local"],
|
|
||||||
"settings": {
|
|
||||||
"foreground": "#E6E4D9"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "parameters",
|
|
||||||
"scope": ["variable.parameter", "meta.parameter"],
|
|
||||||
"settings": {
|
|
||||||
"foreground": "#100F0F"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "properties",
|
|
||||||
"scope": ["variable.other.property", "meta.property"],
|
|
||||||
"settings": {
|
|
||||||
"foreground": "#205EA6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "strings",
|
|
||||||
"scope": [
|
|
||||||
"string",
|
|
||||||
"string.other.link",
|
|
||||||
"markup.inline.raw.string.markdown"
|
|
||||||
],
|
|
||||||
"settings": {
|
|
||||||
"foreground": "#24837B"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "stringEscapeSequences",
|
|
||||||
"scope": ["constant.character.escape", "constant.other.placeholder"],
|
|
||||||
"settings": {
|
|
||||||
"foreground": "#100F0F"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "keywords",
|
|
||||||
"scope": ["keyword"],
|
|
||||||
"settings": {
|
|
||||||
"foreground": "#66800B"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "keywordsControl",
|
|
||||||
"scope": [
|
|
||||||
"keyword.control.import",
|
|
||||||
"keyword.control.from",
|
|
||||||
"keyword.import"
|
|
||||||
],
|
|
||||||
"settings": {
|
|
||||||
"foreground": "#AF3029"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "storageModifiers",
|
|
||||||
"scope": ["storage.modifier", "keyword.modifier", "storage.type"],
|
|
||||||
"settings": {
|
|
||||||
"foreground": "#205EA6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "comments",
|
|
||||||
"scope": ["comment", "punctuation.definition.comment"],
|
|
||||||
"settings": {
|
|
||||||
"foreground": "#6F6E69"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "docComments",
|
|
||||||
"scope": ["comment.documentation", "comment.line.documentation"],
|
|
||||||
"settings": {
|
|
||||||
"foreground": "#B7B5AC"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "numbers",
|
|
||||||
"scope": ["constant.numeric"],
|
|
||||||
"settings": {
|
|
||||||
"foreground": "#5E409D"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "booleans",
|
|
||||||
"scope": ["constant.language.boolean", "constant.language.json"],
|
|
||||||
"settings": {
|
|
||||||
"foreground": "#AD8301"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "operators",
|
|
||||||
"scope": ["keyword.operator"],
|
|
||||||
"settings": {
|
|
||||||
"foreground": "#AF3029"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "macros",
|
|
||||||
"scope": ["entity.name.function.preprocessor", "meta.preprocessor"],
|
|
||||||
"settings": {
|
|
||||||
"foreground": "#205EA6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "preprocessor",
|
|
||||||
"scope": ["meta.preprocessor"],
|
|
||||||
"settings": {
|
|
||||||
"foreground": "#A02F6F"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "urls",
|
|
||||||
"scope": ["markup.underline.link"],
|
|
||||||
"settings": {
|
|
||||||
"foreground": "#205EA6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "tags",
|
|
||||||
"scope": ["entity.name.tag"],
|
|
||||||
"settings": {
|
|
||||||
"foreground": "#205EA6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "jsxTags",
|
|
||||||
"scope": ["support.class.component"],
|
|
||||||
"settings": {
|
|
||||||
"foreground": "#A02F6F"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "attributes",
|
|
||||||
"scope": ["entity.other.attribute-name", "meta.attribute"],
|
|
||||||
"settings": {
|
|
||||||
"foreground": "#AD8301"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "types",
|
|
||||||
"scope": ["support.type"],
|
|
||||||
"settings": {
|
|
||||||
"foreground": "#AD8301"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "constants",
|
|
||||||
"scope": ["variable.other.constant", "variable.readonly"],
|
|
||||||
"settings": {
|
|
||||||
"foreground": "#100F0F"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "labels",
|
|
||||||
"scope": ["entity.name.label", "punctuation.definition.label"],
|
|
||||||
"settings": {
|
|
||||||
"foreground": "#A02F6F"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "namespaces",
|
|
||||||
"scope": [
|
|
||||||
"entity.name.namespace",
|
|
||||||
"storage.modifier.namespace",
|
|
||||||
"markup.bold.markdown"
|
|
||||||
],
|
|
||||||
"settings": {
|
|
||||||
"foreground": "#AD8301"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "modules",
|
|
||||||
"scope": ["entity.name.module", "storage.modifier.module"],
|
|
||||||
"settings": {
|
|
||||||
"foreground": "#AF3029"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "typeParameters",
|
|
||||||
"scope": ["variable.type.parameter", "variable.parameter.type"],
|
|
||||||
"settings": {
|
|
||||||
"foreground": "#BC5215"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "exceptions",
|
|
||||||
"scope": ["keyword.control.exception", "keyword.control.trycatch"],
|
|
||||||
"settings": {
|
|
||||||
"foreground": "#A02F6F"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "decorators",
|
|
||||||
"scope": [
|
|
||||||
"meta.decorator",
|
|
||||||
"punctuation.decorator",
|
|
||||||
"entity.name.function.decorator"
|
|
||||||
],
|
|
||||||
"settings": {
|
|
||||||
"foreground": "#AD8301"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "calls",
|
|
||||||
"scope": ["variable.function"],
|
|
||||||
"settings": {
|
|
||||||
"foreground": "#100F0F"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "punctuation",
|
|
||||||
"scope": [
|
|
||||||
"punctuation",
|
|
||||||
"punctuation.terminator",
|
|
||||||
"punctuation.definition.tag",
|
|
||||||
"punctuation.separator",
|
|
||||||
"punctuation.definition.string",
|
|
||||||
"punctuation.section.block"
|
|
||||||
],
|
|
||||||
"settings": {
|
|
||||||
"foreground": "#6F6E69"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "yellow",
|
|
||||||
"scope": [
|
|
||||||
"storage.type.numeric.go",
|
|
||||||
"storage.type.byte.go",
|
|
||||||
"storage.type.boolean.go",
|
|
||||||
"storage.type.string.go",
|
|
||||||
"storage.type.uintptr.go",
|
|
||||||
"storage.type.error.go",
|
|
||||||
"storage.type.rune.go",
|
|
||||||
"constant.language.go",
|
|
||||||
"support.class.dart",
|
|
||||||
"keyword.other.documentation",
|
|
||||||
"storage.modifier.import.java",
|
|
||||||
"punctuation.definition.list.begin.markdown",
|
|
||||||
"punctuation.definition.quote.begin.markdown",
|
|
||||||
"meta.separator.markdown",
|
|
||||||
"entity.name.section.markdown"
|
|
||||||
],
|
|
||||||
"settings": {
|
|
||||||
"foreground": "#AD8301"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "green",
|
|
||||||
"scope": [],
|
|
||||||
"settings": {
|
|
||||||
"foreground": "#66800B"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "cyan",
|
|
||||||
"scope": [
|
|
||||||
"markup.italic.markdown",
|
|
||||||
"support.type.python",
|
|
||||||
"variable.legacy.builtin.python",
|
|
||||||
"support.constant.property-value.css",
|
|
||||||
"storage.modifier.attribute.swift"
|
|
||||||
],
|
|
||||||
"settings": {
|
|
||||||
"foreground": "#24837B"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "blue",
|
|
||||||
"scope": [],
|
|
||||||
"settings": {
|
|
||||||
"foreground": "#205EA6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "purple",
|
|
||||||
"scope": ["keyword.channel.go", "keyword.other.platform.os.swift"],
|
|
||||||
"settings": {
|
|
||||||
"foreground": "#5E409D"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "magenta",
|
|
||||||
"scope": ["punctuation.definition.heading.markdown"],
|
|
||||||
"settings": {
|
|
||||||
"foreground": "#A02F6F"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "red",
|
|
||||||
"scope": [],
|
|
||||||
"settings": {
|
|
||||||
"foreground": "#AF3029"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "orange",
|
|
||||||
"scope": [],
|
|
||||||
"settings": {
|
|
||||||
"foreground": "#BC5215"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user