Explorar el Código

feat(app): align edit project modal with v2 style (#36213)

Co-authored-by: Brendan Allan <git@brendonovich.dev>
Aarav Sareen hace 1 mes
padre
commit
17cd4a8c32

+ 156 - 0
packages/app/src/components/dialog-edit-project-v2.tsx

@@ -0,0 +1,156 @@
+import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2"
+import { Dialog, DialogBody, DialogFooter, DialogHeader, DialogTitle } from "@opencode-ai/ui/v2/dialog-v2"
+import { DividerV2 } from "@opencode-ai/ui/v2/divider-v2"
+import { Field } from "@opencode-ai/ui/v2/field-v2"
+import { Icon } from "@opencode-ai/ui/v2/icon"
+import { ProjectAvatar, PROJECT_AVATAR_VARIANTS } from "@opencode-ai/ui/v2/project-avatar-v2"
+import { TextareaV2 } from "@opencode-ai/ui/v2/textarea-v2"
+import { TextInputV2 } from "@opencode-ai/ui/v2/text-input-v2"
+import { For, Show } from "solid-js"
+import { useLanguage } from "@/context/language"
+import { getProjectAvatarVariant, type LocalProject } from "@/context/layout"
+import { ServerConnection } from "@/context/server"
+import { getProjectAvatarSource } from "@/pages/layout/helpers"
+import { createEditProjectModel } from "./edit-project"
+
+export function DialogEditProjectV2(props: { project: LocalProject; server: ServerConnection.Any }) {
+  const language = useLanguage()
+  const model = createEditProjectModel(props)
+
+  return (
+    <Dialog fit>
+      <form onSubmit={model.submit} class="contents">
+        <DialogHeader>
+          <DialogTitle>{language.t("dialog.project.edit.title")}</DialogTitle>
+        </DialogHeader>
+        <DividerV2 />
+        <DialogBody class="flex max-h-[min(560px,calc(100vh-160px))] w-full flex-col gap-6 overflow-y-auto px-4 pt-4 pb-1">
+          <Field>
+            <Field.Label>{language.t("dialog.project.edit.name")}</Field.Label>
+            <TextInputV2
+              autofocus
+              appearance="large"
+              class="!w-full"
+              value={model.store.name}
+              placeholder={model.folderName()}
+              onInput={(event) => model.setStore("name", event.currentTarget.value)}
+            />
+          </Field>
+
+          <div class="flex w-full flex-col gap-2">
+            <div class="select-none text-[13px] font-[530] leading-none tracking-[-0.04px] text-v2-text-text-base">
+              {language.t("dialog.project.edit.icon")}
+            </div>
+            <div class="flex items-center gap-3">
+              <button
+                type="button"
+                aria-label={language.t("dialog.project.edit.icon.alt")}
+                class="relative size-16 shrink-0 cursor-pointer overflow-hidden rounded-[6px] outline outline-1 outline-transparent transition-[background-color,outline-color] focus-visible:outline-v2-border-border-focus"
+                classList={{
+                  "bg-v2-overlay-simple-overlay-hover outline-v2-border-border-focus": model.store.dragOver,
+                }}
+                onMouseEnter={() => model.setStore("iconHover", true)}
+                onMouseLeave={() => model.setStore("iconHover", false)}
+                onDrop={model.drop}
+                onDragOver={model.dragOver}
+                onDragLeave={model.dragLeave}
+                onClick={model.iconClick}
+              >
+                <ProjectAvatar
+                  fallback={model.store.name || model.defaultName()}
+                  src={getProjectAvatarSource(props.project.id, {
+                    color: model.store.color,
+                    url: props.project.icon?.url,
+                    override: model.store.iconOverride,
+                  })}
+                  variant={getProjectAvatarVariant(model.store.color)}
+                  class="!size-16 [&_[data-slot=project-avatar-surface]]:!rounded-[6px] [&_[data-slot=project-avatar-surface]]:!text-[32px]"
+                />
+                <span
+                  class="pointer-events-none absolute inset-0 flex items-center justify-center rounded-[6px] bg-v2-background-bg-contrast/80 text-v2-icon-icon-contrast backdrop-blur-[2px] transition-opacity"
+                  classList={{
+                    "opacity-100": model.store.iconHover,
+                    "opacity-0": !model.store.iconHover,
+                  }}
+                >
+                  <Icon name={model.store.iconOverride ? "close" : "outline-share"} />
+                </span>
+              </button>
+              <input
+                ref={(element) => {
+                  model.setIconInput(element)
+                }}
+                type="file"
+                accept="image/*"
+                class="hidden"
+                onChange={model.inputChange}
+              />
+              <div class="flex select-none flex-col gap-[6px] text-[11px] font-[440] leading-none tracking-[0.05px] text-v2-text-text-muted">
+                <span>{language.t("dialog.project.edit.icon.hint")}</span>
+                <span>{language.t("dialog.project.edit.icon.recommended")}</span>
+              </div>
+            </div>
+          </div>
+
+          <Show when={!model.store.iconOverride}>
+            <div class="flex w-full flex-col gap-2">
+              <div class="select-none text-[13px] font-[530] leading-none tracking-[-0.04px] text-v2-text-text-base">
+                {language.t("dialog.project.edit.color")}
+              </div>
+              <div class="-ml-1 flex gap-1.5">
+                <For each={PROJECT_AVATAR_VARIANTS}>
+                  {(color) => (
+                    <button
+                      type="button"
+                      aria-label={language.t("dialog.project.edit.color.select", { color })}
+                      aria-pressed={getProjectAvatarVariant(model.store.color) === color}
+                      class="flex size-8 items-center justify-center rounded-[10px] p-1 outline outline-1 outline-transparent transition-[background-color,outline-color] hover:bg-v2-overlay-simple-overlay-hover focus-visible:outline-v2-border-border-focus"
+                      classList={{
+                        "bg-v2-overlay-simple-overlay-hover [box-shadow:inset_0_0_0_2px_var(--v2-border-border-focus)]":
+                          getProjectAvatarVariant(model.store.color) === color,
+                      }}
+                      onClick={() => {
+                        if (getProjectAvatarVariant(model.store.color) === color && !props.project.icon?.url) return
+                        model.setStore(
+                          "color",
+                          getProjectAvatarVariant(model.store.color) === color ? undefined : color,
+                        )
+                      }}
+                    >
+                      <ProjectAvatar
+                        fallback={model.store.name || model.defaultName()}
+                        variant={getProjectAvatarVariant(color)}
+                        class="!size-6 [&_[data-slot=project-avatar-surface]]:!rounded-[6px]"
+                      />
+                    </button>
+                  )}
+                </For>
+              </div>
+            </div>
+          </Show>
+
+          <Field>
+            <Field.Label>{language.t("dialog.project.edit.worktree.startup")}</Field.Label>
+            <Field.Prefix>{language.t("dialog.project.edit.worktree.startup.description")}</Field.Prefix>
+            <TextareaV2
+              class="!w-full [&_[data-slot=textarea-v2-textarea]]:font-mono"
+              rows={3}
+              value={model.store.startup}
+              placeholder={language.t("dialog.project.edit.worktree.startup.placeholder")}
+              spellcheck={false}
+              onInput={(event) => model.setStore("startup", event.currentTarget.value)}
+            />
+          </Field>
+        </DialogBody>
+        <DialogFooter>
+          <ButtonV2 type="button" variant="neutral" disabled={model.save.isPending} onClick={model.close}>
+            {language.t("common.cancel")}
+          </ButtonV2>
+          <ButtonV2 type="submit" variant="contrast" disabled={model.save.isPending}>
+            {model.save.isPending ? language.t("common.saving") : language.t("common.save")}
+          </ButtonV2>
+        </DialogFooter>
+      </form>
+    </Dialog>
+  )
+}

+ 38 - 135
packages/app/src/components/dialog-edit-project.tsx

@@ -1,123 +1,32 @@
 import { Button } from "@opencode-ai/ui/button"
-import { useDialog } from "@opencode-ai/ui/context/dialog"
 import { Dialog } from "@opencode-ai/ui/dialog"
 import { TextField } from "@opencode-ai/ui/text-field"
-import { useMutation } from "@tanstack/solid-query"
 import { Icon } from "@opencode-ai/ui/icon"
-import { createMemo, For, Show } from "solid-js"
-import { createStore } from "solid-js/store"
+import { For, Show } from "solid-js"
 import { type LocalProject, getAvatarColors } from "@/context/layout"
-import { getFilename } from "@opencode-ai/core/util/path"
 import { Avatar } from "@opencode-ai/ui/avatar"
 import { useLanguage } from "@/context/language"
 import { getProjectAvatarSource } from "@/pages/layout/helpers"
 import { ServerConnection } from "@/context/server"
-import { useGlobal } from "@/context/global"
+import { createEditProjectModel } from "./edit-project"
 
 const AVATAR_COLOR_KEYS = ["pink", "mint", "orange", "purple", "cyan", "lime"] as const
 
 export function DialogEditProject(props: { project: LocalProject; server: ServerConnection.Any }) {
-  const dialog = useDialog()
-  const global = useGlobal()
   const language = useLanguage()
-  const serverCtx = createMemo(() => global.ensureServerCtx(props.server))
-  const serverSDK = () => serverCtx().sdk
-  const serverSync = () => serverCtx().sync
-
-  const folderName = createMemo(() => getFilename(props.project.worktree))
-  const defaultName = createMemo(() => props.project.name || folderName())
-
-  const [store, setStore] = createStore({
-    name: defaultName(),
-    color: props.project.icon?.color,
-    iconOverride: props.project.icon?.override,
-    startup: props.project.commands?.start ?? "",
-    dragOver: false,
-    iconHover: false,
-  })
-
-  let iconInput: HTMLInputElement | undefined
-
-  function handleFileSelect(file: File) {
-    if (!file.type.startsWith("image/")) return
-    const reader = new FileReader()
-    reader.onload = (e) => {
-      setStore("iconOverride", e.target?.result as string)
-      setStore("iconHover", false)
-    }
-    reader.readAsDataURL(file)
-  }
-
-  function handleDrop(e: DragEvent) {
-    e.preventDefault()
-    setStore("dragOver", false)
-    const file = e.dataTransfer?.files[0]
-    if (file) handleFileSelect(file)
-  }
-
-  function handleDragOver(e: DragEvent) {
-    e.preventDefault()
-    setStore("dragOver", true)
-  }
-
-  function handleDragLeave() {
-    setStore("dragOver", false)
-  }
-
-  function handleInputChange(e: Event) {
-    const input = e.target as HTMLInputElement
-    const file = input.files?.[0]
-    if (file) handleFileSelect(file)
-  }
-
-  function clearIcon() {
-    setStore("iconOverride", "")
-  }
-
-  const saveMutation = useMutation(() => ({
-    mutationFn: async () => {
-      const name = store.name.trim() === folderName() ? "" : store.name.trim()
-      const start = store.startup.trim()
-
-      if (props.project.id && props.project.id !== "global") {
-        await serverSDK().client.project.update({
-          projectID: props.project.id,
-          directory: props.project.worktree,
-          name,
-          icon: { color: store.color || "", override: store.iconOverride || "" },
-          commands: { start },
-        })
-        serverSync().project.icon(props.project.worktree, store.iconOverride || undefined)
-        dialog.close()
-        return
-      }
-
-      serverSync().project.meta(props.project.worktree, {
-        name,
-        icon: { color: store.color || undefined, override: store.iconOverride || undefined },
-        commands: { start: start || undefined },
-      })
-      dialog.close()
-    },
-  }))
-
-  function handleSubmit(e: SubmitEvent) {
-    e.preventDefault()
-    if (saveMutation.isPending) return
-    saveMutation.mutate()
-  }
+  const model = createEditProjectModel(props)
 
   return (
     <Dialog title={language.t("dialog.project.edit.title")} class="w-full max-w-[480px] mx-auto">
-      <form onSubmit={handleSubmit} class="flex flex-col gap-6 p-6 pt-0">
+      <form onSubmit={model.submit} class="flex flex-col gap-6 p-6 pt-0">
         <div class="flex flex-col gap-4">
           <TextField
             autofocus
             type="text"
             label={language.t("dialog.project.edit.name")}
-            placeholder={folderName()}
-            value={store.name}
-            onChange={(v) => setStore("name", v)}
+            placeholder={model.folderName()}
+            value={model.store.name}
+            onChange={(v) => model.setStore("name", v)}
           />
 
           <div class="flex flex-col gap-2">
@@ -125,38 +34,32 @@ export function DialogEditProject(props: { project: LocalProject; server: Server
             <div class="flex gap-3 items-start">
               <div
                 class="relative"
-                onMouseEnter={() => setStore("iconHover", true)}
-                onMouseLeave={() => setStore("iconHover", false)}
+                onMouseEnter={() => model.setStore("iconHover", true)}
+                onMouseLeave={() => model.setStore("iconHover", false)}
               >
                 <div
                   class="relative size-16 rounded-md transition-colors cursor-pointer"
                   classList={{
-                    "border-text-interactive-base bg-surface-info-base/20": store.dragOver,
-                    "border-border-base hover:border-border-strong": !store.dragOver,
-                    "overflow-hidden": !!store.iconOverride,
-                  }}
-                  onDrop={handleDrop}
-                  onDragOver={handleDragOver}
-                  onDragLeave={handleDragLeave}
-                  onClick={() => {
-                    if (store.iconOverride && store.iconHover) {
-                      clearIcon()
-                    } else {
-                      iconInput?.click()
-                    }
+                    "border-text-interactive-base bg-surface-info-base/20": model.store.dragOver,
+                    "border-border-base hover:border-border-strong": !model.store.dragOver,
+                    "overflow-hidden": !!model.store.iconOverride,
                   }}
+                  onDrop={model.drop}
+                  onDragOver={model.dragOver}
+                  onDragLeave={model.dragLeave}
+                  onClick={model.iconClick}
                 >
                   <Show
                     when={getProjectAvatarSource(props.project.id, {
-                      color: store.color,
+                      color: model.store.color,
                       url: props.project.icon?.url,
-                      override: store.iconOverride,
+                      override: model.store.iconOverride,
                     })}
                     fallback={
                       <div class="size-full flex items-center justify-center">
                         <Avatar
-                          fallback={store.name || defaultName()}
-                          {...getAvatarColors(store.color)}
+                          fallback={model.store.name || model.defaultName()}
+                          {...getAvatarColors(model.store.color)}
                           class="size-full text-[32px]"
                         />
                       </div>
@@ -174,8 +77,8 @@ export function DialogEditProject(props: { project: LocalProject; server: Server
                 <div
                   class="absolute inset-0 size-16 bg-surface-raised-stronger-non-alpha/90 rounded-[6px] z-10 pointer-events-none flex items-center justify-center transition-opacity"
                   classList={{
-                    "opacity-100": store.iconHover && !store.iconOverride,
-                    "opacity-0": !(store.iconHover && !store.iconOverride),
+                    "opacity-100": model.store.iconHover && !model.store.iconOverride,
+                    "opacity-0": !(model.store.iconHover && !model.store.iconOverride),
                   }}
                 >
                   <Icon name="cloud-upload" size="large" class="text-icon-on-interactive-base drop-shadow-sm" />
@@ -183,8 +86,8 @@ export function DialogEditProject(props: { project: LocalProject; server: Server
                 <div
                   class="absolute inset-0 size-16 bg-surface-raised-stronger-non-alpha/90 rounded-[6px] z-10 pointer-events-none flex items-center justify-center transition-opacity"
                   classList={{
-                    "opacity-100": store.iconHover && !!store.iconOverride,
-                    "opacity-0": !(store.iconHover && !!store.iconOverride),
+                    "opacity-100": model.store.iconHover && !!model.store.iconOverride,
+                    "opacity-0": !(model.store.iconHover && !!model.store.iconOverride),
                   }}
                 >
                   <Icon name="trash" size="large" class="text-icon-on-interactive-base drop-shadow-sm" />
@@ -193,12 +96,12 @@ export function DialogEditProject(props: { project: LocalProject; server: Server
               <input
                 id="icon-upload"
                 ref={(el) => {
-                  iconInput = el
+                  model.setIconInput(el)
                 }}
                 type="file"
                 accept="image/*"
                 class="hidden"
-                onChange={handleInputChange}
+                onChange={model.inputChange}
               />
               <div class="flex flex-col gap-1.5 text-12-regular text-text-weak self-center">
                 <span>{language.t("dialog.project.edit.icon.hint")}</span>
@@ -207,7 +110,7 @@ export function DialogEditProject(props: { project: LocalProject; server: Server
             </div>
           </div>
 
-          <Show when={!store.iconOverride}>
+          <Show when={!model.store.iconOverride}>
             <div class="flex flex-col gap-2">
               <label class="text-12-medium text-text-weak">{language.t("dialog.project.edit.color")}</label>
               <div class="flex gap-1.5">
@@ -216,21 +119,21 @@ export function DialogEditProject(props: { project: LocalProject; server: Server
                     <button
                       type="button"
                       aria-label={language.t("dialog.project.edit.color.select", { color })}
-                      aria-pressed={store.color === color}
+                      aria-pressed={model.store.color === color}
                       classList={{
                         "flex items-center justify-center size-10 p-0.5 rounded-lg overflow-hidden transition-colors cursor-default": true,
                         "bg-transparent border-2 border-icon-strong-base hover:bg-surface-base-hover":
-                          store.color === color,
+                          model.store.color === color,
                         "bg-transparent border border-transparent hover:bg-surface-base-hover hover:border-border-weak-base":
-                          store.color !== color,
+                          model.store.color !== color,
                       }}
                       onClick={() => {
-                        if (store.color === color && !props.project.icon?.url) return
-                        setStore("color", store.color === color ? undefined : color)
+                        if (model.store.color === color && !props.project.icon?.url) return
+                        model.setStore("color", model.store.color === color ? undefined : color)
                       }}
                     >
                       <Avatar
-                        fallback={store.name || defaultName()}
+                        fallback={model.store.name || model.defaultName()}
                         {...getAvatarColors(color)}
                         class="size-full rounded"
                       />
@@ -246,19 +149,19 @@ export function DialogEditProject(props: { project: LocalProject; server: Server
             label={language.t("dialog.project.edit.worktree.startup")}
             description={language.t("dialog.project.edit.worktree.startup.description")}
             placeholder={language.t("dialog.project.edit.worktree.startup.placeholder")}
-            value={store.startup}
-            onChange={(v) => setStore("startup", v)}
+            value={model.store.startup}
+            onChange={(v) => model.setStore("startup", v)}
             spellcheck={false}
             class="max-h-14 w-full overflow-y-auto font-mono text-xs"
           />
         </div>
 
         <div class="flex justify-end gap-2">
-          <Button type="button" variant="ghost" size="large" onClick={() => dialog.close()}>
+          <Button type="button" variant="ghost" size="large" onClick={model.close}>
             {language.t("common.cancel")}
           </Button>
-          <Button type="submit" variant="primary" size="large" disabled={saveMutation.isPending}>
-            {saveMutation.isPending ? language.t("common.saving") : language.t("common.save")}
+          <Button type="submit" variant="primary" size="large" disabled={model.save.isPending}>
+            {model.save.isPending ? language.t("common.saving") : language.t("common.save")}
           </Button>
         </div>
       </form>

+ 119 - 0
packages/app/src/components/edit-project.ts

@@ -0,0 +1,119 @@
+import { getFilename } from "@opencode-ai/core/util/path"
+import { useDialog } from "@opencode-ai/ui/context/dialog"
+import { useMutation } from "@tanstack/solid-query"
+import { createMemo } from "solid-js"
+import { createStore } from "solid-js/store"
+import { useGlobal } from "@/context/global"
+import { type LocalProject } from "@/context/layout"
+import { ServerConnection } from "@/context/server"
+
+export function createEditProjectModel(props: { project: LocalProject; server: ServerConnection.Any }) {
+  const dialog = useDialog()
+  const global = useGlobal()
+  const serverCtx = createMemo(() => global.ensureServerCtx(props.server))
+  const folderName = createMemo(() => getFilename(props.project.worktree))
+  const defaultName = createMemo(() => props.project.name || folderName())
+  const [store, setStore] = createStore({
+    name: defaultName(),
+    color: props.project.icon?.color,
+    iconOverride: props.project.icon?.override,
+    startup: props.project.commands?.start ?? "",
+    dragOver: false,
+    iconHover: false,
+  })
+  let iconInput: HTMLInputElement | undefined
+
+  function selectFile(file: File) {
+    if (!file.type.startsWith("image/")) return
+    const reader = new FileReader()
+    reader.onload = (event) => {
+      const result = event.target?.result
+      if (typeof result !== "string") return
+      setStore("iconOverride", result)
+      setStore("iconHover", false)
+    }
+    reader.readAsDataURL(file)
+  }
+
+  function drop(event: DragEvent) {
+    event.preventDefault()
+    setStore("dragOver", false)
+    const file = event.dataTransfer?.files[0]
+    if (file) selectFile(file)
+  }
+
+  function dragOver(event: DragEvent) {
+    event.preventDefault()
+    setStore("dragOver", true)
+  }
+
+  function dragLeave() {
+    setStore("dragOver", false)
+  }
+
+  function inputChange(event: Event) {
+    const file = (event.currentTarget as HTMLInputElement).files?.[0]
+    if (file) selectFile(file)
+  }
+
+  function iconClick() {
+    if (store.iconOverride && store.iconHover) {
+      setStore("iconOverride", "")
+      return
+    }
+    iconInput?.click()
+  }
+
+  const save = useMutation(() => ({
+    mutationFn: async () => {
+      const name = store.name.trim() === folderName() ? "" : store.name.trim()
+      const start = store.startup.trim()
+
+      if (props.project.id && props.project.id !== "global") {
+        await serverCtx().sdk.client.project.update({
+          projectID: props.project.id,
+          directory: props.project.worktree,
+          name,
+          icon: { color: store.color || "", override: store.iconOverride || "" },
+          commands: { start },
+        })
+        serverCtx().sync.project.icon(props.project.worktree, store.iconOverride || undefined)
+        dialog.close()
+        return
+      }
+
+      serverCtx().sync.project.meta(props.project.worktree, {
+        name,
+        icon: { color: store.color || undefined, override: store.iconOverride || undefined },
+        commands: { start: start || undefined },
+      })
+      dialog.close()
+    },
+  }))
+
+  function submit(event: SubmitEvent) {
+    event.preventDefault()
+    if (save.isPending) return
+    save.mutate()
+  }
+
+  return {
+    store,
+    setStore,
+    folderName,
+    defaultName,
+    save,
+    submit,
+    drop,
+    dragOver,
+    dragLeave,
+    inputChange,
+    iconClick,
+    close() {
+      dialog.close()
+    },
+    setIconInput(input: HTMLInputElement) {
+      iconInput = input
+    },
+  }
+}

+ 12 - 4
packages/app/src/context/layout.tsx

@@ -47,12 +47,20 @@ export function getAvatarColors(key?: string) {
 }
 
 export function getProjectAvatarVariant(key?: string): ProjectAvatarVariant {
-  if (key === "orange") return "orange"
-  if (key === "pink") return "pink"
-  if (key === "cyan") return "cyan"
-  if (key === "purple") return "purple"
   if (key === "mint") return "cyan"
   if (key === "lime") return "green"
+  if (
+    key === "orange" ||
+    key === "yellow" ||
+    key === "cyan" ||
+    key === "green" ||
+    key === "red" ||
+    key === "pink" ||
+    key === "blue" ||
+    key === "purple" ||
+    key === "gray"
+  )
+    return key
   return "gray"
 }
 

+ 2 - 2
packages/app/src/pages/home.tsx

@@ -466,8 +466,8 @@ export function NewHome() {
   }
 
   function editProject(conn: ServerConnection.Any, project: LocalProject) {
-    void import("@/components/dialog-edit-project").then((x) => {
-      dialog.show(() => <x.DialogEditProject server={conn} project={project} />)
+    void import("@/components/dialog-edit-project-v2").then((x) => {
+      void dialog.show(() => <x.DialogEditProjectV2 server={conn} project={project} />)
     })
   }
 

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

@@ -129,6 +129,10 @@ const icons = {
     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"/>`,
   },
+  "outline-share": {
+    viewBox: "0 0 16 16",
+    body: `<path d="M13.5554 10.4445V13.5556C13.5554 13.5556 12.7599 13.5556 11.7777 13.5556H4.22211C3.23989 13.5556 2.44434 13.5556 2.44434 13.5556V10.4445M4.88878 5.55557L7.99989 2.44446L11.111 5.55557M7.99989 2.44446L7.99989 9.11112" stroke="currentColor"/>`,
+  },
   reset: {
     viewBox: "0 0 20 20",
     body: `<path d="M5.83333 4.16406L2.5 7.4974L5.83333 10.8307M3.33333 7.4974H17.9167V15.4141H10" stroke="currentColor" stroke-linecap="square"/>`,

+ 2 - 3
packages/ui/src/v2/components/project-avatar-v2.tsx

@@ -38,7 +38,6 @@ export interface ProjectAvatarProps extends ComponentProps<"div"> {
 
 export function ProjectAvatar(props: ProjectAvatarProps) {
   const [split, rest] = splitProps(props, ["fallback", "src", "variant", "unread", "class", "classList", "style"])
-  const src = split.src
   return (
     <div
       {...rest}
@@ -53,9 +52,9 @@ export function ProjectAvatar(props: ProjectAvatarProps) {
       <div
         data-slot="project-avatar-surface"
         data-variant={split.variant ?? "gray"}
-        data-has-image={src ? "" : undefined}
+        data-has-image={split.src ? "" : undefined}
       >
-        <Show when={src} fallback={first(split.fallback)}>
+        <Show when={split.src} fallback={first(split.fallback)}>
           {(value) => <img src={value()} draggable={false} data-slot="project-avatar-image" />}
         </Show>
       </div>