ソースを参照

chore(app): use radio group in prompt input (#14025)

Adam 2 ヶ月 前
コミット
d327a2b1cf

+ 49 - 40
packages/app/src/components/prompt-input.tsx

@@ -1,5 +1,5 @@
 import { useFilteredList } from "@opencode-ai/ui/hooks"
-import { createEffect, on, Component, Show, For, onCleanup, Switch, Match, createMemo, createSignal } from "solid-js"
+import { createEffect, on, Component, Show, onCleanup, Switch, Match, createMemo, createSignal } from "solid-js"
 import { createStore } from "solid-js/store"
 import { createFocusSignal } from "@solid-primitives/active-element"
 import { useLocal } from "@/context/local"
@@ -26,6 +26,7 @@ import type { IconName } from "@opencode-ai/ui/icons/provider"
 import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip"
 import { IconButton } from "@opencode-ai/ui/icon-button"
 import { Select } from "@opencode-ai/ui/select"
+import { RadioGroup } from "@opencode-ai/ui/radio-group"
 import { useDialog } from "@opencode-ai/ui/context/dialog"
 import { ModelSelectorPopover } from "@/components/dialog-select-model"
 import { DialogSelectModelUnpaid } from "@/components/dialog-select-model-unpaid"
@@ -249,7 +250,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
     return messages.some((m) => m.role === "user")
   })
 
