Browse Source

feat(app): polish Open in icon treatment

Bring in the Open in button-group and transparent icon updates from #12641 while keeping locale strings unchanged. Replace CSS inversion with dedicated light/dark Zed icon assets for cleaner theme handling.

Co-authored-by: Edin <[email protected]>
Adam 2 weeks ago
parent
commit
03f3029dc6

+ 53 - 50
packages/app/src/components/session/session-header.tsx

@@ -322,57 +322,60 @@ export function SessionHeader() {
                     }
                   >
                     <div class="flex items-center">
-                      <Button
-                        variant="ghost"
-                        class="rounded-sm h-[24px] py-1.5 pr-3 pl-2 gap-2 border-none shadow-none rounded-r-none"
-                        onClick={() => openDir(current().id)}
-                        aria-label={language.t("session.header.open.ariaLabel", { app: current().label })}
-                      >
-                        <AppIcon id={current().icon} class="size-5" />
-                        <span class="text-12-regular text-text-strong">
-                          {language.t("session.header.open.action", { app: current().label })}
-                        </span>
-                      </Button>
-                      <DropdownMenu>
-                        <DropdownMenu.Trigger
-                          as={IconButton}
-                          icon="chevron-down"
+                      <div class="flex items-center rounded-full border border-border-base bg-surface-panel">
+                        <Button
                           variant="ghost"
-                          class="rounded-sm h-[24px] w-auto px-1.5 border-none shadow-none rounded-l-none data-[expanded]:bg-surface-raised-base-active"
-                          aria-label={language.t("session.header.open.menu")}
-                        />
-                        <DropdownMenu.Portal>
-                          <DropdownMenu.Content placement="bottom-end" gutter={6}>
-                            <DropdownMenu.Group>
-                              <DropdownMenu.GroupLabel>{language.t("session.header.openIn")}</DropdownMenu.GroupLabel>
-                              <DropdownMenu.RadioGroup
-                                value={prefs.app}
-                                onChange={(value) => {
-                                  if (!OPEN_APPS.includes(value as OpenApp)) return
-                                  setPrefs("app", value as OpenApp)
-                                }}
-                              >
-                                {options().map((o) => (
-                                  <DropdownMenu.RadioItem value={o.id} onSelect={() => openDir(o.id)}>
-                                    <AppIcon id={o.icon} class="size-5" />
-                                    <DropdownMenu.ItemLabel>{o.label}</DropdownMenu.ItemLabel>
-                                    <DropdownMenu.ItemIndicator>
-                                      <Icon name="check-small" size="small" class="text-icon-weak" />
-                                    </DropdownMenu.ItemIndicator>
-                                  </DropdownMenu.RadioItem>
-                                ))}
-                              </DropdownMenu.RadioGroup>
-                            </DropdownMenu.Group>
-                            <DropdownMenu.Separator />
-                            <DropdownMenu.Item onSelect={copyPath}>
-                              <Icon name="copy" size="small" class="text-icon-weak" />
-                              <DropdownMenu.ItemLabel>
-                                {language.t("session.header.open.copyPath")}
-                              </DropdownMenu.ItemLabel>
-                            </DropdownMenu.Item>
-                          </DropdownMenu.Content>
-                        </DropdownMenu.Portal>
-                      </DropdownMenu>
+                          class="rounded-full h-[28px] py-1.5 pr-4 pl-3 gap-2 border-none shadow-none"
+                          onClick={() => openDir(current().id)}
+                          aria-label={language.t("session.header.open.ariaLabel", { app: current().label })}
+                        >
+                          <AppIcon id={current().icon} class="size-5" />
+                          <span class="text-12-regular text-text-strong">
+                            {language.t("session.header.open.action", { app: current().label })}
+                          </span>
+                        </Button>
+                        <div class="h-6 w-px bg-border-base" />
+                        <DropdownMenu>
+                          <DropdownMenu.Trigger
+                            as={IconButton}
+                            icon="chevron-down"
+                            variant="ghost"
+                            class="rounded-full h-[28px] w-auto px-2 border-none shadow-none data-[expanded]:bg-surface-raised-base-active"
+                            aria-label={language.t("session.header.open.menu")}
+                          />
+                          <DropdownMenu.Portal>
+                            <DropdownMenu.Content placement="bottom-end" gutter={6}>
+                              <DropdownMenu.Group>
+                                <DropdownMenu.GroupLabel>{language.t("session.header.openIn")}</DropdownMenu.GroupLabel>
+                                <DropdownMenu.RadioGroup
+                                  value={prefs.app}
+                                  onChange={(value) => {
+                                    if (!OPEN_APPS.includes(value as OpenApp)) return
+                                    setPrefs("app", value as OpenApp)
+                                  }}
+                                >
+                                  {options().map((o) => (
+                                    <DropdownMenu.RadioItem value={o.id} onSelect={() => openDir(o.id)}>
+                                      <AppIcon id={o.icon} class="size-5" />
+                                      <DropdownMenu.ItemLabel>{o.label}</DropdownMenu.ItemLabel>
+                                      <DropdownMenu.ItemIndicator>
+                                        <Icon name="check-small" size="small" class="text-icon-weak" />
+                                      </DropdownMenu.ItemIndicator>
+                                    </DropdownMenu.RadioItem>
+                                  ))}
+                                </DropdownMenu.RadioGroup>
+                              </DropdownMenu.Group>
+                              <DropdownMenu.Separator />
+                              <DropdownMenu.Item onSelect={copyPath}>
+                                <Icon name="copy" size="small" class="text-icon-weak" />
+                                <DropdownMenu.ItemLabel>
+                                  {language.t("session.header.open.copyPath")}
+                                </DropdownMenu.ItemLabel>
+                              </DropdownMenu.Item>
+                            </DropdownMenu.Content>
+                          </DropdownMenu.Portal>
+                        </DropdownMenu>
+                      </div>
                     </div>
                   </Show>
                 </div>

