refactor: restyle selectors via tokens
This commit is contained in:
@@ -78,18 +78,18 @@ export default function ModelSelector(props: ModelSelectorProps) {
|
||||
itemComponent={(itemProps) => (
|
||||
<Combobox.Item
|
||||
item={itemProps.item}
|
||||
class="px-3 py-2 cursor-pointer rounded outline-none transition-colors hover:bg-blue-50 dark:hover:bg-blue-900/40 data-[highlighted]:bg-blue-100 dark:data-[highlighted]:bg-blue-900/60 flex items-start gap-2"
|
||||
class="selector-option"
|
||||
>
|
||||
<div class="flex flex-col flex-1 min-w-0">
|
||||
<Combobox.ItemLabel class="font-medium text-sm text-gray-900 dark:text-gray-100">
|
||||
<div class="selector-option-content">
|
||||
<Combobox.ItemLabel class="selector-option-label">
|
||||
{itemProps.item.rawValue.name}
|
||||
</Combobox.ItemLabel>
|
||||
<Combobox.ItemDescription class="text-xs text-gray-600 dark:text-gray-300">
|
||||
<Combobox.ItemDescription class="selector-option-description">
|
||||
{itemProps.item.rawValue.providerName} • {itemProps.item.rawValue.providerId}/
|
||||
{itemProps.item.rawValue.id}
|
||||
</Combobox.ItemDescription>
|
||||
</div>
|
||||
<Combobox.ItemIndicator class="flex-shrink-0 mt-0.5 text-blue-600 dark:text-blue-400">
|
||||
<Combobox.ItemIndicator class="selector-option-indicator">
|
||||
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
@@ -101,38 +101,38 @@ export default function ModelSelector(props: ModelSelectorProps) {
|
||||
<Combobox.Input class="sr-only" data-model-selector />
|
||||
<Combobox.Trigger
|
||||
ref={triggerRef}
|
||||
class="inline-flex items-center justify-between gap-2 px-2 py-1 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded hover:bg-gray-50 dark:hover:bg-gray-700 outline-none focus:ring-2 focus:ring-blue-500 text-xs min-w-[180px] transition-colors"
|
||||
class="selector-trigger"
|
||||
>
|
||||
<div class="flex flex-col items-start min-w-0">
|
||||
<span class="text-gray-700 dark:text-gray-200 font-medium">
|
||||
<div class="selector-trigger-label">
|
||||
<span class="selector-trigger-primary">
|
||||
Model: {currentModelValue()?.name ?? "None"}
|
||||
</span>
|
||||
{currentModelValue() && (
|
||||
<span class="text-gray-500 dark:text-gray-400 text-[10px]">
|
||||
<span class="selector-trigger-secondary">
|
||||
{currentModelValue()!.providerId}/{currentModelValue()!.id}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<Combobox.Icon class="flex-shrink-0">
|
||||
<ChevronDown class="w-3 h-3 text-gray-500 dark:text-gray-300" />
|
||||
<Combobox.Icon class="selector-trigger-icon">
|
||||
<ChevronDown class="w-3 h-3" />
|
||||
</Combobox.Icon>
|
||||
</Combobox.Trigger>
|
||||
</Combobox.Control>
|
||||
|
||||
<Combobox.Portal>
|
||||
<Combobox.Content class="bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-md shadow-lg overflow-hidden z-50 min-w-[300px]">
|
||||
<div class="p-2 border-b border-gray-200 dark:border-gray-700">
|
||||
<Combobox.Content class="selector-popover">
|
||||
<div class="selector-search-container">
|
||||
<Combobox.Input
|
||||
ref={searchInputRef}
|
||||
class="w-full px-3 py-1.5 text-xs border border-gray-300 dark:border-gray-600 rounded outline-none focus:ring-2 focus:ring-blue-500 bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100"
|
||||
class="selector-search-input"
|
||||
placeholder="Search models..."
|
||||
/>
|
||||
</div>
|
||||
<Combobox.Listbox class="max-h-64 overflow-auto p-1 bg-white dark:bg-gray-800" />
|
||||
<Combobox.Listbox class="selector-listbox" />
|
||||
</Combobox.Content>
|
||||
</Combobox.Portal>
|
||||
</Combobox>
|
||||
<span class="text-xs text-gray-400 dark:text-gray-500">
|
||||
<span class="hint">
|
||||
<Kbd shortcut="cmd+shift+m" />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -214,22 +214,30 @@ const OpenCodeBinarySelector: Component<OpenCodeBinarySelectorProps> = (props) =
|
||||
type="button"
|
||||
onClick={handleButtonClick}
|
||||
disabled={props.disabled}
|
||||
class="w-full px-3 py-2 text-left bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg shadow-sm hover:border-gray-400 dark:hover:border-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-between"
|
||||
classList={{
|
||||
"selector-trigger": true,
|
||||
"w-full": true,
|
||||
"px-3": true,
|
||||
"py-2": true,
|
||||
"text-sm": true,
|
||||
"shadow-sm": true,
|
||||
"selector-trigger-disabled": props.disabled
|
||||
}}
|
||||
ref={buttonRef}
|
||||
>
|
||||
<div class="flex items-center gap-2 flex-1 min-w-0">
|
||||
<Show when={validating()} fallback={<FolderOpen class="w-4 h-4 text-gray-400 dark:text-gray-500" />}>
|
||||
<Loader2 class="w-4 h-4 text-blue-500 animate-spin" />
|
||||
<Show when={validating()} fallback={<FolderOpen class="w-4 h-4 selector-trigger-icon" />}>
|
||||
<Loader2 class="w-4 h-4 selector-loading-spinner" />
|
||||
</Show>
|
||||
<span class="text-sm text-gray-900 dark:text-gray-100 truncate">
|
||||
<span class="truncate">
|
||||
{getDisplayName(props.selectedBinary || "opencode")}
|
||||
</span>
|
||||
<Show when={versionInfo().get(props.selectedBinary)}>
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400">v{versionInfo().get(props.selectedBinary)}</span>
|
||||
<span class="selector-badge-version">v{versionInfo().get(props.selectedBinary)}</span>
|
||||
</Show>
|
||||
</div>
|
||||
<ChevronDown
|
||||
class={`w-4 h-4 text-gray-400 dark:text-gray-500 transition-transform ${isOpen() ? "rotate-180" : ""}`}
|
||||
class={`w-4 h-4 selector-trigger-icon transition-transform ${isOpen() ? "rotate-180" : ""}`}
|
||||
/>
|
||||
</button>
|
||||
|
||||
@@ -237,19 +245,19 @@ const OpenCodeBinarySelector: Component<OpenCodeBinarySelectorProps> = (props) =
|
||||
<Show when={isOpen()}>
|
||||
<div
|
||||
data-binary-dropdown
|
||||
class="absolute top-full left-0 right-0 z-50 mt-1 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg shadow-lg max-h-96 overflow-y-auto"
|
||||
class="selector-popover absolute top-full left-0 right-0 mt-1 max-h-96 overflow-y-auto"
|
||||
style={{
|
||||
position: "absolute",
|
||||
"z-index": 500,
|
||||
"max-height": "24rem",
|
||||
}}
|
||||
>
|
||||
<div class="p-3 border-b border-gray-200 dark:border-gray-700">
|
||||
<div class="text-xs font-medium text-gray-700 dark:text-gray-300 mb-2">OpenCode Binary Selection</div>
|
||||
<div class="selector-section">
|
||||
<div class="selector-section-title mb-2">OpenCode Binary Selection</div>
|
||||
|
||||
{/* Custom path input */}
|
||||
<div class="space-y-2">
|
||||
<div class="flex gap-2">
|
||||
<div class="selector-input-group">
|
||||
<input
|
||||
type="text"
|
||||
value={customPath()}
|
||||
@@ -263,12 +271,12 @@ const OpenCodeBinarySelector: Component<OpenCodeBinarySelectorProps> = (props) =
|
||||
}
|
||||
}}
|
||||
placeholder="Enter path to opencode binary..."
|
||||
class="flex-1 px-2 py-1.5 text-sm border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-1 focus:ring-blue-500"
|
||||
class="selector-input"
|
||||
/>
|
||||
<button
|
||||
onClick={handleCustomPathSubmit}
|
||||
disabled={!customPath().trim() || validating()}
|
||||
class="px-3 py-1.5 text-sm bg-blue-600 text-white rounded hover:bg-blue-700 disabled:bg-gray-300 dark:disabled:bg-gray-600 disabled:cursor-not-allowed"
|
||||
class="selector-button selector-button-primary"
|
||||
>
|
||||
Add
|
||||
</button>
|
||||
@@ -278,7 +286,7 @@ const OpenCodeBinarySelector: Component<OpenCodeBinarySelectorProps> = (props) =
|
||||
<button
|
||||
onClick={handleBrowseBinary}
|
||||
disabled={validating()}
|
||||
class="w-full px-3 py-1.5 text-sm bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded hover:bg-gray-200 dark:hover:bg-gray-600 disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
|
||||
class="selector-button selector-button-secondary w-full flex items-center justify-center gap-2"
|
||||
>
|
||||
<FolderOpen class="w-4 h-4" />
|
||||
Browse for Binary...
|
||||
@@ -287,10 +295,10 @@ const OpenCodeBinarySelector: Component<OpenCodeBinarySelectorProps> = (props) =
|
||||
|
||||
{/* Validation error */}
|
||||
<Show when={validationError()}>
|
||||
<div class="mt-2 p-2 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded">
|
||||
<div class="flex items-start gap-2">
|
||||
<AlertCircle class="w-4 h-4 text-red-500 mt-0.5 flex-shrink-0" />
|
||||
<span class="text-xs text-red-700 dark:text-red-400">{validationError()}</span>
|
||||
<div class="selector-validation-error mt-2">
|
||||
<div class="selector-validation-error-content">
|
||||
<AlertCircle class="selector-validation-error-icon" />
|
||||
<span class="selector-validation-error-text">{validationError()}</span>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
@@ -301,7 +309,7 @@ const OpenCodeBinarySelector: Component<OpenCodeBinarySelectorProps> = (props) =
|
||||
<Show
|
||||
when={binaries().length > 0}
|
||||
fallback={
|
||||
<div class="p-4 text-center text-sm text-gray-500 dark:text-gray-400">
|
||||
<div class="selector-empty-state">
|
||||
No recent binaries. Add one above or use system PATH.
|
||||
</div>
|
||||
}
|
||||
@@ -317,8 +325,8 @@ const OpenCodeBinarySelector: Component<OpenCodeBinarySelectorProps> = (props) =
|
||||
|
||||
return (
|
||||
<div
|
||||
class={`px-3 py-2 hover:bg-gray-50 dark:hover:bg-gray-700 cursor-pointer border-b border-gray-100 dark:border-gray-700 last:border-b-0 ${
|
||||
isSelected() ? "bg-blue-50 dark:bg-blue-900/20" : ""
|
||||
class={`selector-option border-b last:border-b-0 ${
|
||||
isSelected() ? "selector-option-selected" : ""
|
||||
}`}
|
||||
onClick={() => handleSelectBinary(binary.path)}
|
||||
>
|
||||
@@ -326,19 +334,19 @@ const OpenCodeBinarySelector: Component<OpenCodeBinarySelectorProps> = (props) =
|
||||
<div class="flex items-center gap-2 flex-1 min-w-0">
|
||||
<Show
|
||||
when={isSelected()}
|
||||
fallback={<FolderOpen class="w-4 h-4 text-gray-400 dark:text-gray-500" />}
|
||||
fallback={<FolderOpen class="w-4 h-4 selector-trigger-icon" />}
|
||||
>
|
||||
<Check class="w-4 h-4 text-blue-600 dark:text-blue-400" />
|
||||
<Check class="w-4 h-4 selector-option-indicator" />
|
||||
</Show>
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="text-sm font-medium text-gray-900 dark:text-gray-100 truncate">
|
||||
<div class="selector-option-label truncate">
|
||||
{getDisplayName(binary.path)}
|
||||
</div>
|
||||
<div class="flex items-center gap-2 mt-0.5">
|
||||
<Show when={version()}>
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400">v{version()}</span>
|
||||
<span class="selector-badge-version">v{version()}</span>
|
||||
</Show>
|
||||
<span class="text-xs text-gray-400 dark:text-gray-500">
|
||||
<span class="selector-badge-time">
|
||||
{formatRelativeTime(binary.lastUsed)}
|
||||
</span>
|
||||
</div>
|
||||
@@ -360,30 +368,30 @@ const OpenCodeBinarySelector: Component<OpenCodeBinarySelectorProps> = (props) =
|
||||
</div>
|
||||
|
||||
{/* Default option */}
|
||||
<div class="p-2 border-t border-gray-200 dark:border-gray-700">
|
||||
<div class="selector-section">
|
||||
<div
|
||||
class={`px-3 py-2 hover:bg-gray-50 dark:hover:bg-gray-700 cursor-pointer rounded ${
|
||||
props.selectedBinary === "opencode" ? "bg-blue-50 dark:bg-blue-900/20" : ""
|
||||
class={`selector-option ${
|
||||
props.selectedBinary === "opencode" ? "selector-option-selected" : ""
|
||||
}`}
|
||||
onClick={() => handleSelectBinary("opencode")}
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<Show
|
||||
when={props.selectedBinary === "opencode"}
|
||||
fallback={<FolderOpen class="w-4 h-4 text-gray-400 dark:text-gray-500" />}
|
||||
fallback={<FolderOpen class="w-4 h-4 selector-trigger-icon" />}
|
||||
>
|
||||
<Check class="w-4 h-4 text-blue-600 dark:text-blue-400" />
|
||||
<Check class="w-4 h-4 selector-option-indicator" />
|
||||
</Show>
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="text-sm font-medium text-gray-900 dark:text-gray-100">opencode (system PATH)</div>
|
||||
<div class="selector-option-label">opencode (system PATH)</div>
|
||||
<div class="flex items-center gap-2 mt-0.5">
|
||||
<Show when={versionInfo().get("opencode")}>
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400">v{versionInfo().get("opencode")}</span>
|
||||
<span class="selector-badge-version">v{versionInfo().get("opencode")}</span>
|
||||
</Show>
|
||||
<Show when={!versionInfo().get("opencode") && validating()}>
|
||||
<span class="text-xs text-gray-400 dark:text-gray-500">Checking...</span>
|
||||
<span class="selector-badge-time">Checking...</span>
|
||||
</Show>
|
||||
<span class="text-xs text-gray-400 dark:text-gray-500">Use binary from system PATH</span>
|
||||
<span class="selector-badge-time">Use binary from system PATH</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -798,4 +798,246 @@
|
||||
|
||||
[data-theme="dark"] .dropdown-icon-accent {
|
||||
color: var(--accent-primary);
|
||||
}
|
||||
|
||||
/* Selector component utilities */
|
||||
.selector-trigger {
|
||||
@apply inline-flex items-center justify-between gap-2 px-2 py-1 border rounded outline-none transition-colors text-xs min-w-[180px];
|
||||
background-color: var(--surface-base);
|
||||
border-color: var(--border-base);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.selector-trigger:hover {
|
||||
background-color: var(--surface-hover);
|
||||
}
|
||||
|
||||
.selector-trigger:focus {
|
||||
@apply ring-2;
|
||||
ring-color: var(--accent-primary);
|
||||
}
|
||||
|
||||
.selector-trigger-disabled {
|
||||
@apply opacity-50 cursor-not-allowed;
|
||||
}
|
||||
|
||||
.selector-trigger-label {
|
||||
@apply flex flex-col items-start min-w-0;
|
||||
}
|
||||
|
||||
.selector-trigger-primary {
|
||||
color: var(--text-primary);
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
|
||||
.selector-trigger-secondary {
|
||||
color: var(--text-muted);
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.selector-trigger-icon {
|
||||
@apply flex-shrink-0;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.selector-popover {
|
||||
@apply rounded-md shadow-lg overflow-hidden z-50 min-w-[300px];
|
||||
background-color: var(--surface-base);
|
||||
border: 1px solid var(--border-base);
|
||||
}
|
||||
|
||||
.selector-search-container {
|
||||
@apply p-2 border-b;
|
||||
border-color: var(--border-base);
|
||||
}
|
||||
|
||||
.selector-search-input {
|
||||
@apply w-full px-3 py-1.5 text-xs border rounded outline-none transition-colors;
|
||||
background-color: var(--surface-base);
|
||||
border-color: var(--border-base);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.selector-search-input:focus {
|
||||
@apply ring-2;
|
||||
ring-color: var(--accent-primary);
|
||||
}
|
||||
|
||||
.selector-search-input::placeholder {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.selector-listbox {
|
||||
@apply max-h-64 overflow-auto p-1;
|
||||
background-color: var(--surface-base);
|
||||
}
|
||||
|
||||
.selector-option {
|
||||
@apply px-3 py-2 cursor-pointer rounded outline-none transition-colors flex items-start gap-2;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.selector-option:hover {
|
||||
background-color: var(--surface-hover);
|
||||
}
|
||||
|
||||
.selector-option-highlighted {
|
||||
background-color: rgba(0, 102, 255, 0.1);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .selector-option-highlighted {
|
||||
background-color: rgba(0, 128, 255, 0.2);
|
||||
}
|
||||
|
||||
.selector-option-selected {
|
||||
background-color: rgba(0, 102, 255, 0.15);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .selector-option-selected {
|
||||
background-color: rgba(0, 128, 255, 0.25);
|
||||
}
|
||||
|
||||
.selector-option-content {
|
||||
@apply flex flex-col flex-1 min-w-0;
|
||||
}
|
||||
|
||||
.selector-option-label {
|
||||
@apply font-medium text-sm;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.selector-option-description {
|
||||
@apply text-xs;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.selector-option-indicator {
|
||||
@apply flex-shrink-0 mt-0.5;
|
||||
color: var(--accent-primary);
|
||||
}
|
||||
|
||||
.selector-section {
|
||||
@apply px-3 py-2 border-b;
|
||||
border-color: var(--border-base);
|
||||
background-color: var(--surface-secondary);
|
||||
}
|
||||
|
||||
.selector-section-title {
|
||||
@apply text-xs font-medium;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.selector-badge {
|
||||
@apply rounded px-1.5 py-0.5 text-xs font-normal;
|
||||
background-color: var(--accent-primary);
|
||||
color: var(--text-inverted);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .selector-badge {
|
||||
background-color: rgba(0, 128, 255, 0.2);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.selector-badge-version {
|
||||
@apply text-xs;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.selector-badge-time {
|
||||
@apply text-xs;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.selector-validation-error {
|
||||
@apply p-2 rounded border;
|
||||
background-color: rgba(244, 67, 54, 0.1);
|
||||
border-color: var(--status-error);
|
||||
color: var(--status-error);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .selector-validation-error {
|
||||
background-color: rgba(244, 67, 54, 0.2);
|
||||
}
|
||||
|
||||
.selector-validation-error-content {
|
||||
@apply flex items-start gap-2;
|
||||
}
|
||||
|
||||
.selector-validation-error-icon {
|
||||
@apply w-4 h-4 mt-0.5 flex-shrink-0;
|
||||
color: var(--status-error);
|
||||
}
|
||||
|
||||
.selector-validation-error-text {
|
||||
@apply text-xs;
|
||||
}
|
||||
|
||||
.selector-input-group {
|
||||
@apply flex gap-2;
|
||||
}
|
||||
|
||||
.selector-input {
|
||||
@apply flex-1 px-2 py-1.5 text-sm border rounded outline-none transition-colors;
|
||||
background-color: var(--surface-base);
|
||||
border-color: var(--border-base);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.selector-input:focus {
|
||||
@apply ring-1;
|
||||
ring-color: var(--accent-primary);
|
||||
}
|
||||
|
||||
.selector-input::placeholder {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.selector-button {
|
||||
@apply px-3 py-1.5 text-sm rounded transition-colors cursor-pointer;
|
||||
}
|
||||
|
||||
.selector-button-primary {
|
||||
background-color: var(--accent-primary);
|
||||
color: var(--text-inverted);
|
||||
}
|
||||
|
||||
.selector-button-primary:hover:not(:disabled) {
|
||||
background-color: var(--accent-hover);
|
||||
}
|
||||
|
||||
.selector-button-primary:disabled {
|
||||
@apply opacity-50 cursor-not-allowed;
|
||||
background-color: var(--surface-muted);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .selector-button-primary:disabled {
|
||||
background-color: var(--surface-secondary);
|
||||
}
|
||||
|
||||
.selector-button-secondary {
|
||||
background-color: var(--surface-secondary);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.selector-button-secondary:hover:not(:disabled) {
|
||||
background-color: var(--surface-hover);
|
||||
}
|
||||
|
||||
.selector-button-secondary:disabled {
|
||||
@apply opacity-50 cursor-not-allowed;
|
||||
}
|
||||
|
||||
.selector-empty-state {
|
||||
@apply p-4 text-center text-sm;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.selector-loading {
|
||||
@apply flex items-center gap-2;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.selector-loading-spinner {
|
||||
@apply w-4 h-4 animate-spin;
|
||||
color: var(--accent-primary);
|
||||
}
|
||||
Reference in New Issue
Block a user