Jelajahi Sumber

feat(console): Update /black plan selection, light rays effect. mobile styles (#8731)

Co-authored-by: Github Action <[email protected]>
Aaron Iker 1 bulan lalu
induk
melakukan
fe58c649cb

+ 4 - 1
bun.lock

@@ -95,6 +95,7 @@
       },
       "devDependencies": {
         "@typescript/native-preview": "catalog:",
+        "@webgpu/types": "0.1.54",
         "typescript": "catalog:",
         "wrangler": "4.50.0",
       },
@@ -1903,7 +1904,7 @@
 
     "@vitest/utils": ["@vitest/[email protected]", "", { "dependencies": { "@vitest/pretty-format": "4.0.16", "tinyrainbow": "^3.0.3" } }, "sha512-h8z9yYhV3e1LEfaQ3zdypIrnAg/9hguReGZoS7Gl0aBG5xgA410zBqECqmaF/+RkTggRsfnzc1XaAHA6bmUufA=="],
 
-    "@webgpu/types": ["@webgpu/[email protected].66", "", {}, "sha512-YA2hLrwLpDsRueNDXIMqN9NTzD6bCDkuXbOSe0heS+f8YE8usA6Gbv1prj81pzVHrbaAma7zObnIC+I6/sXJgA=="],
+    "@webgpu/types": ["@webgpu/[email protected].54", "", {}, "sha512-81oaalC8LFrXjhsczomEQ0u3jG+TqE6V9QHLA8GNZq/Rnot0KDugu3LhSYSlie8tSdooAN1Hov05asrUUp9qgg=="],
 
     "@zip.js/zip.js": ["@zip.js/[email protected]", "", {}, "sha512-OaLvZ8j4gCkLn048ypkZu29KX30r8/OfFF2w4Jo5WXFr+J04J+lzJ5TKZBVgFXhlvSkqNFQdfnY1Q8TMTCyBVA=="],
 
@@ -4291,6 +4292,8 @@
 
     "body-parser/qs": ["[email protected]", "", { "dependencies": { "side-channel": "^1.0.6" } }, "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg=="],
 
+    "bun-webgpu/@webgpu/types": ["@webgpu/[email protected]", "", {}, "sha512-YA2hLrwLpDsRueNDXIMqN9NTzD6bCDkuXbOSe0heS+f8YE8usA6Gbv1prj81pzVHrbaAma7zObnIC+I6/sXJgA=="],
+
     "clean-css/source-map": ["[email protected]", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
 
     "compress-commons/is-stream": ["[email protected]", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="],

+ 1 - 1
nix/hashes.json

@@ -1,6 +1,6 @@
 {
   "nodeModules": {
-    "x86_64-linux": "sha256-4ndHIlS9t1ynRdFszJ1nvcu3YhunhuOc7jcuHI1FbnM=",
+    "x86_64-linux": "sha256-Fl1BdjNSg19LJVSgDMiBX8JuTaGlL2I5T+rqLfjSeO4=",
     "aarch64-darwin": "sha256-C0E9KAEj3GI83HwirIL2zlXYIe92T+7Iv6F51BB6slY="
   }
 }

+ 1 - 0
packages/console/app/package.json

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

+ 186 - 0
packages/console/app/src/component/light-rays.css

@@ -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 - 0
packages/console/app/src/component/light-rays.tsx

@@ -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.15,
+  rayLength: 4.0,
+  sourceWidth: 0.1,
+  pulsating: true,
+  pulsatingMin: 0.9,
+  pulsatingMax: 1.0,
+  fadeDistance: 1.15,
+  saturation: 0.325,
+  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>
+  )
+}

+ 7 - 6
packages/console/app/src/lib/github.ts

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

+ 306 - 262
packages/console/app/src/routes/black.css

@@ -1,3 +1,135 @@
+::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(action-20),
+::view-transition-old(action-100),
+::view-transition-old(action-200) {
+  animation: fade-out 100ms cubic-bezier(0.4, 0, 0.2, 1) forwards;
+}
+
+::view-transition-new(action-20),
+::view-transition-new(action-100),
+::view-transition-new(action-200) {
+  animation: fade-in-up 200ms cubic-bezier(0.16, 1, 0.3, 1) 250ms forwards;
+  opacity: 0;
+}
+
+::view-transition-group(plan-card-20),
+::view-transition-group(plan-card-100),
+::view-transition-group(plan-card-200) {
+  animation-duration: 200ms;
+  animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
+}
+
+::view-transition-image-pair(plan-card-20),
+::view-transition-image-pair(plan-card-100),
+::view-transition-image-pair(plan-card-200) {
+  isolation: isolate;
+}
+
+::view-transition-old(plan-card-20),
+::view-transition-old(plan-card-100),
+::view-transition-old(plan-card-200) {
+  animation: fade-out 120ms cubic-bezier(0.4, 0, 0.2, 1) forwards;
+  mix-blend-mode: normal;
+}
+
+::view-transition-new(plan-card-20),
+::view-transition-new(plan-card-100),
+::view-transition-new(plan-card-200) {
+  animation: fade-in 150ms cubic-bezier(0.4, 0, 0.2, 1) 50ms forwards;
+  opacity: 0;
+  mix-blend-mode: normal;
+}
+
 [data-page="black"] {
   background: #000;
   min-height: 100vh;
@@ -8,13 +140,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 +185,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 +221,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 +258,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 +280,6 @@
           display: inline-block;
         }
       }
