chore: migrate remaining ui to token utilities

This commit is contained in:
Shantur Rathore
2025-10-28 21:22:19 +00:00
parent ad86154d47
commit 311c1bcd76
9 changed files with 274 additions and 162 deletions

View File

@@ -64,19 +64,17 @@ export default function AgentSelector(props: AgentSelectorProps) {
itemComponent={(itemProps) => (
<Select.Item
item={itemProps.item}
class="px-3 py-2 cursor-pointer rounded outline-none transition-colors hover:bg-gray-100 focus:bg-gray-100 dark:hover:bg-gray-800 dark:focus:bg-gray-800"
class="selector-option"
>
<div class="flex flex-col">
<Select.ItemLabel class="font-medium text-sm text-gray-900 dark:text-gray-100 flex items-center gap-2">
<div class="flex flex-col flex-1 min-w-0">
<Select.ItemLabel class="selector-option-label flex items-center gap-2">
<span>{itemProps.item.rawValue.name}</span>
<Show when={itemProps.item.rawValue.mode === "subagent"}>
<span class="neutral-badge">
subagent
</span>
<span class="neutral-badge">subagent</span>
</Show>
</Select.ItemLabel>
<Show when={itemProps.item.rawValue.description}>
<Select.ItemDescription class="text-xs text-gray-600 dark:text-gray-300">
<Select.ItemDescription class="selector-option-description">
{itemProps.item.rawValue.description.length > 50
? itemProps.item.rawValue.description.slice(0, 50) + "..."
: itemProps.item.rawValue.description}
@@ -92,11 +90,15 @@ export default function AgentSelector(props: AgentSelectorProps) {
>
<Select.Value<Agent>>
{(state) => (
<span class="text-gray-700 dark:text-gray-200">Agent: {state.selectedOption()?.name ?? "None"}</span>
<div class="selector-trigger-label">
<span class="selector-trigger-primary">
Agent: {state.selectedOption()?.name ?? "None"}
</span>
</div>
)}
</Select.Value>
<Select.Icon>
<ChevronDown class="w-3 h-3 text-gray-500 dark:text-gray-300" />
<Select.Icon class="selector-trigger-icon">
<ChevronDown class="w-3 h-3" />
</Select.Icon>
</Select.Trigger>
@@ -106,7 +108,7 @@ export default function AgentSelector(props: AgentSelectorProps) {
</Select.Content>
</Select.Portal>
</Select>
<span class="text-xs text-gray-400 dark:text-gray-500">
<span class="hint">
<Kbd shortcut="cmd+shift+a" />
</span>
</div>

View File

@@ -8,21 +8,20 @@ interface EmptyStateProps {
const EmptyState: Component<EmptyStateProps> = (props) => {
return (
<div class="flex h-full w-full items-center justify-center" style="background-color: var(--surface-secondary);">
<div class="flex h-full w-full items-center justify-center bg-surface-secondary">
<div class="max-w-[500px] px-8 py-12 text-center">
<div class="mb-8 flex justify-center">
<Folder class="h-16 w-16 text-gray-400 dark:text-gray-600" />
<Folder class="h-16 w-16 icon-muted" />
</div>
<h1 class="mb-4 text-2xl font-semibold text-gray-900 dark:text-gray-100">Welcome to OpenCode Client</h1>
<h1 class="mb-4 text-2xl font-semibold text-primary">Welcome to OpenCode Client</h1>
<p class="mb-8 text-base text-gray-600 dark:text-gray-400">Select a folder to start coding with AI</p>
<p class="mb-8 text-base text-secondary">Select a folder to start coding with AI</p>
<button
onClick={props.onSelectFolder}
disabled={props.isLoading}
class="mb-4 inline-flex items-center gap-2 rounded-lg px-6 py-3 text-base font-medium transition-colors disabled:cursor-not-allowed disabled:opacity-50 focus:outline-none focus:ring-2 focus:ring-offset-2 hover:opacity-90"
style="background-color: var(--accent-primary); color: var(--text-inverted); ring-color: var(--accent-primary); ring-offset-color: var(--surface-base);"
class="mb-4 button-primary"
>
{props.isLoading ? (
<>
@@ -34,11 +33,11 @@ const EmptyState: Component<EmptyStateProps> = (props) => {
)}
</button>
<p class="text-sm text-gray-500 dark:text-gray-500">
<p class="text-sm text-muted">
Keyboard shortcut: {navigator.platform.includes("Mac") ? "Cmd" : "Ctrl"}+N
</p>
<div class="mt-6 space-y-1 text-sm text-gray-400 dark:text-gray-600">
<div class="mt-6 space-y-1 text-sm text-muted">
<p>Examples: ~/projects/my-app</p>
<p>You can have multiple instances of the same folder</p>
</div>

View File

@@ -52,9 +52,9 @@ const EnvironmentVariablesEditor: Component<EnvironmentVariablesEditorProps> = (
return (
<div class="space-y-3">
<div class="flex items-center gap-2 mb-3">
<Globe class="w-4 h-4 text-gray-500 dark:text-gray-400" />
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">Environment Variables</span>
<span class="text-xs text-gray-500 dark:text-gray-400">
<Globe class="w-4 h-4 icon-muted" />
<span class="text-sm font-medium text-secondary">Environment Variables</span>
<span class="text-xs text-muted">
({entries().length} variable{entries().length !== 1 ? "s" : ""})
</span>
</div>
@@ -66,12 +66,12 @@ const EnvironmentVariablesEditor: Component<EnvironmentVariablesEditorProps> = (
{([key, value]) => (
<div class="flex items-center gap-2">
<div class="flex-1 flex items-center gap-2">
<Key class="w-3.5 h-3.5 text-gray-400 dark:text-gray-500 flex-shrink-0" />
<Key class="w-3.5 h-3.5 icon-muted flex-shrink-0" />
<input
type="text"
value={key}
disabled={props.disabled}
class="flex-1 px-2.5 py-1.5 text-sm bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-600 rounded text-gray-500 dark:text-gray-400 cursor-not-allowed"
class="flex-1 px-2.5 py-1.5 text-sm bg-surface-secondary border border-base rounded text-muted cursor-not-allowed"
placeholder="Variable name"
title="Variable name (read-only)"
/>
@@ -80,14 +80,14 @@ const EnvironmentVariablesEditor: Component<EnvironmentVariablesEditorProps> = (
value={value}
disabled={props.disabled}
onInput={(e) => handleUpdateVariable(key, e.currentTarget.value)}
class="flex-1 px-2.5 py-1.5 text-sm bg-white dark:bg-gray-700 border border-gray-200 dark:border-gray-600 rounded text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent disabled:opacity-50 disabled:cursor-not-allowed"
class="flex-1 px-2.5 py-1.5 text-sm bg-surface-base border border-base rounded text-primary focus-ring-accent disabled:opacity-50 disabled:cursor-not-allowed"
placeholder="Variable value"
/>
</div>
<button
onClick={() => handleRemoveVariable(key)}
disabled={props.disabled}
class="p-1.5 text-gray-400 dark:text-gray-500 hover:text-red-600 dark:hover:text-red-400 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
class="p-1.5 icon-muted icon-danger-hover disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
title="Remove variable"
>
<Trash2 class="w-3.5 h-3.5" />
@@ -99,16 +99,16 @@ const EnvironmentVariablesEditor: Component<EnvironmentVariablesEditorProps> = (
</Show>
{/* Add new variable */}
<div class="flex items-center gap-2 pt-2 border-t border-gray-200 dark:border-gray-600">
<div class="flex items-center gap-2 pt-2 border-t border-base">
<div class="flex-1 flex items-center gap-2">
<Key class="w-3.5 h-3.5 text-gray-400 dark:text-gray-500 flex-shrink-0" />
<Key class="w-3.5 h-3.5 icon-muted flex-shrink-0" />
<input
type="text"
value={newKey()}
onInput={(e) => setNewKey(e.currentTarget.value)}
onKeyPress={handleKeyPress}
disabled={props.disabled}
class="flex-1 px-2.5 py-1.5 text-sm bg-white dark:bg-gray-700 border border-gray-200 dark:border-gray-600 rounded text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent disabled:opacity-50 disabled:cursor-not-allowed"
class="flex-1 px-2.5 py-1.5 text-sm bg-surface-base border border-base rounded text-primary focus-ring-accent disabled:opacity-50 disabled:cursor-not-allowed"
placeholder="Variable name"
/>
<input
@@ -117,14 +117,14 @@ const EnvironmentVariablesEditor: Component<EnvironmentVariablesEditorProps> = (
onInput={(e) => setNewValue(e.currentTarget.value)}
onKeyPress={handleKeyPress}
disabled={props.disabled}
class="flex-1 px-2.5 py-1.5 text-sm bg-white dark:bg-gray-700 border border-gray-200 dark:border-gray-600 rounded text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent disabled:opacity-50 disabled:cursor-not-allowed"
class="flex-1 px-2.5 py-1.5 text-sm bg-surface-base border border-base rounded text-primary focus-ring-accent disabled:opacity-50 disabled:cursor-not-allowed"
placeholder="Variable value"
/>
</div>
<button
onClick={handleAddVariable}
disabled={props.disabled || !newKey().trim()}
class="p-1.5 text-gray-400 dark:text-gray-500 hover:text-blue-600 dark:hover:text-blue-400 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
class="p-1.5 icon-muted icon-accent-hover disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
title="Add variable"
>
<Plus class="w-3.5 h-3.5" />
@@ -132,12 +132,12 @@ const EnvironmentVariablesEditor: Component<EnvironmentVariablesEditorProps> = (
</div>
<Show when={entries().length === 0}>
<div class="text-xs text-gray-500 dark:text-gray-400 text-center py-2">
<div class="text-xs text-muted text-center py-2">
No environment variables configured. Add variables above to customize the OpenCode environment.
</div>
</Show>
<div class="text-xs text-gray-500 dark:text-gray-400 mt-2">
<div class="text-xs text-muted mt-2">
These variables will be available in the OpenCode environment when starting instances.
</div>
</div>

View File

@@ -6,7 +6,7 @@ interface HintRowProps {
}
const HintRow: Component<HintRowProps> = (props) => {
return <span class={`text-xs text-gray-500 dark:text-gray-400 ${props.class || ""}`}>{props.children}</span>
return <span class={`text-xs text-muted ${props.class || ""}`}>{props.children}</span>
}
export default HintRow

View File

@@ -81,8 +81,8 @@ const InstanceInfo: Component<InstanceInfoProps> = (props) => {
</div>
<div class="panel-body space-y-3">
<div>
<div class="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-1">Folder</div>
<div class="text-xs text-gray-900 dark:text-gray-100 font-mono break-all px-2 py-1.5 rounded border" style="background-color: var(--surface-secondary); border-color: var(--border-base); color: var(--text-primary);">
<div class="text-xs font-medium text-muted uppercase tracking-wide mb-1">Folder</div>
<div class="text-xs text-primary font-mono break-all px-2 py-1.5 rounded border bg-surface-secondary border-base">
{props.instance.folder}
</div>
</div>
@@ -91,22 +91,23 @@ const InstanceInfo: Component<InstanceInfoProps> = (props) => {
{(project) => (
<>
<div>
<div class="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-1">
<div class="text-xs font-medium text-muted uppercase tracking-wide mb-1">
Project
</div>
<div class="text-xs font-mono px-2 py-1.5 rounded border truncate" style="background-color: var(--surface-secondary); border-color: var(--border-base); color: var(--text-primary);">
<div class="text-xs font-mono px-2 py-1.5 rounded border truncate bg-surface-secondary border-base text-primary">
{project().id}
</div>
</div>
<Show when={project().vcs}>
<div>
<div class="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-1">
<div class="text-xs font-medium text-muted uppercase tracking-wide mb-1">
Version Control
</div>
<div class="flex items-center gap-2 text-xs text-gray-900 dark:text-gray-100">
<div class="flex items-center gap-2 text-xs text-primary">
<svg
class="w-3.5 h-3.5 text-orange-600 dark:text-orange-500"
class="w-3.5 h-3.5"
style="color: var(--status-warning);"
fill="currentColor"
viewBox="0 0 24 24"
>
@@ -122,10 +123,10 @@ const InstanceInfo: Component<InstanceInfoProps> = (props) => {
<Show when={metadata()?.version}>
<div>
<div class="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-1">
<div class="text-xs font-medium text-muted uppercase tracking-wide mb-1">
OpenCode Version
</div>
<div class="text-xs px-2 py-1.5 rounded border" style="background-color: var(--surface-secondary); border-color: var(--border-base); color: var(--text-primary);">
<div class="text-xs px-2 py-1.5 rounded border bg-surface-secondary border-base text-primary">
v{metadata()?.version}
</div>
</div>
@@ -133,10 +134,10 @@ const InstanceInfo: Component<InstanceInfoProps> = (props) => {
<Show when={props.instance.binaryPath}>
<div>
<div class="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-1">
<div class="text-xs font-medium text-muted uppercase tracking-wide mb-1">
Binary Path
</div>
<div class="text-xs font-mono break-all px-2 py-1.5 rounded border" style="background-color: var(--surface-secondary); border-color: var(--border-base); color: var(--text-primary);">
<div class="text-xs font-mono break-all px-2 py-1.5 rounded border bg-surface-secondary border-base text-primary">
{props.instance.binaryPath}
</div>
</div>
@@ -144,17 +145,17 @@ const InstanceInfo: Component<InstanceInfoProps> = (props) => {
<Show when={props.instance.environmentVariables && Object.keys(props.instance.environmentVariables).length > 0}>
<div>
<div class="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-1.5">
<div class="text-xs font-medium text-muted uppercase tracking-wide mb-1.5">
Environment Variables ({Object.keys(props.instance.environmentVariables!).length})
</div>
<div class="space-y-1">
<For each={Object.entries(props.instance.environmentVariables!)}>
{([key, value]) => (
<div class="flex items-center gap-2 px-2 py-1.5 rounded border" style="background-color: var(--surface-secondary); border-color: var(--border-base);">
<span class="text-xs font-mono font-medium flex-1" title={key} style="color: var(--text-primary);">
<div class="flex items-center gap-2 px-2 py-1.5 rounded border bg-surface-secondary border-base">
<span class="text-xs font-mono font-medium flex-1 text-primary" title={key}>
{key}
</span>
<span class="text-xs font-mono flex-1" title={value} style="color: var(--text-secondary);">
<span class="text-xs font-mono flex-1 text-secondary" title={value}>
{value}
</span>
</div>
@@ -166,27 +167,23 @@ const InstanceInfo: Component<InstanceInfoProps> = (props) => {
<Show when={!isLoadingMetadata() && mcpServers().length > 0}>
<div>
<div class="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-1.5">
<div class="text-xs font-medium text-muted uppercase tracking-wide mb-1.5">
MCP Servers
</div>
<div class="space-y-1.5">
<For each={mcpServers()}>
{(server) => (
<div class="flex items-center justify-between px-2 py-1.5 rounded border" style="background-color: var(--surface-secondary); border-color: var(--border-base);">
<span class="text-xs text-gray-900 dark:text-gray-100 font-medium truncate">{server.name}</span>
<div class="flex items-center justify-between px-2 py-1.5 rounded border bg-surface-secondary border-base">
<span class="text-xs text-primary font-medium truncate">{server.name}</span>
<div class="flex items-center gap-1.5 flex-shrink-0">
<Show
when={server.status === "running"}
fallback={
<Show
when={server.status === "error"}
fallback={<div class="w-1.5 h-1.5 rounded-full bg-gray-400" />}
>
<div class="w-1.5 h-1.5 rounded-full bg-red-500" />
</Show>
}
>
<div class="w-1.5 h-1.5 rounded-full bg-green-500 animate-pulse" />
<Show when={server.status === "running"}>
<div class="status-dot ready animate-pulse" />
</Show>
<Show when={server.status === "error"}>
<div class="status-dot error" />
</Show>
<Show when={server.status === "stopped"}>
<div class="status-dot stopped" />
</Show>
</div>
</div>
@@ -197,9 +194,9 @@ const InstanceInfo: Component<InstanceInfoProps> = (props) => {
</Show>
<Show when={isLoadingMetadata()}>
<div class="text-xs text-gray-500 dark:text-gray-400 py-1">
<div class="text-xs text-muted py-1">
<div class="flex items-center gap-1.5">
<svg class="animate-spin h-3 w-3" fill="none" viewBox="0 0 24 24">
<svg class="animate-spin h-3 w-3 icon-muted" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" />
<path
class="opacity-75"
@@ -213,23 +210,23 @@ const InstanceInfo: Component<InstanceInfoProps> = (props) => {
</Show>
<div>
<div class="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-1.5">Server</div>
<div class="text-xs font-medium text-muted uppercase tracking-wide mb-1.5">Server</div>
<div class="space-y-1 text-xs">
<div class="flex justify-between items-center">
<span class="text-gray-600 dark:text-gray-400">Port:</span>
<span class="text-gray-900 dark:text-gray-100 font-mono">{props.instance.port}</span>
<span class="text-secondary">Port:</span>
<span class="text-primary font-mono">{props.instance.port}</span>
</div>
<div class="flex justify-between items-center">
<span class="text-gray-600 dark:text-gray-400">PID:</span>
<span class="text-gray-900 dark:text-gray-100 font-mono">{props.instance.pid}</span>
<span class="text-secondary">PID:</span>
<span class="text-primary font-mono">{props.instance.pid}</span>
</div>
<div class="flex justify-between items-center">
<span class="text-gray-600 dark:text-gray-400">Status:</span>
<span class="text-secondary">Status:</span>
<span
class={`status-badge ${props.instance.status}`}
>
<div
class={`status-dot ${props.instance.status} ${props.instance.status === "ready" || props.instance.status === "starting" ? "animate-pulse" : ""}`}
class={`status-dot ${props.instance.status === "ready" ? "ready" : props.instance.status === "starting" ? "starting" : props.instance.status === "error" ? "error" : "stopped"} ${props.instance.status === "ready" || props.instance.status === "starting" ? "animate-pulse" : ""}`}
/>
{props.instance.status}
</span>

View File

@@ -3,6 +3,7 @@ import type { Instance } from "../types/instance"
import { getParentSessions, createSession, setActiveParentSession, agents } from "../stores/sessions"
import InstanceInfo from "./instance-info"
interface InstanceWelcomeViewProps {
instance: Instance
}
@@ -146,14 +147,14 @@ const InstanceWelcomeView: Component<InstanceWelcomeViewProps> = (props) => {
}
return (
<div class="flex-1 flex flex-col overflow-hidden bg-gray-50 dark:bg-gray-950">
<div class="flex-1 flex flex-col overflow-hidden bg-surface-secondary">
<div class="flex-1 flex flex-col lg:flex-row gap-4 p-4 overflow-auto">
<div class="flex-1 flex flex-col gap-4 min-h-0">
<Show
when={parentSessions().length > 0}
fallback={
<div class="bg-white dark:bg-gray-900 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-6 text-center flex-shrink-0">
<div class="text-gray-400 dark:text-gray-500 mb-2">
<div class="panel panel-empty-state flex-shrink-0">
<div class="panel-empty-state-icon">
<svg class="w-12 h-12 mx-auto" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
@@ -163,83 +164,81 @@ const InstanceWelcomeView: Component<InstanceWelcomeViewProps> = (props) => {
/>
</svg>
</div>
<p class="text-gray-600 dark:text-gray-200 font-medium text-sm">No Previous Sessions</p>
<p class="text-xs text-gray-500 dark:text-gray-400">Create a new session below to get started</p>
<p class="panel-empty-state-title">No Previous Sessions</p>
<p class="panel-empty-state-description">Create a new session below to get started</p>
</div>
}
>
<div class="bg-white dark:bg-gray-900 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden flex-shrink-0">
<div class="px-4 py-3 border-b border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900">
<h2 class="text-base font-semibold text-gray-900 dark:text-gray-100">Resume Session</h2>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
<div class="panel flex-shrink-0">
<div class="panel-header">
<h2 class="panel-title">Resume Session</h2>
<p class="panel-subtitle">
{parentSessions().length} {parentSessions().length === 1 ? "session" : "sessions"} available
</p>
</div>
<div class="max-h-[400px] overflow-y-auto">
<div class="panel-list">
<For each={parentSessions()}>
{(session, index) => (
<button
data-session-index={index()}
class="w-full text-left px-4 py-2.5 border-b border-gray-100 dark:border-gray-800 transition-all group focus:outline-none hover:bg-blue-50 dark:hover:bg-blue-900/30"
classList={{
"bg-blue-100 dark:bg-blue-900/50 ring-2 ring-blue-500 dark:ring-blue-400 ring-inset":
focusMode() === "sessions" && selectedIndex() === index(),
}}
onClick={() => handleSessionSelect(session.id)}
onMouseEnter={() => {
setFocusMode("sessions")
setSelectedIndex(index())
}}
>
<div class="flex items-center justify-between gap-3">
<div class="flex-1 min-w-0">
<div class="flex items-center gap-2">
<span
class="text-sm font-medium text-gray-900 dark:text-gray-100 group-hover:text-blue-700 dark:group-hover:text-blue-300 truncate"
classList={{
"text-blue-700 dark:text-blue-300":
focusMode() === "sessions" && selectedIndex() === index(),
}}
>
{session.title || "Untitled Session"}
</span>
</div>
<div class="flex items-center gap-3 text-xs text-gray-500 dark:text-gray-400 mt-0.5">
<span>{session.agent}</span>
<span></span>
<span>{formatRelativeTime(session.time.updated)}</span>
<div class="panel-list-item">
<button
data-session-index={index()}
class="panel-list-item-content group"
classList={{
"panel-list-item-highlight ring-accent-inset":
focusMode() === "sessions" && selectedIndex() === index(),
}}
onClick={() => handleSessionSelect(session.id)}
onMouseEnter={() => {
setFocusMode("sessions")
setSelectedIndex(index())
}}
>
<div class="flex items-center justify-between gap-3 w-full">
<div class="flex-1 min-w-0">
<div class="flex items-center gap-2">
<span
class="text-sm font-medium text-primary truncate transition-colors"
classList={{
"text-accent":
focusMode() === "sessions" && selectedIndex() === index(),
}}
>
{session.title || "Untitled Session"}
</span>
</div>
<div class="flex items-center gap-3 text-xs text-muted mt-0.5">
<span>{session.agent}</span>
<span></span>
<span>{formatRelativeTime(session.time.updated)}</span>
</div>
</div>
<Show when={focusMode() === "sessions" && selectedIndex() === index()}>
<kbd class="kbd flex-shrink-0"></kbd>
</Show>
</div>
<Show when={focusMode() === "sessions" && selectedIndex() === index()}>
<kbd class="kbd flex-shrink-0">
</kbd>
</Show>
</div>
</button>
</button>
</div>
)}
</For>
</div>
</div>
</Show>
<div class="bg-white dark:bg-gray-900 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden flex-shrink-0">
<div class="px-4 py-3 border-b border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900">
<h2 class="text-base font-semibold text-gray-900 dark:text-gray-100">Start New Session</h2>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
Create a fresh conversation with your chosen agent
</p>
<div class="panel flex-shrink-0">
<div class="panel-header">
<h2 class="panel-title">Start New Session</h2>
<p class="panel-subtitle">Create a fresh conversation with your chosen agent</p>
</div>
<div class="p-4">
<div class="panel-body">
<Show
when={agentList().length > 0}
fallback={<div class="text-sm text-gray-500 dark:text-gray-400">Loading agents...</div>}
fallback={<div class="text-sm text-muted">Loading agents...</div>}
>
<div class="space-y-3">
<div>
<label class="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1.5">Agent</label>
<label class="block text-xs font-medium text-secondary mb-1.5">Agent</label>
<select
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg text-sm bg-white dark:bg-gray-800 dark:text-gray-100 hover:border-gray-400 dark:hover:border-gray-500 focus:border-blue-500 focus:ring-2 focus:ring-blue-500/20 transition-all"
class="selector-input w-full"
value={selectedAgent()}
onChange={(e) => setSelectedAgent(e.currentTarget.value)}
>
@@ -255,14 +254,15 @@ const InstanceWelcomeView: Component<InstanceWelcomeViewProps> = (props) => {
</div>
<button
class="w-full px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 dark:hover:bg-blue-500 disabled:bg-gray-300 dark:disabled:bg-gray-600 disabled:cursor-not-allowed transition-all font-medium flex items-center justify-between text-sm relative group focus:outline-none focus:ring-2 focus:ring-blue-500/40"
type="button"
class="selector-button selector-button-primary w-full flex items-center justify-between gap-2 font-medium"
onClick={handleNewSession}
disabled={isCreating() || agentList().length === 0}
>
<Show
when={!isCreating()}
fallback={
<>
<div class="flex items-center gap-2 w-full justify-center text-sm">
<svg class="animate-spin h-4 w-4" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" />
<path
@@ -272,18 +272,16 @@ const InstanceWelcomeView: Component<InstanceWelcomeViewProps> = (props) => {
/>
</svg>
Creating...
</>
</div>
}
>
<div class="flex items-center gap-2 flex-1 justify-center">
<div class="flex items-center gap-2 flex-1 justify-center text-sm">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
</svg>
<span>Create Session</span>
</div>
<kbd class="kbd flex-shrink-0">
Cmd+Enter
</kbd>
<kbd class="kbd flex-shrink-0">Cmd+Enter</kbd>
</Show>
</button>
</div>
@@ -299,8 +297,8 @@ const InstanceWelcomeView: Component<InstanceWelcomeViewProps> = (props) => {
</div>
</div>
<div class="px-4 py-2 bg-white dark:bg-gray-900 border-t border-gray-200 dark:border-gray-700 flex-shrink-0">
<div class="flex items-center justify-center flex-wrap gap-3 text-xs text-gray-500 dark:text-gray-400">
<div class="panel-footer">
<div class="panel-footer-hints">
<div class="flex items-center gap-1.5">
<kbd class="kbd"></kbd>
<kbd class="kbd"></kbd>
@@ -331,3 +329,4 @@ const InstanceWelcomeView: Component<InstanceWelcomeViewProps> = (props) => {
}
export default InstanceWelcomeView

View File

@@ -354,10 +354,10 @@ const OpenCodeBinarySelector: Component<OpenCodeBinarySelectorProps> = (props) =
</div>
<button
onClick={(e) => handleRemoveBinary(binary.path, e)}
class="opacity-0 group-hover:opacity-100 p-1 hover:bg-red-100 dark:hover:bg-red-900/30 rounded transition-all"
class="remove-binary-button"
title="Remove binary"
>
<Trash2 class="w-3.5 h-3.5 text-gray-400 dark:text-gray-500 hover:text-red-600 dark:hover:text-red-400" />
<Trash2 class="w-3.5 h-3.5" />
</button>
</div>
</div>

View File

@@ -64,39 +64,38 @@ const SessionPicker: Component<SessionPickerProps> = (props) => {
return (
<Dialog open={props.open} onOpenChange={(open) => !open && handleCancel()}>
<Dialog.Portal>
<Dialog.Overlay class="fixed inset-0 bg-black/50 z-50" />
<Dialog.Overlay class="modal-overlay" />
<div class="fixed inset-0 z-50 flex items-center justify-center p-4">
<Dialog.Content class="modal-surface w-full max-w-lg p-6">
<Dialog.Title class="text-xl font-semibold mb-4" style="color: var(--text-primary);">
<Dialog.Title class="text-xl font-semibold text-primary mb-4">
OpenCode {instance()?.folder.split("/").pop()}
</Dialog.Title>
<div class="space-y-6">
<Show
when={parentSessions().length > 0}
fallback={
<div class="text-center py-4 text-gray-500 dark:text-gray-400 text-sm">No previous sessions</div>
}
fallback={<div class="text-center py-4 text-sm text-muted">No previous sessions</div>}
>
<div>
<h3 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
<h3 class="text-sm font-medium text-secondary mb-2">
Resume a session ({parentSessions().length}):
</h3>
<div class="space-y-1 max-h-[400px] overflow-y-auto">
<For each={parentSessions()}>
{(session) => (
<button
class="w-full text-left px-3 py-2 rounded transition-colors group hover:bg-gray-100 dark:hover:bg-gray-800"
type="button"
class="selector-option w-full text-left"
onClick={() => handleSessionSelect(session.id)}
>
<div class="flex justify-between items-start">
<span class="text-sm text-gray-900 dark:text-gray-100 truncate flex-1">
<div class="selector-option-content">
<span class="selector-option-label truncate">
{session.title || "Untitled"}
</span>
<span class="text-xs text-gray-500 dark:text-gray-400 ml-2 flex-shrink-0">
{formatRelativeTime(session.time.updated)}
</span>
</div>
<span class="selector-badge-time flex-shrink-0">
{formatRelativeTime(session.time.updated)}
</span>
</button>
)}
</For>
@@ -106,22 +105,22 @@ const SessionPicker: Component<SessionPickerProps> = (props) => {
<div class="relative">
<div class="absolute inset-0 flex items-center">
<div class="w-full border-t border-gray-300 dark:border-gray-700" />
<div class="w-full border-t border-base" />
</div>
<div class="relative flex justify-center text-sm">
<span class="px-2" style="background-color: var(--surface-base); color: var(--text-muted);">or</span>
<span class="px-2 bg-surface-base text-muted">or</span>
</div>
</div>
<div>
<h3 class="text-sm font-medium mb-2" style="color: var(--text-secondary);">Start new session:</h3>
<h3 class="text-sm font-medium text-secondary mb-2">Start new session:</h3>
<div class="space-y-3">
<Show
when={agentList().length > 0}
fallback={<div class="text-sm text-gray-500 dark:text-gray-400">Loading agents...</div>}
fallback={<div class="text-sm text-muted">Loading agents...</div>}
>
<select
class="selector-input"
class="selector-input w-full"
value={selectedAgent()}
onChange={(e) => setSelectedAgent(e.currentTarget.value)}
>
@@ -130,7 +129,7 @@ const SessionPicker: Component<SessionPickerProps> = (props) => {
</Show>
<button
class="selector-button-primary w-full"
class="selector-button selector-button-primary w-full"
onClick={handleNewSession}
disabled={isCreating() || agentList().length === 0}
>
@@ -142,7 +141,8 @@ const SessionPicker: Component<SessionPickerProps> = (props) => {
<div class="mt-6 flex justify-end">
<button
class="px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 transition-colors"
type="button"
class="selector-button selector-button-secondary"
onClick={handleCancel}
>
Cancel

View File

@@ -1,6 +1,83 @@
/* Reusable component utilities using tokens */
/* Base token utility helpers */
.text-primary {
color: var(--text-primary);
}
.text-secondary {
color: var(--text-secondary);
}
.text-muted {
color: var(--text-muted);
}
.text-inverted {
color: var(--text-inverted);
}
.text-accent {
color: var(--accent-primary);
}
.bg-surface-base {
background-color: var(--surface-base);
}
.bg-surface-secondary {
background-color: var(--surface-secondary);
}
.bg-surface-muted {
background-color: var(--surface-muted);
}
.border-base {
border-color: var(--border-base);
}
.icon-muted {
color: var(--text-muted);
}
.icon-accent {
color: var(--accent-primary);
}
.icon-danger-hover:hover {
color: var(--status-error);
}
.icon-accent-hover:hover {
color: var(--accent-primary);
}
.ring-accent-inset {
box-shadow: inset 0 0 0 2px var(--accent-primary);
}
.button-primary {
@apply inline-flex items-center justify-center gap-2 rounded-lg px-6 py-3 text-base font-medium transition-colors;
background-color: var(--accent-primary);
color: var(--text-inverted);
}
.button-primary:hover:not(:disabled) {
opacity: 0.9;
}
.button-primary:focus-visible {
outline: none;
box-shadow: 0 0 0 2px var(--surface-base), 0 0 0 4px var(--accent-primary);
}
.button-primary:disabled {
@apply cursor-not-allowed opacity-50;
}
/* Message item base styles */
.message-item-base {
@apply flex flex-col gap-2 p-3 rounded-lg w-full;
}
@@ -960,18 +1037,28 @@
background-color: var(--surface-hover);
}
.selector-option-highlighted {
.selector-option[data-highlighted],
.selector-option[data-focused] {
background-color: rgba(0, 102, 255, 0.1);
}
[data-theme="dark"] .selector-option-highlighted {
[data-theme="dark"] .selector-option[data-highlighted],
[data-theme="dark"] .selector-option[data-focused] {
background-color: rgba(0, 128, 255, 0.2);
}
.selector-option[data-selected] {
background-color: rgba(0, 102, 255, 0.15);
}
.selector-option-selected {
background-color: rgba(0, 102, 255, 0.15);
}
[data-theme="dark"] .selector-option[data-selected] {
background-color: rgba(0, 128, 255, 0.25);
}
[data-theme="dark"] .selector-option-selected {
background-color: rgba(0, 128, 255, 0.25);
}
@@ -990,6 +1077,28 @@
color: var(--text-muted);
}
.selector-option .remove-binary-button {
opacity: 0;
}
.selector-option:hover .remove-binary-button {
opacity: 1;
}
.remove-binary-button {
@apply p-1 rounded transition-all;
color: var(--text-muted);
}
.remove-binary-button:hover {
background-color: rgba(239, 68, 68, 0.1);
color: var(--status-error);
}
[data-theme="dark"] .remove-binary-button:hover {
background-color: rgba(239, 68, 68, 0.2);
}
.selector-option-indicator {
@apply flex-shrink-0 mt-0.5;
color: var(--accent-primary);
@@ -1106,6 +1215,12 @@
@apply opacity-50 cursor-not-allowed;
}
.focus-ring-accent:focus {
outline: none;
border-color: transparent;
box-shadow: 0 0 0 2px var(--accent-primary);
}
.selector-empty-state {
@apply p-4 text-center text-sm;
color: var(--text-muted);