diff --git a/packages/app/e2e/regression/session-project-menu.spec.ts b/packages/app/e2e/regression/session-project-menu.spec.ts index 8e582b79125..3c32189ab56 100644 --- a/packages/app/e2e/regression/session-project-menu.spec.ts +++ b/packages/app/e2e/regression/session-project-menu.spec.ts @@ -53,7 +53,7 @@ for (const direction of ["ltr", "rtl"] as const) { await expect(trigger).toBeEnabled() await expect(trigger.locator("use")).toHaveAttribute( "href", - `#opencode-v2-icon-${workspace ? "workspace-isolated" : "monitor"}`, + `#opencode-v2-icon-${workspace ? "outline-worktree" : "monitor"}`, ) const background = await trigger.evaluate((element) => getComputedStyle(element).backgroundColor) await trigger.hover() @@ -267,7 +267,7 @@ for (const state of ["closed", "unopened"] as const) { messages.resolve() await expect(header.getByRole("button", { name: "More options", exact: true })).toBeVisible() } - await expect(trigger.locator("use")).toHaveAttribute("href", "#opencode-v2-icon-workspace-isolated") + await expect(trigger.locator("use")).toHaveAttribute("href", "#opencode-v2-icon-outline-worktree") await trigger.click() await expect(menu.getByRole("menuitem", { name: fixture.project.name, exact: true })).toBeEnabled() await expect(menu.getByRole("menuitem", { name: directory, exact: true })).toBeDisabled() diff --git a/packages/app/e2e/regression/settings-loading.spec.ts b/packages/app/e2e/regression/settings-loading.spec.ts index d7784ff205e..bb587554780 100644 --- a/packages/app/e2e/regression/settings-loading.spec.ts +++ b/packages/app/e2e/regression/settings-loading.spec.ts @@ -154,8 +154,8 @@ test("worktree deletion sends the project location separately from the target", const settings = page.getByTestId("settings-screen") await settings.getByRole("tab", { name: "Worktrees", exact: true }).click() await expect(settings.getByText(sandboxes[0], { exact: true })).toBeVisible() - await settings.getByRole("button", { name: 'Delete worktree "workspace-1"?', exact: true }).click() - const confirmation = page.getByRole("dialog", { name: "Delete worktree", exact: true }) + await settings.getByRole("button", { name: "Delete “workspace-1”?", exact: true }).click() + const confirmation = page.getByRole("dialog", { name: "Delete “workspace-1”?", exact: true }) const remove = confirmation.getByRole("button", { name: "Delete worktree", exact: true }) await expect(remove).toBeEnabled() const deleting = page.waitForRequest( diff --git a/packages/app/src/new-session/view.tsx b/packages/app/src/new-session/view.tsx index f9f721b7c92..05a2bd8b49e 100644 --- a/packages/app/src/new-session/view.tsx +++ b/packages/app/src/new-session/view.tsx @@ -34,6 +34,8 @@ export const ProviderTipSchema = Persistence.struct({ dismissedAt: Schema.Finite, }) +export const WorkspaceTipSchema = ProviderTipSchema + export function NewSessionView(props: { composer: ComposerModel project: PromptProjectController @@ -99,7 +101,15 @@ export function NewSessionView(props: { - + select("create")} + /> ) @@ -118,56 +128,90 @@ export function NewSessionStatus(props: { visible: boolean }) { ) } -function ProviderTip() { +function NewSessionTips(props: { workspaceEligible: boolean; onWorkspace: () => void }) { const language = useLanguage() const dialog = useDialog() const sdk = useWorkspaceLocation() const providers = useProviders(() => sdk().directory) - const [persistedState, setPersistedState, , persistedReady] = persisted( + const [providerState, setProviderState, , providerReady] = persisted( Persist.global("new-session.provider-tip"), ProviderTipSchema, { dismissedAt: 0 }, ) - const visible = createMemo( + const [workspaceState, setWorkspaceState, , workspaceReady] = persisted( + Persist.global("new-session.workspace-tip"), + WorkspaceTipSchema, + { dismissedAt: 0 }, + ) + const workspaceVisible = createMemo( + () => + props.workspaceEligible && + workspaceReady() && + Date.now() - workspaceState.dismissedAt >= providerTipDismissalDuration, + ) + const providerVisible = createMemo( () => providers.ready() && - persistedReady() && + providerReady() && providers.paid().length === 0 && - Date.now() - persistedState.dismissedAt >= providerTipDismissalDuration, + Date.now() - providerState.dismissedAt >= providerTipDismissalDuration, ) + const tip = createMemo<"workspace" | "provider" | undefined>(() => { + if (providerVisible()) return "provider" + if (workspaceVisible()) return "workspace" + }) + const displayed = createMemo<"workspace" | "provider" | undefined>((previous) => tip() ?? previous) const [ref, setRef] = createSignal() const presence = createPresence({ - show: visible, + show: () => tip() !== undefined, element: () => ref() ?? null, }) - const openProviders = () => { + const open = () => { + const current = tip() + if (!current) return + if (current === "workspace") { + setWorkspaceState("dismissedAt", Date.now()) + props.onWorkspace() + return + } void import("@/providers/connect/dialog").then(({ DialogConnectProvider }) => { void dialog.show(() => ) }) } + const dismiss = () => { + const current = tip() + if (!current) return + if (current === "workspace") { + setWorkspaceState("dismissedAt", Date.now()) + return + } + setProviderState("dismissedAt", Date.now()) + } return (
setPersistedState("dismissedAt", Date.now())} + onClick={dismiss} > diff --git a/packages/app/src/new-session/workspace/controller.ts b/packages/app/src/new-session/workspace/controller.ts index b0e42ddb91f..5772b4a3a48 100644 --- a/packages/app/src/new-session/workspace/controller.ts +++ b/packages/app/src/new-session/workspace/controller.ts @@ -1,5 +1,5 @@ import { debounce } from "@solid-primitives/scheduled" -import { createEffect, createMemo, createResource } from "solid-js" +import { createEffect, createMemo, createResource, onCleanup } from "solid-js" import { createStore } from "solid-js/store" import { useWorkspaceLocation } from "@/workspaces/location" import { useServerSDK } from "@/runtime/server/client" @@ -11,8 +11,8 @@ import { normalizeProjectInfo } from "@/runtime/server/global-sync/utils" import { isWorkspaceDirectory, isWorkspaceSelection, + sameDirectory, workspaceDefaultSelection, - workspaceDirectories, workspaceSelectionDestination, } from "@/workspaces/paths" @@ -56,6 +56,50 @@ export function createNewSessionWorkspaceController(input: { const current = projectID ? data.project.get(projectID) : undefined return current ? normalizeProjectInfo(current) : undefined }) + const worktreeSource = createMemo( + () => { + const project = currentProject() + return project ? { projectID: project.id, directory: project.worktree } : undefined + }, + undefined, + { equals: (a, b) => a?.projectID === b?.projectID && a?.directory === b?.directory }, + ) + const [worktrees, worktreeActions] = createResource(worktreeSource, async (source) => ({ + projectID: source.projectID, + items: await serverSDK.api.worktree + .list({ location: { directory: source.directory } }) + .catch(() => (currentProject()?.id === source.projectID ? currentProject()?.worktrees : undefined) ?? []), + })) + onCleanup( + serverSDK.event.listen((event) => { + if (event.type === "worktree.updated") void worktreeActions.refetch() + }), + ) + const worktreeItems = createMemo(() => { + const project = currentProject() + if (!project) return [] + const loaded = worktrees.latest + return loaded?.projectID === project.id ? loaded.items : project.worktrees + }) + const worktreeDirectories = createMemo(() => { + const project = currentProject() + if (!project) return [] + const directories = [ + ...worktreeItems().map((item) => item.directory), + ...project.worktrees.map((item) => item.directory), + ...(project.sandboxes ?? []), + ] + return directories + .filter((directory) => !sameDirectory(project.worktree, directory)) + .filter((directory, index, items) => items.findIndex((item) => sameDirectory(item, directory)) === index) + }) + const managedWorktrees = createMemo(() => { + const project = currentProject() + if (!project) return 0 + return worktreeItems().filter( + (item) => item.strategy !== undefined && !sameDirectory(project.worktree, item.directory), + ).length + }) const visible = createMemo(() => resolveNewSessionGit({ projectVcs: currentProject()?.vcs, @@ -66,7 +110,10 @@ export function createNewSessionWorkspaceController(input: { const project = currentProject() const worktree = input.selectedWorktree() if (!project || !worktree) return - return isWorkspaceSelection(project, worktree) ? worktree : undefined + return isWorkspaceSelection(project, worktree) || + worktreeDirectories().some((item) => sameDirectory(item, worktree)) + ? worktree + : undefined }) const fallback = createMemo(() => { const project = currentProject() @@ -97,7 +144,7 @@ export function createNewSessionWorkspaceController(input: { () => undefined, ) const project = currentProject() - const directories = project ? [project.worktree, ...workspaceDirectories(project)] : [sdk().directory] + const directories = project ? [project.worktree, ...worktreeDirectories()] : [sdk().directory] directories.forEach((directory) => void data.location.vcs.sync({ directory }).catch(() => undefined)) }) const branch = createMemo(() => @@ -122,7 +169,12 @@ export function createNewSessionWorkspaceController(input: { workspace: createMemo(() => { const project = currentProject() const current = value() - return current === "create" || (!!project && isWorkspaceDirectory(project, current)) + return ( + current === "create" || + (!!project && + (isWorkspaceDirectory(project, current) || + worktreeDirectories().some((item) => sameDirectory(item, current)))) + ) }), reset: () => { input.setSelectedWorktree(undefined) @@ -142,10 +194,8 @@ export function createNewSessionWorkspaceController(input: { }, project: { root: projectRoot, - workspaces: () => { - const project = currentProject() - return project ? workspaceDirectories(project) : [] - }, + workspaces: worktreeDirectories, + managed: managedWorktrees, git: visible, branches: () => { const current = data.location.vcs.info({ directory: sdk().directory })?.branch.current diff --git a/packages/app/src/new-session/workspace/selector.tsx b/packages/app/src/new-session/workspace/selector.tsx index e39175abef9..06e383477fe 100644 --- a/packages/app/src/new-session/workspace/selector.tsx +++ b/packages/app/src/new-session/workspace/selector.tsx @@ -1,5 +1,6 @@ -import { createMemo, For, Show } from "solid-js" +import { createMemo, createSignal, For, Show } from "solid-js" import { createStore } from "solid-js/store" +import { createResizeObserver } from "@solid-primitives/resize-observer" import { Menu } from "@opencode-ai/ui/menu" import { Tooltip } from "@opencode-ai/ui/tooltip" import { Icon } from "@opencode-ai/ui/icon" @@ -25,6 +26,9 @@ export function PromptWorkspaceSelector(props: { let searchInput: HTMLInputElement | undefined let branchSearchInput: HTMLInputElement | undefined let focusSearch = false + const branchTruncation = createTruncatedText() + const focusWorktreeSearch = () => + requestAnimationFrame(() => requestAnimationFrame(() => searchInput?.focus({ preventScroll: true }))) let pending: { type: "select"; value: string } | { type: "create"; branch: string } | { type: "viewAll" } | undefined const selected = () => (sameDirectory(props.value, props.projectRoot) ? "main" : props.value) const workspaces = createMemo(() => { @@ -34,8 +38,8 @@ export function PromptWorkspaceSelector(props: { }) const icon = () => { if (selected() === "main") return "monitor" - if (selected() === "create") return "workspace-new" - return "workspace-isolated" + if (selected() === "create") return "plus" + return "outline-worktree" } const select = (value: string) => { pending = { type: "select", value } @@ -66,13 +70,14 @@ export function PromptWorkspaceSelector(props: { <>
- + {language.t("workspace.onboarding.title")}
{language.t("workspace.onboarding.description")} @@ -89,7 +94,10 @@ export function PromptWorkspaceSelector(props: { aria-description={language.t("session.new.workspace.trigger.tooltip")} class="flex h-6 min-w-0 max-w-[203px] items-center gap-1.5 rounded-sm px-1.5 hover:bg-v2-overlay-simple-overlay-hover focus-visible:bg-v2-overlay-simple-overlay-hover focus-visible:outline-none data-[expanded]:bg-v2-overlay-simple-overlay-pressed data-[expanded]:text-v2-text-text-muted" > - + {label()} - {language.t("session.new.workspace.local")} - - {language.t("session.new.workspace.local.tooltip")} - - - } + value={language.t("session.new.workspace.local.tooltip")} + contentClass="max-w-[140px]" class="min-w-0 flex-1" > {language.t("session.new.workspace.local")} @@ -126,24 +128,22 @@ export function PromptWorkspaceSelector(props: { select("create")}> - - {language.t("workspace.new")} + + + {language.t("workspace.new")} + - 0} - fallback={ - <> - - (pending = { type: "viewAll" })}> - {language.t("common.viewAll")} - - - } - > + 0}> searchInput?.focus()) + focusWorktreeSearch() }} > { if ( event.key === "ArrowRight" || @@ -170,7 +171,7 @@ export function PromptWorkspaceSelector(props: { focusSearch = true }} > - + {language.t("session.new.workspace.existing").replace(/(…|\.{3})$/, "")} @@ -205,7 +206,7 @@ export function PromptWorkspaceSelector(props: { {(workspace) => ( select(workspace)}> - + {getFilename(workspace)} @@ -213,6 +214,11 @@ export function PromptWorkspaceSelector(props: { )} + +
+ {language.t("session.new.workspace.search.empty")} +
+
(pending = { type: "viewAll" })}> {language.t("common.viewAll")} @@ -232,6 +238,7 @@ export function PromptWorkspaceSelector(props: { @@ -245,7 +252,7 @@ export function PromptWorkspaceSelector(props: { > - + {language.t("session.new.workspace.fromBranch", { branch: props.branch! })} @@ -319,6 +326,7 @@ export function PromptWorkspaceSelector(props: { export function PromptGitStatus(props: { branch?: string; noGit?: boolean; from?: boolean; class?: string }) { const language = useLanguage() + const truncation = createTruncatedText() const label = () => { if (props.noGit) return language.t("session.new.git.none") if (!props.branch) return undefined @@ -338,15 +346,27 @@ export function PromptGitStatus(props: { branch?: string; noGit?: boolean; from?
- {value()} + + {value()} +
)}
) } + +function createTruncatedText() { + const [truncated, setTruncated] = createSignal(false) + return { + truncated, + observe: (element: HTMLSpanElement) => + createResizeObserver(element, () => setTruncated(element.scrollWidth > element.clientWidth)), + } +} diff --git a/packages/app/src/runtime/i18n/en.ts b/packages/app/src/runtime/i18n/en.ts index 00dfa776209..45fe4ae654a 100644 --- a/packages/app/src/runtime/i18n/en.ts +++ b/packages/app/src/runtime/i18n/en.ts @@ -659,6 +659,7 @@ export const dict = { "home.sessions.group.yesterday": "Yesterday", "home.sessions.group.older": "Older", "home.providerTip": "Connect to 75+ providers to use other models, including Claude, GPT, Gemini, etc", + "home.workspaceTip": "Start next session in a new workspace to keep changes isolated", "session.tab.session": "Session", "session.tab.files": "Files", @@ -1250,30 +1251,43 @@ export const dict = { "workspace.new": "New worktree", "common.viewAll": "View all", - "session.new.workspace.local.tooltip": "Use current checkout", - "session.new.workspace.new.tooltip": "Create isolated checkout", + "session.new.workspace.local.tooltip": "Uses project’s current checkout", + "session.new.workspace.new.tooltip": "Creates isolated copy from current checkout", "session.new.workspace.fromBranch": "from {{branch}}", "session.new.workspace.createFrom": "Create from branch", "session.new.workspace.branch.search.placeholder": "Search branches", "session.new.workspace.trigger.tooltip": "Select where to run session", "session.new.workspace.search.placeholder": "Search worktrees", + "session.new.workspace.search.empty": "No matching worktrees", "settings.tab.workspaces": "Worktrees", + "settings.workspaces.description": "Review worktrees and manage disk usage", "settings.workspaces.filter.all": "All projects", - "settings.workspaces.empty": "No worktrees", + "settings.workspaces.empty": "No worktrees yet", + "settings.workspaces.empty.description": "Worktrees created in OpenCode will appear here", "settings.workspaces.count.one": "{{count}} worktree", "settings.workspaces.count.other": "{{count}} worktrees", "settings.workspaces.sessions.one": "{{count}} session in {{project}}", "settings.workspaces.sessions.other": "{{count}} sessions in {{project}}", + "settings.workspaces.sessions.filtered.one": "{{count}} session", + "settings.workspaces.sessions.filtered.other": "{{count}} sessions", "settings.workspaces.lastActiveSession": "Last active session", "settings.workspaces.deleteAll": "Delete all worktrees", - "settings.workspaces.deleteAll.confirm": "Delete all {{count}} worktrees?", - "settings.workspaces.delete.warning": - "The worktree directory and branch will be permanently removed, including any unmerged changes shown below.", + "settings.workspaces.deleteAll.confirm.one": "This will permanently delete {{count}} selected worktree.", + "settings.workspaces.deleteAll.confirm.other": "This will permanently delete {{count}} selected worktrees.", + "settings.workspaces.deleteWithoutSessions": "Delete worktrees without sessions", + "settings.workspaces.deleteWithoutSessions.confirm.one": + "This will permanently delete {{count}} worktree without linked sessions.", + "settings.workspaces.deleteWithoutSessions.confirm.other": + "This will permanently delete {{count}} worktrees without linked sessions.", + "settings.workspaces.deleteWithoutSessions.warning": + "Worktrees with unmerged changes or active locations will be kept.", + "settings.workspaces.delete.button": "Delete worktrees", + "settings.workspaces.delete.warning": "This permanently deletes the worktree and its branch.", "settings.workspaces.deleteAll.warning": - "The {{count}} selected worktrees in {{project}} will be permanently removed only if each is clean, inactive, and has no linked sessions.", + "Worktrees with unmerged changes, linked sessions, or active locations will be kept.", "settings.workspaces.delete.blocked.active": "The active worktree cannot be deleted.", "settings.workspaces.delete.blocked.linked": - "Linked sessions will remain, but their working directory will be permanently removed.", + "Linked sessions will remain, but their working directory will no longer exist.", "settings.workspaces.default.title": "Default environment", "settings.workspaces.default.description": "Choose where new sessions start", "settings.workspaces.default.lastUsed": "Last used per project", @@ -1284,9 +1298,10 @@ export const dict = { "workspace.move.failed": "Failed to move session", "workspace.lifecycle.creating": "Creating worktree", "workspace.lifecycle.created": "Worktree created", + "workspace.lifecycle.deleting": "Deleting…", "workspace.lifecycle.starting": "Starting session", "workspace.onboarding.title": "Isolate sessions with worktrees", - "workspace.onboarding.description": "Each gets its own checkout, so nothing interferes with your local repository", + "workspace.onboarding.description": "Each gets its own checkout", "workspace.lifecycle.moving": "Moving to worktree", "workspace.lifecycle.set": "Worktree set", "session.summary.title": "Session details", @@ -1305,9 +1320,10 @@ export const dict = { "workspace.status.checking": "Checking for unmerged changes…", "workspace.status.error": "Unable to verify git status.", "workspace.status.clean": "No unmerged changes detected.", - "workspace.status.dirty": "Unmerged changes detected in this worktree.", + "workspace.status.dirty": "Unmerged changes will be lost.", "workspace.delete.title": "Delete worktree", - "workspace.delete.confirm": 'Delete worktree "{{name}}"?', + "workspace.delete.confirm": "Delete “{{name}}”?", + "workspace.delete.location": "Location", "workspace.delete.button": "Delete worktree", "workspace.reset.title": "Reset worktree", "workspace.reset.confirm": 'Reset worktree "{{name}}"?', diff --git a/packages/app/src/runtime/persistence/consumers.test.ts b/packages/app/src/runtime/persistence/consumers.test.ts index f4f691ea54f..ed70db0a7b6 100644 --- a/packages/app/src/runtime/persistence/consumers.test.ts +++ b/packages/app/src/runtime/persistence/consumers.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from "bun:test" import { Schema } from "effect" -import { WorkspaceOnboardingSchema, ProviderTipSchema } from "@/new-session/view" +import { WorkspaceOnboardingSchema, ProviderTipSchema, WorkspaceTipSchema } from "@/new-session/view" import { ModelSelectionSchema } from "@/providers/models/selection" import { Persistence } from "@/runtime/persistence/schema" import { FileViewsSchema } from "@/workspaces/files/view-cache" @@ -12,6 +12,7 @@ describe("persisted consumer schemas", () => { test("onboarding and provider tip retain defaults and validate stored values", () => { const onboarding = Schema.decodeUnknownSync(Persistence.withInitial(WorkspaceOnboardingSchema, { used: false })) const tip = Schema.decodeUnknownSync(Persistence.withInitial(ProviderTipSchema, { dismissedAt: 0 })) + const workspaceTip = Schema.decodeUnknownSync(Persistence.withInitial(WorkspaceTipSchema, { dismissedAt: 0 })) expect(onboarding({})).toEqual({ used: false }) expect(onboarding({ used: "true" })).toEqual({ used: false }) expect(onboarding({ used: true })).toEqual({ used: true }) @@ -19,6 +20,7 @@ describe("persisted consumer schemas", () => { expect(tip({ dismissedAt: "yesterday" })).toEqual({ dismissedAt: 0 }) expect(tip({ dismissedAt: Infinity })).toEqual({ dismissedAt: 0 }) expect(tip({ dismissedAt: 123 })).toEqual({ dismissedAt: 123 }) + expect(workspaceTip({ dismissedAt: 123 })).toEqual({ dismissedAt: 123 }) }) test("collapse records recover malformed entries without losing valid siblings", () => { diff --git a/packages/app/src/session/session-identity-header.tsx b/packages/app/src/session/session-identity-header.tsx index 4cbd1ee1763..9a5202e9537 100644 --- a/packages/app/src/session/session-identity-header.tsx +++ b/packages/app/src/session/session-identity-header.tsx @@ -91,7 +91,7 @@ export function SessionProjectMenu(props: { when={props.showProjectIcon} fallback={ - + } > diff --git a/packages/app/src/session/timeline/message-timeline.tsx b/packages/app/src/session/timeline/message-timeline.tsx index 8733bbb4115..9cd68dddfa9 100644 --- a/packages/app/src/session/timeline/message-timeline.tsx +++ b/packages/app/src/session/timeline/message-timeline.tsx @@ -184,7 +184,7 @@ function WorkspaceMoveAction(props: { : "flex h-[46px] w-full items-center gap-2 rounded-b-[6px] px-3 pe-9 pt-2.5 text-[13px] font-[440] leading-5 tracking-[-0.04px] text-v2-text-text-muted focus-visible:outline-none" } > - + {language.t("workspace.move.title")} @@ -472,7 +560,7 @@ function DialogDeleteWorkspace(props: { staleTime: 0, })) const descriptions = () => { - if (status.isPending) return [language.t("workspace.status.checking")] + if (status.isPending) return [] if (status.isError) return [language.t("workspace.status.error")] if (!status.data) return [] return props.inspectionMessages(status.data.result) @@ -487,18 +575,20 @@ function DialogDeleteWorkspace(props: { - {language.t("workspace.delete.confirm", { name: getFilename(props.workspace.directory) })} -
- - {props.workspace.directory} - -
- {language.t("settings.workspaces.delete.warning")} +
+
+ + {language.t(status.isPending ? "workspace.status.checking" : "workspace.delete.location")} + + + {props.workspace.directory} + +
+
{language.t("settings.workspaces.delete.warning")}
{(description) =>
{description}
}
- +
} />
@@ -506,12 +596,7 @@ function DialogDeleteWorkspace(props: { - diff --git a/packages/storybook/playwright/config.d.ts b/packages/storybook/playwright/config.d.ts new file mode 100644 index 00000000000..c246667101a --- /dev/null +++ b/packages/storybook/playwright/config.d.ts @@ -0,0 +1 @@ +export declare function componentConfig(directory: string): import("@playwright/test").PlaywrightTestConfig<{}, {}>; diff --git a/packages/storybook/playwright/story.d.ts b/packages/storybook/playwright/story.d.ts new file mode 100644 index 00000000000..29c35dc330f --- /dev/null +++ b/packages/storybook/playwright/story.d.ts @@ -0,0 +1,9 @@ +import { expect } from "@playwright/test"; +import type { Locator } from "@playwright/test"; +export { expect }; +export declare const story: import("@playwright/test").TestType | undefined; + globals?: Record | undefined; + } | undefined) => Promise; +}, import("@playwright/test").PlaywrightWorkerArgs & import("@playwright/test").PlaywrightWorkerOptions>; diff --git a/packages/ui/src/actions/button/button.css b/packages/ui/src/actions/button/button.css index 7433bac33bc..f96d804913f 100644 --- a/packages/ui/src/actions/button/button.css +++ b/packages/ui/src/actions/button/button.css @@ -231,6 +231,7 @@ [data-component="button-v2"][data-variant="ghost-muted"] { background-color: transparent; color: var(--v2-text-text-muted); + font-weight: 440; } [data-component="button-v2"][data-variant="ghost-muted"] [data-slot="icon-svg"] { diff --git a/packages/ui/src/icons/icon/icon.tsx b/packages/ui/src/icons/icon/icon.tsx index bfe66bb1769..021f4c8b35a 100644 --- a/packages/ui/src/icons/icon/icon.tsx +++ b/packages/ui/src/icons/icon/icon.tsx @@ -100,6 +100,14 @@ const icons = { viewBox: "0 0 16 16", body: ``, }, + "outline-worktree": { + viewBox: "0 0 16 16", + body: ``, + }, + "outline-trash": { + viewBox: "0 0 16 16", + body: ``, + }, close: { viewBox: "0 0 20 20", body: ``, diff --git a/packages/ui/src/overlays/tooltip/tooltip.css b/packages/ui/src/overlays/tooltip/tooltip.css index b78a8da0812..8c03633e128 100644 --- a/packages/ui/src/overlays/tooltip/tooltip.css +++ b/packages/ui/src/overlays/tooltip/tooltip.css @@ -65,6 +65,13 @@ } } +[data-component="tooltip-v2"][data-appearance="large"] { + padding: 12px; + font-size: 13px; + line-height: var(--line-height-compact); + letter-spacing: -0.04px; +} + @keyframes tooltipV2In { from { opacity: 0; diff --git a/packages/ui/src/overlays/tooltip/tooltip.stories.tsx b/packages/ui/src/overlays/tooltip/tooltip.stories.tsx index 3089b9e27ca..1536ab89ee0 100644 --- a/packages/ui/src/overlays/tooltip/tooltip.stories.tsx +++ b/packages/ui/src/overlays/tooltip/tooltip.stories.tsx @@ -8,7 +8,7 @@ Floating tooltip built on Kobalte's tooltip primitive. - \`value\`: Content rendered inside the floating tooltip. - \`children\`: The trigger element that activates the tooltip on hover/focus. - \`placement\`: Kobalte placement string (e.g. "top", "bottom", "left", "right"). -- \`appearance\`: \`compact\` (default) or \`standard\`. +- \`appearance\`: \`compact\` (default), \`standard\`, or \`large\`. - \`inactive\`: When true, renders only the trigger without tooltip behavior. - \`forceOpen\`: Forces the tooltip to stay open. - Inherits Kobalte Tooltip root props. @@ -49,6 +49,9 @@ export const Appearances = { Compact + + Large +
), } diff --git a/packages/ui/src/overlays/tooltip/tooltip.tsx b/packages/ui/src/overlays/tooltip/tooltip.tsx index 86a5939e9ec..6e3f93801ae 100644 --- a/packages/ui/src/overlays/tooltip/tooltip.tsx +++ b/packages/ui/src/overlays/tooltip/tooltip.tsx @@ -6,7 +6,7 @@ import "./tooltip.css" export interface TooltipProps extends ComponentProps { value: JSX.Element - appearance?: "standard" | "compact" + appearance?: "standard" | "compact" | "large" class?: string contentClass?: string contentStyle?: JSX.CSSProperties