mirror of
https://gitee.com/zhangsken/opencode.git
synced 2026-10-16 10:30:01 +08:00
feat(desktop): add animated first-launch loading screen (#49408)
This commit is contained in:
@@ -15,6 +15,7 @@ export type {
|
||||
BrowserPaneTarget,
|
||||
} from "./runtime/platform/browser-pane"
|
||||
export { ServerConnection, useServers } from "./runtime/server/registry"
|
||||
export { useGlobal } from "./runtime/server/runtime"
|
||||
export { useTabs } from "./shell/tabs/tabs"
|
||||
export { createDraftStore } from "./runtime/persistence/drafts"
|
||||
export { createNamespaceStorage, type NamespaceStorage } from "./runtime/persistence/namespace"
|
||||
|
||||
@@ -15,6 +15,31 @@ test.each(["build", "serve"] as const)("configures minification for %s", async (
|
||||
expect(result?.config.renderer?.build?.sourcemap).toBe(true)
|
||||
})
|
||||
|
||||
test("onboarding preview is enabled only by the development test flag", async () => {
|
||||
const previous = process.env.OPENCODE_TEST_ONBOARDING
|
||||
try {
|
||||
process.env.OPENCODE_TEST_ONBOARDING = "1"
|
||||
for (const command of ["build", "serve"] as const) {
|
||||
const result = await loadConfigFromFile(
|
||||
{ command, mode: command === "build" ? "production" : "development" },
|
||||
`${import.meta.dirname}/electron.vite.config.ts`,
|
||||
)
|
||||
expect(result?.config.renderer?.define?.["import.meta.env.OPENCODE_TEST_ONBOARDING"]).toBe(
|
||||
JSON.stringify(command === "serve"),
|
||||
)
|
||||
}
|
||||
delete process.env.OPENCODE_TEST_ONBOARDING
|
||||
const result = await loadConfigFromFile(
|
||||
{ command: "serve", mode: "development" },
|
||||
`${import.meta.dirname}/electron.vite.config.ts`,
|
||||
)
|
||||
expect(result?.config.renderer?.define?.["import.meta.env.OPENCODE_TEST_ONBOARDING"]).toBe("false")
|
||||
} finally {
|
||||
delete process.env.OPENCODE_TEST_ONBOARDING
|
||||
if (previous !== undefined) process.env.OPENCODE_TEST_ONBOARDING = previous
|
||||
}
|
||||
})
|
||||
|
||||
test("does not package external copies of bundled dependencies", () => {
|
||||
for (const name of ["effect", "@effect/platform-node", "@effect/platform-node-shared", "drizzle-orm"]) {
|
||||
expect(Object.keys(pkg.dependencies)).not.toContain(name)
|
||||
|
||||
@@ -96,6 +96,9 @@ const require = __cjs_mod__.createRequire(import.meta.url);
|
||||
define: {
|
||||
"import.meta.env.OPENCODE_VERSION": JSON.stringify(process.env.OPENCODE_VERSION),
|
||||
"import.meta.env.VITE_OPENCODE_CHANNEL": JSON.stringify(channel),
|
||||
"import.meta.env.OPENCODE_TEST_ONBOARDING": JSON.stringify(
|
||||
command === "serve" && process.env.OPENCODE_TEST_ONBOARDING === "1",
|
||||
),
|
||||
},
|
||||
plugins: [pickerPlugin(), appPlugin, sentry],
|
||||
publicDir: "../../../app/public",
|
||||
|
||||
@@ -42,7 +42,10 @@ export const configureApplication = Effect.fn("Application.configure")(function*
|
||||
|
||||
const testRoot = yield* createTestRoot()
|
||||
app.setPath("userData", testRoot ? path.join(testRoot, "desktop") : path.join(app.getPath("appData"), appID))
|
||||
if (testRoot) app.setPath("sessionData", path.join(testRoot, "session"))
|
||||
if (testRoot) {
|
||||
app.setPath("sessionData", path.join(testRoot, "session"))
|
||||
if (testOnboarding) app.setPath("documents", path.join(testRoot, "documents"))
|
||||
}
|
||||
})
|
||||
|
||||
export function acquireApplicationLock() {
|
||||
@@ -99,7 +102,7 @@ const createTestRoot = Effect.fn("Application.createTestRoot")(function* () {
|
||||
if (!root) return undefined
|
||||
if (testOnboarding) yield* fs.remove(root, { recursive: true, force: true })
|
||||
yield* Effect.forEach(
|
||||
["data", "config", "cache", "state", "desktop", "session"],
|
||||
["data", "config", "cache", "state", "desktop", "session", "documents"],
|
||||
(dir) => fs.makeDirectory(path.join(root, dir), { recursive: true }),
|
||||
{ discard: true },
|
||||
)
|
||||
|
||||
@@ -120,6 +120,10 @@ export const makeMainWindows = Effect.fn("Window.make")(function* () {
|
||||
if (!contentReady || !appliedTheme || revealed || win.isDestroyed()) return
|
||||
revealed = true
|
||||
win.show()
|
||||
if (!app.isPackaged && process.env.OPENCODE_TEST_ONBOARDING === "1") {
|
||||
if (process.platform === "darwin") app.focus({ steal: true })
|
||||
win.focus()
|
||||
}
|
||||
runFork(Effect.logInfo("main window visible", { window: id }))
|
||||
}
|
||||
const ready = () => {
|
||||
|
||||
@@ -23,7 +23,7 @@ import { createEffect, createMemo, createResource, lazy, Show, Suspense } from "
|
||||
import { createStore } from "solid-js/store"
|
||||
import type { ElectronAPI } from "./api-types"
|
||||
import { DesktopFirstLaunchOnboarding } from "./onboarding"
|
||||
import { createDesktopPlatform, type DesktopWindowState } from "./platform"
|
||||
import { createDesktopPlatform } from "./platform"
|
||||
import { bindDesktopMenu } from "./platform/menu"
|
||||
import { createSidecarResolver, initializationData, sidecarHttp } from "./startup/initialization"
|
||||
import { preloadStoredLocale } from "./startup/locale"
|
||||
@@ -37,53 +37,41 @@ const MigrationStatus = lazy(() => import("./migration-status").then((module) =>
|
||||
|
||||
export function DesktopApp(props: { api: ElectronAPI; updater: UpdaterPlatform; version: string }) {
|
||||
const windowState = { id: props.api.getWindowID(), version: props.version }
|
||||
const url = new URL(getLastActiveUrl(windowState.id), "http://localhost")
|
||||
const initialUrl = getLastActiveUrl(windowState.id)
|
||||
const url = new URL(initialUrl, "http://localhost")
|
||||
const route = currentRoute(url.pathname, url.search)
|
||||
const [startup, setStartup] = createStore<{ ready: boolean; visible: boolean; route: LayoutRoute }>({
|
||||
const [startup, setStartup] = createStore({
|
||||
ready: false,
|
||||
visible: true,
|
||||
themeReady: false,
|
||||
onboardingReady: false,
|
||||
drawingReady: false,
|
||||
route,
|
||||
})
|
||||
return (
|
||||
<>
|
||||
<DesktopWindow
|
||||
api={props.api}
|
||||
updater={props.updater}
|
||||
windowState={windowState}
|
||||
onReady={() => setStartup("ready", true)}
|
||||
onRoute={(route) => setStartup("route", route)}
|
||||
/>
|
||||
<Show when={startup.visible}>
|
||||
<div
|
||||
class="fixed inset-0 z-[100] transition-opacity duration-300 ease-out"
|
||||
classList={{ "pointer-events-none opacity-0": startup.ready }}
|
||||
onTransitionEnd={(event) => {
|
||||
if (event.target !== event.currentTarget || !startup.ready) return
|
||||
setStartup("visible", false)
|
||||
}}
|
||||
>
|
||||
<LoadingSplash deep={startup.route.type === "draft"} />
|
||||
</div>
|
||||
</Show>
|
||||
</>
|
||||
const [firstLaunch] = createResource(() =>
|
||||
props.api.isFirstLaunchOnboardingPending().catch((error) => {
|
||||
console.error("[desktop-onboarding] first launch check failed", error)
|
||||
return false
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
function DesktopWindow(props: {
|
||||
api: ElectronAPI
|
||||
updater: UpdaterPlatform
|
||||
windowState: DesktopWindowState
|
||||
onReady: () => void
|
||||
onRoute: (route: LayoutRoute) => void
|
||||
}) {
|
||||
const platform = createDesktopPlatform(props.api, props.windowState, props.updater)
|
||||
const platform = createDesktopPlatform(props.api, windowState, props.updater)
|
||||
const [sidecar, { mutate: setSidecar }] = createResource(() => props.api.awaitInitialization())
|
||||
const [defaultServer] = createResource(() => platform.getDefaultServer?.())
|
||||
const [locale] = createResource(() => preloadStoredLocale(platform))
|
||||
const [initialRoute] = createResource(() => preloadRoute(getLastActiveUrl(props.windowState.id)))
|
||||
const router = (routerProps: BaseRouterProps) => (
|
||||
<DesktopMemoryRouter {...routerProps} windowID={props.windowState.id} />
|
||||
const [initialRoute] = createResource(
|
||||
() => !firstLaunch.loading && (firstLaunch() && initialUrl === "/" ? "/new-session" : initialUrl),
|
||||
preloadRoute,
|
||||
)
|
||||
const router = (routerProps: BaseRouterProps) => <DesktopMemoryRouter {...routerProps} windowID={windowState.id} />
|
||||
const readyToReveal = () =>
|
||||
startup.ready &&
|
||||
(!import.meta.env.OPENCODE_TEST_ONBOARDING || !firstLaunch() || initialUrl !== "/" || startup.drawingReady)
|
||||
|
||||
// Reveal only after the theme and the first-launch splash choice are both resolved.
|
||||
createEffect(() => {
|
||||
if (!startup.themeReady || firstLaunch.loading) return
|
||||
void props.api.themeReady()
|
||||
})
|
||||
|
||||
function ReadyApp() {
|
||||
const wslServers = useWslServers()
|
||||
@@ -91,7 +79,13 @@ function DesktopWindow(props: {
|
||||
const sshConnections = createSshConnections(props.api.sshServers)
|
||||
const language = useLanguage()
|
||||
const ready = createMemo(
|
||||
() => !defaultServer.loading && !sidecar.loading && !locale.loading && !wslServers.isLoading && !ssh.loading,
|
||||
() =>
|
||||
!firstLaunch.loading &&
|
||||
!defaultServer.loading &&
|
||||
!sidecar.loading &&
|
||||
!locale.loading &&
|
||||
!wslServers.isLoading &&
|
||||
!ssh.loading,
|
||||
)
|
||||
const servers = createMemo(() => {
|
||||
const data = initializationData(sidecar)
|
||||
@@ -119,14 +113,16 @@ function DesktopWindow(props: {
|
||||
{(key) => (
|
||||
<AppInterface defaultServer={key} servers={servers()} router={router}>
|
||||
<DesktopStartupReady
|
||||
routeReady={() => !initialRoute.loading}
|
||||
onReady={props.onReady}
|
||||
onRoute={props.onRoute}
|
||||
routeReady={!initialRoute.loading && startup.onboardingReady}
|
||||
onReady={() => setStartup("ready", true)}
|
||||
onRoute={(route) => setStartup("route", route)}
|
||||
/>
|
||||
<DesktopFirstLaunchOnboarding
|
||||
api={props.api}
|
||||
initialUrl={getLastActiveUrl(props.windowState.id)}
|
||||
initialUrl={initialUrl}
|
||||
serverKey={key}
|
||||
pending={firstLaunch() ?? false}
|
||||
onReady={() => setStartup("onboardingReady", true)}
|
||||
/>
|
||||
<DesktopEffects api={props.api} />
|
||||
<Suspense fallback={null}>
|
||||
@@ -148,17 +144,36 @@ function DesktopWindow(props: {
|
||||
onNativeTranslations={(bundle) => void props.api.setNativeTranslations(bundle).catch(() => undefined)}
|
||||
onThemeApplied={(mode, scheme) => {
|
||||
void props.api.setTitlebar({ mode, scheme })
|
||||
void props.api.themeReady()
|
||||
setStartup("themeReady", true)
|
||||
}}
|
||||
>
|
||||
<Show when={true}>{(_) => <ReadyApp />}</Show>
|
||||
<Show when={!firstLaunch.loading && startup.visible}>
|
||||
<div
|
||||
data-component="startup-overlay"
|
||||
class="fixed inset-0 z-[100] transition-opacity duration-300 ease-out"
|
||||
classList={{ "pointer-events-none opacity-0": readyToReveal() }}
|
||||
onTransitionEnd={(event) => {
|
||||
if (event.target !== event.currentTarget || !readyToReveal()) return
|
||||
setStartup("visible", false)
|
||||
}}
|
||||
>
|
||||
<LoadingSplash
|
||||
firstLaunch={!!firstLaunch() && initialUrl === "/"}
|
||||
deep={startup.route.type === "draft"}
|
||||
platform={platform}
|
||||
preview={import.meta.env.OPENCODE_TEST_ONBOARDING}
|
||||
onDrawEnd={() => setStartup("drawingReady", true)}
|
||||
/>
|
||||
</div>
|
||||
</Show>
|
||||
</AppBaseProviders>
|
||||
</PlatformProvider>
|
||||
)
|
||||
}
|
||||
|
||||
function DesktopStartupReady(props: {
|
||||
routeReady: () => boolean
|
||||
routeReady: boolean
|
||||
onReady: () => void
|
||||
onRoute: (route: LayoutRoute) => void
|
||||
}) {
|
||||
@@ -166,7 +181,7 @@ function DesktopStartupReady(props: {
|
||||
const route = useCurrentRoute()
|
||||
createEffect(() => props.onRoute(route()))
|
||||
createEffect(() => {
|
||||
if (!props.routeReady() || !tabs.ready() || !tabs.infoReady()) return
|
||||
if (!props.routeReady || !tabs.ready() || !tabs.infoReady()) return
|
||||
props.onReady()
|
||||
})
|
||||
return null
|
||||
|
||||
+4
@@ -1,6 +1,10 @@
|
||||
import type { ElectronNative } from "../preload/types"
|
||||
|
||||
declare global {
|
||||
interface ImportMetaEnv {
|
||||
readonly OPENCODE_TEST_ONBOARDING: boolean
|
||||
}
|
||||
|
||||
interface Window {
|
||||
electron: ElectronNative
|
||||
__OPENCODE__?: {
|
||||
|
||||
@@ -1,31 +1,38 @@
|
||||
import { ServerConnection, useServers, useTabs } from "@opencode/app/desktop"
|
||||
import { onMount } from "solid-js"
|
||||
import { ServerConnection, useCurrentRoute, useGlobal, useServers, useTabs } from "@opencode/app/desktop"
|
||||
import { createResource } from "solid-js"
|
||||
import type { ElectronAPI } from "../api-types"
|
||||
|
||||
export function DesktopFirstLaunchOnboarding(props: {
|
||||
api: ElectronAPI
|
||||
serverKey: ServerConnection.Key
|
||||
initialUrl: string
|
||||
pending: boolean
|
||||
onReady: () => void
|
||||
}) {
|
||||
const server = useServers()
|
||||
const global = useGlobal()
|
||||
const tabs = useTabs()
|
||||
const route = useCurrentRoute()
|
||||
|
||||
onMount(() => {
|
||||
void runFirstLaunchOnboarding()
|
||||
const [completed] = createResource(async () => {
|
||||
await runFirstLaunchOnboarding()
|
||||
return null
|
||||
})
|
||||
|
||||
async function runFirstLaunchOnboarding() {
|
||||
try {
|
||||
const pending = await props.api.isFirstLaunchOnboardingPending()
|
||||
if (!pending) return
|
||||
if (!props.pending) return
|
||||
|
||||
await Promise.all([tabs.ready.promise, tabs.recentReady.promise].map((p) => p ?? Promise.resolve()))
|
||||
|
||||
const shouldTrigger =
|
||||
props.initialUrl === "/" && tabs.store.length === 0 && server.list.every(ServerConnection.builtin)
|
||||
props.initialUrl === "/" &&
|
||||
route().type === "home" &&
|
||||
tabs.store.length === 0 &&
|
||||
server.list.every(ServerConnection.builtin)
|
||||
|
||||
console.info("[desktop-onboarding] first launch onboarding evaluated", {
|
||||
pending,
|
||||
pending: props.pending,
|
||||
shouldTrigger,
|
||||
initialUrl: props.initialUrl,
|
||||
tabs: tabs.store.length,
|
||||
@@ -39,11 +46,18 @@ export function DesktopFirstLaunchOnboarding(props: {
|
||||
const projects = server.projects.forServer(props.serverKey)
|
||||
projects.open(directory)
|
||||
projects.touch(directory)
|
||||
const connection = server.list.find((connection) => ServerConnection.key(connection) === props.serverKey)
|
||||
if (connection) {
|
||||
const data = global.ensureServerCtx(connection).data
|
||||
// Load the initial provider/model state before the draft transition exposes the composer.
|
||||
await Promise.all([data.location.provider.sync({ directory }), data.location.model.sync({ directory })])
|
||||
}
|
||||
tabs.select(await tabs.newDraft({ server: props.serverKey, directory }))
|
||||
} catch (error) {
|
||||
console.error("[desktop-onboarding] first launch onboarding failed", error)
|
||||
} finally {
|
||||
props.onReady()
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
// Let startup failures reach the app's recovery screen, including its splash boundary.
|
||||
return <>{completed()}</>
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
[data-component="first-launch-splash"] path {
|
||||
/* Outline paths use a normalized length so the drawing scales with the wordmark. */
|
||||
stroke-dasharray: 1;
|
||||
animation: first-launch-wordmark-draw 1000ms ease-out both;
|
||||
}
|
||||
|
||||
[data-component="first-launch-splash"][data-drawing="pending"] path {
|
||||
animation-play-state: paused;
|
||||
}
|
||||
|
||||
@keyframes first-launch-wordmark-draw {
|
||||
from {
|
||||
stroke-dashoffset: 1;
|
||||
}
|
||||
to {
|
||||
stroke-dashoffset: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
[data-component="first-launch-splash"] path {
|
||||
animation: none;
|
||||
stroke-dasharray: none;
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,87 @@
|
||||
import { Splash } from "@opencode/ui/logo"
|
||||
import { Wordmark } from "@opencode/ui/wordmark"
|
||||
import type { Platform } from "@opencode/app/desktop"
|
||||
import { onCleanup, onMount, Show } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import "./splash.css"
|
||||
|
||||
export function LoadingSplash(props: {
|
||||
deep: boolean
|
||||
firstLaunch: boolean
|
||||
platform: Platform
|
||||
preview: boolean
|
||||
onDrawEnd: () => void
|
||||
}) {
|
||||
const [drawing, setDrawing] = createStore({ started: false })
|
||||
onMount(() => {
|
||||
if (!props.preview || !props.firstLaunch) return
|
||||
const motion = window.matchMedia("(prefers-reduced-motion: reduce)")
|
||||
const start = () => {
|
||||
if (document.visibilityState !== "visible" || !document.hasFocus()) return
|
||||
setDrawing("started", true)
|
||||
if (motion.matches) props.onDrawEnd()
|
||||
}
|
||||
window.addEventListener("focus", start)
|
||||
document.addEventListener("visibilitychange", start)
|
||||
motion.addEventListener("change", start)
|
||||
start()
|
||||
onCleanup(() => {
|
||||
window.removeEventListener("focus", start)
|
||||
document.removeEventListener("visibilitychange", start)
|
||||
motion.removeEventListener("change", start)
|
||||
})
|
||||
})
|
||||
|
||||
const titlebarHeight = () => {
|
||||
const zoom = props.platform.webviewZoom?.() ?? 1
|
||||
if (props.platform.os === "macos") return `max(36px, ${36 / zoom}px)`
|
||||
if (props.platform.os === "windows")
|
||||
return `max(36px, env(titlebar-area-height, ${44 / Math.min(Math.max(zoom, 0.25), 1)}px))`
|
||||
return "36px"
|
||||
}
|
||||
|
||||
export function LoadingSplash(props: { deep: boolean }) {
|
||||
return (
|
||||
<div
|
||||
class="h-dvh w-screen flex flex-col items-center justify-center"
|
||||
classList={{
|
||||
"bg-v2-background-bg-deep": props.deep,
|
||||
"bg-v2-background-bg-base": !props.deep,
|
||||
}}
|
||||
<Show
|
||||
when={props.firstLaunch}
|
||||
fallback={
|
||||
<div
|
||||
data-component="startup-splash"
|
||||
class="h-dvh w-screen flex flex-col items-center justify-center"
|
||||
classList={{
|
||||
"bg-v2-background-bg-deep": props.deep,
|
||||
"bg-v2-background-bg-base": !props.deep,
|
||||
}}
|
||||
>
|
||||
<Splash class="w-16 h-20 opacity-50 animate-pulse" />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Splash class="w-16 h-20 opacity-50 animate-pulse" />
|
||||
</div>
|
||||
<div
|
||||
data-component="first-launch-splash"
|
||||
data-drawing={props.preview && !drawing.started ? "pending" : "running"}
|
||||
aria-hidden="true"
|
||||
class="h-dvh w-screen bg-v2-background-bg-base"
|
||||
style={{
|
||||
// Match the default session's titlebar, panel insets, and wordmark position.
|
||||
"padding-block-start": `calc(${titlebarHeight()} + ${props.platform.os === "windows" ? 1 : 8}px)`,
|
||||
"padding-block-end": "8px",
|
||||
"padding-inline": "32px",
|
||||
}}
|
||||
onAnimationEnd={(event) => {
|
||||
if (event.animationName === "first-launch-wordmark-draw") props.onDrawEnd()
|
||||
}}
|
||||
>
|
||||
<div class="relative size-full">
|
||||
<div class="absolute inset-x-0 top-[25.375%]">
|
||||
<Wordmark
|
||||
outline
|
||||
fade={false}
|
||||
muted={false}
|
||||
class="mx-auto block h-auto w-full max-w-[720px] text-v2-icon-icon-faint opacity-50"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { createUniqueId, type ComponentProps } from "solid-js"
|
||||
|
||||
export function Wordmark(props: Pick<ComponentProps<"svg">, "class"> & { fade?: boolean; muted?: boolean }) {
|
||||
export function Wordmark(
|
||||
props: Pick<ComponentProps<"svg">, "class"> & { fade?: boolean; muted?: boolean; outline?: boolean },
|
||||
) {
|
||||
const mask = createUniqueId()
|
||||
const maskGradient = createUniqueId()
|
||||
|
||||
@@ -9,19 +11,51 @@ export function Wordmark(props: Pick<ComponentProps<"svg">, "class"> & { fade?:
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 720 129"
|
||||
fill="none"
|
||||
classList={{ [props.class ?? ""]: !!props.class }}
|
||||
classList={{
|
||||
[props.class ?? ""]: !!props.class,
|
||||
"overflow-visible [&_path]:[vector-effect:non-scaling-stroke]": props.outline,
|
||||
}}
|
||||
>
|
||||
<g opacity={props.muted === false ? 1 : 0.6} class="[[data-color-scheme=dark]_&]:opacity-100">
|
||||
<g mask={props.fade === false ? undefined : `url(#${mask})`}>
|
||||
<g opacity={props.muted === false ? 1 : 0.16 * 0.7} fill="currentColor">
|
||||
<path d="M55.3846 36.4286H18.4615V91.7143H55.3846V36.4286ZM73.8462 110.143H0V18H73.8462V110.143Z" />
|
||||
<path d="M110.462 91.7143H147.385V36.4286H110.462V91.7143ZM165.846 110.143H110.462V128.571H92V18H165.846V110.143Z" />
|
||||
<path d="M258.846 73.2857H203.462V91.7143H258.846V110.143H185V18H258.846V73.2857ZM203.462 54.8571H240.385V36.4286H203.462V54.8571Z" />
|
||||
<path d="M332.385 36.4286H295.462V110.143H277V18H332.385V36.4286ZM350.846 110.143H332.385V36.4286H350.846V110.143Z" />
|
||||
<path d="M442.846 36.4286H387.462V91.7143H442.846V110.143H369V18H442.846V36.4286Z" />
|
||||
<path d="M517.385 36.4286H480.462V91.7143H517.385V36.4286ZM535.846 110.143H462V18H535.846V110.143Z" />
|
||||
<path d="M609.385 36.8571H572.462V92.1429H609.385V36.8571ZM627.846 110.571H554V18.4286H609.385V0H627.846V110.571Z" />
|
||||
<path d="M664.462 36.4286V54.8571H701.385V36.4286H664.462ZM719.846 73.2857H664.462V91.7143H719.846V110.143H646V18H719.846V73.2857Z" />
|
||||
<g
|
||||
opacity={props.muted === false ? 1 : 0.16 * 0.7}
|
||||
fill={props.outline ? "none" : "currentColor"}
|
||||
stroke={props.outline ? "currentColor" : undefined}
|
||||
stroke-width={props.outline ? 1 : undefined}
|
||||
>
|
||||
<path
|
||||
pathLength={props.outline ? 1 : undefined}
|
||||
d="M55.3846 36.4286H18.4615V91.7143H55.3846V36.4286ZM73.8462 110.143H0V18H73.8462V110.143Z"
|
||||
/>
|
||||
<path
|
||||
pathLength={props.outline ? 1 : undefined}
|
||||
d="M110.462 91.7143H147.385V36.4286H110.462V91.7143ZM165.846 110.143H110.462V128.571H92V18H165.846V110.143Z"
|
||||
/>
|
||||
<path
|
||||
pathLength={props.outline ? 1 : undefined}
|
||||
d="M258.846 73.2857H203.462V91.7143H258.846V110.143H185V18H258.846V73.2857ZM203.462 54.8571H240.385V36.4286H203.462V54.8571Z"
|
||||
/>
|
||||
<path
|
||||
pathLength={props.outline ? 1 : undefined}
|
||||
d="M332.385 36.4286H295.462V110.143H277V18H332.385V36.4286ZM350.846 110.143H332.385V36.4286H350.846V110.143Z"
|
||||
/>
|
||||
<path
|
||||
pathLength={props.outline ? 1 : undefined}
|
||||
d="M442.846 36.4286H387.462V91.7143H442.846V110.143H369V18H442.846V36.4286Z"
|
||||
/>
|
||||
<path
|
||||
pathLength={props.outline ? 1 : undefined}
|
||||
d="M517.385 36.4286H480.462V91.7143H517.385V36.4286ZM535.846 110.143H462V18H535.846V110.143Z"
|
||||
/>
|
||||
<path
|
||||
pathLength={props.outline ? 1 : undefined}
|
||||
d="M609.385 36.8571H572.462V92.1429H609.385V36.8571ZM627.846 110.571H554V18.4286H609.385V0H627.846V110.571Z"
|
||||
/>
|
||||
<path
|
||||
pathLength={props.outline ? 1 : undefined}
|
||||
d="M664.462 36.4286V54.8571H701.385V36.4286H664.462ZM719.846 73.2857H664.462V91.7143H719.846V110.143H646V18H719.846V73.2857Z"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
|
||||
Reference in New Issue
Block a user