+ 1 - 1
packages/ui/src/assets/icons/app/cursor.svg

@@ -1 +1 @@
-<?xml version="1.0" encoding="UTF-8"?><svg id="cursor_light__Ebene_1" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 466.73 532.09"><!--Generator: Adobe Illustrator 29.6.1, SVG Export Plug-In . SVG Version: 2.1.1 Build 9)--><defs><style>.cursor_light__st0{fill:#26251e}</style></defs><path class="cursor_light__st0" d="M457.43,125.94L244.42,2.96c-6.84-3.95-15.28-3.95-22.12,0L9.3,125.94c-5.75,3.32-9.3,9.46-9.3,16.11v247.99c0,6.65,3.55,12.79,9.3,16.11l213.01,122.98c6.84,3.95,15.28,3.95,22.12,0l213.01-122.98c5.75-3.32,9.3-9.46,9.3-16.11v-247.99c0-6.65-3.55-12.79-9.3-16.11h-.01ZM444.05,151.99l-205.63,356.16c-1.39,2.4-5.06,1.42-5.06-1.36v-233.21c0-4.66-2.49-8.97-6.53-11.31L24.87,145.67c-2.4-1.39-1.42-5.06,1.36-5.06h411.26c5.84,0,9.49,6.33,6.57,11.39h-.01Z"/></svg>
+<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><g clip-path="url(#prefix__clip0_5_17)"><rect width="512" height="512" rx="122" fill="#000"/><g clip-path="url(#prefix__clip1_5_17)"><mask id="prefix__a" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="85" y="89" width="343" height="334"><path d="M85 89h343v334H85V89z" fill="#fff"/></mask><g mask="url(#prefix__a)"><path d="M255.428 423l148.991-83.5L255.428 256l-148.99 83.5 148.99 83.5z" fill="url(#prefix__paint0_linear_5_17)"/><path d="M404.419 339.5v-167L255.428 89v167l148.991 83.5z" fill="url(#prefix__paint1_linear_5_17)"/><path d="M255.428 89l-148.99 83.5v167l148.99-83.5V89z" fill="url(#prefix__paint2_linear_5_17)"/><path d="M404.419 172.5L255.428 423V256l148.991-83.5z" fill="#E4E4E4"/><path d="M404.419 172.5L255.428 256l-148.99-83.5h297.981z" fill="#fff"/></g></g></g><defs><linearGradient id="prefix__paint0_linear_5_17" x1="255.428" y1="256" x2="255.428" y2="423" gradientUnits="userSpaceOnUse"><stop offset=".16" stop-color="#fff" stop-opacity=".39"/><stop offset=".658" stop-color="#fff" stop-opacity=".8"/></linearGradient><linearGradient id="prefix__paint1_linear_5_17" x1="404.419" y1="173.015" x2="257.482" y2="261.497" gradientUnits="userSpaceOnUse"><stop offset=".182" stop-color="#fff" stop-opacity=".31"/><stop offset=".715" stop-color="#fff" stop-opacity="0"/></linearGradient><linearGradient id="prefix__paint2_linear_5_17" x1="255.428" y1="89" x2="112.292" y2="342.802" gradientUnits="userSpaceOnUse"><stop stop-color="#fff" stop-opacity=".6"/><stop offset=".667" stop-color="#fff" stop-opacity=".22"/></linearGradient><clipPath id="prefix__clip0_5_17"><path fill="#fff" d="M0 0h512v512H0z"/></clipPath><clipPath id="prefix__clip1_5_17"><path fill="#fff" transform="translate(85 89)" d="M0 0h343v334H0z"/></clipPath></defs></svg>

