Compare commits

..

59 Commits

Author SHA1 Message Date
Aaron Iker
b30c91de78 Merge branch 'dev' into update-design-subscriptions 2026-01-15 22:18:51 +01:00
Aaron Iker
2a22111b5e fix: small style adjustments, light rays params 2026-01-15 22:16:58 +01:00
Nhan Nguyen
f5fd54598f docs: add /thinking command documentation (#8722) 2026-01-15 15:14:23 -06:00
David Hill
0f7b17b1b4 fix: thinking animation opacity and design 2026-01-15 20:42:14 +00:00
David Hill
4d3e983edb fix: session icon and name alignment 2026-01-15 20:42:14 +00:00
Github Action
50badbd779 Update aarch64-darwin hash 2026-01-15 20:41:35 +00:00
Github Action
d3fc29bdec Update aarch64-darwin hash 2026-01-15 20:39:01 +00:00
Aaron Iker
fe58c649cb feat(console): Update /black plan selection, light rays effect. mobile styles (#8731)
Co-authored-by: Github Action <action@github.com>
2026-01-15 21:31:50 +01:00
Github Action
87eebad14e Update Nix flake.lock and x86_64-linux hash 2026-01-15 20:30:52 +00:00
Aaron Iker
e258662178 Merge branch 'dev' into update-design-subscriptions 2026-01-15 21:28:10 +01:00
Aaron Iker
591f54cd0d feat: light rays improvement, mobile styles 2026-01-15 21:26:46 +01:00
Aaron Iker
fdea599939 Merge branch 'update-design-subscriptions' of https://github.com/anomalyco/opencode into update-design-subscriptions 2026-01-15 21:15:07 +01:00
Adam
af2a09940c fix(core): more defensive project list 2026-01-15 13:58:39 -06:00
Adam
7e016fdda6 chore: cleanup 2026-01-15 13:34:53 -06:00
Adam
beb97d21ff fix(app): show session busy even for active session 2026-01-15 13:33:49 -06:00
Adam
b0345284f9 fix(core): filter dead worktrees 2026-01-15 13:33:49 -06:00
Adam
d71153eae6 fix(core): loading models.dev in dev 2026-01-15 13:33:48 -06:00
Aaron Iker
ccac97c7c4 feat: transition improvements 2026-01-15 20:23:22 +01:00
dbpolito
e60ded01df chore(desktop): Stop Killing opencode-cli on dev 2026-01-15 13:17:57 -06:00
dbpolito
4b2a14c154 chore(desktop): Question Tools Updates 2026-01-15 13:17:31 -06:00
David Hill
b4717d8092 bun/package.json updates
this may not be required
2026-01-15 19:15:21 +00:00
David Hill
dc8f8cc567 fix: current session background color 2026-01-15 19:15:21 +00:00
David Hill
99110d12c4 fix: remove the active state from load more button after press 2026-01-15 19:15:21 +00:00
David Hill
74b1349cf6 fix: new session tooltip position and add shortcut 2026-01-15 19:15:21 +00:00
David Hill
3b3505cfe8 fix: remove more options tooltip 2026-01-15 19:15:21 +00:00
David Hill
55bd6e487e fix: workspace name color 2026-01-15 19:15:21 +00:00
David Hill
1ee916a3c3 fix: hide view all sessions on active project 2026-01-15 19:15:21 +00:00
David Hill
a5d47f076e fix: avatar button states 2026-01-15 19:15:21 +00:00
David Hill
acd1eb574d fix: load more button font size 2026-01-15 19:15:21 +00:00
David Hill
a71dcc189e fix: recent sessions title color 2026-01-15 19:15:21 +00:00
David Hill
3789a31423 fix: project dropdown labels and order 2026-01-15 19:15:21 +00:00
David Hill
bb6e350d68 fix: move left panel toggle over
- not sure how this impacts on the titlebar when the traffic lights are there
2026-01-15 19:15:21 +00:00
David Hill
f9a441d4f4 fix: avatar background 2026-01-15 19:15:21 +00:00
David Hill
1c05ebaea2 fix: show project options on hover of row 2026-01-15 19:15:21 +00:00
David Hill
520c47e81d fix: increase delay on session list tooltips 2026-01-15 19:15:21 +00:00
David Hill
e5b08da0f1 fix: tooltip gutter spacing on session items and archive buttons 2026-01-15 19:15:21 +00:00
David Hill
fe2cc0cff1 fix: archive icon replaces diff count on hover 2026-01-15 19:15:21 +00:00
David Hill
fbc8f6eba9 fix: recent sessions hover gutter 2026-01-15 19:15:21 +00:00
David Hill
8cba7d7f53 fix: tooltips cleanup 2026-01-15 19:15:21 +00:00
David Hill
6450ba1b79 fix: search bar in header 2026-01-15 19:15:21 +00:00
Aiden Cline
dc1c25cff5 fix: ensure frontmatter can process same content as other agents (#8719) 2026-01-15 13:06:14 -06:00
Github Action
3f3550a16e Update aarch64-darwin hash 2026-01-15 18:29:11 +00:00
Github Action
57b457f568 Update aarch64-darwin hash 2026-01-15 18:22:50 +00:00
Github Action
161e3db795 Update Nix flake.lock and x86_64-linux hash 2026-01-15 18:17:44 +00:00
Github Action
08068c3b91 Update Nix flake.lock and x86_64-linux hash 2026-01-15 18:13:32 +00:00
Aaron Iker
64edbb6b82 fix: webgp buffer 2026-01-15 19:12:17 +01:00
Aaron Iker
864f7ce129 feat: small style fixes, webgpu types 2026-01-15 19:08:24 +01:00
Aaron Iker
977827c9a4 feat: refacor light rays to WEBGPU 2026-01-15 19:08:08 +01:00
Aaron Iker
d8b8854795 feat: remove ogl, add webgpu types 2026-01-15 19:07:51 +01:00
Aaron Iker
d79dc295fd Merge branch 'update-design-subscriptions' of https://github.com/anomalyco/opencode into update-design-subscriptions 2026-01-15 18:24:03 +01:00
Aaron Iker
abadacdce7 feat: small light rays tweaks 2026-01-15 18:22:12 +01:00
Github Action
bd5a9002a8 Update aarch64-darwin hash 2026-01-15 17:19:44 +00:00
Github Action
ecf33a72c3 Update Nix flake.lock and x86_64-linux hash 2026-01-15 17:12:46 +00:00
Aaron Iker
f2711bf5ae Merge branch 'dev' into update-design-subscriptions 2026-01-15 18:12:20 +01:00
Aaron Iker
769c34c94f fix: desktop shellOpen 2026-01-15 18:11:34 +01:00
Aaron Iker
ad33807627 feat: update select plan UI 2026-01-15 17:52:59 +01:00
Aaron Iker
cf4fe5dc82 add light rays 2026-01-15 17:52:48 +01:00
Aaron Iker
56a7fbe131 feat: add ogl 2026-01-15 17:52:31 +01:00
Aaron Iker
3bc995dbe1 feat: restore former layout 2026-01-15 10:56:39 +01:00
22 changed files with 1786 additions and 496 deletions

View File

@@ -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=="],

View File

@@ -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="
}
}

View File

@@ -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>
)}

View File

@@ -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")}

View File

@@ -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>

View File

@@ -34,6 +34,7 @@
},
"devDependencies": {
"@typescript/native-preview": "catalog:",
"@webgpu/types": "0.1.54",
"typescript": "catalog:",
"wrangler": "4.50.0"
},

View 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;
}

View 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>
)
}

View File

@@ -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: {

View File

@@ -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;
}
}

View File

@@ -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}

View File

@@ -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>

View File

@@ -12,7 +12,7 @@
"allowJs": true,
"strict": true,
"noEmit": true,
"types": ["vite/client"],
"types": ["vite/client", "@webgpu/types"],
"isolatedModules": true,
"paths": {
"~/*": ["./src/*"]

View File

@@ -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();

View File

@@ -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}>

View File

@@ -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(

View File

@@ -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>
}

View File

@@ -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:",

View File

@@ -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);
}
}

View File

@@ -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`,
}}
/>
)}

View File

@@ -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);

View File

@@ -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.