chore: migrate remaining ui to token utilities
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user