Przeglądaj źródła

feat(app): new timeline header (#34192)

Aarav Sareen 2 miesięcy temu
rodzic
commit
84bb706537

+ 19 - 0
packages/app/src/components/session-context-usage.tsx

@@ -1,7 +1,9 @@
 import { Match, Show, Switch, createMemo } from "solid-js"
 import { Tooltip, type TooltipProps } from "@opencode-ai/ui/tooltip"
 import { ProgressCircle } from "@opencode-ai/ui/progress-circle"
+import { ProgressCircleV2 } from "@opencode-ai/ui/v2/progress-circle-v2"
 import { Button } from "@opencode-ai/ui/button"
+import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2"
 
 import { useFile } from "@/context/file"
 import { useLayout } from "@/context/layout"
@@ -15,6 +17,7 @@ import { createSessionTabs } from "@/pages/session/helpers"
 
 interface SessionContextUsageProps {
   variant?: "button" | "indicator"
+  buttonAppearance?: "default" | "v2"
   placement?: TooltipProps["placement"]
 }
 
@@ -39,6 +42,7 @@ export function SessionContextUsage(props: SessionContextUsageProps) {
   const { params, tabs, view } = useSessionLayout()
 
   const variant = createMemo(() => props.variant ?? "button")
+  const buttonAppearance = createMemo(() => props.buttonAppearance ?? "default")
   const tabState = createSessionTabs({
     tabs,
     pathFromTab: file.pathFromTab,
@@ -80,6 +84,11 @@ export function SessionContextUsage(props: SessionContextUsageProps) {
       <ProgressCircle size={16} strokeWidth={2} percentage={context()?.usage ?? 0} />
     </div>
   )
+  const circleV2 = () => (
+    <div class="flex items-center justify-center">
+      <ProgressCircleV2 percentage={context()?.usage ?? 0} />
+    </div>
+  )
 
   const tooltipValue = () => (
     <div>
@@ -113,6 +122,16 @@ export function SessionContextUsage(props: SessionContextUsageProps) {
       <Tooltip value={tooltipValue()} placement={props.placement ?? "top"}>
         <Switch>
           <Match when={variant() === "indicator"}>{circle()}</Match>
+          <Match when={buttonAppearance() === "v2"}>
+            <IconButtonV2
+              type="button"
+              variant="ghost-muted"
+              size="large"
+              icon={circleV2()}
+              onClick={openContext}
+              aria-label={language.t("context.usage.view")}
+            />
+          </Match>
           <Match when={true}>
             <Button
               type="button"

+ 337 - 109
packages/app/src/pages/session/timeline/message-timeline.tsx

@@ -31,9 +31,14 @@ import { DiffChanges } from "@opencode-ai/ui/diff-changes"
 import { FileIcon } from "@opencode-ai/ui/file-icon"
 import { Icon } from "@opencode-ai/ui/icon"
 import { IconButton } from "@opencode-ai/ui/icon-button"
+import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon"
+import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2"
 import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
+import { MenuV2 } from "@opencode-ai/ui/v2/menu-v2"
 import { Dialog } from "@opencode-ai/ui/dialog"
+import { DialogFooter, DialogHeader, DialogTitleGroup, DialogV2 } from "@opencode-ai/ui/v2/dialog-v2"
 import { InlineInput } from "@opencode-ai/ui/inline-input"
+import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2"
 import { SessionRetry } from "@opencode-ai/session-ui/session-retry"
 import { ScrollView } from "@opencode-ai/ui/scroll-view"
 import { StickyAccordionHeader } from "@opencode-ai/ui/sticky-accordion-header"
@@ -671,6 +676,34 @@ export function MessageTimeline(props: {
     if (!shareEnabled()) return
     unshareMutation.mutate(id)
   }
+  const copyShareUrl = () => {
+    const url = shareUrl()
+    if (!url) return
+    void navigator.clipboard
+      .writeText(url)
+      .then(() =>
+        showToast({
+          variant: "success",
+          icon: "circle-check",
+          title: language.t("session.share.copy.copied"),
+          description: url,
+        }),
+      )
+      .catch((err: unknown) =>
+        showToast({
+          title: language.t("common.requestFailed"),
+          description: errorMessage(err),
+        }),
+      )
+  }
+  const selectShareUrlText: JSX.EventHandler<HTMLDivElement, MouseEvent> = (event) => {
+    const selection = window.getSelection()
+    if (!selection) return
+    const range = document.createRange()
+    range.selectNodeContents(event.currentTarget)
+    selection.removeAllRanges()
+    selection.addRange(range)
+  }
 
   createEffect(
     on(
@@ -856,6 +889,26 @@ export function MessageTimeline(props: {
       dialog.close()
     }
 
+    if (settings.general.newLayoutDesigns())
+      return (
+        <DialogV2 fit>
+          <DialogHeader hideClose>
+            <DialogTitleGroup
+              title={language.t("session.delete.title")}
+              description={language.t("session.delete.confirm", { name: name() })}
+            />
+          </DialogHeader>
+          <DialogFooter>
+            <ButtonV2 variant="ghost" onClick={() => dialog.close()}>
+              {language.t("common.cancel")}
+            </ButtonV2>
+            <ButtonV2 variant="danger" onClick={handleDelete}>
+              {language.t("session.delete.button")}
+            </ButtonV2>
+          </DialogFooter>
+        </DialogV2>
+      )
+
     return (
       <Dialog title={language.t("session.delete.title")} fit>
         <div class="flex flex-col gap-4 pl-6 pr-2.5 pb-3">
@@ -1304,11 +1357,16 @@ export function MessageTimeline(props: {
               "pr-3": true,
               "pl-4": settings.general.newLayoutDesigns(),
               "pl-2 md:pl-4": !settings.general.newLayoutDesigns(),
-              "md:max-w-200 md:mx-auto 2xl:max-w-[1000px]": props.centered,
+              "md:max-w-200 md:mx-auto 2xl:max-w-[1000px]": props.centered && !settings.general.newLayoutDesigns(),
             }}
           >
             <div class="h-12 w-full flex items-center justify-between gap-2">
-              <div class="flex items-center gap-1 min-w-0 flex-1 pr-3">
+              <div
+                classList={{
+                  "flex items-center gap-1 min-w-0 flex-1": true,
+                  "pr-3": !settings.general.newLayoutDesigns(),
+                }}
+              >
                 <div class="flex items-center min-w-0 grow-1">
                   <Show when={parentID()}>
                     <button
@@ -1347,8 +1405,17 @@ export function MessageTimeline(props: {
                         data-slot="session-title-child"
                         value={title.draft}
                         disabled={titleMutation.isPending}
-                        class="text-14-medium text-text-strong grow-1 min-w-0 rounded-[6px] pl-1 -ml-1"
-                        style={{ "--inline-input-shadow": "var(--shadow-xs-border-select)" }}
+                        classList={{
+                          "text-14-medium text-text-strong grow-1 min-w-0 pl-1 -ml-1": true,
+                          "h-6 leading-4 rounded-[3px] focus:shadow-none focus:outline focus:outline-1 focus:outline-offset-[-1px] focus:outline-v2-border-border-focus":
+                            settings.general.newLayoutDesigns(),
+                          "rounded-[6px]": !settings.general.newLayoutDesigns(),
+                        }}
+                        style={{
+                          "--inline-input-shadow": settings.general.newLayoutDesigns()
+                            ? "none"
+                            : "var(--shadow-xs-border-select)",
+                        }}
                         onInput={(event) => setTitle("draft", event.currentTarget.value)}
                         onKeyDown={(event) => {
                           event.stopPropagation()
@@ -1370,88 +1437,170 @@ export function MessageTimeline(props: {
               </div>
               <Show when={sessionID()} keyed>
                 {(id) => (
-                  <div class="shrink-0 flex items-center gap-3">
-                    <SessionContextUsage placement="bottom" />
+                  <div
+                    classList={{
+                      "shrink-0 flex items-center": true,
+                      "gap-2": settings.general.newLayoutDesigns(),
+                      "gap-3": !settings.general.newLayoutDesigns(),
+                    }}
+                  >
+                    <SessionContextUsage
+                      placement="bottom"
+                      buttonAppearance={settings.general.newLayoutDesigns() ? "v2" : "default"}
+                    />
                     <Show when={!parentID()}>
-                      <DropdownMenu
-                        gutter={4}
-                        placement="bottom-end"
-                        open={title.menuOpen}
-                        onOpenChange={(open) => {
-                          setTitle("menuOpen", open)
-                          if (open) return
-                        }}
+                      <Show
+                        when={settings.general.newLayoutDesigns()}
+                        fallback={
+                          <DropdownMenu
+                            gutter={4}
+                            placement="bottom-end"
+                            open={title.menuOpen}
+                            onOpenChange={(open) => {
+                              setTitle("menuOpen", open)
+                              if (open) return
+                            }}
+                          >
+                            <DropdownMenu.Trigger
+                              as={IconButton}
+                              icon="dot-grid"
+                              variant="ghost"
+                              class="size-6 rounded-md data-[expanded]:bg-surface-base-active"
+                              classList={{
+                                "bg-surface-base-active": share.open || title.pendingShare,
+                              }}
+                              aria-label={language.t("common.moreOptions")}
+                              aria-expanded={title.menuOpen || share.open || title.pendingShare}
+                              ref={(el: HTMLButtonElement) => {
+                                more = el
+                              }}
+                            />
+                            <DropdownMenu.Portal>
+                              <DropdownMenu.Content
+                                style={{ "min-width": "104px" }}
+                                onCloseAutoFocus={(event) => {
+                                  if (title.pendingRename) {
+                                    event.preventDefault()
+                                    setTitle("pendingRename", false)
+                                    openTitleEditor()
+                                    return
+                                  }
+                                  if (title.pendingShare) {
+                                    event.preventDefault()
+                                    requestAnimationFrame(() => {
+                                      setShare({ open: true, dismiss: null })
+                                      setTitle("pendingShare", false)
+                                    })
+                                  }
+                                }}
+                              >
+                                <DropdownMenu.Item
+                                  onSelect={() => {
+                                    setTitle("pendingRename", true)
+                                    setTitle("menuOpen", false)
+                                  }}
+                                >
+                                  <DropdownMenu.ItemLabel>{language.t("common.rename")}</DropdownMenu.ItemLabel>
+                                </DropdownMenu.Item>
+                                <Show when={shareEnabled()}>
+                                  <DropdownMenu.Item
+                                    onSelect={() => {
+                                      setTitle({ pendingShare: true, menuOpen: false })
+                                    }}
+                                  >
+                                    <DropdownMenu.ItemLabel>
+                                      {language.t("session.share.action.share")}
+                                    </DropdownMenu.ItemLabel>
+                                  </DropdownMenu.Item>
+                                </Show>
+                                <DropdownMenu.Item onSelect={() => void archiveSession(id)}>
+                                  <DropdownMenu.ItemLabel>{language.t("common.archive")}</DropdownMenu.ItemLabel>
+                                </DropdownMenu.Item>
+                                <DropdownMenu.Separator />
+                                <DropdownMenu.Item
+                                  onSelect={() => dialog.show(() => <DialogDeleteSession sessionID={id} />)}
+                                >
+                                  <DropdownMenu.ItemLabel>{language.t("common.delete")}</DropdownMenu.ItemLabel>
+                                </DropdownMenu.Item>
+                              </DropdownMenu.Content>
+                            </DropdownMenu.Portal>
+                          </DropdownMenu>
+                        }
                       >
-                        <DropdownMenu.Trigger
-                          as={IconButton}
-                          icon="dot-grid"
-                          variant="ghost"
-                          class="size-6 rounded-md data-[expanded]:bg-surface-base-active"
-                          classList={{
-                            "bg-surface-base-active": share.open || title.pendingShare,
-                          }}
-                          aria-label={language.t("common.moreOptions")}
-                          aria-expanded={title.menuOpen || share.open || title.pendingShare}
-                          ref={(el: HTMLButtonElement) => {
-                            more = el
+                        <MenuV2
+                          gutter={6}
+                          placement="bottom-end"
+                          open={title.menuOpen}
+                          onOpenChange={(open) => {
+                            setTitle("menuOpen", open)
+                            if (open) return
                           }}
-                        />
-                        <DropdownMenu.Portal>
-                          <DropdownMenu.Content
-                            style={{ "min-width": "104px" }}
-                            onCloseAutoFocus={(event) => {
-                              if (title.pendingRename) {
-                                event.preventDefault()
-                                setTitle("pendingRename", false)
-                                openTitleEditor()
-                                return
-                              }
-                              if (title.pendingShare) {
-                                event.preventDefault()
-                                requestAnimationFrame(() => {
-                                  setShare({ open: true, dismiss: null })
-                                  setTitle("pendingShare", false)
-                                })
-                              }
+                        >
+                          <MenuV2.Trigger
+                            as={IconButtonV2}
+                            icon={<IconV2 name="outline-dots" />}
+                            variant="ghost-muted"
+                            size="large"
+                            state={share.open || title.pendingShare ? "pressed" : undefined}
+                            aria-label={language.t("common.moreOptions")}
+                            aria-expanded={title.menuOpen || share.open || title.pendingShare}
+                            ref={(el: HTMLButtonElement) => {
+                              more = el
                             }}
-                          >
-                            <DropdownMenu.Item
-                              onSelect={() => {
-                                setTitle("pendingRename", true)
-                                setTitle("menuOpen", false)
+                          />
+                          <MenuV2.Portal>
+                            <MenuV2.Content
+                              style={{ width: "120px", "min-width": "120px" }}
+                              onCloseAutoFocus={(event) => {
+                                if (title.pendingRename) {
+                                  event.preventDefault()
+                                  setTitle("pendingRename", false)
+                                  openTitleEditor()
+                                  return
+                                }
+                                if (title.pendingShare) {
+                                  event.preventDefault()
+                                  requestAnimationFrame(() => {
+                                    setShare({ open: true, dismiss: null })
+                                    setTitle("pendingShare", false)
+                                  })
+                                }
                               }}
                             >
-                              <DropdownMenu.ItemLabel>{language.t("common.rename")}</DropdownMenu.ItemLabel>
-                            </DropdownMenu.Item>
-                            <Show when={shareEnabled()}>
-                              <DropdownMenu.Item
+                              <MenuV2.Item
                                 onSelect={() => {
-                                  setTitle({ pendingShare: true, menuOpen: false })
+                                  setTitle("pendingRename", true)
+                                  setTitle("menuOpen", false)
                                 }}
                               >
-                                <DropdownMenu.ItemLabel>
-                                  {language.t("session.share.action.share")}
-                                </DropdownMenu.ItemLabel>
-                              </DropdownMenu.Item>
-                            </Show>
-                            <DropdownMenu.Item onSelect={() => void archiveSession(id)}>
-                              <DropdownMenu.ItemLabel>{language.t("common.archive")}</DropdownMenu.ItemLabel>
-                            </DropdownMenu.Item>
-                            <DropdownMenu.Separator />
-                            <DropdownMenu.Item
-                              onSelect={() => dialog.show(() => <DialogDeleteSession sessionID={id} />)}
-                            >
-                              <DropdownMenu.ItemLabel>{language.t("common.delete")}</DropdownMenu.ItemLabel>
-                            </DropdownMenu.Item>
-                          </DropdownMenu.Content>
-                        </DropdownMenu.Portal>
-                      </DropdownMenu>
+                                {language.t("common.rename")}
+                              </MenuV2.Item>
+                              <Show when={shareEnabled()}>
+                                <MenuV2.Item
+                                  onSelect={() => {
+                                    setTitle({ pendingShare: true, menuOpen: false })
+                                  }}
+                                >
+                                  {language.t("session.share.action.share")}...
+                                </MenuV2.Item>
+                              </Show>
+                              <MenuV2.Item onSelect={() => void archiveSession(id)}>
+                                {language.t("common.archive")}
+                              </MenuV2.Item>
+                              <MenuV2.Separator />
+                              <MenuV2.Item onSelect={() => dialog.show(() => <DialogDeleteSession sessionID={id} />)}>
+                                {language.t("common.delete")}...
+                              </MenuV2.Item>
+                            </MenuV2.Content>
+                          </MenuV2.Portal>
+                        </MenuV2>
+                      </Show>
 
                       <KobaltePopover
                         open={share.open}
                         anchorRef={() => more}
                         placement="bottom-end"
-                        gutter={4}
+                        gutter={settings.general.newLayoutDesigns() ? 6 : 4}
                         modal={false}
                         onOpenChange={(open) => {
                           if (open) setShare("dismiss", null)
@@ -1461,6 +1610,10 @@ export function MessageTimeline(props: {
                         <KobaltePopover.Portal>
                           <KobaltePopover.Content
                             data-component="popover-content"
+                            classList={{
+                              "flex w-80 max-w-none flex-col items-start gap-3 rounded-[10px] border-0 bg-v2-background-bg-layer-01 p-3 shadow-[var(--v2-elevation-floating)]":
+                                settings.general.newLayoutDesigns(),
+                            }}
                             style={{ "min-width": "320px" }}
                             onEscapeKeyDown={(event) => {
                               setShare({ dismiss: "escape", open: false })
@@ -1478,24 +1631,90 @@ export function MessageTimeline(props: {
                               setShare("dismiss", null)
                             }}
                           >
-                            <div class="flex flex-col p-3">
-                              <div class="flex flex-col gap-1">
-                                <div class="text-13-medium text-text-strong">
+                            <Show
+                              when={settings.general.newLayoutDesigns()}
+                              fallback={
+                                <div class="flex flex-col p-3">
+                                  <div class="flex flex-col gap-1">
+                                    <div class="text-13-medium text-text-strong">
+                                      {language.t("session.share.popover.title")}
+                                    </div>
+                                    <div class="text-12-regular text-text-weak">
+                                      {shareUrl()
+                                        ? language.t("session.share.popover.description.shared")
+                                        : language.t("session.share.popover.description.unshared")}
+                                    </div>
+                                  </div>
+                                  <div class="mt-3 flex flex-col gap-2">
+                                    <Show
+                                      when={shareUrl()}
+                                      fallback={
+                                        <Button
+                                          size="large"
+                                          variant="primary"
+                                          class="w-full"
+                                          onClick={shareSession}
+                                          disabled={shareMutation.isPending}
+                                        >
+                                          {shareMutation.isPending
+                                            ? language.t("session.share.action.publishing")
+                                            : language.t("session.share.action.publish")}
+                                        </Button>
+                                      }
+                                    >
+                                      <div class="flex flex-col gap-2">
+                                        <TextField
+                                          value={shareUrl() ?? ""}
+                                          readOnly
+                                          copyable
+                                          copyKind="link"
+                                          tabIndex={-1}
+                                          class="w-full"
+                                        />
+                                        <div class="grid grid-cols-2 gap-2">
+                                          <Button
+                                            size="large"
+                                            variant="secondary"
+                                            class="w-full shadow-none border border-border-weak-base"
+                                            onClick={unshareSession}
+                                            disabled={unshareMutation.isPending}
+                                          >
+                                            {unshareMutation.isPending
+                                              ? language.t("session.share.action.unpublishing")
+                                              : language.t("session.share.action.unpublish")}
+                                          </Button>
+                                          <Button
+                                            size="large"
+                                            variant="primary"
+                                            class="w-full"
+                                            onClick={viewShare}
+                                            disabled={unshareMutation.isPending}
+                                          >
+                                            {language.t("session.share.action.view")}
+                                          </Button>
+                                        </div>
+                                      </div>
+                                    </Show>
+                                  </div>
+                                </div>
+                              }
+                            >
+                              <div class="flex w-full flex-col gap-1.5 px-0.5 pt-0.5">
+                                <div class="select-none text-[13px] font-[530] leading-none tracking-[-0.04px] text-v2-text-text-base [font-variation-settings:'slnt'_0]">
                                   {language.t("session.share.popover.title")}
                                 </div>
-                                <div class="text-12-regular text-text-weak">
+                                <div class="select-none text-[13px] font-[440] leading-5 tracking-[-0.04px] text-v2-text-text-muted [font-variation-settings:'slnt'_0]">
                                   {shareUrl()
                                     ? language.t("session.share.popover.description.shared")
                                     : language.t("session.share.popover.description.unshared")}
                                 </div>
                               </div>
-                              <div class="mt-3 flex flex-col gap-2">
+                              <div class="flex w-full flex-col gap-2">
                                 <Show
                                   when={shareUrl()}
                                   fallback={
-                                    <Button
-                                      size="large"
-                                      variant="primary"
+                                    <ButtonV2
+                                      variant="contrast"
                                       class="w-full"
                                       onClick={shareSession}
                                       disabled={shareMutation.isPending}
@@ -1503,48 +1722,57 @@ export function MessageTimeline(props: {
                                       {shareMutation.isPending
                                         ? language.t("session.share.action.publishing")
                                         : language.t("session.share.action.publish")}
-                                    </Button>
+                                    </ButtonV2>
                                   }
                                 >
                                   <div class="flex flex-col gap-2">
-                                    <TextField
-                                      value={shareUrl() ?? ""}
-                                      readOnly
-                                      copyable
-                                      copyKind="link"
-                                      tabIndex={-1}
-                                      class="w-full"
-                                    />
-                                    <div class="grid grid-cols-2 gap-2">
-                                      <Button
-                                        size="large"
-                                        variant="secondary"
-                                        class={
-                                          settings.general.newLayoutDesigns()
-                                            ? "w-full shadow-none border-[0.5px] border-border-weak-base"
-                                            : "w-full shadow-none border border-border-weak-base"
-                                        }
+                                    <div
+                                      class="flex h-8 w-full items-center gap-1.5 rounded-[6px] py-1 pl-2.5 pr-1.5 shadow-[var(--v2-elevation-button-neutral)]"
+                                      style={{
+                                        background:
+                                          "linear-gradient(180deg, var(--v2-alpha-light-2) 0%, var(--v2-alpha-light-0) 100%), var(--v2-background-bg-button-neutral)",
+                                      }}
+                                    >
+                                      <div
+                                        class="min-w-0 flex-1 truncate select-text cursor-text text-[13px] font-[440] leading-5 tracking-[-0.04px] text-v2-text-text-base [font-variation-settings:'slnt'_0]"
+                                        onClick={selectShareUrlText}
+                                      >
+                                        {shareUrl()}
+                                      </div>
+                                      <IconButtonV2
+                                        type="button"
+                                        size="small"
+                                        variant="ghost-muted"
+                                        icon={<IconV2 name="outline-copy" />}
+                                        aria-label={language.t("session.share.copy.copyLink")}
+                                        onClick={copyShareUrl}
+                                      />
+                                      <IconButtonV2
+                                        type="button"
+                                        size="small"
+                                        variant="ghost-muted"
+                                        icon={<IconV2 name="outline-square-arrow" />}
+                                        aria-label={language.t("session.share.action.view")}
+                                        onClick={viewShare}
+                                        disabled={unshareMutation.isPending}
+                                      />
+                                    </div>
+                                    <div class="flex w-full">
+                                      <ButtonV2
+                                        variant="outline"
+                                        class="w-full"
                                         onClick={unshareSession}
                                         disabled={unshareMutation.isPending}
                                       >
                                         {unshareMutation.isPending
                                           ? language.t("session.share.action.unpublishing")
                                           : language.t("session.share.action.unpublish")}
-                                      </Button>
-                                      <Button
-                                        size="large"
-                                        variant="primary"
-                                        class="w-full"
-                                        onClick={viewShare}
-                                        disabled={unshareMutation.isPending}
-                                      >
-                                        {language.t("session.share.action.view")}
-                                      </Button>
+                                      </ButtonV2>
                                     </div>
                                   </div>
                                 </Show>
                               </div>
-                            </div>
+                            </Show>
                           </KobaltePopover.Content>
                         </KobaltePopover.Portal>
                       </KobaltePopover>

+ 20 - 0
packages/ui/src/v2/components/button-v2.css

@@ -113,6 +113,26 @@
   cursor: not-allowed;
 }
 
+/* Outline */
+[data-component="button-v2"][data-variant="outline"] {
+  background-color: transparent;
+  color: var(--v2-text-text-base);
+  box-shadow: inset 0 0 0 1px var(--v2-border-border-muted);
+}
+
+[data-component="button-v2"][data-variant="outline"]:is(:hover, [data-state="hover"]):not(:disabled) {
+  background-color: var(--v2-overlay-simple-overlay-hover);
+}
+
+[data-component="button-v2"][data-variant="outline"]:is(:active, [data-state="pressed"]):not(:disabled) {
+  background-color: var(--v2-overlay-simple-overlay-pressed);
+}
+
+[data-component="button-v2"][data-variant="outline"]:is(:disabled, [data-state="disabled"]) {
+  opacity: 0.5;
+  cursor: not-allowed;
+}
+
 /* Contrast */
 [data-component="button-v2"][data-variant="contrast"] {
   background-image:

+ 1 - 1
packages/ui/src/v2/components/button-v2.tsx

@@ -7,7 +7,7 @@ export interface ButtonV2Props
   extends ComponentProps<typeof Kobalte>,
     Pick<ComponentProps<"button">, "class" | "classList" | "children"> {
   size?: "small" | "normal" | "large"
-  variant?: "neutral" | "danger" | "contrast" | "ghost" | "ghost-muted"
+  variant?: "neutral" | "danger" | "contrast" | "ghost" | "ghost-muted" | "outline"
   icon?: IconProps["name"]
 }
 

+ 3 - 0
packages/ui/src/v2/components/dialog-v2.css

@@ -59,6 +59,7 @@
           margin: 0;
           flex: 1;
           min-width: 0;
+          user-select: none;
           font-weight: 530;
           font-size: 13px;
           line-height: 16px;
@@ -80,6 +81,7 @@
           margin: 0;
           flex: none;
           flex-grow: 0;
+          user-select: none;
           font-weight: 530;
           font-size: 15px;
           line-height: 20px;
@@ -91,6 +93,7 @@
         [data-slot="dialog-description"] {
           flex: none;
           flex-grow: 0;
+          user-select: none;
           font-weight: 440;
           font-size: 13px;
           line-height: 20px;

+ 2 - 0
packages/ui/src/v2/components/dialog-v2.tsx

@@ -110,3 +110,5 @@ export function Dialog(props: DialogProps) {
     </div>
   )
 }
+
+export const DialogV2 = Dialog

+ 8 - 0
packages/ui/src/v2/components/icon.tsx

@@ -85,6 +85,14 @@ const icons = {
     viewBox: "0 0 16 16",
     body: `<path d="M2.5 7.5H3.5V8.5H2.5V7.5Z" stroke="currentColor"/><path d="M7.5 7.5H8.5V8.5H7.5V7.5Z" stroke="currentColor"/><path d="M12.5 7.5H13.5V8.5H12.5V7.5Z" stroke="currentColor"/>`,
   },
+  "outline-copy": {
+    viewBox: "0 0 16 16",
+    body: `<path d="M4.14908 11.0081H1.76282V1.51758H9.1038V2.55588M14.2225 4.99681H6.75397V14.4873H14.2225V4.99681Z" stroke="currentColor"/>`,
+  },
+  "outline-square-arrow": {
+    viewBox: "0 0 16 16",
+    body: `<path d="M13.5555 6.66656V2.44434H9.33326M13.5555 2.44434L7.99993 7.99989M13.5555 9.33324V13.5555C13.5555 13.5555 12.7599 13.5555 11.7777 13.5555H2.44438C2.44438 13.5555 2.44438 12.7599 2.44438 11.7777V4.22213C2.44438 3.2399 2.44434 2.44435 2.44434 2.44435H6.66661" stroke="currentColor"/>`,
+  },
   archive: {
     viewBox: "0 0 16 16",
     body: `<path d="M13.1112 13.5555V14.0555H13.6112V13.5555H13.1112ZM2.889 13.5555H2.389L2.389 14.0555H2.889V13.5555ZM3.38901 5.55546L3.38901 5.05546L2.38901 5.05546L2.38901 5.55546L2.88901 5.55546L3.38901 5.55546ZM14.4446 2.44434H14.9446V1.94434L14.4446 1.94434L14.4446 2.44434ZM14.4446 5.55545L14.4446 6.05545L14.9446 6.05545V5.55545H14.4446ZM1.55566 5.55546L1.05566 5.55545L1.05566 6.05546L1.55566 6.05546L1.55566 5.55546ZM1.5557 2.44436L1.5557 1.94436L1.05571 1.94436L1.0557 2.44435L1.5557 2.44436ZM13.1112 5.55546H12.6112V13.5555H13.1112H13.6112V5.55546H13.1112ZM2.889 13.5555H3.389L3.38901 5.55546L2.88901 5.55546L2.38901 5.55546L2.389 13.5555H2.889ZM14.4446 2.44434H13.9446V5.55545H14.4446H14.9446V2.44434H14.4446ZM1.55566 5.55546L2.05566 5.55547L2.0557 2.44436L1.5557 2.44436L1.0557 2.44435L1.05566 5.55545L1.55566 5.55546ZM6.22234 8.22213V8.72213H9.7779V8.22213V7.72213H6.22234V8.22213ZM13.1112 13.5555V13.0555H2.889V13.5555V14.0555H13.1112V13.5555ZM1.5557 2.44436L1.5557 2.94436L14.4446 2.94434L14.4446 2.44434L14.4446 1.94434L1.5557 1.94436L1.5557 2.44436ZM14.4446 5.55545L14.4446 5.05545L1.55566 5.05546L1.55566 5.55546L1.55566 6.05546L14.4446 6.05545L14.4446 5.55545Z" fill="currentColor"/>`,

+ 13 - 0
packages/ui/src/v2/components/progress-circle-v2.css

@@ -0,0 +1,13 @@
+[data-component="progress-circle-v2"] {
+  display: block;
+  transform: rotate(-90deg);
+}
+
+[data-component="progress-circle-v2"] [data-slot="progress-circle-v2-background"] {
+  stroke: var(--v2-background-bg-layer-04);
+}
+
+[data-component="progress-circle-v2"] [data-slot="progress-circle-v2-progress"] {
+  stroke: var(--v2-icon-icon-muted);
+  transition: stroke-dashoffset 0.35s cubic-bezier(0.65, 0, 0.35, 1);
+}

+ 46 - 0
packages/ui/src/v2/components/progress-circle-v2.tsx

@@ -0,0 +1,46 @@
+import { type ComponentProps, createMemo, splitProps } from "solid-js"
+import "./progress-circle-v2.css"
+
+export interface ProgressCircleV2Props extends Pick<ComponentProps<"svg">, "class" | "classList"> {
+  percentage: number
+  size?: number
+  strokeWidth?: number
+}
+
+export function ProgressCircleV2(props: ProgressCircleV2Props) {
+  const [split, rest] = splitProps(props, ["percentage", "size", "strokeWidth", "class", "classList"])
+
+  const size = () => split.size ?? 14
+  const strokeWidth = () => split.strokeWidth ?? 1.5
+  const viewBoxSize = 14
+  const center = viewBoxSize / 2
+  const radius = () => center - strokeWidth() / 2
+  const circumference = createMemo(() => 2 * Math.PI * radius())
+  const offset = createMemo(() => circumference() * (1 - Math.max(0, Math.min(100, split.percentage || 0)) / 100))
+
+  return (
+    <svg
+      {...rest}
+      width={size()}
+      height={size()}
+      viewBox={`0 0 ${viewBoxSize} ${viewBoxSize}`}
+      fill="none"
+      data-component="progress-circle-v2"
+      classList={{
+        ...split.classList,
+        [split.class ?? ""]: !!split.class,
+      }}
+    >
+      <circle cx={center} cy={center} r={radius()} data-slot="progress-circle-v2-background" stroke-width={strokeWidth()} />
+      <circle
+        cx={center}
+        cy={center}
+        r={radius()}
+        data-slot="progress-circle-v2-progress"
+        stroke-width={strokeWidth()}
+        stroke-dasharray={circumference().toString()}
+        stroke-dashoffset={offset()}
+      />
+    </svg>
+  )
+}