mirror of
https://github.com/anomalyco/opencode.git
synced 2026-02-22 08:44:32 +00:00
Compare commits
59 Commits
fix-markdo
...
update-des
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b30c91de78 | ||
|
|
2a22111b5e | ||
|
|
f5fd54598f | ||
|
|
0f7b17b1b4 | ||
|
|
4d3e983edb | ||
|
|
50badbd779 | ||
|
|
d3fc29bdec | ||
|
|
fe58c649cb | ||
|
|
87eebad14e | ||
|
|
e258662178 | ||
|
|
591f54cd0d | ||
|
|
fdea599939 | ||
|
|
af2a09940c | ||
|
|
7e016fdda6 | ||
|
|
beb97d21ff | ||
|
|
b0345284f9 | ||
|
|
d71153eae6 | ||
|
|
ccac97c7c4 | ||
|
|
e60ded01df | ||
|
|
4b2a14c154 | ||
|
|
b4717d8092 | ||
|
|
dc8f8cc567 | ||
|
|
99110d12c4 | ||
|
|
74b1349cf6 | ||
|
|
3b3505cfe8 | ||
|
|
55bd6e487e | ||
|
|
1ee916a3c3 | ||
|
|
a5d47f076e | ||
|
|
acd1eb574d | ||
|
|
a71dcc189e | ||
|
|
3789a31423 | ||
|
|
bb6e350d68 | ||
|
|
f9a441d4f4 | ||
|
|
1c05ebaea2 | ||
|
|
520c47e81d | ||
|
|
e5b08da0f1 | ||
|
|
fe2cc0cff1 | ||
|
|
fbc8f6eba9 | ||
|
|
8cba7d7f53 | ||
|
|
6450ba1b79 | ||
|
|
dc1c25cff5 | ||
|
|
3f3550a16e | ||
|
|
57b457f568 | ||
|
|
161e3db795 | ||
|
|
08068c3b91 | ||
|
|
64edbb6b82 | ||
|
|
864f7ce129 | ||
|
|
977827c9a4 | ||
|
|
d8b8854795 | ||
|
|
d79dc295fd | ||
|
|
abadacdce7 | ||
|
|
bd5a9002a8 | ||
|
|
ecf33a72c3 | ||
|
|
f2711bf5ae | ||
|
|
769c34c94f | ||
|
|
ad33807627 | ||
|
|
cf4fe5dc82 | ||
|
|
56a7fbe131 | ||
|
|
3bc995dbe1 |
7
bun.lock
7
bun.lock
@@ -95,6 +95,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@typescript/native-preview": "catalog:",
|
||||
"@webgpu/types": "0.1.54",
|
||||
"typescript": "catalog:",
|
||||
"wrangler": "4.50.0",
|
||||
},
|
||||
@@ -410,7 +411,7 @@
|
||||
"@solid-primitives/resize-observer": "2.1.3",
|
||||
"@solidjs/meta": "catalog:",
|
||||
"@typescript/native-preview": "catalog:",
|
||||
"dompurify": "catalog:",
|
||||
"dompurify": "3.3.1",
|
||||
"fuzzysort": "catalog:",
|
||||
"katex": "0.16.27",
|
||||
"luxon": "catalog:",
|
||||
@@ -1903,7 +1904,7 @@
|
||||
|
||||
"@vitest/utils": ["@vitest/utils@4.0.16", "", { "dependencies": { "@vitest/pretty-format": "4.0.16", "tinyrainbow": "^3.0.3" } }, "sha512-h8z9yYhV3e1LEfaQ3zdypIrnAg/9hguReGZoS7Gl0aBG5xgA410zBqECqmaF/+RkTggRsfnzc1XaAHA6bmUufA=="],
|
||||
|
||||
"@webgpu/types": ["@webgpu/types@0.1.66", "", {}, "sha512-YA2hLrwLpDsRueNDXIMqN9NTzD6bCDkuXbOSe0heS+f8YE8usA6Gbv1prj81pzVHrbaAma7zObnIC+I6/sXJgA=="],
|
||||
"@webgpu/types": ["@webgpu/types@0.1.54", "", {}, "sha512-81oaalC8LFrXjhsczomEQ0u3jG+TqE6V9QHLA8GNZq/Rnot0KDugu3LhSYSlie8tSdooAN1Hov05asrUUp9qgg=="],
|
||||
|
||||
"@zip.js/zip.js": ["@zip.js/zip.js@2.7.62", "", {}, "sha512-OaLvZ8j4gCkLn048ypkZu29KX30r8/OfFF2w4Jo5WXFr+J04J+lzJ5TKZBVgFXhlvSkqNFQdfnY1Q8TMTCyBVA=="],
|
||||
|
||||
@@ -4291,6 +4292,8 @@
|
||||
|
||||
"body-parser/qs": ["qs@6.13.0", "", { "dependencies": { "side-channel": "^1.0.6" } }, "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg=="],
|
||||
|
||||
"bun-webgpu/@webgpu/types": ["@webgpu/types@0.1.66", "", {}, "sha512-YA2hLrwLpDsRueNDXIMqN9NTzD6bCDkuXbOSe0heS+f8YE8usA6Gbv1prj81pzVHrbaAma7zObnIC+I6/sXJgA=="],
|
||||
|
||||
"clean-css/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
|
||||
|
||||
"compress-commons/is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"nodeModules": {
|
||||
"x86_64-linux": "sha256-XP1DXs1Fcfog99rjMryki9mMqn1g1H4ykHx7WDsnrnw=",
|
||||
"aarch64-darwin": "sha256-fupiqvXkW3Cl44K+n1cDz81vOboMXIHPHTey6TewX70="
|
||||
"x86_64-linux": "sha256-Fl1BdjNSg19LJVSgDMiBX8JuTaGlL2I5T+rqLfjSeO4=",
|
||||
"aarch64-darwin": "sha256-7UajHu40n7JKqurU/+CGlitErsVFA2qDneUytI8+/zQ="
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,14 +54,17 @@ export function SessionHeader() {
|
||||
<Portal mount={mount()}>
|
||||
<button
|
||||
type="button"
|
||||
class="hidden md:flex w-[320px] h-7 px-1.5 items-center gap-2 rounded-md border border-border-weak-base bg-surface-raised-base transition-colors cursor-default hover:bg-surface-raised-base-hover focus:bg-surface-raised-base-hover active:bg-surface-raised-base-active"
|
||||
class="hidden md:flex w-[320px] h-8 p-1.5 items-center gap-2 justify-between rounded-md border border-border-weak-base bg-surface-raised-base transition-colors cursor-default hover:bg-surface-raised-base-hover focus:bg-surface-raised-base-hover active:bg-surface-raised-base-active"
|
||||
onClick={() => command.trigger("file.open")}
|
||||
>
|
||||
<Icon name="magnifying-glass" size="small" class="text-text-weak" />
|
||||
<span class="flex-1 min-w-0 text-14-regular text-text-weak truncate">Search {name()}</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<Icon name="magnifying-glass" size="normal" class="icon-base" />
|
||||
<span class="flex-1 min-w-0 text-14-regular text-text-weak truncate">Search {name()}</span>
|
||||
</div>
|
||||
|
||||
<Show when={hotkey()}>
|
||||
{(keybind) => (
|
||||
<span class="shrink-0 flex items-center justify-center h-5 px-2 rounded-md border border-border-weak-base bg-surface-base text-12-medium text-text-weak">
|
||||
<span class="shrink-0 flex items-center justify-center h-5 px-2 rounded-[2px] border border-border-weak-base bg-surface-base text-12-medium text-text-weak">
|
||||
{keybind()}
|
||||
</span>
|
||||
)}
|
||||
|
||||
@@ -88,7 +88,7 @@ export function Titlebar() {
|
||||
onClick={layout.mobileSidebar.toggle}
|
||||
/>
|
||||
<TooltipKeybind
|
||||
class="hidden xl:flex shrink-0"
|
||||
class="hidden xl:flex shrink-0 ml-14"
|
||||
placement="bottom"
|
||||
title="Toggle sidebar"
|
||||
keybind={command.keybind("sidebar.toggle")}
|
||||
|
||||
@@ -161,53 +161,64 @@ export default function Layout(props: ParentProps) {
|
||||
})
|
||||
|
||||
onMount(() => {
|
||||
const alerts = {
|
||||
"permission.asked": {
|
||||
title: "Permission required",
|
||||
icon: "checklist" as const,
|
||||
description: (sessionTitle: string, projectName: string) =>
|
||||
`${sessionTitle} in ${projectName} needs permission`,
|
||||
},
|
||||
"question.asked": {
|
||||
title: "Question",
|
||||
icon: "bubble-5" as const,
|
||||
description: (sessionTitle: string, projectName: string) => `${sessionTitle} in ${projectName} has a question`,
|
||||
},
|
||||
}
|
||||
|
||||
const toastBySession = new Map<string, number>()
|
||||
const alertedAtBySession = new Map<string, number>()
|
||||
const permissionAlertCooldownMs = 5000
|
||||
const cooldownMs = 5000
|
||||
|
||||
const unsub = globalSDK.event.listen((e) => {
|
||||
if (e.details?.type !== "permission.asked") return
|
||||
if (e.details?.type !== "permission.asked" && e.details?.type !== "question.asked") return
|
||||
const config = alerts[e.details.type]
|
||||
const directory = e.name
|
||||
const perm = e.details.properties
|
||||
if (permission.autoResponds(perm, directory)) return
|
||||
const props = e.details.properties
|
||||
if (e.details.type === "permission.asked" && permission.autoResponds(e.details.properties, directory)) return
|
||||
|
||||
const [store] = globalSync.child(directory)
|
||||
const session = store.session.find((s) => s.id === perm.sessionID)
|
||||
const sessionKey = `${directory}:${perm.sessionID}`
|
||||
const session = store.session.find((s) => s.id === props.sessionID)
|
||||
const sessionKey = `${directory}:${props.sessionID}`
|
||||
|
||||
const sessionTitle = session?.title ?? "New session"
|
||||
const projectName = getFilename(directory)
|
||||
const description = `${sessionTitle} in ${projectName} needs permission`
|
||||
const href = `/${base64Encode(directory)}/session/${perm.sessionID}`
|
||||
const description = config.description(sessionTitle, projectName)
|
||||
const href = `/${base64Encode(directory)}/session/${props.sessionID}`
|
||||
|
||||
const now = Date.now()
|
||||
const lastAlerted = alertedAtBySession.get(sessionKey) ?? 0
|
||||
if (now - lastAlerted < permissionAlertCooldownMs) return
|
||||
if (now - lastAlerted < cooldownMs) return
|
||||
alertedAtBySession.set(sessionKey, now)
|
||||
|
||||
void platform.notify("Permission required", description, href)
|
||||
void platform.notify(config.title, description, href)
|
||||
|
||||
const currentDir = params.dir ? base64Decode(params.dir) : undefined
|
||||
const currentSession = params.id
|
||||
if (directory === currentDir && perm.sessionID === currentSession) return
|
||||
if (directory === currentDir && props.sessionID === currentSession) return
|
||||
if (directory === currentDir && session?.parentID === currentSession) return
|
||||
|
||||
const existingToastId = toastBySession.get(sessionKey)
|
||||
if (existingToastId !== undefined) {
|
||||
toaster.dismiss(existingToastId)
|
||||
}
|
||||
if (existingToastId !== undefined) toaster.dismiss(existingToastId)
|
||||
|
||||
const toastId = showToast({
|
||||
persistent: true,
|
||||
icon: "checklist",
|
||||
title: "Permission required",
|
||||
icon: config.icon,
|
||||
title: config.title,
|
||||
description,
|
||||
actions: [
|
||||
{
|
||||
label: "Go to session",
|
||||
onClick: () => {
|
||||
navigate(href)
|
||||
},
|
||||
onClick: () => navigate(href),
|
||||
},
|
||||
{
|
||||
label: "Dismiss",
|
||||
@@ -848,7 +859,6 @@ export default function Layout(props: ParentProps) {
|
||||
return false
|
||||
})
|
||||
const isWorking = createMemo(() => {
|
||||
if (props.session.id === params.id) return false
|
||||
if (hasPermissions()) return false
|
||||
const status = sessionStore.session_status[props.session.id]
|
||||
return status?.type === "busy" || status?.type === "retry"
|
||||
@@ -870,10 +880,10 @@ export default function Layout(props: ParentProps) {
|
||||
return (
|
||||
<div
|
||||
data-session-id={props.session.id}
|
||||
class="group/session relative w-full rounded-md cursor-default transition-colors px-3
|
||||
hover:bg-surface-raised-base-hover focus-within:bg-surface-raised-base-hover has-[.active]:bg-surface-raised-base-hover"
|
||||
class="group/session relative w-full rounded-md cursor-default transition-colors pl-2 pr-3
|
||||
hover:bg-surface-raised-base-hover focus-within:bg-surface-raised-base-hover has-[.active]:bg-surface-base-active"
|
||||
>
|
||||
<Tooltip placement={props.mobile ? "bottom" : "right"} value={props.session.title} gutter={10}>
|
||||
<Tooltip placement={props.mobile ? "bottom" : "right"} value={props.session.title} gutter={16} openDelay={1000}>
|
||||
<A
|
||||
href={`${props.slug}/session/${props.session.id}`}
|
||||
class={`flex items-center justify-between gap-3 min-w-0 text-left w-full focus:outline-none transition-[padding] group-hover/session:pr-7 group-focus-within/session:pr-7 group-active/session:pr-7 ${props.dense ? "py-0.5" : "py-1"}`}
|
||||
@@ -887,7 +897,7 @@ export default function Layout(props: ParentProps) {
|
||||
>
|
||||
<Switch>
|
||||
<Match when={isWorking()}>
|
||||
<Spinner class="size-[15px]" />
|
||||
<Spinner class="size-[15px] opacity-50" />
|
||||
</Match>
|
||||
<Match when={hasPermissions()}>
|
||||
<div class="size-1.5 rounded-full bg-surface-warning-strong" />
|
||||
@@ -903,7 +913,13 @@ export default function Layout(props: ParentProps) {
|
||||
<span class="text-14-regular text-text-strong grow-1 min-w-0 overflow-hidden text-ellipsis truncate">
|
||||
{props.session.title}
|
||||
</span>
|
||||
<Show when={props.session.summary}>{(summary) => <DiffChanges changes={summary()} />}</Show>
|
||||
<Show when={props.session.summary}>
|
||||
{(summary) => (
|
||||
<div class="group-hover/session:hidden group-active/session:hidden group-focus-within/session:hidden">
|
||||
<DiffChanges changes={summary()} />
|
||||
</div>
|
||||
)}
|
||||
</Show>
|
||||
</div>
|
||||
</A>
|
||||
</Tooltip>
|
||||
@@ -914,6 +930,7 @@ export default function Layout(props: ParentProps) {
|
||||
placement={props.mobile ? "bottom" : "right"}
|
||||
title="Archive session"
|
||||
keybind={command.keybind("session.archive")}
|
||||
gutter={8}
|
||||
>
|
||||
<IconButton icon="archive" variant="ghost" onClick={() => archiveSession(props.session)} />
|
||||
</TooltipKeybind>
|
||||
@@ -961,7 +978,7 @@ export default function Layout(props: ParentProps) {
|
||||
type="button"
|
||||
classList={{
|
||||
"flex items-center justify-center size-10 p-1 rounded-md border transition-colors cursor-default": true,
|
||||
"bg-surface-base-hover border-icon-strong-base": selected(),
|
||||
"bg-transparent border-icon-strong-base hover:bg-surface-base-hover": selected(),
|
||||
"bg-transparent border-transparent hover:bg-surface-base-hover hover:border-border-weak-base": !selected(),
|
||||
}}
|
||||
onClick={() => navigateToProject(props.project.worktree)}
|
||||
@@ -973,9 +990,9 @@ export default function Layout(props: ParentProps) {
|
||||
return (
|
||||
// @ts-ignore
|
||||
<div use:sortable classList={{ "opacity-30": sortable.isActiveDraggable }}>
|
||||
<HoverCard openDelay={0} closeDelay={0} placement="right-start" gutter={10} trigger={trigger}>
|
||||
<HoverCard openDelay={0} closeDelay={0} placement="right-start" gutter={8} trigger={trigger}>
|
||||
<div class="-m-3 flex flex-col w-72">
|
||||
<div class="px-3 py-2 text-12-medium text-text-strong">Recent sessions</div>
|
||||
<div class="px-3 py-2 text-12-medium text-text-weak">Recent sessions</div>
|
||||
<div class="px-2 pb-2 flex flex-col gap-2">
|
||||
<Show
|
||||
when={workspaceEnabled()}
|
||||
@@ -999,7 +1016,7 @@ export default function Layout(props: ParentProps) {
|
||||
<div class="shrink-0 size-6 flex items-center justify-center">
|
||||
<Icon name="branch" size="small" class="text-icon-base" />
|
||||
</div>
|
||||
<span class="truncate text-14-medium text-text-strong">{label(directory)}</span>
|
||||
<span class="truncate text-14-medium text-text-base">{label(directory)}</span>
|
||||
</div>
|
||||
<For each={sessions(directory)}>
|
||||
{(session) => (
|
||||
@@ -1011,18 +1028,20 @@ export default function Layout(props: ParentProps) {
|
||||
</For>
|
||||
</Show>
|
||||
</div>
|
||||
<div class="px-2 py-2 border-t border-border-weak-base">
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="flex w-full text-left justify-start text-text-base px-2"
|
||||
onClick={() => {
|
||||
layout.sidebar.open()
|
||||
navigateToProject(props.project.worktree)
|
||||
}}
|
||||
>
|
||||
View all sessions
|
||||
</Button>
|
||||
</div>
|
||||
<Show when={!selected()}>
|
||||
<div class="px-2 py-2 border-t border-border-weak-base">
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="flex w-full text-left justify-start text-text-base px-2"
|
||||
onClick={() => {
|
||||
layout.sidebar.open()
|
||||
navigateToProject(props.project.worktree)
|
||||
}}
|
||||
>
|
||||
View all sessions
|
||||
</Button>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</HoverCard>
|
||||
</div>
|
||||
@@ -1104,7 +1123,7 @@ export default function Layout(props: ParentProps) {
|
||||
<div class="flex items-center justify-center shrink-0 size-6">
|
||||
<Icon name="branch" size="small" />
|
||||
</div>
|
||||
<span class="truncate text-14-medium text-text-strong">{title()}</span>
|
||||
<span class="truncate text-14-medium text-text-base">{title()}</span>
|
||||
<Icon
|
||||
name={open() ? "chevron-down" : "chevron-right"}
|
||||
size="small"
|
||||
@@ -1113,17 +1132,20 @@ export default function Layout(props: ParentProps) {
|
||||
</div>
|
||||
</Collapsible.Trigger>
|
||||
<div class="absolute right-1 top-1/2 -translate-y-1/2 hidden items-center gap-0.5 pointer-events-none group-hover/trigger:flex group-focus-within/trigger:flex">
|
||||
<Tooltip class="pointer-events-auto" value="More options" placement="top">
|
||||
<IconButton icon="dot-grid" variant="ghost" class="size-6 rounded-md" />
|
||||
</Tooltip>
|
||||
<Tooltip class="pointer-events-auto" value="New session" placement="top">
|
||||
<IconButton icon="dot-grid" variant="ghost" class="size-6 rounded-md pointer-events-auto" />
|
||||
<TooltipKeybind
|
||||
class="pointer-events-auto"
|
||||
placement="right"
|
||||
title="New session"
|
||||
keybind={command.keybind("session.new")}
|
||||
>
|
||||
<IconButton
|
||||
icon="plus-small"
|
||||
variant="ghost"
|
||||
class="size-6 rounded-md"
|
||||
onClick={() => navigate(`/${slug()}/session`)}
|
||||
/>
|
||||
</Tooltip>
|
||||
</TooltipKeybind>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1146,9 +1168,12 @@ export default function Layout(props: ParentProps) {
|
||||
<div class="relative w-full py-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="flex w-full text-left justify-start text-12-medium text-text-weak px-10"
|
||||
class="flex w-full text-left justify-start text-14-regular text-text-weak px-10"
|
||||
size="large"
|
||||
onClick={loadMore}
|
||||
onClick={(e: MouseEvent) => {
|
||||
loadMore()
|
||||
;(e.currentTarget as HTMLButtonElement).blur()
|
||||
}}
|
||||
>
|
||||
Load more
|
||||
</Button>
|
||||
@@ -1191,9 +1216,12 @@ export default function Layout(props: ParentProps) {
|
||||
<div class="relative w-full py-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="flex w-full text-left justify-start text-12-medium text-text-weak px-10"
|
||||
class="flex w-full text-left justify-start text-14-regular text-text-weak px-10"
|
||||
size="large"
|
||||
onClick={loadMore}
|
||||
onClick={(e: MouseEvent) => {
|
||||
loadMore()
|
||||
;(e.currentTarget as HTMLButtonElement).blur()
|
||||
}}
|
||||
>
|
||||
Load more
|
||||
</Button>
|
||||
@@ -1312,7 +1340,7 @@ export default function Layout(props: ParentProps) {
|
||||
{(p) => (
|
||||
<>
|
||||
<div class="shrink-0 px-2 py-1">
|
||||
<div class="flex items-start justify-between gap-2 p-2">
|
||||
<div class="group/project flex items-start justify-between gap-2 p-2 pr-1">
|
||||
<div class="flex flex-col min-w-0">
|
||||
<span class="text-16-medium text-text-strong truncate">{projectName()}</span>
|
||||
<Tooltip placement="right" value={project()?.worktree} class="shrink-0">
|
||||
@@ -1326,22 +1354,22 @@ export default function Layout(props: ParentProps) {
|
||||
as={IconButton}
|
||||
icon="dot-grid"
|
||||
variant="ghost"
|
||||
class="shrink-0 size-6 rounded-md"
|
||||
class="shrink-0 size-6 rounded-md opacity-0 group-hover/project:opacity-100 data-[expanded]:opacity-100 data-[expanded]:bg-surface-base-active"
|
||||
/>
|
||||
<DropdownMenu.Portal>
|
||||
<DropdownMenu.Content>
|
||||
<DropdownMenu.Content class="mt-1">
|
||||
<DropdownMenu.Item onSelect={() => dialog.show(() => <DialogEditProject project={p} />)}>
|
||||
<DropdownMenu.ItemLabel>Edit project</DropdownMenu.ItemLabel>
|
||||
<DropdownMenu.ItemLabel>Edit</DropdownMenu.ItemLabel>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item onSelect={() => closeProject(p.worktree)}>
|
||||
<DropdownMenu.ItemLabel>Close project</DropdownMenu.ItemLabel>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Separator />
|
||||
<DropdownMenu.Item onSelect={() => layout.sidebar.toggleWorkspaces(p.worktree)}>
|
||||
<DropdownMenu.ItemLabel>
|
||||
{layout.sidebar.workspaces(p.worktree)() ? "Disable workspaces" : "Enable workspaces"}
|
||||
</DropdownMenu.ItemLabel>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Separator />
|
||||
<DropdownMenu.Item onSelect={() => closeProject(p.worktree)}>
|
||||
<DropdownMenu.ItemLabel>Close</DropdownMenu.ItemLabel>
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Portal>
|
||||
</DropdownMenu>
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@typescript/native-preview": "catalog:",
|
||||
"@webgpu/types": "0.1.54",
|
||||
"typescript": "catalog:",
|
||||
"wrangler": "4.50.0"
|
||||
},
|
||||
|
||||
186
packages/console/app/src/component/light-rays.css
Normal file
186
packages/console/app/src/component/light-rays.css
Normal file
@@ -0,0 +1,186 @@
|
||||
.light-rays-container {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.light-rays-container canvas {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.light-rays-controls {
|
||||
position: fixed;
|
||||
top: 16px;
|
||||
right: 16px;
|
||||
z-index: 9999;
|
||||
font-family: var(--font-mono, monospace);
|
||||
font-size: 12px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.light-rays-controls-toggle {
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 4px;
|
||||
padding: 8px 12px;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.light-rays-controls-toggle:hover {
|
||||
background: rgba(0, 0, 0, 0.9);
|
||||
border-color: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.light-rays-controls-panel {
|
||||
background: rgba(0, 0, 0, 0.85);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 4px;
|
||||
padding: 12px;
|
||||
margin-top: 4px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
min-width: 240px;
|
||||
max-height: calc(100vh - 100px);
|
||||
overflow-y: auto;
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
|
||||
.control-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.control-group label {
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.control-group.checkbox {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.control-group.checkbox label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
.control-group input[type="range"] {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
width: 100%;
|
||||
height: 4px;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 2px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.control-group input[type="range"]::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
background: #fff;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
transition: transform 0.1s;
|
||||
}
|
||||
|
||||
.control-group input[type="range"]::-webkit-slider-thumb:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.control-group input[type="range"]::-moz-range-thumb {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
background: #fff;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.control-group input[type="color"] {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
width: 100%;
|
||||
height: 32px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 4px;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
.control-group input[type="color"]::-webkit-color-swatch-wrapper {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.control-group input[type="color"]::-webkit-color-swatch {
|
||||
border: none;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.control-group select {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 4px;
|
||||
padding: 6px 8px;
|
||||
color: #fff;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.control-group select:hover {
|
||||
border-color: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.control-group select option {
|
||||
background: #1a1a1a;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.control-group input[type="checkbox"] {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
accent-color: #fff;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.reset-button {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 4px;
|
||||
padding: 8px 12px;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
margin-top: 4px;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
|
||||
.reset-button:hover {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
border-color: rgba(255, 255, 255, 0.3);
|
||||
color: #fff;
|
||||
}
|
||||
924
packages/console/app/src/component/light-rays.tsx
Normal file
924
packages/console/app/src/component/light-rays.tsx
Normal file
@@ -0,0 +1,924 @@
|
||||
import { createSignal, createEffect, onMount, onCleanup, Show, For, Accessor, Setter } from "solid-js"
|
||||
import "./light-rays.css"
|
||||
|
||||
export type RaysOrigin =
|
||||
| "top-center"
|
||||
| "top-left"
|
||||
| "top-right"
|
||||
| "right"
|
||||
| "left"
|
||||
| "bottom-center"
|
||||
| "bottom-right"
|
||||
| "bottom-left"
|
||||
|
||||
export interface LightRaysConfig {
|
||||
raysOrigin: RaysOrigin
|
||||
raysColor: string
|
||||
raysSpeed: number
|
||||
lightSpread: number
|
||||
rayLength: number
|
||||
sourceWidth: number
|
||||
pulsating: boolean
|
||||
pulsatingMin: number
|
||||
pulsatingMax: number
|
||||
fadeDistance: number
|
||||
saturation: number
|
||||
followMouse: boolean
|
||||
mouseInfluence: number
|
||||
noiseAmount: number
|
||||
distortion: number
|
||||
opacity: number
|
||||
}
|
||||
|
||||
export const defaultConfig: LightRaysConfig = {
|
||||
raysOrigin: "top-center",
|
||||
raysColor: "#ffffff",
|
||||
raysSpeed: 1.0,
|
||||
lightSpread: 1.2,
|
||||
rayLength: 4.5,
|
||||
sourceWidth: 0.1,
|
||||
pulsating: true,
|
||||
pulsatingMin: 0.9,
|
||||
pulsatingMax: 1.05,
|
||||
fadeDistance: 1.25,
|
||||
saturation: 0.35,
|
||||
followMouse: false,
|
||||
mouseInfluence: 0.05,
|
||||
noiseAmount: 0.5,
|
||||
distortion: 0.0,
|
||||
opacity: 0.35,
|
||||
}
|
||||
|
||||
export interface LightRaysAnimationState {
|
||||
time: number
|
||||
intensity: number
|
||||
pulseValue: number
|
||||
}
|
||||
|
||||
interface LightRaysProps {
|
||||
config: Accessor<LightRaysConfig>
|
||||
class?: string
|
||||
onAnimationFrame?: (state: LightRaysAnimationState) => void
|
||||
}
|
||||
|
||||
const hexToRgb = (hex: string): [number, number, number] => {
|
||||
const m = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
|
||||
return m ? [parseInt(m[1], 16) / 255, parseInt(m[2], 16) / 255, parseInt(m[3], 16) / 255] : [1, 1, 1]
|
||||
}
|
||||
|
||||
const getAnchorAndDir = (
|
||||
origin: RaysOrigin,
|
||||
w: number,
|
||||
h: number,
|
||||
): { anchor: [number, number]; dir: [number, number] } => {
|
||||
const outside = 0.2
|
||||
switch (origin) {
|
||||
case "top-left":
|
||||
return { anchor: [0, -outside * h], dir: [0, 1] }
|
||||
case "top-right":
|
||||
return { anchor: [w, -outside * h], dir: [0, 1] }
|
||||
case "left":
|
||||
return { anchor: [-outside * w, 0.5 * h], dir: [1, 0] }
|
||||
case "right":
|
||||
return { anchor: [(1 + outside) * w, 0.5 * h], dir: [-1, 0] }
|
||||
case "bottom-left":
|
||||
return { anchor: [0, (1 + outside) * h], dir: [0, -1] }
|
||||
case "bottom-center":
|
||||
return { anchor: [0.5 * w, (1 + outside) * h], dir: [0, -1] }
|
||||
case "bottom-right":
|
||||
return { anchor: [w, (1 + outside) * h], dir: [0, -1] }
|
||||
default: // "top-center"
|
||||
return { anchor: [0.5 * w, -outside * h], dir: [0, 1] }
|
||||
}
|
||||
}
|
||||
|
||||
interface UniformData {
|
||||
iTime: number
|
||||
iResolution: [number, number]
|
||||
rayPos: [number, number]
|
||||
rayDir: [number, number]
|
||||
raysColor: [number, number, number]
|
||||
raysSpeed: number
|
||||
lightSpread: number
|
||||
rayLength: number
|
||||
sourceWidth: number
|
||||
pulsating: number
|
||||
pulsatingMin: number
|
||||
pulsatingMax: number
|
||||
fadeDistance: number
|
||||
saturation: number
|
||||
mousePos: [number, number]
|
||||
mouseInfluence: number
|
||||
noiseAmount: number
|
||||
distortion: number
|
||||
}
|
||||
|
||||
const WGSL_SHADER = `
|
||||
struct Uniforms {
|
||||
iTime: f32,
|
||||
_pad0: f32,
|
||||
iResolution: vec2<f32>,
|
||||
rayPos: vec2<f32>,
|
||||
rayDir: vec2<f32>,
|
||||
raysColor: vec3<f32>,
|
||||
raysSpeed: f32,
|
||||
lightSpread: f32,
|
||||
rayLength: f32,
|
||||
sourceWidth: f32,
|
||||
pulsating: f32,
|
||||
pulsatingMin: f32,
|
||||
pulsatingMax: f32,
|
||||
fadeDistance: f32,
|
||||
saturation: f32,
|
||||
mousePos: vec2<f32>,
|
||||
mouseInfluence: f32,
|
||||
noiseAmount: f32,
|
||||
distortion: f32,
|
||||
_pad1: f32,
|
||||
_pad2: f32,
|
||||
_pad3: f32,
|
||||
};
|
||||
|
||||
@group(0) @binding(0) var<uniform> uniforms: Uniforms;
|
||||
|
||||
struct VertexOutput {
|
||||
@builtin(position) position: vec4<f32>,
|
||||
@location(0) vUv: vec2<f32>,
|
||||
};
|
||||
|
||||
@vertex
|
||||
fn vertexMain(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
|
||||
var positions = array<vec2<f32>, 3>(
|
||||
vec2<f32>(-1.0, -1.0),
|
||||
vec2<f32>(3.0, -1.0),
|
||||
vec2<f32>(-1.0, 3.0)
|
||||
);
|
||||
|
||||
var output: VertexOutput;
|
||||
let pos = positions[vertexIndex];
|
||||
output.position = vec4<f32>(pos, 0.0, 1.0);
|
||||
output.vUv = pos * 0.5 + 0.5;
|
||||
return output;
|
||||
}
|
||||
|
||||
fn noise(st: vec2<f32>) -> f32 {
|
||||
return fract(sin(dot(st, vec2<f32>(12.9898, 78.233))) * 43758.5453123);
|
||||
}
|
||||
|
||||
fn rayStrength(raySource: vec2<f32>, rayRefDirection: vec2<f32>, coord: vec2<f32>,
|
||||
seedA: f32, seedB: f32, speed: f32) -> f32 {
|
||||
let sourceToCoord = coord - raySource;
|
||||
let dirNorm = normalize(sourceToCoord);
|
||||
let cosAngle = dot(dirNorm, rayRefDirection);
|
||||
|
||||
let distortedAngle = cosAngle + uniforms.distortion * sin(uniforms.iTime * 2.0 + length(sourceToCoord) * 0.01) * 0.2;
|
||||
|
||||
let spreadFactor = pow(max(distortedAngle, 0.0), 1.0 / max(uniforms.lightSpread, 0.001));
|
||||
|
||||
let distance = length(sourceToCoord);
|
||||
let maxDistance = uniforms.iResolution.x * uniforms.rayLength;
|
||||
let lengthFalloff = clamp((maxDistance - distance) / maxDistance, 0.0, 1.0);
|
||||
|
||||
let fadeFalloff = clamp((uniforms.iResolution.x * uniforms.fadeDistance - distance) / (uniforms.iResolution.x * uniforms.fadeDistance), 0.5, 1.0);
|
||||
let pulseCenter = (uniforms.pulsatingMin + uniforms.pulsatingMax) * 0.5;
|
||||
let pulseAmplitude = (uniforms.pulsatingMax - uniforms.pulsatingMin) * 0.5;
|
||||
var pulse: f32;
|
||||
if (uniforms.pulsating > 0.5) {
|
||||
pulse = pulseCenter + pulseAmplitude * sin(uniforms.iTime * speed * 3.0);
|
||||
} else {
|
||||
pulse = 1.0;
|
||||
}
|
||||
|
||||
let baseStrength = clamp(
|
||||
(0.45 + 0.15 * sin(distortedAngle * seedA + uniforms.iTime * speed)) +
|
||||
(0.3 + 0.2 * cos(-distortedAngle * seedB + uniforms.iTime * speed)),
|
||||
0.0, 1.0
|
||||
);
|
||||
|
||||
return baseStrength * lengthFalloff * fadeFalloff * spreadFactor * pulse;
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn fragmentMain(@builtin(position) fragCoord: vec4<f32>, @location(0) vUv: vec2<f32>) -> @location(0) vec4<f32> {
|
||||
let coord = vec2<f32>(fragCoord.x, fragCoord.y);
|
||||
|
||||
let normalizedX = (coord.x / uniforms.iResolution.x) - 0.5;
|
||||
let widthOffset = -normalizedX * uniforms.sourceWidth * uniforms.iResolution.x;
|
||||
|
||||
let perpDir = vec2<f32>(-uniforms.rayDir.y, uniforms.rayDir.x);
|
||||
let adjustedRayPos = uniforms.rayPos + perpDir * widthOffset;
|
||||
|
||||
var finalRayDir = uniforms.rayDir;
|
||||
if (uniforms.mouseInfluence > 0.0) {
|
||||
let mouseScreenPos = uniforms.mousePos * uniforms.iResolution;
|
||||
let mouseDirection = normalize(mouseScreenPos - adjustedRayPos);
|
||||
finalRayDir = normalize(mix(uniforms.rayDir, mouseDirection, uniforms.mouseInfluence));
|
||||
}
|
||||
|
||||
let rays1 = vec4<f32>(1.0) *
|
||||
rayStrength(adjustedRayPos, finalRayDir, coord, 36.2214, 21.11349,
|
||||
1.5 * uniforms.raysSpeed);
|
||||
let rays2 = vec4<f32>(1.0) *
|
||||
rayStrength(adjustedRayPos, finalRayDir, coord, 22.3991, 18.0234,
|
||||
1.1 * uniforms.raysSpeed);
|
||||
|
||||
var fragColor = rays1 * 0.5 + rays2 * 0.4;
|
||||
|
||||
if (uniforms.noiseAmount > 0.0) {
|
||||
let n = noise(coord * 0.01 + uniforms.iTime * 0.1);
|
||||
fragColor = vec4<f32>(fragColor.rgb * (1.0 - uniforms.noiseAmount + uniforms.noiseAmount * n), fragColor.a);
|
||||
}
|
||||
|
||||
let brightness = 1.0 - (coord.y / uniforms.iResolution.y);
|
||||
fragColor.x = fragColor.x * (0.1 + brightness * 0.8);
|
||||
fragColor.y = fragColor.y * (0.3 + brightness * 0.6);
|
||||
fragColor.z = fragColor.z * (0.5 + brightness * 0.5);
|
||||
|
||||
if (uniforms.saturation != 1.0) {
|
||||
let gray = dot(fragColor.rgb, vec3<f32>(0.299, 0.587, 0.114));
|
||||
fragColor = vec4<f32>(mix(vec3<f32>(gray), fragColor.rgb, uniforms.saturation), fragColor.a);
|
||||
}
|
||||
|
||||
fragColor = vec4<f32>(fragColor.rgb * uniforms.raysColor, fragColor.a);
|
||||
|
||||
return fragColor;
|
||||
}
|
||||
`
|
||||
|
||||
const UNIFORM_BUFFER_SIZE = 96
|
||||
|
||||
function createUniformBuffer(data: UniformData): Float32Array {
|
||||
const buffer = new Float32Array(24)
|
||||
buffer[0] = data.iTime
|
||||
buffer[1] = 0
|
||||
buffer[2] = data.iResolution[0]
|
||||
buffer[3] = data.iResolution[1]
|
||||
buffer[4] = data.rayPos[0]
|
||||
buffer[5] = data.rayPos[1]
|
||||
buffer[6] = data.rayDir[0]
|
||||
buffer[7] = data.rayDir[1]
|
||||
buffer[8] = data.raysColor[0]
|
||||
buffer[9] = data.raysColor[1]
|
||||
buffer[10] = data.raysColor[2]
|
||||
buffer[11] = data.raysSpeed
|
||||
buffer[12] = data.lightSpread
|
||||
buffer[13] = data.rayLength
|
||||
buffer[14] = data.sourceWidth
|
||||
buffer[15] = data.pulsating
|
||||
buffer[16] = data.pulsatingMin
|
||||
buffer[17] = data.pulsatingMax
|
||||
buffer[18] = data.fadeDistance
|
||||
buffer[19] = data.saturation
|
||||
buffer[20] = data.mousePos[0]
|
||||
buffer[21] = data.mousePos[1]
|
||||
buffer[22] = data.mouseInfluence
|
||||
buffer[23] = data.noiseAmount
|
||||
return buffer
|
||||
}
|
||||
|
||||
const UNIFORM_BUFFER_SIZE_CORRECTED = 112
|
||||
|
||||
function createUniformBufferCorrected(data: UniformData): Float32Array {
|
||||
const buffer = new Float32Array(28)
|
||||
buffer[0] = data.iTime
|
||||
buffer[1] = 0
|
||||
buffer[2] = data.iResolution[0]
|
||||
buffer[3] = data.iResolution[1]
|
||||
buffer[4] = data.rayPos[0]
|
||||
buffer[5] = data.rayPos[1]
|
||||
buffer[6] = data.rayDir[0]
|
||||
buffer[7] = data.rayDir[1]
|
||||
buffer[8] = data.raysColor[0]
|
||||
buffer[9] = data.raysColor[1]
|
||||
buffer[10] = data.raysColor[2]
|
||||
buffer[11] = data.raysSpeed
|
||||
buffer[12] = data.lightSpread
|
||||
buffer[13] = data.rayLength
|
||||
buffer[14] = data.sourceWidth
|
||||
buffer[15] = data.pulsating
|
||||
buffer[16] = data.pulsatingMin
|
||||
buffer[17] = data.pulsatingMax
|
||||
buffer[18] = data.fadeDistance
|
||||
buffer[19] = data.saturation
|
||||
buffer[20] = data.mousePos[0]
|
||||
buffer[21] = data.mousePos[1]
|
||||
buffer[22] = data.mouseInfluence
|
||||
buffer[23] = data.noiseAmount
|
||||
buffer[24] = data.distortion
|
||||
buffer[25] = 0
|
||||
buffer[26] = 0
|
||||
buffer[27] = 0
|
||||
return buffer
|
||||
}
|
||||
|
||||
export default function LightRays(props: LightRaysProps) {
|
||||
let containerRef: HTMLDivElement | undefined
|
||||
let canvasRef: HTMLCanvasElement | null = null
|
||||
let deviceRef: GPUDevice | null = null
|
||||
let contextRef: GPUCanvasContext | null = null
|
||||
let pipelineRef: GPURenderPipeline | null = null
|
||||
let uniformBufferRef: GPUBuffer | null = null
|
||||
let bindGroupRef: GPUBindGroup | null = null
|
||||
let animationIdRef: number | null = null
|
||||
let cleanupFunctionRef: (() => void) | null = null
|
||||
let uniformDataRef: UniformData | null = null
|
||||
|
||||
const mouseRef = { x: 0.5, y: 0.5 }
|
||||
const smoothMouseRef = { x: 0.5, y: 0.5 }
|
||||
|
||||
const [isVisible, setIsVisible] = createSignal(false)
|
||||
|
||||
onMount(() => {
|
||||
if (!containerRef) return
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
const entry = entries[0]
|
||||
setIsVisible(entry.isIntersecting)
|
||||
},
|
||||
{ threshold: 0.1 },
|
||||
)
|
||||
|
||||
observer.observe(containerRef)
|
||||
|
||||
onCleanup(() => {
|
||||
observer.disconnect()
|
||||
})
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
const visible = isVisible()
|
||||
const config = props.config()
|
||||
if (!visible || !containerRef) {
|
||||
return
|
||||
}
|
||||
|
||||
if (cleanupFunctionRef) {
|
||||
cleanupFunctionRef()
|
||||
cleanupFunctionRef = null
|
||||
}
|
||||
|
||||
const initializeWebGPU = async () => {
|
||||
if (!containerRef) {
|
||||
return
|
||||
}
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 10))
|
||||
|
||||
if (!containerRef) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!navigator.gpu) {
|
||||
console.warn("WebGPU is not supported in this browser")
|
||||
return
|
||||
}
|
||||
|
||||
const adapter = await navigator.gpu.requestAdapter()
|
||||
if (!adapter) {
|
||||
console.warn("Failed to get WebGPU adapter")
|
||||
return
|
||||
}
|
||||
|
||||
const device = await adapter.requestDevice()
|
||||
deviceRef = device
|
||||
|
||||
const canvas = document.createElement("canvas")
|
||||
canvas.style.width = "100%"
|
||||
canvas.style.height = "100%"
|
||||
canvasRef = canvas
|
||||
|
||||
while (containerRef.firstChild) {
|
||||
containerRef.removeChild(containerRef.firstChild)
|
||||
}
|
||||
containerRef.appendChild(canvas)
|
||||
|
||||
const context = canvas.getContext("webgpu")
|
||||
if (!context) {
|
||||
console.warn("Failed to get WebGPU context")
|
||||
return
|
||||
}
|
||||
contextRef = context
|
||||
|
||||
const presentationFormat = navigator.gpu.getPreferredCanvasFormat()
|
||||
context.configure({
|
||||
device,
|
||||
format: presentationFormat,
|
||||
alphaMode: "premultiplied",
|
||||
})
|
||||
|
||||
const shaderModule = device.createShaderModule({
|
||||
code: WGSL_SHADER,
|
||||
})
|
||||
|
||||
const uniformBuffer = device.createBuffer({
|
||||
size: UNIFORM_BUFFER_SIZE_CORRECTED,
|
||||
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
||||
})
|
||||
uniformBufferRef = uniformBuffer
|
||||
|
||||
const bindGroupLayout = device.createBindGroupLayout({
|
||||
entries: [
|
||||
{
|
||||
binding: 0,
|
||||
visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
|
||||
buffer: { type: "uniform" },
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
const bindGroup = device.createBindGroup({
|
||||
layout: bindGroupLayout,
|
||||
entries: [
|
||||
{
|
||||
binding: 0,
|
||||
resource: { buffer: uniformBuffer },
|
||||
},
|
||||
],
|
||||
})
|
||||
bindGroupRef = bindGroup
|
||||
|
||||
const pipelineLayout = device.createPipelineLayout({
|
||||
bindGroupLayouts: [bindGroupLayout],
|
||||
})
|
||||
|
||||
const pipeline = device.createRenderPipeline({
|
||||
layout: pipelineLayout,
|
||||
vertex: {
|
||||
module: shaderModule,
|
||||
entryPoint: "vertexMain",
|
||||
},
|
||||
fragment: {
|
||||
module: shaderModule,
|
||||
entryPoint: "fragmentMain",
|
||||
targets: [
|
||||
{
|
||||
format: presentationFormat,
|
||||
blend: {
|
||||
color: {
|
||||
srcFactor: "src-alpha",
|
||||
dstFactor: "one-minus-src-alpha",
|
||||
operation: "add",
|
||||
},
|
||||
alpha: {
|
||||
srcFactor: "one",
|
||||
dstFactor: "one-minus-src-alpha",
|
||||
operation: "add",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
primitive: {
|
||||
topology: "triangle-list",
|
||||
},
|
||||
})
|
||||
pipelineRef = pipeline
|
||||
|
||||
const { clientWidth: wCSS, clientHeight: hCSS } = containerRef
|
||||
const dpr = Math.min(window.devicePixelRatio, 2)
|
||||
const w = wCSS * dpr
|
||||
const h = hCSS * dpr
|
||||
const { anchor, dir } = getAnchorAndDir(config.raysOrigin, w, h)
|
||||
|
||||
uniformDataRef = {
|
||||
iTime: 0,
|
||||
iResolution: [w, h],
|
||||
rayPos: anchor,
|
||||
rayDir: dir,
|
||||
raysColor: hexToRgb(config.raysColor),
|
||||
raysSpeed: config.raysSpeed,
|
||||
lightSpread: config.lightSpread,
|
||||
rayLength: config.rayLength,
|
||||
sourceWidth: config.sourceWidth,
|
||||
pulsating: config.pulsating ? 1.0 : 0.0,
|
||||
pulsatingMin: config.pulsatingMin,
|
||||
pulsatingMax: config.pulsatingMax,
|
||||
fadeDistance: config.fadeDistance,
|
||||
saturation: config.saturation,
|
||||
mousePos: [0.5, 0.5],
|
||||
mouseInfluence: config.mouseInfluence,
|
||||
noiseAmount: config.noiseAmount,
|
||||
distortion: config.distortion,
|
||||
}
|
||||
|
||||
const updatePlacement = () => {
|
||||
if (!containerRef || !canvasRef || !uniformDataRef) {
|
||||
return
|
||||
}
|
||||
|
||||
const dpr = Math.min(window.devicePixelRatio, 2)
|
||||
const { clientWidth: wCSS, clientHeight: hCSS } = containerRef
|
||||
const w = Math.floor(wCSS * dpr)
|
||||
const h = Math.floor(hCSS * dpr)
|
||||
|
||||
canvasRef.width = w
|
||||
canvasRef.height = h
|
||||
|
||||
uniformDataRef.iResolution = [w, h]
|
||||
|
||||
const currentConfig = props.config()
|
||||
const { anchor, dir } = getAnchorAndDir(currentConfig.raysOrigin, w, h)
|
||||
uniformDataRef.rayPos = anchor
|
||||
uniformDataRef.rayDir = dir
|
||||
}
|
||||
|
||||
const loop = (t: number) => {
|
||||
if (!deviceRef || !contextRef || !pipelineRef || !uniformBufferRef || !bindGroupRef || !uniformDataRef) {
|
||||
return
|
||||
}
|
||||
|
||||
const currentConfig = props.config()
|
||||
const timeSeconds = t * 0.001
|
||||
uniformDataRef.iTime = timeSeconds
|
||||
|
||||
if (currentConfig.followMouse && currentConfig.mouseInfluence > 0.0) {
|
||||
const smoothing = 0.92
|
||||
|
||||
smoothMouseRef.x = smoothMouseRef.x * smoothing + mouseRef.x * (1 - smoothing)
|
||||
smoothMouseRef.y = smoothMouseRef.y * smoothing + mouseRef.y * (1 - smoothing)
|
||||
|
||||
uniformDataRef.mousePos = [smoothMouseRef.x, smoothMouseRef.y]
|
||||
}
|
||||
|
||||
if (props.onAnimationFrame) {
|
||||
const pulseCenter = (currentConfig.pulsatingMin + currentConfig.pulsatingMax) * 0.5
|
||||
const pulseAmplitude = (currentConfig.pulsatingMax - currentConfig.pulsatingMin) * 0.5
|
||||
const pulseValue = currentConfig.pulsating
|
||||
? pulseCenter + pulseAmplitude * Math.sin(timeSeconds * currentConfig.raysSpeed * 3.0)
|
||||
: 1.0
|
||||
|
||||
const baseIntensity1 = 0.45 + 0.15 * Math.sin(timeSeconds * currentConfig.raysSpeed * 1.5)
|
||||
const baseIntensity2 = 0.3 + 0.2 * Math.cos(timeSeconds * currentConfig.raysSpeed * 1.1)
|
||||
const intensity = (baseIntensity1 + baseIntensity2) * pulseValue
|
||||
|
||||
props.onAnimationFrame({
|
||||
time: timeSeconds,
|
||||
intensity,
|
||||
pulseValue,
|
||||
})
|
||||
}
|
||||
|
||||
try {
|
||||
const uniformData = createUniformBufferCorrected(uniformDataRef)
|
||||
deviceRef.queue.writeBuffer(uniformBufferRef, 0, uniformData.buffer)
|
||||
|
||||
const commandEncoder = deviceRef.createCommandEncoder()
|
||||
|
||||
const textureView = contextRef.getCurrentTexture().createView()
|
||||
|
||||
const renderPass = commandEncoder.beginRenderPass({
|
||||
colorAttachments: [
|
||||
{
|
||||
view: textureView,
|
||||
clearValue: { r: 0, g: 0, b: 0, a: 0 },
|
||||
loadOp: "clear",
|
||||
storeOp: "store",
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
renderPass.setPipeline(pipelineRef)
|
||||
renderPass.setBindGroup(0, bindGroupRef)
|
||||
renderPass.draw(3)
|
||||
renderPass.end()
|
||||
|
||||
deviceRef.queue.submit([commandEncoder.finish()])
|
||||
|
||||
animationIdRef = requestAnimationFrame(loop)
|
||||
} catch (error) {
|
||||
console.warn("WebGPU rendering error:", error)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("resize", updatePlacement)
|
||||
updatePlacement()
|
||||
animationIdRef = requestAnimationFrame(loop)
|
||||
|
||||
cleanupFunctionRef = () => {
|
||||
if (animationIdRef) {
|
||||
cancelAnimationFrame(animationIdRef)
|
||||
animationIdRef = null
|
||||
}
|
||||
|
||||
window.removeEventListener("resize", updatePlacement)
|
||||
|
||||
if (uniformBufferRef) {
|
||||
uniformBufferRef.destroy()
|
||||
uniformBufferRef = null
|
||||
}
|
||||
|
||||
if (deviceRef) {
|
||||
deviceRef.destroy()
|
||||
deviceRef = null
|
||||
}
|
||||
|
||||
if (canvasRef && canvasRef.parentNode) {
|
||||
canvasRef.parentNode.removeChild(canvasRef)
|
||||
}
|
||||
|
||||
canvasRef = null
|
||||
contextRef = null
|
||||
pipelineRef = null
|
||||
bindGroupRef = null
|
||||
uniformDataRef = null
|
||||
}
|
||||
}
|
||||
|
||||
initializeWebGPU()
|
||||
|
||||
onCleanup(() => {
|
||||
if (cleanupFunctionRef) {
|
||||
cleanupFunctionRef()
|
||||
cleanupFunctionRef = null
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
if (!uniformDataRef || !containerRef) {
|
||||
return
|
||||
}
|
||||
|
||||
const config = props.config()
|
||||
|
||||
uniformDataRef.raysColor = hexToRgb(config.raysColor)
|
||||
uniformDataRef.raysSpeed = config.raysSpeed
|
||||
uniformDataRef.lightSpread = config.lightSpread
|
||||
uniformDataRef.rayLength = config.rayLength
|
||||
uniformDataRef.sourceWidth = config.sourceWidth
|
||||
uniformDataRef.pulsating = config.pulsating ? 1.0 : 0.0
|
||||
uniformDataRef.pulsatingMin = config.pulsatingMin
|
||||
uniformDataRef.pulsatingMax = config.pulsatingMax
|
||||
uniformDataRef.fadeDistance = config.fadeDistance
|
||||
uniformDataRef.saturation = config.saturation
|
||||
uniformDataRef.mouseInfluence = config.mouseInfluence
|
||||
uniformDataRef.noiseAmount = config.noiseAmount
|
||||
uniformDataRef.distortion = config.distortion
|
||||
|
||||
const dpr = Math.min(window.devicePixelRatio, 2)
|
||||
const { clientWidth: wCSS, clientHeight: hCSS } = containerRef
|
||||
const { anchor, dir } = getAnchorAndDir(config.raysOrigin, wCSS * dpr, hCSS * dpr)
|
||||
uniformDataRef.rayPos = anchor
|
||||
uniformDataRef.rayDir = dir
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
const config = props.config()
|
||||
if (!config.followMouse) {
|
||||
return
|
||||
}
|
||||
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
if (!containerRef) {
|
||||
return
|
||||
}
|
||||
const rect = containerRef.getBoundingClientRect()
|
||||
const x = (e.clientX - rect.left) / rect.width
|
||||
const y = (e.clientY - rect.top) / rect.height
|
||||
mouseRef.x = x
|
||||
mouseRef.y = y
|
||||
}
|
||||
|
||||
window.addEventListener("mousemove", handleMouseMove)
|
||||
|
||||
onCleanup(() => {
|
||||
window.removeEventListener("mousemove", handleMouseMove)
|
||||
})
|
||||
})
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
class={`light-rays-container ${props.class ?? ""}`.trim()}
|
||||
style={{ opacity: props.config().opacity }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
interface LightRaysControlsProps {
|
||||
config: Accessor<LightRaysConfig>
|
||||
setConfig: Setter<LightRaysConfig>
|
||||
}
|
||||
|
||||
export function LightRaysControls(props: LightRaysControlsProps) {
|
||||
const [isOpen, setIsOpen] = createSignal(true)
|
||||
|
||||
const updateConfig = <K extends keyof LightRaysConfig>(key: K, value: LightRaysConfig[K]) => {
|
||||
props.setConfig((prev) => ({ ...prev, [key]: value }))
|
||||
}
|
||||
|
||||
const origins: RaysOrigin[] = [
|
||||
"top-center",
|
||||
"top-left",
|
||||
"top-right",
|
||||
"left",
|
||||
"right",
|
||||
"bottom-center",
|
||||
"bottom-left",
|
||||
"bottom-right",
|
||||
]
|
||||
|
||||
return (
|
||||
<div class="light-rays-controls">
|
||||
<button class="light-rays-controls-toggle" onClick={() => setIsOpen(!isOpen())}>
|
||||
{isOpen() ? "▼" : "▶"} Light Rays
|
||||
</button>
|
||||
<Show when={isOpen()}>
|
||||
<div class="light-rays-controls-panel">
|
||||
<div class="control-group">
|
||||
<label>Origin</label>
|
||||
<select
|
||||
value={props.config().raysOrigin}
|
||||
onChange={(e) => updateConfig("raysOrigin", e.currentTarget.value as RaysOrigin)}
|
||||
>
|
||||
<For each={origins}>{(origin) => <option value={origin}>{origin}</option>}</For>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<label>Color</label>
|
||||
<input
|
||||
type="color"
|
||||
value={props.config().raysColor}
|
||||
onInput={(e) => updateConfig("raysColor", e.currentTarget.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<label>Speed: {props.config().raysSpeed.toFixed(2)}</label>
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="3"
|
||||
step="0.01"
|
||||
value={props.config().raysSpeed}
|
||||
onInput={(e) => updateConfig("raysSpeed", parseFloat(e.currentTarget.value))}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<label>Light Spread: {props.config().lightSpread.toFixed(2)}</label>
|
||||
<input
|
||||
type="range"
|
||||
min="0.1"
|
||||
max="5"
|
||||
step="0.01"
|
||||
value={props.config().lightSpread}
|
||||
onInput={(e) => updateConfig("lightSpread", parseFloat(e.currentTarget.value))}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<label>Ray Length: {props.config().rayLength.toFixed(2)}</label>
|
||||
<input
|
||||
type="range"
|
||||
min="0.1"
|
||||
max="5"
|
||||
step="0.01"
|
||||
value={props.config().rayLength}
|
||||
onInput={(e) => updateConfig("rayLength", parseFloat(e.currentTarget.value))}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<label>Source Width: {props.config().sourceWidth.toFixed(2)}</label>
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="2"
|
||||
step="0.01"
|
||||
value={props.config().sourceWidth}
|
||||
onInput={(e) => updateConfig("sourceWidth", parseFloat(e.currentTarget.value))}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<label>Fade Distance: {props.config().fadeDistance.toFixed(2)}</label>
|
||||
<input
|
||||
type="range"
|
||||
min="0.1"
|
||||
max="3"
|
||||
step="0.01"
|
||||
value={props.config().fadeDistance}
|
||||
onInput={(e) => updateConfig("fadeDistance", parseFloat(e.currentTarget.value))}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<label>Saturation: {props.config().saturation.toFixed(2)}</label>
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="2"
|
||||
step="0.01"
|
||||
value={props.config().saturation}
|
||||
onInput={(e) => updateConfig("saturation", parseFloat(e.currentTarget.value))}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<label>Mouse Influence: {props.config().mouseInfluence.toFixed(2)}</label>
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="1"
|
||||
step="0.01"
|
||||
value={props.config().mouseInfluence}
|
||||
onInput={(e) => updateConfig("mouseInfluence", parseFloat(e.currentTarget.value))}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<label>Noise: {props.config().noiseAmount.toFixed(2)}</label>
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="1"
|
||||
step="0.01"
|
||||
value={props.config().noiseAmount}
|
||||
onInput={(e) => updateConfig("noiseAmount", parseFloat(e.currentTarget.value))}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<label>Distortion: {props.config().distortion.toFixed(2)}</label>
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="2"
|
||||
step="0.01"
|
||||
value={props.config().distortion}
|
||||
onInput={(e) => updateConfig("distortion", parseFloat(e.currentTarget.value))}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<label>Opacity: {props.config().opacity.toFixed(2)}</label>
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="1"
|
||||
step="0.01"
|
||||
value={props.config().opacity}
|
||||
onInput={(e) => updateConfig("opacity", parseFloat(e.currentTarget.value))}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="control-group checkbox">
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={props.config().pulsating}
|
||||
onChange={(e) => updateConfig("pulsating", e.currentTarget.checked)}
|
||||
/>
|
||||
Pulsating
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<Show when={props.config().pulsating}>
|
||||
<div class="control-group">
|
||||
<label>Pulse Min: {props.config().pulsatingMin.toFixed(2)}</label>
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="1"
|
||||
step="0.01"
|
||||
value={props.config().pulsatingMin}
|
||||
onInput={(e) => updateConfig("pulsatingMin", parseFloat(e.currentTarget.value))}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<label>Pulse Max: {props.config().pulsatingMax.toFixed(2)}</label>
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="2"
|
||||
step="0.01"
|
||||
value={props.config().pulsatingMax}
|
||||
onInput={(e) => updateConfig("pulsatingMax", parseFloat(e.currentTarget.value))}
|
||||
/>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<div class="control-group checkbox">
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={props.config().followMouse}
|
||||
onChange={(e) => updateConfig("followMouse", e.currentTarget.checked)}
|
||||
/>
|
||||
Follow Mouse
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<button class="reset-button" onClick={() => props.setConfig(defaultConfig)}>
|
||||
Reset to Defaults
|
||||
</button>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -14,13 +14,14 @@ export const github = query(async () => {
|
||||
fetch(`${apiBaseUrl}/releases`, { headers }).then((res) => res.json()),
|
||||
fetch(`${apiBaseUrl}/contributors?per_page=1`, { headers }),
|
||||
])
|
||||
if (!Array.isArray(releases) || releases.length === 0) {
|
||||
return undefined
|
||||
}
|
||||
const [release] = releases
|
||||
const contributorCount = Number.parseInt(
|
||||
contributors.headers
|
||||
.get("Link")!
|
||||
.match(/&page=(\d+)>; rel="last"/)!
|
||||
.at(1)!,
|
||||
)
|
||||
const linkHeader = contributors.headers.get("Link")
|
||||
const contributorCount = linkHeader
|
||||
? Number.parseInt(linkHeader.match(/&page=(\d+)>; rel="last"/)?.at(1) ?? "0")
|
||||
: 0
|
||||
return {
|
||||
stars: meta.stargazers_count,
|
||||
release: {
|
||||
|
||||
@@ -1,3 +1,133 @@
|
||||
::view-transition-group(*) {
|
||||
animation-duration: 250ms;
|
||||
animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
::view-transition-old(root),
|
||||
::view-transition-new(root) {
|
||||
animation-duration: 250ms;
|
||||
animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
::view-transition-image-pair(root) {
|
||||
isolation: isolate;
|
||||
}
|
||||
|
||||
::view-transition-old(root) {
|
||||
animation: none;
|
||||
mix-blend-mode: normal;
|
||||
}
|
||||
|
||||
::view-transition-new(root) {
|
||||
animation: none;
|
||||
mix-blend-mode: normal;
|
||||
}
|
||||
|
||||
@keyframes fade-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fade-out {
|
||||
from {
|
||||
opacity: 1;
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fade-in-up {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(8px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes reveal-terms {
|
||||
from {
|
||||
mask-position: 0% 200%;
|
||||
}
|
||||
to {
|
||||
mask-position: 0% 50%;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes hide-terms {
|
||||
from {
|
||||
mask-position: 0% 50%;
|
||||
}
|
||||
to {
|
||||
mask-position: 0% 200%;
|
||||
}
|
||||
}
|
||||
|
||||
::view-transition-old(terms-20),
|
||||
::view-transition-old(terms-100),
|
||||
::view-transition-old(terms-200) {
|
||||
mask-image: linear-gradient(to bottom, transparent, black 25% 75%, transparent);
|
||||
mask-repeat: no-repeat;
|
||||
mask-size: 100% 200%;
|
||||
animation: hide-terms 200ms cubic-bezier(0.25, 0, 0.5, 1) forwards;
|
||||
}
|
||||
|
||||
::view-transition-new(terms-20),
|
||||
::view-transition-new(terms-100),
|
||||
::view-transition-new(terms-200) {
|
||||
mask-image: linear-gradient(to bottom, transparent, black 25% 75%, transparent);
|
||||
mask-repeat: no-repeat;
|
||||
mask-position: 0% 200%;
|
||||
mask-size: 100% 200%;
|
||||
animation: reveal-terms 300ms cubic-bezier(0.25, 0, 0.5, 1) 50ms forwards;
|
||||
}
|
||||
|
||||
::view-transition-old(actions-20),
|
||||
::view-transition-old(actions-100),
|
||||
::view-transition-old(actions-200) {
|
||||
animation: fade-out 80ms cubic-bezier(0.4, 0, 0.2, 1) forwards;
|
||||
}
|
||||
|
||||
::view-transition-new(actions-20),
|
||||
::view-transition-new(actions-100),
|
||||
::view-transition-new(actions-200) {
|
||||
animation: fade-in-up 200ms cubic-bezier(0.16, 1, 0.3, 1) 300ms forwards;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
::view-transition-group(card-20),
|
||||
::view-transition-group(card-100),
|
||||
::view-transition-group(card-200) {
|
||||
animation-duration: 250ms;
|
||||
animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
::view-transition-image-pair(card-20),
|
||||
::view-transition-image-pair(card-100),
|
||||
::view-transition-image-pair(card-200) {
|
||||
isolation: isolate;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
::view-transition-old(card-20),
|
||||
::view-transition-old(card-100),
|
||||
::view-transition-old(card-200) {
|
||||
mix-blend-mode: normal;
|
||||
}
|
||||
|
||||
::view-transition-new(card-20),
|
||||
::view-transition-new(card-100),
|
||||
::view-transition-new(card-200) {
|
||||
mix-blend-mode: normal;
|
||||
}
|
||||
|
||||
[data-page="black"] {
|
||||
background: #000;
|
||||
min-height: 100vh;
|
||||
@@ -8,13 +138,18 @@
|
||||
font-family: var(--font-mono);
|
||||
color: #fff;
|
||||
|
||||
[data-component="header-gradient"] {
|
||||
[data-component="header-logo"] {
|
||||
filter: drop-shadow(0 8px 24px rgba(0, 0, 0, 0.25)) drop-shadow(0 4px 16px rgba(0, 0, 0, 0.1));
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.header-light-rays {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 288px;
|
||||
background: linear-gradient(180deg, rgba(255, 255, 255, 0.1) 0%, rgba(0, 0, 0, 0) 100%);
|
||||
inset: 0 0 auto 0;
|
||||
height: 30dvh;
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
[data-component="header"] {
|
||||
@@ -48,27 +183,35 @@
|
||||
|
||||
h1 {
|
||||
color: rgba(255, 255, 255, 0.92);
|
||||
font-size: 18px;
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 160%;
|
||||
line-height: 1.45;
|
||||
margin: 0;
|
||||
|
||||
@media (min-width: 768px) {
|
||||
font-size: 22px;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
color: rgba(255, 255, 255, 0.59);
|
||||
font-size: 18px;
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 160%;
|
||||
line-height: 1.45;
|
||||
margin: 0;
|
||||
|
||||
@media (min-width: 768px) {
|
||||
font-size: 22px;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -76,30 +219,36 @@
|
||||
[data-slot="hero-black"] {
|
||||
margin-top: 40px;
|
||||
padding: 0 20px;
|
||||
position: relative;
|
||||
|
||||
@media (min-width: 768px) {
|
||||
margin-top: 60px;
|
||||
}
|
||||
|
||||
svg {
|
||||
--hero-black-fill-from: hsl(0 0% 100%);
|
||||
--hero-black-fill-to: hsl(0 0% 100% / 0%);
|
||||
--hero-black-stroke-from: hsl(0 0% 100% / 60%);
|
||||
--hero-black-stroke-to: hsl(0 0% 100% / 0%);
|
||||
|
||||
width: 100%;
|
||||
max-width: 590px;
|
||||
height: auto;
|
||||
filter: drop-shadow(0 0 20px rgba(255, 255, 255, 0.1));
|
||||
overflow: visible;
|
||||
filter: drop-shadow(0 0 20px rgba(255, 255, 255, calc(0.1 + var(--hero-black-glow-intensity, 0) * 0.15)))
|
||||
drop-shadow(0 -5px 30px rgba(255, 255, 255, calc(var(--hero-black-glow-intensity, 0) * 0.2)));
|
||||
mask-image: linear-gradient(to bottom, black, transparent);
|
||||
stroke-width: 1.5;
|
||||
|
||||
[data-slot="black-fill"] {
|
||||
[data-slot="black-base"] {
|
||||
fill: url(#hero-black-fill-gradient);
|
||||
stroke: url(#hero-black-stroke-gradient);
|
||||
}
|
||||
|
||||
[data-slot="black-stroke"] {
|
||||
fill: url(#hero-black-stroke-gradient);
|
||||
[data-slot="black-glow"] {
|
||||
fill: url(#hero-black-top-glow);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
[data-slot="black-shimmer"] {
|
||||
fill: url(#hero-black-shimmer-gradient);
|
||||
pointer-events: none;
|
||||
mix-blend-mode: overlay;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -107,14 +256,14 @@
|
||||
[data-slot="cta"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 32px;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
margin-top: -32px;
|
||||
margin-top: -40px;
|
||||
width: 100%;
|
||||
|
||||
@media (min-width: 768px) {
|
||||
margin-top: -16px;
|
||||
margin-top: -20px;
|
||||
}
|
||||
|
||||
[data-slot="heading"] {
|
||||
@@ -129,7 +278,6 @@
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="subheading"] {
|
||||
color: rgba(255, 255, 255, 0.59);
|
||||
font-size: 15px;
|
||||
@@ -142,7 +290,6 @@
|
||||
line-height: 160%;
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="button"] {
|
||||
display: inline-flex;
|
||||
height: 40px;
|
||||
@@ -154,7 +301,7 @@
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
text-decoration: none;
|
||||
color: #000;
|
||||
font-family: var(--font-mono);
|
||||
font-family: "JetBrains Mono Nerd Font";
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
@@ -168,16 +315,14 @@
|
||||
transform: scale(0.98);
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="back-soon"] {
|
||||
color: rgba(255, 255, 255, 0.59);
|
||||
text-align: center;
|
||||
font-size: 13px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 160%;
|
||||
line-height: 160%; /* 20.8px */
|
||||
}
|
||||
|
||||
[data-slot="follow-us"] {
|
||||
display: inline-flex;
|
||||
height: 40px;
|
||||
@@ -201,98 +346,38 @@
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
width: 100%;
|
||||
max-width: 680px;
|
||||
max-width: 660px;
|
||||
padding: 0 20px;
|
||||
box-sizing: border-box;
|
||||
|
||||
@media (min-width: 768px) {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="pricing-card"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
padding: 24px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.17);
|
||||
border-radius: 5px;
|
||||
background: black;
|
||||
background-clip: padding-box;
|
||||
border-radius: 4px;
|
||||
text-decoration: none;
|
||||
background: #000;
|
||||
transition: border-color 0.15s ease;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
transition: border-color 200ms ease;
|
||||
|
||||
&:hover:not([data-selected="true"]) {
|
||||
@media (max-width: 480px) {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
&:hover:not(:active) {
|
||||
border-color: rgba(255, 255, 255, 0.35);
|
||||
}
|
||||
|
||||
[data-slot="card-trigger"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
width: 100%;
|
||||
padding: 24px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
text-align: left;
|
||||
transition: padding 200ms ease;
|
||||
|
||||
&:disabled {
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
|
||||
&[data-selected="true"] {
|
||||
[data-slot="amount"] {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
[data-slot="terms"] {
|
||||
animation: reveal 500ms cubic-bezier(0.25, 0, 0.5, 1) forwards;
|
||||
}
|
||||
|
||||
[data-slot="actions"] {
|
||||
[data-slot="continue"] {
|
||||
animation-delay: 200ms;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&[data-collapsed="true"] {
|
||||
[data-slot="card-trigger"] {
|
||||
padding: 20px 24px;
|
||||
}
|
||||
|
||||
[data-slot="plan-header"] {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
[data-slot="amount"] {
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
&[data-selected="false"][data-collapsed="false"] {
|
||||
[data-slot="amount"] {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
[data-slot="period"],
|
||||
[data-slot="multiplier"] {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="plan-header"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
gap: 12px;
|
||||
transition: gap 200ms ease;
|
||||
}
|
||||
|
||||
[data-slot="plan-icon"] {
|
||||
[data-slot="icon"] {
|
||||
color: rgba(255, 255, 255, 0.59);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
[data-slot="price"] {
|
||||
@@ -300,31 +385,81 @@
|
||||
flex-wrap: wrap;
|
||||
align-items: baseline;
|
||||
gap: 8px;
|
||||
line-height: 24px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
[data-slot="amount"] {
|
||||
color: rgba(255, 255, 255, 0.92);
|
||||
font-size: 24px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
[data-slot="content"] {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
[data-slot="period"],
|
||||
[data-slot="multiplier"] {
|
||||
color: rgba(255, 255, 255, 0.59);
|
||||
}
|
||||
|
||||
[data-slot="billing"] {
|
||||
[data-slot="period"] {
|
||||
color: rgba(255, 255, 255, 0.59);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
[data-slot="multiplier"] {
|
||||
color: rgba(255, 255, 255, 0.39);
|
||||
font-size: 14px;
|
||||
|
||||
&::before {
|
||||
content: "·";
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="selected-plan"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 32px;
|
||||
width: 100%;
|
||||
max-width: 660px;
|
||||
margin: 0 auto;
|
||||
position: relative;
|
||||
background-color: rgba(0, 0, 0, 0.75);
|
||||
z-index: 1;
|
||||
|
||||
@media (max-width: 480px) {
|
||||
margin: 0 20px;
|
||||
width: calc(100% - 40px);
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="selected-card"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding: 24px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.17);
|
||||
border-radius: 4px;
|
||||
width: 100%;
|
||||
|
||||
[data-slot="icon"] {
|
||||
color: rgba(255, 255, 255, 0.59);
|
||||
}
|
||||
|
||||
[data-slot="price"] {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: baseline;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
[data-slot="amount"] {
|
||||
color: rgba(255, 255, 255, 0.92);
|
||||
font-size: 24px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
[data-slot="period"] {
|
||||
color: rgba(255, 255, 255, 0.59);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
[data-slot="multiplier"] {
|
||||
color: rgba(255, 255, 255, 0.39);
|
||||
font-size: 14px;
|
||||
|
||||
&::before {
|
||||
content: "·";
|
||||
@@ -334,32 +469,30 @@
|
||||
|
||||
[data-slot="terms"] {
|
||||
list-style: none;
|
||||
padding: 0 24px 24px 24px;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
gap: 8px;
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
opacity: 0;
|
||||
mask-image: linear-gradient(to bottom, black 0%, black 50%, transparent 100%);
|
||||
mask-repeat: no-repeat;
|
||||
mask-size: 100% 200%;
|
||||
mask-position: 0% 320%;
|
||||
}
|
||||
|
||||
[data-slot="terms"] li {
|
||||
color: rgba(255, 255, 255, 0.59);
|
||||
font-size: 13px;
|
||||
line-height: 1.2;
|
||||
padding-left: 16px;
|
||||
position: relative;
|
||||
li {
|
||||
color: rgba(255, 255, 255, 0.59);
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
padding-left: 16px;
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: "▪";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
color: rgba(255, 255, 255, 0.39);
|
||||
&::before {
|
||||
content: "▪";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
color: rgba(255, 255, 255, 0.39);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -367,48 +500,45 @@
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
margin-top: 8px;
|
||||
padding: 0 24px 24px 24px;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
[data-slot="actions"] button,
|
||||
[data-slot="actions"] a {
|
||||
flex: 1;
|
||||
display: inline-flex;
|
||||
height: 48px;
|
||||
padding: 0 16px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 4px;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 16px;
|
||||
font-weight: 400;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
transition-property: background-color, border-color;
|
||||
transition-duration: 200ms;
|
||||
transition-timing-function: cubic-bezier(0.25, 0, 0.5, 1);
|
||||
}
|
||||
|
||||
[data-slot="cancel"] {
|
||||
border: 1px solid var(--border-base, rgba(255, 255, 255, 0.17));
|
||||
background: var(--surface-raised-base, rgba(255, 255, 255, 0.06));
|
||||
background-clip: border-box;
|
||||
color: rgba(255, 255, 255, 0.92);
|
||||
|
||||
&:hover {
|
||||
background: var(--surface-raised-base, rgba(255, 255, 255, 0.08));
|
||||
border-color: rgba(255, 255, 255, 0.25);
|
||||
button,
|
||||
a {
|
||||
flex: 1;
|
||||
display: inline-flex;
|
||||
height: 48px;
|
||||
padding: 0 16px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 4px;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 16px;
|
||||
font-weight: 400;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="continue"] {
|
||||
background: rgb(255, 255, 255);
|
||||
color: rgb(0, 0, 0);
|
||||
[data-slot="cancel"] {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(255, 255, 255, 0.17);
|
||||
color: rgba(255, 255, 255, 0.92);
|
||||
transition-property: background-color, border-color;
|
||||
transition-duration: 150ms;
|
||||
transition-timing-function: cubic-bezier(0.25, 0, 0.5, 1);
|
||||
|
||||
&:hover {
|
||||
background: rgb(255, 255, 255, 0.9);
|
||||
&:hover {
|
||||
background-color: rgba(255, 255, 255, 0.08);
|
||||
border-color: rgba(255, 255, 255, 0.25);
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="continue"] {
|
||||
background: rgb(255, 255, 255);
|
||||
color: rgb(0, 0, 0);
|
||||
transition: background-color 150ms cubic-bezier(0.25, 0, 0.5, 1);
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -419,7 +549,8 @@
|
||||
font-size: 13px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 160%;
|
||||
line-height: 160%; /* 20.8px */
|
||||
font-style: italic;
|
||||
|
||||
a {
|
||||
color: rgba(255, 255, 255, 0.39);
|
||||
@@ -436,7 +567,7 @@
|
||||
align-items: center;
|
||||
margin-top: -18px;
|
||||
width: 100%;
|
||||
max-width: 540px;
|
||||
max-width: 660px;
|
||||
padding: 0 20px;
|
||||
|
||||
@media (min-width: 768px) {
|
||||
@@ -491,7 +622,7 @@
|
||||
|
||||
[data-slot="multiplier"] {
|
||||
color: rgba(255, 255, 255, 0.39);
|
||||
font-size: 13px;
|
||||
font-size: 14px;
|
||||
|
||||
&::before {
|
||||
content: "·";
|
||||
@@ -510,39 +641,6 @@
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
[data-slot="tax-id-section"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
|
||||
[data-slot="label"] {
|
||||
color: rgba(255, 255, 255, 0.59);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
[data-slot="input"] {
|
||||
width: 100%;
|
||||
height: 44px;
|
||||
padding: 0 12px;
|
||||
background: #1a1a1a;
|
||||
border: 1px solid rgba(255, 255, 255, 0.17);
|
||||
border-radius: 4px;
|
||||
color: #ffffff;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 14px;
|
||||
outline: none;
|
||||
transition: border-color 0.15s ease;
|
||||
|
||||
&::placeholder {
|
||||
color: rgba(255, 255, 255, 0.39);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border-color: rgba(255, 255, 255, 0.35);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="checkout-form"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -583,52 +681,6 @@
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
[data-slot="success"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
|
||||
[data-slot="title"] {
|
||||
color: rgba(255, 255, 255, 0.92);
|
||||
font-size: 18px;
|
||||
font-weight: 400;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
[data-slot="details"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
|
||||
> div {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
dt {
|
||||
color: rgba(255, 255, 255, 0.59);
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
dd {
|
||||
color: rgba(255, 255, 255, 0.92);
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
margin: 0;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="charge-notice"] {
|
||||
color: #d4a500;
|
||||
font-size: 14px;
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="loading"] {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
@@ -645,6 +697,7 @@
|
||||
text-align: center;
|
||||
font-size: 13px;
|
||||
font-style: italic;
|
||||
view-transition-name: fine-print;
|
||||
|
||||
a {
|
||||
color: rgba(255, 255, 255, 0.39);
|
||||
@@ -739,7 +792,7 @@
|
||||
span,
|
||||
a {
|
||||
color: rgba(255, 255, 255, 0.39);
|
||||
font-family: var(--font-mono);
|
||||
font-family: "JetBrains Mono Nerd Font";
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
@@ -749,7 +802,7 @@
|
||||
|
||||
[data-slot="github-stars"] {
|
||||
color: rgba(255, 255, 255, 0.25);
|
||||
font-family: var(--font-mono);
|
||||
font-family: "JetBrains Mono Nerd Font";
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
@@ -764,10 +817,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="anomaly-alt"] {
|
||||
color: rgba(255, 255, 255, 0.39);
|
||||
font-family: var(--font-mono);
|
||||
font-family: "JetBrains Mono Nerd Font";
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
@@ -777,7 +829,7 @@
|
||||
|
||||
a {
|
||||
color: rgba(255, 255, 255, 0.39);
|
||||
font-family: "JetBrains Mono Nerd Font", monospace;
|
||||
font-family: "JetBrains Mono Nerd Font";
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
@@ -791,15 +843,3 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
::view-transition-group(*) {
|
||||
animation-duration: 200ms;
|
||||
animation-timing-function: cubic-bezier(0.25, 0, 0.5, 1);
|
||||
}
|
||||
|
||||
@keyframes reveal {
|
||||
100% {
|
||||
mask-position: 0% 0%;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { A, createAsync, RouteSectionProps } from "@solidjs/router"
|
||||
import { Title, Meta, Link } from "@solidjs/meta"
|
||||
import { createMemo } from "solid-js"
|
||||
import { createMemo, createSignal } from "solid-js"
|
||||
import { github } from "~/lib/github"
|
||||
import { config } from "~/config"
|
||||
import LightRays, { defaultConfig, type LightRaysConfig, type LightRaysAnimationState } from "~/component/light-rays"
|
||||
import "./black.css"
|
||||
|
||||
export default function BlackLayout(props: RouteSectionProps) {
|
||||
@@ -16,6 +17,49 @@ export default function BlackLayout(props: RouteSectionProps) {
|
||||
: config.github.starsFormatted.compact,
|
||||
)
|
||||
|
||||
const [lightRaysConfig, setLightRaysConfig] = createSignal<LightRaysConfig>(defaultConfig)
|
||||
const [rayAnimationState, setRayAnimationState] = createSignal<LightRaysAnimationState>({
|
||||
time: 0,
|
||||
intensity: 0.5,
|
||||
pulseValue: 1,
|
||||
})
|
||||
|
||||
const svgLightingValues = createMemo(() => {
|
||||
const state = rayAnimationState()
|
||||
const t = state.time
|
||||
|
||||
const wave1 = Math.sin(t * 1.5) * 0.5 + 0.5
|
||||
const wave2 = Math.sin(t * 2.3 + 1.2) * 0.5 + 0.5
|
||||
const wave3 = Math.sin(t * 0.8 + 2.5) * 0.5 + 0.5
|
||||
|
||||
const shimmerPos = Math.sin(t * 0.7) * 0.5 + 0.5
|
||||
const glowIntensity = state.intensity * state.pulseValue * 0.35
|
||||
const fillOpacity = 0.1 + wave1 * 0.08 * state.pulseValue
|
||||
const strokeBrightness = 55 + wave2 * 25 * state.pulseValue
|
||||
|
||||
const shimmerIntensity = wave3 * 0.15 * state.pulseValue
|
||||
|
||||
return {
|
||||
glowIntensity,
|
||||
fillOpacity,
|
||||
strokeBrightness,
|
||||
shimmerPos,
|
||||
shimmerIntensity,
|
||||
}
|
||||
})
|
||||
|
||||
const svgLightingStyle = createMemo(() => {
|
||||
const values = svgLightingValues()
|
||||
return {
|
||||
"--hero-black-glow-intensity": values.glowIntensity.toFixed(3),
|
||||
"--hero-black-stroke-brightness": `${values.strokeBrightness.toFixed(0)}%`,
|
||||
} as Record<string, string>
|
||||
})
|
||||
|
||||
const handleAnimationFrame = (state: LightRaysAnimationState) => {
|
||||
setRayAnimationState(state)
|
||||
}
|
||||
|
||||
return (
|
||||
<div data-page="black">
|
||||
<Title>OpenCode Black | Access all the world's best coding models</Title>
|
||||
@@ -39,7 +83,9 @@ export default function BlackLayout(props: RouteSectionProps) {
|
||||
content="Get access to Claude, GPT, Gemini and more with OpenCode Black subscription plans."
|
||||
/>
|
||||
<Meta name="twitter:image" content="/social-share-black.png" />
|
||||
<div data-component="header-gradient" />
|
||||
|
||||
<LightRays config={lightRaysConfig} class="header-light-rays" onAnimationFrame={handleAnimationFrame} />
|
||||
|
||||
<header data-component="header">
|
||||
<A href="/" data-component="header-logo">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="179" height="32" viewBox="0 0 179 32" fill="none">
|
||||
@@ -112,15 +158,8 @@ export default function BlackLayout(props: RouteSectionProps) {
|
||||
<h1>Access all the world's best coding models</h1>
|
||||
<p>Including Claude, GPT, Gemini and more</p>
|
||||
</div>
|
||||
<div data-slot="hero-black">
|
||||
<div data-slot="hero-black" style={svgLightingStyle()}>
|
||||
<svg width="591" height="90" viewBox="0 0 591 90" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M425.56 0.75C429.464 0.750017 432.877 1.27807 435.78 2.35645C438.656 3.42455 441.138 4.86975 443.215 6.69727C445.268 8.50382 446.995 10.5587 448.394 12.8604C449.77 15.0464 450.986 17.2741 452.04 19.5439L452.357 20.2275L451.672 20.542L443.032 24.502L442.311 24.833L442.021 24.0938C441.315 22.2906 440.494 20.6079 439.557 19.0459L439.552 19.0391L439.548 19.0322C438.626 17.419 437.517 16.0443 436.223 14.9023L436.206 14.8867L436.189 14.8701C434.989 13.6697 433.518 12.7239 431.766 12.0381L431.755 12.0342V12.0332C430.111 11.3607 428.053 11.0098 425.56 11.0098C419.142 11.0098 414.433 13.4271 411.308 18.2295C408.212 23.109 406.629 29.6717 406.629 37.9805V51.6602C406.629 59.9731 408.214 66.5377 411.312 71.418C414.438 76.2157 419.145 78.6299 425.56 78.6299C428.054 78.6299 430.111 78.2782 431.756 77.6055L431.766 77.6016L432.413 77.333C433.893 76.6811 435.154 75.8593 436.206 74.873C437.512 73.644 438.625 72.2626 439.548 70.7275C440.489 69.0801 441.314 67.3534 442.021 65.5469L442.311 64.8076L443.032 65.1387L451.672 69.0986L452.348 69.4082L452.044 70.0869C450.99 72.439 449.773 74.7099 448.395 76.8994C446.995 79.1229 445.266 81.1379 443.215 82.9434C441.138 84.7708 438.656 86.2151 435.78 87.2832C432.877 88.3616 429.464 88.8896 425.56 88.8896C415.111 88.8896 407.219 85.0777 402.019 77.4004L402.016 77.3965C396.939 69.7818 394.449 58.891 394.449 44.8203C394.449 30.7495 396.939 19.8589 402.016 12.2441L402.019 12.2393C407.219 4.56202 415.111 0.75 425.56 0.75ZM29.9404 2.19043C37.2789 2.19051 43.125 4.19131 47.3799 8.2793C51.6307 12.3635 53.7305 17.8115 53.7305 24.54C53.7305 29.6953 52.4605 33.8451 49.835 36.8994L49.8359 36.9004C47.7064 39.4558 45.0331 41.367 41.835 42.6445C45.893 43.8751 49.3115 45.9006 52.0703 48.7295C55.2954 51.9546 56.8496 56.6143 56.8496 62.5801C56.8496 66.0251 56.2751 69.2753 55.1211 72.3252C53.9689 75.3702 52.3185 78.014 50.1689 80.249L50.1699 80.25C48.0996 82.4858 45.6172 84.2628 42.7314 85.582L42.7227 85.5859C39.9002 86.8312 36.8362 87.4502 33.54 87.4502H0.75V2.19043H29.9404ZM148.123 2.19043V77.1904H187.843V87.4502H136.543V2.19043H148.123ZM298.121 2.19043L298.283 2.71973L323.963 86.4805L324.261 87.4502H312.006L311.848 86.9131L304.927 63.5703H276.646L269.726 86.9131L269.566 87.4502H257.552L257.85 86.4805L283.529 2.71973L283.691 2.19043H298.121ZM539.782 2.19043V44.9209L549.845 32.2344L549.851 32.2275L549.855 32.2207L574.575 2.46094L574.801 2.19043H588.874L587.849 3.41992L558.795 38.2832L588.749 86.3027L589.464 87.4502H575.934L575.714 87.0938L550.937 46.9316L539.782 60.0947V87.4502H528.202V2.19043H539.782ZM12.3301 77.1904H30.54C35.0749 77.1904 38.5307 76.1729 40.9961 74.2305C43.4059 72.3317 44.6699 69.3811 44.6699 65.2197V60.2998C44.6699 56.2239 43.4093 53.3106 40.9961 51.4092L40.9854 51.4004C38.5207 49.3838 35.0691 48.3301 30.54 48.3301H12.3301V77.1904ZM279.485 53.3096H302.087L290.786 14.4482L279.485 53.3096ZM12.3301 38.5498H28.8604C33 38.5498 36.1378 37.6505 38.3633 35.9443C40.5339 34.2015 41.6698 31.5679 41.6699 27.9004V23.2197C41.6699 19.5455 40.5299 16.9088 38.3516 15.166C36.1272 13.3865 32.9938 12.4502 28.8604 12.4502H12.3301V38.5498Z"
|
||||
fill="url(#hero-black-fill-gradient)"
|
||||
fill-opacity="0.1"
|
||||
stroke="url(#hero-black-stroke-gradient)"
|
||||
stroke-width="1.5"
|
||||
/>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="hero-black-fill-gradient"
|
||||
@@ -130,9 +169,10 @@ export default function BlackLayout(props: RouteSectionProps) {
|
||||
y2="87.0326"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stop-color="var(--hero-black-fill-from)" />
|
||||
<stop offset="1" stop-color="var(--hero-black-fill-to)" />
|
||||
<stop stop-color="white" />
|
||||
<stop offset="1" stop-color="white" stop-opacity="0" />
|
||||
</linearGradient>
|
||||
|
||||
<linearGradient
|
||||
id="hero-black-stroke-gradient"
|
||||
x1="290.82"
|
||||
@@ -141,10 +181,80 @@ export default function BlackLayout(props: RouteSectionProps) {
|
||||
y2="87.0325"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stop-color="var(--hero-black-stroke-from)" />
|
||||
<stop offset="1" stop-color="var(--hero-black-stroke-to)" />
|
||||
<stop stop-color={`hsl(0 0% ${svgLightingValues().strokeBrightness}%)`} />
|
||||
<stop offset="1" stop-color="white" stop-opacity="0" />
|
||||
</linearGradient>
|
||||
|
||||
<linearGradient
|
||||
id="hero-black-shimmer-gradient"
|
||||
x1="0"
|
||||
y1="0"
|
||||
x2="591"
|
||||
y2="0"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop offset={Math.max(0, svgLightingValues().shimmerPos - 0.12)} stop-color="transparent" />
|
||||
<stop
|
||||
offset={svgLightingValues().shimmerPos}
|
||||
stop-color={`rgba(255, 255, 255, ${svgLightingValues().shimmerIntensity})`}
|
||||
/>
|
||||
<stop offset={Math.min(1, svgLightingValues().shimmerPos + 0.12)} stop-color="transparent" />
|
||||
</linearGradient>
|
||||
|
||||
<linearGradient
|
||||
id="hero-black-top-glow"
|
||||
x1="290.82"
|
||||
y1="0"
|
||||
x2="290.82"
|
||||
y2="45"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop offset="0" stop-color={`rgba(255, 255, 255, ${svgLightingValues().glowIntensity})`} />
|
||||
<stop offset="1" stop-color="transparent" />
|
||||
</linearGradient>
|
||||
|
||||
<linearGradient
|
||||
id="hero-black-shimmer-mask"
|
||||
x1="290.82"
|
||||
y1="0"
|
||||
x2="290.82"
|
||||
y2="50"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop offset="0" stop-color="white" />
|
||||
<stop offset="0.8" stop-color="white" stop-opacity="0.5" />
|
||||
<stop offset="1" stop-color="white" stop-opacity="0" />
|
||||
</linearGradient>
|
||||
|
||||
<mask id="shimmer-top-mask">
|
||||
<rect x="0" y="0" width="591" height="90" fill="url(#hero-black-shimmer-mask)" />
|
||||
</mask>
|
||||
</defs>
|
||||
|
||||
<path
|
||||
d="M425.56 0.75C429.464 0.750017 432.877 1.27807 435.78 2.35645C438.656 3.42455 441.138 4.86975 443.215 6.69727C445.268 8.50382 446.995 10.5587 448.394 12.8604C449.77 15.0464 450.986 17.2741 452.04 19.5439L452.357 20.2275L451.672 20.542L443.032 24.502L442.311 24.833L442.021 24.0938C441.315 22.2906 440.494 20.6079 439.557 19.0459L439.552 19.0391L439.548 19.0322C438.626 17.419 437.517 16.0443 436.223 14.9023L436.206 14.8867L436.189 14.8701C434.989 13.6697 433.518 12.7239 431.766 12.0381L431.755 12.0342V12.0332C430.111 11.3607 428.053 11.0098 425.56 11.0098C419.142 11.0098 414.433 13.4271 411.308 18.2295C408.212 23.109 406.629 29.6717 406.629 37.9805V51.6602C406.629 59.9731 408.214 66.5377 411.312 71.418C414.438 76.2157 419.145 78.6299 425.56 78.6299C428.054 78.6299 430.111 78.2782 431.756 77.6055L431.766 77.6016L432.413 77.333C433.893 76.6811 435.154 75.8593 436.206 74.873C437.512 73.644 438.625 72.2626 439.548 70.7275C440.489 69.0801 441.314 67.3534 442.021 65.5469L442.311 64.8076L443.032 65.1387L451.672 69.0986L452.348 69.4082L452.044 70.0869C450.99 72.439 449.773 74.7099 448.395 76.8994C446.995 79.1229 445.266 81.1379 443.215 82.9434C441.138 84.7708 438.656 86.2151 435.78 87.2832C432.877 88.3616 429.464 88.8896 425.56 88.8896C415.111 88.8896 407.219 85.0777 402.019 77.4004L402.016 77.3965C396.939 69.7818 394.449 58.891 394.449 44.8203C394.449 30.7495 396.939 19.8589 402.016 12.2441L402.019 12.2393C407.219 4.56202 415.111 0.75 425.56 0.75ZM29.9404 2.19043C37.2789 2.19051 43.125 4.19131 47.3799 8.2793C51.6307 12.3635 53.7305 17.8115 53.7305 24.54C53.7305 29.6953 52.4605 33.8451 49.835 36.8994L49.8359 36.9004C47.7064 39.4558 45.0331 41.367 41.835 42.6445C45.893 43.8751 49.3115 45.9006 52.0703 48.7295C55.2954 51.9546 56.8496 56.6143 56.8496 62.5801C56.8496 66.0251 56.2751 69.2753 55.1211 72.3252C53.9689 75.3702 52.3185 78.014 50.1689 80.249L50.1699 80.25C48.0996 82.4858 45.6172 84.2628 42.7314 85.582L42.7227 85.5859C39.9002 86.8312 36.8362 87.4502 33.54 87.4502H0.75V2.19043H29.9404ZM148.123 2.19043V77.1904H187.843V87.4502H136.543V2.19043H148.123ZM298.121 2.19043L298.283 2.71973L323.963 86.4805L324.261 87.4502H312.006L311.848 86.9131L304.927 63.5703H276.646L269.726 86.9131L269.566 87.4502H257.552L257.85 86.4805L283.529 2.71973L283.691 2.19043H298.121ZM539.782 2.19043V44.9209L549.845 32.2344L549.851 32.2275L549.855 32.2207L574.575 2.46094L574.801 2.19043H588.874L587.849 3.41992L558.795 38.2832L588.749 86.3027L589.464 87.4502H575.934L575.714 87.0938L550.937 46.9316L539.782 60.0947V87.4502H528.202V2.19043H539.782ZM12.3301 77.1904H30.54C35.0749 77.1904 38.5307 76.1729 40.9961 74.2305C43.4059 72.3317 44.6699 69.3811 44.6699 65.2197V60.2998C44.6699 56.2239 43.4093 53.3106 40.9961 51.4092L40.9854 51.4004C38.5207 49.3838 35.0691 48.3301 30.54 48.3301H12.3301V77.1904ZM279.485 53.3096H302.087L290.786 14.4482L279.485 53.3096ZM12.3301 38.5498H28.8604C33 38.5498 36.1378 37.6505 38.3633 35.9443C40.5339 34.2015 41.6698 31.5679 41.6699 27.9004V23.2197C41.6699 19.5455 40.5299 16.9088 38.3516 15.166C36.1272 13.3865 32.9938 12.4502 28.8604 12.4502H12.3301V38.5498Z"
|
||||
fill="url(#hero-black-fill-gradient)"
|
||||
fill-opacity={svgLightingValues().fillOpacity}
|
||||
stroke="url(#hero-black-stroke-gradient)"
|
||||
stroke-width="1.5"
|
||||
data-slot="black-base"
|
||||
/>
|
||||
|
||||
<path
|
||||
d="M425.56 0.75C429.464 0.750017 432.877 1.27807 435.78 2.35645C438.656 3.42455 441.138 4.86975 443.215 6.69727C445.268 8.50382 446.995 10.5587 448.394 12.8604C449.77 15.0464 450.986 17.2741 452.04 19.5439L452.357 20.2275L451.672 20.542L443.032 24.502L442.311 24.833L442.021 24.0938C441.315 22.2906 440.494 20.6079 439.557 19.0459L439.552 19.0391L439.548 19.0322C438.626 17.419 437.517 16.0443 436.223 14.9023L436.206 14.8867L436.189 14.8701C434.989 13.6697 433.518 12.7239 431.766 12.0381L431.755 12.0342V12.0332C430.111 11.3607 428.053 11.0098 425.56 11.0098C419.142 11.0098 414.433 13.4271 411.308 18.2295C408.212 23.109 406.629 29.6717 406.629 37.9805V51.6602C406.629 59.9731 408.214 66.5377 411.312 71.418C414.438 76.2157 419.145 78.6299 425.56 78.6299C428.054 78.6299 430.111 78.2782 431.756 77.6055L431.766 77.6016L432.413 77.333C433.893 76.6811 435.154 75.8593 436.206 74.873C437.512 73.644 438.625 72.2626 439.548 70.7275C440.489 69.0801 441.314 67.3534 442.021 65.5469L442.311 64.8076L443.032 65.1387L451.672 69.0986L452.348 69.4082L452.044 70.0869C450.99 72.439 449.773 74.7099 448.395 76.8994C446.995 79.1229 445.266 81.1379 443.215 82.9434C441.138 84.7708 438.656 86.2151 435.78 87.2832C432.877 88.3616 429.464 88.8896 425.56 88.8896C415.111 88.8896 407.219 85.0777 402.019 77.4004L402.016 77.3965C396.939 69.7818 394.449 58.891 394.449 44.8203C394.449 30.7495 396.939 19.8589 402.016 12.2441L402.019 12.2393C407.219 4.56202 415.111 0.75 425.56 0.75ZM29.9404 2.19043C37.2789 2.19051 43.125 4.19131 47.3799 8.2793C51.6307 12.3635 53.7305 17.8115 53.7305 24.54C53.7305 29.6953 52.4605 33.8451 49.835 36.8994L49.8359 36.9004C47.7064 39.4558 45.0331 41.367 41.835 42.6445C45.893 43.8751 49.3115 45.9006 52.0703 48.7295C55.2954 51.9546 56.8496 56.6143 56.8496 62.5801C56.8496 66.0251 56.2751 69.2753 55.1211 72.3252C53.9689 75.3702 52.3185 78.014 50.1689 80.249L50.1699 80.25C48.0996 82.4858 45.6172 84.2628 42.7314 85.582L42.7227 85.5859C39.9002 86.8312 36.8362 87.4502 33.54 87.4502H0.75V2.19043H29.9404ZM148.123 2.19043V77.1904H187.843V87.4502H136.543V2.19043H148.123ZM298.121 2.19043L298.283 2.71973L323.963 86.4805L324.261 87.4502H312.006L311.848 86.9131L304.927 63.5703H276.646L269.726 86.9131L269.566 87.4502H257.552L257.85 86.4805L283.529 2.71973L283.691 2.19043H298.121ZM539.782 2.19043V44.9209L549.845 32.2344L549.851 32.2275L549.855 32.2207L574.575 2.46094L574.801 2.19043H588.874L587.849 3.41992L558.795 38.2832L588.749 86.3027L589.464 87.4502H575.934L575.714 87.0938L550.937 46.9316L539.782 60.0947V87.4502H528.202V2.19043H539.782ZM12.3301 77.1904H30.54C35.0749 77.1904 38.5307 76.1729 40.9961 74.2305C43.4059 72.3317 44.6699 69.3811 44.6699 65.2197V60.2998C44.6699 56.2239 43.4093 53.3106 40.9961 51.4092L40.9854 51.4004C38.5207 49.3838 35.0691 48.3301 30.54 48.3301H12.3301V77.1904ZM279.485 53.3096H302.087L290.786 14.4482L279.485 53.3096ZM12.3301 38.5498H28.8604C33 38.5498 36.1378 37.6505 38.3633 35.9443C40.5339 34.2015 41.6698 31.5679 41.6699 27.9004V23.2197C41.6699 19.5455 40.5299 16.9088 38.3516 15.166C36.1272 13.3865 32.9938 12.4502 28.8604 12.4502H12.3301V38.5498Z"
|
||||
fill="url(#hero-black-top-glow)"
|
||||
stroke="none"
|
||||
data-slot="black-glow"
|
||||
/>
|
||||
|
||||
<path
|
||||
d="M425.56 0.75C429.464 0.750017 432.877 1.27807 435.78 2.35645C438.656 3.42455 441.138 4.86975 443.215 6.69727C445.268 8.50382 446.995 10.5587 448.394 12.8604C449.77 15.0464 450.986 17.2741 452.04 19.5439L452.357 20.2275L451.672 20.542L443.032 24.502L442.311 24.833L442.021 24.0938C441.315 22.2906 440.494 20.6079 439.557 19.0459L439.552 19.0391L439.548 19.0322C438.626 17.419 437.517 16.0443 436.223 14.9023L436.206 14.8867L436.189 14.8701C434.989 13.6697 433.518 12.7239 431.766 12.0381L431.755 12.0342V12.0332C430.111 11.3607 428.053 11.0098 425.56 11.0098C419.142 11.0098 414.433 13.4271 411.308 18.2295C408.212 23.109 406.629 29.6717 406.629 37.9805V51.6602C406.629 59.9731 408.214 66.5377 411.312 71.418C414.438 76.2157 419.145 78.6299 425.56 78.6299C428.054 78.6299 430.111 78.2782 431.756 77.6055L431.766 77.6016L432.413 77.333C433.893 76.6811 435.154 75.8593 436.206 74.873C437.512 73.644 438.625 72.2626 439.548 70.7275C440.489 69.0801 441.314 67.3534 442.021 65.5469L442.311 64.8076L443.032 65.1387L451.672 69.0986L452.348 69.4082L452.044 70.0869C450.99 72.439 449.773 74.7099 448.395 76.8994C446.995 79.1229 445.266 81.1379 443.215 82.9434C441.138 84.7708 438.656 86.2151 435.78 87.2832C432.877 88.3616 429.464 88.8896 425.56 88.8896C415.111 88.8896 407.219 85.0777 402.019 77.4004L402.016 77.3965C396.939 69.7818 394.449 58.891 394.449 44.8203C394.449 30.7495 396.939 19.8589 402.016 12.2441L402.019 12.2393C407.219 4.56202 415.111 0.75 425.56 0.75ZM29.9404 2.19043C37.2789 2.19051 43.125 4.19131 47.3799 8.2793C51.6307 12.3635 53.7305 17.8115 53.7305 24.54C53.7305 29.6953 52.4605 33.8451 49.835 36.8994L49.8359 36.9004C47.7064 39.4558 45.0331 41.367 41.835 42.6445C45.893 43.8751 49.3115 45.9006 52.0703 48.7295C55.2954 51.9546 56.8496 56.6143 56.8496 62.5801C56.8496 66.0251 56.2751 69.2753 55.1211 72.3252C53.9689 75.3702 52.3185 78.014 50.1689 80.249L50.1699 80.25C48.0996 82.4858 45.6172 84.2628 42.7314 85.582L42.7227 85.5859C39.9002 86.8312 36.8362 87.4502 33.54 87.4502H0.75V2.19043H29.9404ZM148.123 2.19043V77.1904H187.843V87.4502H136.543V2.19043H148.123ZM298.121 2.19043L298.283 2.71973L323.963 86.4805L324.261 87.4502H312.006L311.848 86.9131L304.927 63.5703H276.646L269.726 86.9131L269.566 87.4502H257.552L257.85 86.4805L283.529 2.71973L283.691 2.19043H298.121ZM539.782 2.19043V44.9209L549.845 32.2344L549.851 32.2275L549.855 32.2207L574.575 2.46094L574.801 2.19043H588.874L587.849 3.41992L558.795 38.2832L588.749 86.3027L589.464 87.4502H575.934L575.714 87.0938L550.937 46.9316L539.782 60.0947V87.4502H528.202V2.19043H539.782ZM12.3301 77.1904H30.54C35.0749 77.1904 38.5307 76.1729 40.9961 74.2305C43.4059 72.3317 44.6699 69.3811 44.6699 65.2197V60.2998C44.6699 56.2239 43.4093 53.3106 40.9961 51.4092L40.9854 51.4004C38.5207 49.3838 35.0691 48.3301 30.54 48.3301H12.3301V77.1904ZM279.485 53.3096H302.087L290.786 14.4482L279.485 53.3096ZM12.3301 38.5498H28.8604C33 38.5498 36.1378 37.6505 38.3633 35.9443C40.5339 34.2015 41.6698 31.5679 41.6699 27.9004V23.2197C41.6699 19.5455 40.5299 16.9088 38.3516 15.166C36.1272 13.3865 32.9938 12.4502 28.8604 12.4502H12.3301V38.5498Z"
|
||||
fill="url(#hero-black-shimmer-gradient)"
|
||||
stroke="none"
|
||||
data-slot="black-shimmer"
|
||||
mask="url(#shimmer-top-mask)"
|
||||
style={{ "mix-blend-mode": "overlay" }}
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
{props.children}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { A, useSearchParams } from "@solidjs/router"
|
||||
import { Title } from "@solidjs/meta"
|
||||
import { createMemo, createSignal, For, onMount, Show } from "solid-js"
|
||||
import { createMemo, createSignal, For, Match, onMount, Show, Switch } from "solid-js"
|
||||
import { PlanIcon, plans } from "./common"
|
||||
|
||||
export default function Black() {
|
||||
const [params] = useSearchParams()
|
||||
const [selected, setSelected] = createSignal<string | null>((params.plan as string) || null)
|
||||
const [mounted, setMounted] = createSignal(false)
|
||||
const selectedPlan = createMemo(() => plans.find((p) => p.id === selected()))
|
||||
|
||||
onMount(() => {
|
||||
requestAnimationFrame(() => setMounted(true))
|
||||
@@ -37,110 +38,68 @@ export default function Black() {
|
||||
<>
|
||||
<Title>opencode</Title>
|
||||
<section data-slot="cta">
|
||||
<div data-slot="pricing">
|
||||
<For each={plans}>
|
||||
{(plan) => {
|
||||
const isSelected = createMemo(() => selected() === plan.id)
|
||||
const isCollapsed = createMemo(() => selected() !== null && selected() !== plan.id)
|
||||
|
||||
return (
|
||||
<article
|
||||
data-slot="pricing-card"
|
||||
data-plan-id={plan.id}
|
||||
data-selected={isSelected() ? "true" : "false"}
|
||||
data-collapsed={isCollapsed() ? "true" : "false"}
|
||||
>
|
||||
<Switch>
|
||||
<Match when={!selected()}>
|
||||
<div data-slot="pricing">
|
||||
<For each={plans}>
|
||||
{(plan) => (
|
||||
<button
|
||||
type="button"
|
||||
data-slot="card-trigger"
|
||||
onClick={() => select(plan.id)}
|
||||
disabled={isSelected()}
|
||||
data-slot="pricing-card"
|
||||
style={{ "view-transition-name": `card-${plan.id}` }}
|
||||
>
|
||||
<div
|
||||
data-slot="plan-header"
|
||||
style={{
|
||||
"view-transition-name": `plan-header-${plan.id}`,
|
||||
}}
|
||||
>
|
||||
<div data-slot="plan-icon">
|
||||
<PlanIcon plan={plan.id} />
|
||||
</div>
|
||||
<p
|
||||
data-slot="price"
|
||||
style={{
|
||||
"view-transition-name": `price-${plan.id}`,
|
||||
}}
|
||||
>
|
||||
<span
|
||||
data-slot="amount"
|
||||
style={{
|
||||
"view-transition-name": `amount-${plan.id}`,
|
||||
}}
|
||||
>
|
||||
${plan.id}
|
||||
</span>
|
||||
<Show when={!isSelected()}>
|
||||
<span
|
||||
data-slot="period"
|
||||
style={{
|
||||
"view-transition-name": `period-${plan.id}`,
|
||||
}}
|
||||
>
|
||||
per month
|
||||
</span>
|
||||
</Show>
|
||||
|
||||
<Show when={isSelected()}>
|
||||
<span
|
||||
data-slot="billing"
|
||||
style={{
|
||||
"view-transition-name": `billing-${plan.id}`,
|
||||
}}
|
||||
>
|
||||
per person billed monthly
|
||||
</span>
|
||||
</Show>
|
||||
{plan.multiplier && (
|
||||
<span
|
||||
data-slot="multiplier"
|
||||
style={{
|
||||
"view-transition-name": `multiplier-${plan.id}`,
|
||||
}}
|
||||
>
|
||||
{plan.multiplier}
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
<div data-slot="icon" style={{ "view-transition-name": `icon-${plan.id}` }}>
|
||||
<PlanIcon plan={plan.id} />
|
||||
</div>
|
||||
<p data-slot="price" style={{ "view-transition-name": `price-${plan.id}` }}>
|
||||
<span data-slot="amount">${plan.id}</span> <span data-slot="period">per month</span>
|
||||
<Show when={plan.multiplier}>
|
||||
<span data-slot="multiplier">{plan.multiplier}</span>
|
||||
</Show>
|
||||
</p>
|
||||
</button>
|
||||
|
||||
<Show when={isSelected()}>
|
||||
<div data-slot="content">
|
||||
<ul data-slot="terms">
|
||||
<li>You will be added to the waitlist and activated in batches</li>
|
||||
<li>Card won't be charged until subscription is active</li>
|
||||
<li>Not unlimited - limits apply and may be adjusted dynamically</li>
|
||||
<li>Heavily automated usage will hit limits quickly</li>
|
||||
<li>Plans may be discontinued</li>
|
||||
<li>Can cancel subscription at anytime</li>
|
||||
<li>Cannot issue refunds for consumed subscriptions</li>
|
||||
</ul>
|
||||
<div data-slot="actions">
|
||||
<button type="button" onClick={cancel} data-slot="cancel">
|
||||
Cancel
|
||||
</button>
|
||||
<a href={`/black/subscribe/${plan.id}`} data-slot="continue">
|
||||
Continue
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
</article>
|
||||
)
|
||||
}}
|
||||
</For>
|
||||
</div>
|
||||
<p data-slot="fine-print">
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</Match>
|
||||
<Match when={selectedPlan()}>
|
||||
{(plan) => (
|
||||
<div data-slot="selected-plan">
|
||||
<div data-slot="selected-card" style={{ "view-transition-name": `card-${plan().id}` }}>
|
||||
<div data-slot="icon" style={{ "view-transition-name": `icon-${plan().id}` }}>
|
||||
<PlanIcon plan={plan().id} />
|
||||
</div>
|
||||
<p data-slot="price" style={{ "view-transition-name": `price-${plan().id}` }}>
|
||||
<span data-slot="amount">${plan().id}</span>{" "}
|
||||
<span data-slot="period">per person billed monthly</span>
|
||||
<Show when={plan().multiplier}>
|
||||
<span data-slot="multiplier">{plan().multiplier}</span>
|
||||
</Show>
|
||||
</p>
|
||||
<ul data-slot="terms" style={{ "view-transition-name": `terms-${plan().id}` }}>
|
||||
<li>Your subscription will not start immediately</li>
|
||||
<li>You will be added to the waitlist and activated soon</li>
|
||||
<li>Your card will be only charged when your subscription is activated</li>
|
||||
<li>Usage limits apply, heavily automated use may reach limits sooner</li>
|
||||
<li>Subscriptions for individuals, contact Enterprise for teams</li>
|
||||
<li>Limits may be adjusted and plans may be discontinued in the future</li>
|
||||
<li>Cancel your subscription at anytime</li>
|
||||
</ul>
|
||||
<div data-slot="actions" style={{ "view-transition-name": `actions-${plan().id}` }}>
|
||||
<button type="button" onClick={() => cancel()} data-slot="cancel">
|
||||
Cancel
|
||||
</button>
|
||||
<a href={`/black/subscribe/${plan().id}`} data-slot="continue">
|
||||
Continue
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Match>
|
||||
</Switch>
|
||||
<p data-slot="fine-print" style={{ "view-transition-name": "fine-print" }}>
|
||||
Prices shown don't include applicable tax · <A href="/legal/terms-of-service">Terms of Service</A>
|
||||
</p>
|
||||
</section>
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"allowJs": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"types": ["vite/client"],
|
||||
"types": ["vite/client", "@webgpu/types"],
|
||||
"isolatedModules": true,
|
||||
"paths": {
|
||||
"~/*": ["./src/*"]
|
||||
|
||||
@@ -223,7 +223,7 @@ async fn check_server_health(url: &str, password: Option<&str>) -> bool {
|
||||
pub fn run() {
|
||||
let updater_enabled = option_env!("TAURI_SIGNING_PRIVATE_KEY").is_some();
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[cfg(all(target_os = "macos", not(debug_assertions)))]
|
||||
let _ = std::process::Command::new("killall")
|
||||
.arg("opencode-cli")
|
||||
.output();
|
||||
|
||||
@@ -239,7 +239,7 @@ export function Sidebar(props: { sessionID: string; overlay?: boolean }) {
|
||||
{(item) => {
|
||||
return (
|
||||
<box flexDirection="row" gap={1} justifyContent="space-between">
|
||||
<text fg={theme.textMuted} truncate={true} wrapMode="none">
|
||||
<text fg={theme.textMuted} wrapMode="none">
|
||||
{item.file}
|
||||
</text>
|
||||
<box flexDirection="row" gap={1} flexShrink={0}>
|
||||
|
||||
@@ -272,7 +272,11 @@ export namespace Project {
|
||||
|
||||
export async function list() {
|
||||
const keys = await Storage.list(["project"])
|
||||
return await Promise.all(keys.map((x) => Storage.read<Info>(x)))
|
||||
const projects = await Promise.all(keys.map((x) => Storage.read<Info>(x)))
|
||||
return projects.map((project) => ({
|
||||
...project,
|
||||
sandboxes: project.sandboxes?.filter((x) => existsSync(x)),
|
||||
}))
|
||||
}
|
||||
|
||||
export const update = fn(
|
||||
|
||||
@@ -81,7 +81,11 @@ export namespace ModelsDev {
|
||||
const file = Bun.file(filepath)
|
||||
const result = await file.json().catch(() => {})
|
||||
if (result) return result as Record<string, Provider>
|
||||
const json = await data()
|
||||
if (typeof data === "function") {
|
||||
const json = await data()
|
||||
return JSON.parse(json) as Record<string, Provider>
|
||||
}
|
||||
const json = await fetch("https://models.dev/api.json").then((x) => x.text())
|
||||
return JSON.parse(json) as Record<string, Provider>
|
||||
}
|
||||
|
||||
|
||||
@@ -48,10 +48,10 @@
|
||||
"@solid-primitives/resize-observer": "2.1.3",
|
||||
"@solidjs/meta": "catalog:",
|
||||
"@typescript/native-preview": "catalog:",
|
||||
"dompurify": "3.3.1",
|
||||
"fuzzysort": "catalog:",
|
||||
"katex": "0.16.27",
|
||||
"luxon": "catalog:",
|
||||
"dompurify": "catalog:",
|
||||
"marked": "catalog:",
|
||||
"marked-katex-extension": "5.1.6",
|
||||
"marked-shiki": "catalog:",
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
background: var(--surface-base);
|
||||
border: 1px solid var(--border-base);
|
||||
border: 1px solid var(--border-weak-base);
|
||||
transition: border-color 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
@@ -416,7 +416,30 @@
|
||||
box-shadow: var(--shadow-xs-border-base);
|
||||
background-color: var(--surface-raised-base);
|
||||
overflow: visible;
|
||||
overflow-anchor: none;
|
||||
|
||||
& > *:first-child {
|
||||
border-top-left-radius: 6px;
|
||||
border-top-right-radius: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
& > *:last-child {
|
||||
border-bottom-left-radius: 6px;
|
||||
border-bottom-right-radius: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
[data-component="collapsible"] {
|
||||
border: none;
|
||||
}
|
||||
|
||||
[data-component="card"] {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
&[data-permission="true"] {
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
@@ -438,26 +461,11 @@
|
||||
pointer-events: none;
|
||||
z-index: -1;
|
||||
}
|
||||
}
|
||||
|
||||
& > *:first-child {
|
||||
border-top-left-radius: 6px;
|
||||
border-top-right-radius: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
& > *:last-child {
|
||||
border-bottom-left-radius: 6px;
|
||||
border-bottom-right-radius: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
[data-component="collapsible"] {
|
||||
border: none;
|
||||
}
|
||||
|
||||
[data-component="card"] {
|
||||
border: none;
|
||||
}
|
||||
&[data-question="true"] {
|
||||
background: var(--background-base);
|
||||
border: 1px solid var(--border-weak-base);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ComponentProps, For } from "solid-js"
|
||||
|
||||
const outerIndices = new Set([0, 1, 2, 3, 4, 7, 8, 11, 12, 13, 14, 15])
|
||||
const outerIndices = new Set([1, 2, 4, 7, 8, 11, 13, 14])
|
||||
const cornerIndices = new Set([0, 3, 12, 15])
|
||||
const squares = Array.from({ length: 16 }, (_, i) => ({
|
||||
id: i,
|
||||
x: (i % 4) * 4,
|
||||
@@ -8,6 +9,7 @@ const squares = Array.from({ length: 16 }, (_, i) => ({
|
||||
delay: Math.random() * 1.5,
|
||||
duration: 1 + Math.random() * 1,
|
||||
outer: outerIndices.has(i),
|
||||
corner: cornerIndices.has(i),
|
||||
}))
|
||||
|
||||
export function Spinner(props: {
|
||||
@@ -35,8 +37,11 @@ export function Spinner(props: {
|
||||
height="3"
|
||||
rx="1"
|
||||
style={{
|
||||
animation: `${square.outer ? "pulse-opacity-dim" : "pulse-opacity"} ${square.duration}s ease-in-out infinite`,
|
||||
"animation-delay": `${square.delay}s`,
|
||||
opacity: square.corner ? 0 : undefined,
|
||||
animation: square.corner
|
||||
? undefined
|
||||
: `${square.outer ? "pulse-opacity-dim" : "pulse-opacity"} ${square.duration}s ease-in-out infinite`,
|
||||
"animation-delay": square.corner ? undefined : `${square.delay}s`,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
[data-slot="tooltip-keybind"] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
[data-slot="tooltip-keybind-key"] {
|
||||
@@ -18,11 +18,11 @@
|
||||
[data-component="tooltip"] {
|
||||
z-index: 1000;
|
||||
max-width: 320px;
|
||||
border-radius: var(--radius-md);
|
||||
border-radius: var(--radius-sm);
|
||||
background-color: var(--surface-float-base);
|
||||
color: var(--text-invert-strong);
|
||||
background: var(--surface-float-base);
|
||||
padding: 6px 12px;
|
||||
padding: 2px 8px;
|
||||
border: 1px solid var(--border-weak-base, rgba(0, 0, 0, 0.07));
|
||||
|
||||
box-shadow: var(--shadow-md);
|
||||
|
||||
@@ -242,6 +242,20 @@ List available themes.
|
||||
|
||||
---
|
||||
|
||||
### thinking
|
||||
|
||||
Toggle the visibility of thinking/reasoning blocks in the conversation. When enabled, you can see the model's reasoning process for models that support extended thinking.
|
||||
|
||||
:::note
|
||||
This command only controls whether thinking blocks are **displayed** - it does not enable or disable the model's reasoning capabilities. To toggle actual reasoning capabilities, use `ctrl+t` to cycle through model variants.
|
||||
:::
|
||||
|
||||
```bash frame="none"
|
||||
/thinking
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### undo
|
||||
|
||||
Undo last message in the conversation. Removes the most recent user message, all subsequent responses, and any file changes.
|
||||
|
||||
Reference in New Issue
Block a user