-  const MAX_HISTORY = 100
   const [history, setHistory] = persisted(
     Persist.global("prompt-history", ["prompt-history.v1"]),
     createStore<{
@@ -319,6 +319,9 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
     requestAnimationFrame(() => editorRef?.focus())
   }
 
+  const shellModeKey = "mod+shift+x"
+  const normalModeKey = "mod+shift+e"
+
   command.register("prompt-input", () => [
     {
       id: "file.attach",
@@ -328,6 +331,22 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
       disabled: store.mode !== "normal",
       onSelect: pick,
     },
+    {
+      id: "prompt.mode.shell",
+      title: language.t("command.prompt.mode.shell"),
+      category: language.t("command.category.session"),
+      keybind: shellModeKey,
+      disabled: store.mode === "shell",
+      onSelect: () => setMode("shell"),
+    },
+    {
+      id: "prompt.mode.normal",
+      title: language.t("command.prompt.mode.normal"),
+      category: language.t("command.category.session"),
+      keybind: normalModeKey,
+      disabled: store.mode === "normal",
+      onSelect: () => setMode("normal"),
+    },
   ])
 
   const closePopover = () => setStore("popover", null)
@@ -1339,45 +1358,35 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
                 </TooltipKeybind>
               </Show>
             </div>
-
-            <div class="shrink-0">
-              <div
-                data-component="prompt-mode-toggle"
-                class="relative h-7 w-[68px] rounded-[4px] bg-surface-inset-base border border-[0.5px] border-border-weak-base p-0 flex items-center gap-1 overflow-visible"
-              >
-                <div
-                  class="absolute inset-y-0 left-0 w-[calc((100%-4px)/2)] rounded-[4px] bg-surface-raised-stronger-non-alpha shadow-[var(--shadow-xs-border)] transition-transform duration-200 ease-out will-change-transform"
-                  style={{
-                    transform: store.mode === "shell" ? "translateX(0px)" : "translateX(calc(100% + 4px))",
-                  }}
-                />
-                <button
-                  type="button"
-                  class="relative z-10 flex-1 h-full p-0.5 flex items-center justify-center"
-                  aria-pressed={store.mode === "shell"}
-                  onClick={() => setMode("shell")}
-                >
-                  <div
-                    class="w-full h-full flex items-center justify-center rounded-[2px] transition-colors hover:bg-surface-inset-base"
-                    classList={{ "hover:bg-transparent": store.mode === "shell" }}
-                  >
-                    <Icon name="console" class="size-[18px]" />
-                  </div>
-                </button>
-                <button
-                  type="button"
-                  class="relative z-10 flex-1 h-full p-0.5 flex items-center justify-center"
-                  aria-pressed={store.mode === "normal"}
-                  onClick={() => setMode("normal")}
-                >
-                  <div
-                    class="w-full h-full flex items-center justify-center rounded-[2px] transition-colors hover:bg-surface-inset-base"
-                    classList={{ "hover:bg-transparent": store.mode === "normal" }}
+            <div class="shrink-0" data-component="prompt-mode-toggle">
+              <RadioGroup
+                options={["shell", "normal"] as const}
+                current={store.mode}
+                value={(mode) => mode}
+                label={(mode) => (
+                  <TooltipKeybind
+                    placement="top"
+                    gutter={4}
+                    title={language.t(mode === "shell" ? "command.prompt.mode.shell" : "command.prompt.mode.normal")}
+                    keybind={command.keybind(mode === "shell" ? "prompt.mode.shell" : "prompt.mode.normal")}
+                    class="size-full flex items-center justify-center"
                   >
-                    <Icon name="prompt" class="size-[18px]" />
-                  </div>
-                </button>
-              </div>
+                    <Icon
+                      name={mode === "shell" ? "console" : "prompt"}
+                      class="size-[18px]"
+                      classList={{
+                        "text-icon-strong-base": mode === "shell" && store.mode === "shell",
+                        "text-icon-interactive-base": mode === "normal" && store.mode === "normal",
+                        "text-icon-weak": store.mode !== mode,
+                      }}
+                    />
+                  </TooltipKeybind>
+                )}
+                onSelect={(mode) => mode && setMode(mode)}
+                fill
+                pad="none"
+                class="w-[68px]"
+              />
             </div>
           </div>
         </div>

+ 3 - 0
packages/app/src/i18n/ar.ts

@@ -63,6 +63,8 @@ export const dict = {
   "command.agent.cycle.reverse.description": "التبديل إلى الوكيل السابق",
   "command.model.variant.cycle": "تغيير جهد التفكير",
   "command.model.variant.cycle.description": "التبديل إلى مستوى الجهد التالي",
+  "command.prompt.mode.shell": "التبديل إلى وضع Shell",
+  "command.prompt.mode.normal": "التبديل إلى وضع Prompt",
   "command.permissions.autoaccept.enable": "قبول التعديلات تلقائيًا",
   "command.permissions.autoaccept.disable": "إيقاف قبول التعديلات تلقائيًا",
   "command.workspace.toggle": "تبديل مساحات العمل",
@@ -210,6 +212,7 @@ export const dict = {
   "prompt.placeholder.summarizeComments": "لخّص التعليقات…",
   "prompt.placeholder.summarizeComment": "لخّص التعليق…",
   "prompt.mode.shell": "Shell",
+  "prompt.mode.normal": "Prompt",
   "prompt.mode.shell.exit": "esc للخروج",
   "prompt.example.1": "إصلاح TODO في قاعدة التعليمات البرمجية",
   "prompt.example.2": "ما هو المكدس التقني لهذا المشروع؟",

+ 3 - 0
packages/app/src/i18n/br.ts

@@ -63,6 +63,8 @@ export const dict = {
   "command.agent.cycle.reverse.description": "Mudar para o agente anterior",
   "command.model.variant.cycle": "Alternar nível de raciocínio",
   "command.model.variant.cycle.description": "Mudar para o próximo nível de esforço",
+  "command.prompt.mode.shell": "Alternar para o modo Shell",
+  "command.prompt.mode.normal": "Alternar para o modo Prompt",
   "command.permissions.autoaccept.enable": "Aceitar edições automaticamente",
   "command.permissions.autoaccept.disable": "Parar de aceitar edições automaticamente",
   "command.workspace.toggle": "Alternar espaços de trabalho",
@@ -210,6 +212,7 @@ export const dict = {
   "prompt.placeholder.summarizeComments": "Resumir comentários…",
   "prompt.placeholder.summarizeComment": "Resumir comentário…",
   "prompt.mode.shell": "Shell",
+  "prompt.mode.normal": "Prompt",
   "prompt.mode.shell.exit": "esc para sair",
   "prompt.example.1": "Corrigir um TODO no código",
   "prompt.example.2": "Qual é a stack tecnológica deste projeto?",

+ 3 - 0
packages/app/src/i18n/bs.ts

@@ -69,6 +69,8 @@ export const dict = {
   "command.agent.cycle.reverse.description": "Prebaci na prethodnog agenta",
   "command.model.variant.cycle": "Promijeni nivo razmišljanja",
   "command.model.variant.cycle.description": "Prebaci na sljedeći nivo",
+  "command.prompt.mode.shell": "Prebaci na Shell način",
+  "command.prompt.mode.normal": "Prebaci na Prompt način",
   "command.permissions.autoaccept.enable": "Automatski prihvataj izmjene",
   "command.permissions.autoaccept.disable": "Zaustavi automatsko prihvatanje izmjena",
   "command.workspace.toggle": "Prikaži/sakrij radne prostore",
@@ -228,6 +230,7 @@ export const dict = {
   "prompt.placeholder.summarizeComments": "Sažmi komentare…",
   "prompt.placeholder.summarizeComment": "Sažmi komentar…",
   "prompt.mode.shell": "Shell",
+  "prompt.mode.normal": "Prompt",
   "prompt.mode.shell.exit": "esc za izlaz",
 
   "prompt.example.1": "Popravi TODO u bazi koda",

+ 3 - 0
packages/app/src/i18n/da.ts

@@ -69,6 +69,8 @@ export const dict = {
   "command.agent.cycle.reverse.description": "Skift til forrige agent",
   "command.model.variant.cycle": "Skift tænkeindsats",
   "command.model.variant.cycle.description": "Skift til næste indsatsniveau",
+  "command.prompt.mode.shell": "Skift til shell-tilstand",
+  "command.prompt.mode.normal": "Skift til prompt-tilstand",
   "command.permissions.autoaccept.enable": "Accepter ændringer automatisk",
   "command.permissions.autoaccept.disable": "Stop automatisk accept af ændringer",
   "command.workspace.toggle": "Skift arbejdsområder",
@@ -226,6 +228,7 @@ export const dict = {
   "prompt.placeholder.summarizeComments": "Opsummér kommentarer…",
   "prompt.placeholder.summarizeComment": "Opsummér kommentar…",
   "prompt.mode.shell": "Shell",
+  "prompt.mode.normal": "Prompt",
   "prompt.mode.shell.exit": "esc for at afslutte",
 
   "prompt.example.1": "Ret en TODO i koden",

+ 3 - 0
packages/app/src/i18n/de.ts

@@ -67,6 +67,8 @@ export const dict = {
   "command.agent.cycle.reverse.description": "Zum vorherigen Agenten wechseln",
   "command.model.variant.cycle": "Denkaufwand wechseln",
   "command.model.variant.cycle.description": "Zum nächsten Aufwandslevel wechseln",
+  "command.prompt.mode.shell": "In den Shell-Modus wechseln",
+  "command.prompt.mode.normal": "In den Prompt-Modus wechseln",
   "command.permissions.autoaccept.enable": "Änderungen automatisch akzeptieren",
   "command.permissions.autoaccept.disable": "Automatische Annahme von Änderungen stoppen",
   "command.workspace.toggle": "Arbeitsbereiche umschalten",
@@ -215,6 +217,7 @@ export const dict = {
   "prompt.placeholder.summarizeComments": "Kommentare zusammenfassen…",
   "prompt.placeholder.summarizeComment": "Kommentar zusammenfassen…",
   "prompt.mode.shell": "Shell",
+  "prompt.mode.normal": "Prompt",
   "prompt.mode.shell.exit": "esc zum Verlassen",
   "prompt.example.1": "Ein TODO in der Codebasis beheben",
   "prompt.example.2": "Was ist der Tech-Stack dieses Projekts?",

+ 3 - 0
packages/app/src/i18n/en.ts

@@ -69,6 +69,8 @@ export const dict = {
   "command.agent.cycle.reverse.description": "Switch to the previous agent",
   "command.model.variant.cycle": "Cycle thinking effort",
   "command.model.variant.cycle.description": "Switch to the next effort level",
+  "command.prompt.mode.shell": "Switch to shell mode",
+  "command.prompt.mode.normal": "Switch to prompt mode",
   "command.permissions.autoaccept.enable": "Auto-accept edits",
   "command.permissions.autoaccept.disable": "Stop auto-accepting edits",
   "command.workspace.toggle": "Toggle workspaces",
@@ -228,6 +230,7 @@ export const dict = {
   "prompt.placeholder.summarizeComments": "Summarize comments…",
   "prompt.placeholder.summarizeComment": "Summarize comment…",
   "prompt.mode.shell": "Shell",
+  "prompt.mode.normal": "Prompt",
   "prompt.mode.shell.exit": "esc to exit",
 
   "prompt.example.1": "Fix a TODO in the codebase",

+ 3 - 0
packages/app/src/i18n/es.ts

@@ -69,6 +69,8 @@ export const dict = {
   "command.agent.cycle.reverse.description": "Cambiar al agente anterior",
   "command.model.variant.cycle": "Alternar esfuerzo de pensamiento",
   "command.model.variant.cycle.description": "Cambiar al siguiente nivel de esfuerzo",
+  "command.prompt.mode.shell": "Cambiar al modo Shell",
+  "command.prompt.mode.normal": "Cambiar al modo Prompt",
   "command.permissions.autoaccept.enable": "Aceptar ediciones automáticamente",
   "command.permissions.autoaccept.disable": "Dejar de aceptar ediciones automáticamente",
   "command.workspace.toggle": "Alternar espacios de trabajo",
@@ -227,6 +229,7 @@ export const dict = {
   "prompt.placeholder.summarizeComments": "Resumir comentarios…",
   "prompt.placeholder.summarizeComment": "Resumir comentario…",
   "prompt.mode.shell": "Shell",
+  "prompt.mode.normal": "Prompt",
   "prompt.mode.shell.exit": "esc para salir",
 
   "prompt.example.1": "Arreglar un TODO en el código",

+ 3 - 0
packages/app/src/i18n/fr.ts

@@ -63,6 +63,8 @@ export const dict = {
   "command.agent.cycle.reverse.description": "Passer à l'agent précédent",
   "command.model.variant.cycle": "Changer l'effort de réflexion",
   "command.model.variant.cycle.description": "Passer au niveau d'effort suivant",
+  "command.prompt.mode.shell": "Passer en mode Shell",
+  "command.prompt.mode.normal": "Passer en mode Prompt",
   "command.permissions.autoaccept.enable": "Accepter automatiquement les modifications",
   "command.permissions.autoaccept.disable": "Arrêter l'acceptation automatique des modifications",
   "command.workspace.toggle": "Basculer les espaces de travail",
@@ -210,6 +212,7 @@ export const dict = {
   "prompt.placeholder.summarizeComments": "Résumer les commentaires…",
   "prompt.placeholder.summarizeComment": "Résumer le commentaire…",
   "prompt.mode.shell": "Shell",
+  "prompt.mode.normal": "Prompt",
   "prompt.mode.shell.exit": "esc pour quitter",
   "prompt.example.1": "Corriger un TODO dans la base de code",
   "prompt.example.2": "Quelle est la pile technique de ce projet ?",

+ 3 - 0
packages/app/src/i18n/ja.ts

@@ -63,6 +63,8 @@ export const dict = {
   "command.agent.cycle.reverse.description": "前のエージェントに切り替え",
   "command.model.variant.cycle": "思考レベルの切り替え",
   "command.model.variant.cycle.description": "次の思考レベルに切り替え",
+  "command.prompt.mode.shell": "シェルモードに切り替える",
+  "command.prompt.mode.normal": "プロンプトモードに切り替える",
   "command.permissions.autoaccept.enable": "編集を自動承認",
   "command.permissions.autoaccept.disable": "編集の自動承認を停止",
   "command.workspace.toggle": "ワークスペースを切り替え",
@@ -209,6 +211,7 @@ export const dict = {
   "prompt.placeholder.summarizeComments": "コメントを要約…",
   "prompt.placeholder.summarizeComment": "コメントを要約…",
   "prompt.mode.shell": "シェル",
+  "prompt.mode.normal": "プロンプト",
   "prompt.mode.shell.exit": "escで終了",
   "prompt.example.1": "コードベースのTODOを修正",
   "prompt.example.2": "このプロジェクトの技術スタックは何ですか?",

+ 3 - 0
packages/app/src/i18n/ko.ts

@@ -67,6 +67,8 @@ export const dict = {
   "command.agent.cycle.reverse.description": "이전 에이전트로 전환",
   "command.model.variant.cycle": "생각 수준 순환",
   "command.model.variant.cycle.description": "다음 생각 수준으로 전환",
+  "command.prompt.mode.shell": "셸 모드로 전환",
+  "command.prompt.mode.normal": "프롬프트 모드로 전환",
   "command.permissions.autoaccept.enable": "편집 자동 수락",
   "command.permissions.autoaccept.disable": "편집 자동 수락 중지",
   "command.workspace.toggle": "작업 공간 전환",
@@ -213,6 +215,7 @@ export const dict = {
   "prompt.placeholder.summarizeComments": "댓글 요약…",
   "prompt.placeholder.summarizeComment": "댓글 요약…",
   "prompt.mode.shell": "셸",
+  "prompt.mode.normal": "프롬프트",
   "prompt.mode.shell.exit": "종료하려면 esc",
   "prompt.example.1": "코드베이스의 TODO 수정",
   "prompt.example.2": "이 프로젝트의 기술 스택이 무엇인가요?",

+ 3 - 0
packages/app/src/i18n/no.ts

@@ -72,6 +72,8 @@ export const dict = {
   "command.agent.cycle.reverse.description": "Bytt til forrige agent",
   "command.model.variant.cycle": "Bytt tenkeinnsats",
   "command.model.variant.cycle.description": "Bytt til neste innsatsnivå",
+  "command.prompt.mode.shell": "Bytt til Shell-modus",
+  "command.prompt.mode.normal": "Bytt til Prompt-modus",
   "command.permissions.autoaccept.enable": "Godta endringer automatisk",
   "command.permissions.autoaccept.disable": "Slutt å godta endringer automatisk",
   "command.workspace.toggle": "Veksle arbeidsområder",
@@ -230,6 +232,7 @@ export const dict = {
   "prompt.placeholder.summarizeComments": "Oppsummer kommentarer…",
   "prompt.placeholder.summarizeComment": "Oppsummer kommentar…",
   "prompt.mode.shell": "Shell",
+  "prompt.mode.normal": "Prompt",
   "prompt.mode.shell.exit": "ESC for å avslutte",
 
   "prompt.example.1": "Fiks en TODO i kodebasen",

+ 3 - 0
packages/app/src/i18n/pl.ts

@@ -63,6 +63,8 @@ export const dict = {
   "command.agent.cycle.reverse.description": "Przełącz na poprzedniego agenta",
   "command.model.variant.cycle": "Przełącz wysiłek myślowy",
   "command.model.variant.cycle.description": "Przełącz na następny poziom wysiłku",
+  "command.prompt.mode.shell": "Przełącz na tryb terminala",
+  "command.prompt.mode.normal": "Przełącz na tryb Prompt",
   "command.permissions.autoaccept.enable": "Automatyczne akceptowanie edycji",
   "command.permissions.autoaccept.disable": "Zatrzymaj automatyczne akceptowanie edycji",
   "command.workspace.toggle": "Przełącz przestrzenie robocze",
@@ -211,6 +213,7 @@ export const dict = {
   "prompt.placeholder.summarizeComments": "Podsumuj komentarze…",
   "prompt.placeholder.summarizeComment": "Podsumuj komentarz…",
   "prompt.mode.shell": "Terminal",
+  "prompt.mode.normal": "Prompt",
   "prompt.mode.shell.exit": "esc aby wyjść",
   "prompt.example.1": "Napraw TODO w bazie kodu",
   "prompt.example.2": "Jaki jest stos technologiczny tego projektu?",

+ 3 - 0
packages/app/src/i18n/ru.ts

@@ -69,6 +69,8 @@ export const dict = {
   "command.agent.cycle.reverse.description": "Переключиться к предыдущему агенту",
   "command.model.variant.cycle": "Цикл режимов мышления",
   "command.model.variant.cycle.description": "Переключиться к следующему уровню усилий",
+  "command.prompt.mode.shell": "Переключиться в режим оболочки",
+  "command.prompt.mode.normal": "Переключиться в режим промпта",
   "command.permissions.autoaccept.enable": "Авто-принятие изменений",
   "command.permissions.autoaccept.disable": "Прекратить авто-принятие изменений",
   "command.workspace.toggle": "Переключить рабочие пространства",
@@ -227,6 +229,7 @@ export const dict = {
   "prompt.placeholder.summarizeComments": "Суммировать комментарии…",
   "prompt.placeholder.summarizeComment": "Суммировать комментарий…",
   "prompt.mode.shell": "Оболочка",
+  "prompt.mode.normal": "Промпт",
   "prompt.mode.shell.exit": "esc для выхода",
 
   "prompt.example.1": "Исправить TODO в коде",

+ 3 - 0
packages/app/src/i18n/th.ts

@@ -69,6 +69,8 @@ export const dict = {
   "command.agent.cycle.reverse.description": "สลับไปยังเอเจนต์ก่อนหน้า",
   "command.model.variant.cycle": "เปลี่ยนความพยายามในการคิด",
   "command.model.variant.cycle.description": "สลับไปยังระดับความพยายามถัดไป",
+  "command.prompt.mode.shell": "สลับไปยังโหมดเชลล์",
+  "command.prompt.mode.normal": "สลับไปยังโหมดพรอมต์",
   "command.permissions.autoaccept.enable": "ยอมรับการแก้ไขโดยอัตโนมัติ",
   "command.permissions.autoaccept.disable": "หยุดยอมรับการแก้ไขโดยอัตโนมัติ",
   "command.workspace.toggle": "สลับพื้นที่ทำงาน",
@@ -227,6 +229,7 @@ export const dict = {
   "prompt.placeholder.summarizeComments": "สรุปความคิดเห็น…",
   "prompt.placeholder.summarizeComment": "สรุปความคิดเห็น…",
   "prompt.mode.shell": "เชลล์",
+  "prompt.mode.normal": "พรอมต์",
   "prompt.mode.shell.exit": "กด esc เพื่อออก",
 
   "prompt.example.1": "แก้ไข TODO ในโค้ดเบส",

+ 4 - 0
packages/app/src/i18n/zh.ts

@@ -93,6 +93,9 @@ export const dict = {
   "command.model.variant.cycle": "切换思考强度",
   "command.model.variant.cycle.description": "切换到下一个强度等级",
 
+  "command.prompt.mode.shell": "切换到 Shell 模式",
+  "command.prompt.mode.normal": "切换到 Prompt 模式",
+
   "command.permissions.autoaccept.enable": "自动接受编辑",
   "command.permissions.autoaccept.disable": "停止自动接受编辑",
 
@@ -248,6 +251,7 @@ export const dict = {
   "prompt.placeholder.summarizeComments": "总结评论…",
   "prompt.placeholder.summarizeComment": "总结该评论…",
   "prompt.mode.shell": "Shell",
+  "prompt.mode.normal": "Prompt",
   "prompt.mode.shell.exit": "按 esc 退出",
   "prompt.example.1": "修复代码库中的一个 TODO",
   "prompt.example.2": "这个项目的技术栈是什么?",

+ 3 - 0
packages/app/src/i18n/zht.ts

@@ -73,6 +73,8 @@ export const dict = {
   "command.agent.cycle.reverse.description": "切換到上一個代理程式",
   "command.model.variant.cycle": "循環思考強度",
   "command.model.variant.cycle.description": "切換到下一個強度等級",
+  "command.prompt.mode.shell": "切換到 Shell 模式",
+  "command.prompt.mode.normal": "切換到 Prompt 模式",
   "command.permissions.autoaccept.enable": "自動接受編輯",
   "command.permissions.autoaccept.disable": "停止自動接受編輯",
   "command.workspace.toggle": "切換工作區",
@@ -227,6 +229,7 @@ export const dict = {
   "prompt.placeholder.summarizeComments": "摘要評論…",
   "prompt.placeholder.summarizeComment": "摘要這則評論…",
   "prompt.mode.shell": "Shell",
+  "prompt.mode.normal": "Prompt",
   "prompt.mode.shell.exit": "按 esc 退出",
 
   "prompt.example.1": "修復程式碼庫中的一個 TODO",

+ 99 - 69
packages/ui/src/components/radio-group.css

@@ -1,112 +1,146 @@
 [data-component="radio-group"] {
-  display: flex;
-  flex-direction: column;
-  gap: calc(var(--spacing) * 2);
+  --radio-group-height: 28px;
+  --radio-group-gap: 4px;
+  --radio-group-padding: 2px;
+
+  display: inline-flex;
 
   [data-slot="radio-group-wrapper"] {
     all: unset;
-    background-color: var(--surface-base);
-    border-radius: var(--radius-md);
-    box-shadow: var(--shadow-xs-border);
+    background-color: var(--surface-inset-base);
+    border-radius: var(--radius-sm);
+    box-shadow: var(--shadow-xxs-border);
+    display: inline-flex;
+    height: var(--radio-group-height);
     margin: 0;
+    overflow: visible;
     padding: 0;
     position: relative;
     width: fit-content;
   }
 
+  &[data-fill] [data-slot="radio-group-wrapper"] {
+    width: 100%;
+  }
+
   [data-slot="radio-group-items"] {
     display: inline-flex;
-    list-style: none;
     flex-direction: row;
+    gap: var(--radio-group-gap);
+    height: 100%;
+    list-style: none;
+    position: relative;
+    z-index: 1;
+  }
+
+  &[data-fill] [data-slot="radio-group-items"] {
+    width: 100%;
   }
 
   [data-slot="radio-group-indicator"] {
-    background: var(--button-secondary-base);
-    border-radius: var(--radius-md);
+    background: var(--surface-raised-stronger-non-alpha);
+    border-radius: var(--radius-sm);
     box-shadow: var(--shadow-xs-border);
     content: "";
     opacity: var(--indicator-opacity, 1);
+    pointer-events: none;
     position: absolute;
     transition:
-      opacity 300ms ease-in-out,
+      opacity 200ms ease-out,
       box-shadow 100ms ease-in-out,
-      width 150ms ease,
-      height 150ms ease,
-      transform 150ms ease;
+      width 200ms ease-out,
+      height 200ms ease-out,
+      transform 200ms ease-out;
+    will-change: transform;
+    z-index: 0;
   }
 
   [data-slot="radio-group-item"] {
+    display: flex;
+    height: 100%;
+    min-width: 0;
     position: relative;
   }
 
-  /* Separator between items */
-  [data-slot="radio-group-item"]:not(:first-of-type)::before {
-    background: var(--border-weak-base);
-    border-radius: var(--radius-xs);
-    content: "";
-    inset: 6px 0;
-    position: absolute;
-    transition: opacity 150ms ease;
-    width: 1px;
-    transform: translateX(-0.5px);
+  &[data-fill] [data-slot="radio-group-item"] {
+    flex: 1;
   }
 
-  /* Hide separator when item or previous item is checked */
-  [data-slot="radio-group-item"]:has([data-slot="radio-group-item-input"][data-checked])::before,
-  [data-slot="radio-group-item"]:has([data-slot="radio-group-item-input"][data-checked])
-    + [data-slot="radio-group-item"]::before {
-    opacity: 0;
+  [data-slot="radio-group-item-input"] {
+    border-width: 0;
+    clip: rect(0 0 0 0);
+    height: 1px;
+    margin: -1px;
+    overflow: hidden;
+    padding: 0;
+    position: absolute;
+    white-space: nowrap;
+    width: 1px;
   }
 
   [data-slot="radio-group-item-label"] {
     color: var(--text-weak);
+    cursor: pointer;
+    display: flex;
+    align-items: center;
+    justify-content: center;
     font-family: var(--font-family-sans);
     font-size: var(--font-size-small);
     font-weight: var(--font-weight-medium);
-    border-radius: var(--radius-md);
-    cursor: pointer;
-    display: flex;
-    flex-wrap: nowrap;
-    gap: calc(var(--spacing) * 1);
+    flex: 1;
+    height: 100%;
     line-height: 1;
-    padding: 6px 12px;
-    place-content: center;
+    padding: var(--radio-group-padding);
     position: relative;
-    transition-duration: 150ms;
-    transition-property: color, opacity;
-    transition-timing-function: ease-in-out;
+    transition:
+      color 200ms ease-out,
+      opacity 200ms ease-out;
     user-select: none;
   }
 
-  [data-slot="radio-group-item-input"] {
-    all: unset;
+  [data-slot="radio-group-item-control"] {
+    align-items: center;
+    border-radius: var(--radius-xs);
+    display: inline-flex;
+    height: 100%;
+    justify-content: center;
+    min-width: 0;
+    padding: var(--radio-group-control-padding, 0 10px);
+    transition: background-color 200ms ease-out;
+    width: 100%;
   }
 
-  /* Checked state */
-  [data-slot="radio-group-item-input"][data-checked] + [data-slot="radio-group-item-label"] {
-    color: var(--text-strong);
+  &[data-pad="none"] {
+    --radio-group-control-padding: 0;
   }
 
-  /* Disabled state */
-  [data-slot="radio-group-item-input"][data-disabled] + [data-slot="radio-group-item-label"] {
-    cursor: not-allowed;
-    opacity: 0.5;
+  &[data-pad="normal"] {
+    --radio-group-control-padding: 0 10px;
   }
 
-  /* Hover state for unchecked, enabled items */
-  [data-slot="radio-group-item-input"]:not([data-checked], [data-disabled]) + [data-slot="radio-group-item-label"] {
-    cursor: pointer;
-    user-select: none;
+  /* Checked state */
+  [data-slot="radio-group-item-input"][data-checked] + [data-slot="radio-group-item-label"] {
+    color: var(--text-strong);
   }
 
+  /* Hover state: match the inset background (adds subtle density) */
   [data-slot="radio-group-item-input"]:not([data-checked], [data-disabled])
-    + [data-slot="radio-group-item-label"]:hover {
-    color: var(--text-base);
+    + [data-slot="radio-group-item-label"]:hover
+    [data-slot="radio-group-item-control"] {
+    background-color: var(--surface-inset-base);
   }
 
-  [data-slot="radio-group-item-input"]:not([data-checked], [data-disabled])
-    + [data-slot="radio-group-item-label"]:active {
-    opacity: 0.7;
+  /* Do not overlay hover on the active segment */
+  [data-slot="radio-group-item-input"][data-checked]
+    + [data-slot="radio-group-item-label"]:hover
+    [data-slot="radio-group-item-control"] {
+    background-color: transparent;
+  }
+
+  /* Disabled state */
+  [data-slot="radio-group-item-input"][data-disabled] + [data-slot="radio-group-item-label"] {
+    cursor: not-allowed;
+    opacity: 0.5;
   }
 
   /* Focus state */
@@ -126,27 +160,23 @@
     flex-direction: column;
   }
 
-  &[aria-orientation="vertical"] [data-slot="radio-group-item"]:not(:first-of-type)::before {
-    height: 1px;
-    width: auto;
-    inset: 0 6px;
-    transform: translateY(-0.5px);
-  }
-
   /* Small size variant */
   &[data-size="small"] {
+    --radio-group-height: 24px;
+    --radio-group-gap: 3px;
+    --radio-group-padding: 2px;
+
     [data-slot="radio-group-item-label"] {
       font-size: 12px;
-      padding: 4px 8px;
     }
 
-    [data-slot="radio-group-item"]:not(:first-of-type)::before {
-      inset: 4px 0;
+    [data-slot="radio-group-item-control"] {
+      padding: var(--radio-group-control-padding, 0 8px);
     }
+  }
 
-    &[aria-orientation="vertical"] [data-slot="radio-group-item"]:not(:first-of-type)::before {
-      inset: 0 4px;
-    }
+  &[data-size="small"][data-pad="normal"] {
+    --radio-group-control-padding: 0 8px;
   }
 
   /* Disabled root state */

+ 10 - 2
packages/ui/src/components/radio-group.tsx

@@ -15,6 +15,8 @@ export type RadioGroupProps<T> = Omit<
   class?: ComponentProps<"div">["class"]
   classList?: ComponentProps<"div">["classList"]
   size?: "small" | "medium"
+  fill?: boolean
+  pad?: "none" | "normal"
 }
 
 export function RadioGroup<T>(props: RadioGroupProps<T>) {
@@ -28,6 +30,8 @@ export function RadioGroup<T>(props: RadioGroupProps<T>) {
     "label",
     "onSelect",
     "size",
+    "fill",
+    "pad",
   ])
 
   const getValue = (item: T): string => {
@@ -49,6 +53,8 @@ export function RadioGroup<T>(props: RadioGroupProps<T>) {
       {...others}
       data-component="radio-group"
       data-size={local.size ?? "medium"}
+      data-fill={local.fill ? "" : undefined}
+      data-pad={local.pad ?? "normal"}
       classList={{
         ...(local.classList ?? {}),
         [local.class ?? ""]: !!local.class,
@@ -62,9 +68,11 @@ export function RadioGroup<T>(props: RadioGroupProps<T>) {
         <div role="presentation" data-slot="radio-group-items">
           <For each={local.options}>
             {(option) => (
-              <Kobalte.Item value={getValue(option)} data-slot="radio-group-item">
+              <Kobalte.Item value={getValue(option)} data-slot="radio-group-item" data-value={getValue(option)}>
                 <Kobalte.ItemInput data-slot="radio-group-item-input" />
-                <Kobalte.ItemLabel data-slot="radio-group-item-label">{getLabel(option)}</Kobalte.ItemLabel>
+                <Kobalte.ItemLabel data-slot="radio-group-item-label">
+                  <span data-slot="radio-group-item-control">{getLabel(option)}</span>
+                </Kobalte.ItemLabel>
               </Kobalte.Item>
             )}
           </For>