-
       [data-slot="subheading"] {
         color: rgba(255, 255, 255, 0.59);
         font-size: 15px;
@@ -142,7 +292,6 @@
           line-height: 160%;
         }
       }
-
       [data-slot="button"] {
         display: inline-flex;
         height: 40px;
@@ -154,7 +303,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 +317,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 +348,99 @@
         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-color: rgba(0, 0, 0, 0.75);
+        backdrop-filter: blur(4px);
+        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"]) {
-          border-color: rgba(255, 255, 255, 0.35);
+        @media (max-width: 480px) {
+          padding: 16px;
         }
 
-        [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;
-          }
+        &:hover:not(:active) {
+          border-color: rgba(255, 255, 255, 0.35);
         }
 
-        &[data-selected="true"] {
-          [data-slot="amount"] {
-            font-size: 22px;
-          }
+        [data-slot="icon"] {
+          color: rgba(255, 255, 255, 0.59);
+        }
 
-          [data-slot="terms"] {
-            animation: reveal 500ms cubic-bezier(0.25, 0, 0.5, 1) forwards;
-          }
+        [data-slot="price"] {
+          display: flex;
+          flex-wrap: wrap;
+          align-items: baseline;
+          gap: 8px;
+        }
 
-          [data-slot="actions"] {
-            [data-slot="continue"] {
-              animation-delay: 200ms;
-            }
-          }
+        [data-slot="amount"] {
+          color: rgba(255, 255, 255, 0.92);
+          font-size: 24px;
+          font-weight: 500;
         }
 
-        &[data-collapsed="true"] {
-          [data-slot="card-trigger"] {
-            padding: 20px 24px;
-          }
+        [data-slot="period"] {
+          color: rgba(255, 255, 255, 0.59);
+          font-size: 14px;
+        }
 
-          [data-slot="plan-header"] {
-            flex-direction: row;
-          }
+        [data-slot="multiplier"] {
+          color: rgba(255, 255, 255, 0.39);
+          font-size: 14px;
 
-          [data-slot="amount"] {
-            font-size: 20px;
+          &::before {
+            content: "·";
+            margin-right: 8px;
           }
         }
+      }
 
-        &[data-selected="false"][data-collapsed="false"] {
-          [data-slot="amount"] {
-            font-size: 22px;
-          }
+      [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);
+        backdrop-filter: blur(4px);
+        z-index: 1;
 
-          [data-slot="period"],
-          [data-slot="multiplier"] {
-            font-size: 14px;
-          }
+        @media (max-width: 480px) {
+          margin: 0 20px;
+          width: calc(100% - 40px);
         }
+      }
 
-        [data-slot="plan-header"] {
-          display: flex;
-          flex-direction: column;
-          width: 100%;
-          gap: 12px;
-          transition: gap 200ms ease;
-        }
+      [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="plan-icon"] {
+        [data-slot="icon"] {
           color: rgba(255, 255, 255, 0.59);
-          flex-shrink: 0;
         }
 
         [data-slot="price"] {
@@ -300,31 +448,22 @@
           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: "·";
@@ -334,32 +473,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 +504,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);
+          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;
+          }
 
-          &:hover {
-            background: var(--surface-raised-base, rgba(255, 255, 255, 0.08));
-            border-color: rgba(255, 255, 255, 0.25);
+          [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-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);
+          [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: rgb(255, 255, 255, 0.9);
+            &:hover {
+              background: rgba(255, 255, 255, 0.9);
+            }
           }
         }
       }
@@ -419,7 +553,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 +571,7 @@
       align-items: center;
       margin-top: -18px;
       width: 100%;
-      max-width: 540px;
+      max-width: 660px;
       padding: 0 20px;
 
       @media (min-width: 768px) {
@@ -491,7 +626,7 @@
 
       [data-slot="multiplier"] {
         color: rgba(255, 255, 255, 0.39);
-        font-size: 13px;
+        font-size: 14px;
 
         &::before {
           content: "·";
@@ -510,39 +645,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 +685,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 +701,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 +796,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 +806,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 +821,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 +833,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 +847,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;
-  }
-}

File diff ditekan karena terlalu besar
+ 49 - 4
packages/console/app/src/routes/black.tsx


+ 128 - 99
packages/console/app/src/routes/black/index.tsx

@@ -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,138 @@ 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>
+          <Match when={selectedPlan()}>
+            {(plan) => (
+              <div data-slot="selected-plan">
+                <div data-slot="selected-card">
+                  <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>
+          <Match when={selectedPlan()}>
+            {(plan) => (
+              <div data-slot="selected-plan" style={{ "view-transition-name": "selected-plan" }}>
+                <div data-slot="selected-card">
+                  <div data-slot="icon">
+                    <PlanIcon plan={plan().id} />
+                  </div>
+                  <p data-slot="price">
+                    <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>

+ 1 - 1
packages/console/app/tsconfig.json

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

+ 1 - 1
packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx

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

Beberapa file tidak ditampilkan karena terlalu banyak file yang berubah dalam diff ini