BIN
packages/ui/src/assets/icons/app/finder.png


+ 15 - 0
packages/ui/src/assets/icons/app/zed-dark.svg

@@ -0,0 +1,15 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="96" height="96" fill="none" viewBox="0 0 96 96">
+  <g clip-path="url(#zed_logo-dark-a)">
+    <path
+      fill="#fff"
+      fill-rule="evenodd"
+      d="M9 6a3 3 0 0 0-3 3v66H0V9a9 9 0 0 1 9-9h80.379c4.009 0 6.016 4.847 3.182 7.682L43.055 57.187H57V51h6v7.688a4.5 4.5 0 0 1-4.5 4.5H37.055L26.743 73.5H73.5V36h6v37.5a6 6 0 0 1-6 6H20.743L10.243 90H87a3 3 0 0 0 3-3V21h6v66a9 9 0 0 1-9 9H6.621c-4.009 0-6.016-4.847-3.182-7.682L52.757 39H39v6h-6v-7.5a4.5 4.5 0 0 1 4.5-4.5h21.257l10.5-10.5H22.5V60h-6V22.5a6 6 0 0 1 6-6h52.757L85.757 6H9Z"
+      clip-rule="evenodd"
+    />
+  </g>
+  <defs>
+    <clipPath id="zed_logo-dark-a">
+      <path fill="#fff" d="M0 0h96v96H0z" />
+    </clipPath>
+  </defs>
+</svg>

+ 15 - 1
packages/ui/src/assets/icons/app/zed.svg

@@ -1 +1,15 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="96" height="96" fill="none" viewBox="0 0 96 96"><g clip-path="url(#zed_light__a)"><path fill="currentColor" fill-rule="evenodd" d="M9 6a3 3 0 0 0-3 3v66H0V9a9 9 0 0 1 9-9h80.379c4.009 0 6.016 4.847 3.182 7.682L43.055 57.187H57V51h6v7.688a4.5 4.5 0 0 1-4.5 4.5H37.055L26.743 73.5H73.5V36h6v37.5a6 6 0 0 1-6 6H20.743L10.243 90H87a3 3 0 0 0 3-3V21h6v66a9 9 0 0 1-9 9H6.621c-4.009 0-6.016-4.847-3.182-7.682L52.757 39H39v6h-6v-7.5a4.5 4.5 0 0 1 4.5-4.5h21.257l10.5-10.5H22.5V60h-6V22.5a6 6 0 0 1 6-6h52.757L85.757 6H9Z" clip-rule="evenodd"/></g><defs><clipPath id="zed_light__a"><path fill="#fff" d="M0 0h96v96H0z"/></clipPath></defs></svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="96" height="96" fill="none" viewBox="0 0 96 96">
+  <g clip-path="url(#zed_logo-a)">
+    <path
+      fill="#000"
+      fill-rule="evenodd"
+      d="M9 6a3 3 0 0 0-3 3v66H0V9a9 9 0 0 1 9-9h80.379c4.009 0 6.016 4.847 3.182 7.682L43.055 57.187H57V51h6v7.688a4.5 4.5 0 0 1-4.5 4.5H37.055L26.743 73.5H73.5V36h6v37.5a6 6 0 0 1-6 6H20.743L10.243 90H87a3 3 0 0 0 3-3V21h6v66a9 9 0 0 1-9 9H6.621c-4.009 0-6.016-4.847-3.182-7.682L52.757 39H39v6h-6v-7.5a4.5 4.5 0 0 1 4.5-4.5h21.257l10.5-10.5H22.5V60h-6V22.5a6 6 0 0 1 6-6h52.757L85.757 6H9Z"
+      clip-rule="evenodd"
+    />
+  </g>
+  <defs>
+    <clipPath id="zed_logo-a">
+      <path fill="#fff" d="M0 0h96v96H0z" />
+    </clipPath>
+  </defs>
+</svg>

+ 0 - 4
packages/ui/src/components/app-icon.css

@@ -1,9 +1,5 @@
 img[data-component="app-icon"] {
   display: block;
   box-sizing: border-box;
-  padding: 2px;
-  border-radius: 0.125rem;
-  background: var(--smoke-light-2);
-  border: 1px solid var(--smoke-light-alpha-4);
   object-fit: contain;
 }

+ 29 - 2
packages/ui/src/components/app-icon.tsx

@@ -1,5 +1,5 @@
 import type { Component, ComponentProps } from "solid-js"
-import { splitProps } from "solid-js"
+import { createSignal, onCleanup, onMount, splitProps } from "solid-js"
 import type { IconName } from "./app-icons/types"
 
 import androidStudio from "../assets/icons/app/android-studio.svg"
@@ -15,6 +15,7 @@ import textmate from "../assets/icons/app/textmate.png"
 import vscode from "../assets/icons/app/vscode.svg"
 import xcode from "../assets/icons/app/xcode.png"
 import zed from "../assets/icons/app/zed.svg"
+import zedDark from "../assets/icons/app/zed-dark.svg"
 import sublimetext from "../assets/icons/app/sublimetext.svg"
 
 const icons = {
@@ -34,17 +35,43 @@ const icons = {
   "sublime-text": sublimetext,
 } satisfies Record<IconName, string>
 
+const themed: Partial<Record<IconName, { light: string; dark: string }>> = {
+  zed: {
+    light: zed,
+    dark: zedDark,
+  },
+}
+
+const scheme = () => {
+  if (typeof document !== "object") return "light" as const
+  if (document.documentElement.dataset.colorScheme === "dark") return "dark" as const
+  return "light" as const
+}
+
 export type AppIconProps = Omit<ComponentProps<"img">, "src"> & {
   id: IconName
 }
 
 export const AppIcon: Component<AppIconProps> = (props) => {
   const [local, rest] = splitProps(props, ["id", "class", "classList", "alt", "draggable"])
+  const [mode, setMode] = createSignal(scheme())
+
+  onMount(() => {
+    const sync = () => setMode(scheme())
+    const observer = new MutationObserver(sync)
+    observer.observe(document.documentElement, {
+      attributes: true,
+      attributeFilter: ["data-color-scheme"],
+    })
+    sync()
+    onCleanup(() => observer.disconnect())
+  })
+
   return (
     <img
       data-component="app-icon"
       {...rest}
-      src={icons[local.id]}
+      src={themed[local.id]?.[mode()] ?? icons[local.id]}
       alt={local.alt ?? ""}
       draggable={local.draggable ?? false}
       classList={{