Add files using upload-large-folder tool
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- packages/console/app/public/apple-touch-icon-v3.png +0 -0
- packages/console/app/public/apple-touch-icon.png +0 -0
- packages/console/app/public/favicon-96x96-v3.png +0 -0
- packages/console/app/public/favicon-96x96.png +0 -0
- packages/console/app/public/favicon-v3.ico +0 -0
- packages/console/app/public/favicon-v3.svg +7 -0
- packages/console/app/public/favicon.ico +0 -0
- packages/console/app/public/favicon.svg +7 -0
- packages/console/app/public/robots.txt +9 -0
- packages/console/app/public/site.webmanifest +24 -0
- packages/console/app/public/social-share-black.png +0 -0
- packages/console/app/public/social-share-zen.png +0 -0
- packages/console/app/public/social-share.png +0 -0
- packages/console/app/public/theme.json +182 -0
- packages/console/app/public/web-app-manifest-192x192.png +0 -0
- packages/console/app/public/web-app-manifest-512x512.png +0 -0
- packages/console/app/script/generate-sitemap.ts +108 -0
- packages/console/app/src/app.css +1 -0
- packages/console/app/src/app.tsx +44 -0
- packages/console/app/src/asset/go-ornate-dark.svg +6 -0
- packages/console/app/src/asset/go-ornate-light.svg +6 -0
- packages/console/app/src/asset/logo-ornate-dark.svg +18 -0
- packages/console/app/src/asset/logo-ornate-light.svg +18 -0
- packages/console/app/src/asset/logo.svg +18 -0
- packages/console/app/src/asset/zen-ornate-dark.svg +8 -0
- packages/console/app/src/asset/zen-ornate-light.svg +8 -0
- packages/console/app/src/component/desktop-promo.css +103 -0
- packages/console/app/src/component/desktop-promo.tsx +60 -0
- packages/console/app/src/component/dropdown.css +80 -0
- packages/console/app/src/component/dropdown.tsx +79 -0
- packages/console/app/src/component/email-signup.tsx +47 -0
- packages/console/app/src/component/faq.tsx +33 -0
- packages/console/app/src/component/footer.tsx +48 -0
- packages/console/app/src/component/go-models.ts +53 -0
- packages/console/app/src/component/go-referral.css +289 -0
- packages/console/app/src/component/go-referral.tsx +306 -0
- packages/console/app/src/component/header-context-menu.css +63 -0
- packages/console/app/src/component/header.tsx +288 -0
- packages/console/app/src/component/icon.tsx +286 -0
- packages/console/app/src/component/language-picker.css +135 -0
- packages/console/app/src/component/language-picker.tsx +40 -0
- packages/console/app/src/component/legal.tsx +28 -0
- packages/console/app/src/component/limits-graph.css +300 -0
- packages/console/app/src/component/limits-graph.tsx +166 -0
- packages/console/app/src/component/locale-links.tsx +36 -0
- packages/console/app/src/component/modal.css +134 -0
- packages/console/app/src/component/modal.tsx +46 -0
- packages/console/app/src/component/rolling-number.tsx +58 -0
- packages/console/app/src/component/spotlight.css +15 -0
- packages/console/app/src/component/spotlight.tsx +820 -0
packages/console/app/public/apple-touch-icon-v3.png
ADDED
|
|
packages/console/app/public/apple-touch-icon.png
ADDED
|
|
packages/console/app/public/favicon-96x96-v3.png
ADDED
|
|
packages/console/app/public/favicon-96x96.png
ADDED
|
|
packages/console/app/public/favicon-v3.ico
ADDED
|
|
packages/console/app/public/favicon-v3.svg
ADDED
|
|
packages/console/app/public/favicon.ico
ADDED
|
|
packages/console/app/public/favicon.svg
ADDED
|
|
packages/console/app/public/robots.txt
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
User-agent: *
|
| 2 |
+
Allow: /
|
| 3 |
+
|
| 4 |
+
# Disallow shared content pages
|
| 5 |
+
Disallow: /s/
|
| 6 |
+
Disallow: /share/
|
| 7 |
+
|
| 8 |
+
Sitemap: https://opencode.ai/sitemap.xml
|
| 9 |
+
Sitemap: https://opencode.ai/data/sitemap.xml
|
packages/console/app/public/site.webmanifest
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "OpenCode",
|
| 3 |
+
"short_name": "OpenCode",
|
| 4 |
+
"id": "/",
|
| 5 |
+
"start_url": "/",
|
| 6 |
+
"scope": "/",
|
| 7 |
+
"icons": [
|
| 8 |
+
{
|
| 9 |
+
"src": "/web-app-manifest-192x192.png",
|
| 10 |
+
"sizes": "192x192",
|
| 11 |
+
"type": "image/png",
|
| 12 |
+
"purpose": "maskable"
|
| 13 |
+
},
|
| 14 |
+
{
|
| 15 |
+
"src": "/web-app-manifest-512x512.png",
|
| 16 |
+
"sizes": "512x512",
|
| 17 |
+
"type": "image/png",
|
| 18 |
+
"purpose": "maskable"
|
| 19 |
+
}
|
| 20 |
+
],
|
| 21 |
+
"theme_color": "#080808",
|
| 22 |
+
"background_color": "#080808",
|
| 23 |
+
"display": "standalone"
|
| 24 |
+
}
|
packages/console/app/public/social-share-black.png
ADDED
|
packages/console/app/public/social-share-zen.png
ADDED
|
packages/console/app/public/social-share.png
ADDED
|
packages/console/app/public/theme.json
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
| 3 |
+
"type": "object",
|
| 4 |
+
"properties": {
|
| 5 |
+
"$schema": {
|
| 6 |
+
"type": "string",
|
| 7 |
+
"description": "JSON schema reference for configuration validation"
|
| 8 |
+
},
|
| 9 |
+
"defs": {
|
| 10 |
+
"type": "object",
|
| 11 |
+
"description": "Color definitions that can be referenced in the theme",
|
| 12 |
+
"patternProperties": {
|
| 13 |
+
"^[a-zA-Z][a-zA-Z0-9_]*$": {
|
| 14 |
+
"oneOf": [
|
| 15 |
+
{
|
| 16 |
+
"type": "string",
|
| 17 |
+
"pattern": "^#[0-9a-fA-F]{6}$",
|
| 18 |
+
"description": "Hex color value"
|
| 19 |
+
},
|
| 20 |
+
{
|
| 21 |
+
"type": "integer",
|
| 22 |
+
"minimum": 0,
|
| 23 |
+
"maximum": 255,
|
| 24 |
+
"description": "ANSI color code (0-255)"
|
| 25 |
+
},
|
| 26 |
+
{
|
| 27 |
+
"type": "string",
|
| 28 |
+
"enum": ["none"],
|
| 29 |
+
"description": "No color (uses terminal default)"
|
| 30 |
+
}
|
| 31 |
+
]
|
| 32 |
+
}
|
| 33 |
+
},
|
| 34 |
+
"additionalProperties": false
|
| 35 |
+
},
|
| 36 |
+
"theme": {
|
| 37 |
+
"type": "object",
|
| 38 |
+
"description": "Theme color definitions",
|
| 39 |
+
"properties": {
|
| 40 |
+
"primary": { "$ref": "#/definitions/colorValue" },
|
| 41 |
+
"secondary": { "$ref": "#/definitions/colorValue" },
|
| 42 |
+
"accent": { "$ref": "#/definitions/colorValue" },
|
| 43 |
+
"error": { "$ref": "#/definitions/colorValue" },
|
| 44 |
+
"warning": { "$ref": "#/definitions/colorValue" },
|
| 45 |
+
"success": { "$ref": "#/definitions/colorValue" },
|
| 46 |
+
"info": { "$ref": "#/definitions/colorValue" },
|
| 47 |
+
"text": { "$ref": "#/definitions/colorValue" },
|
| 48 |
+
"textMuted": { "$ref": "#/definitions/colorValue" },
|
| 49 |
+
"background": { "$ref": "#/definitions/colorValue" },
|
| 50 |
+
"backgroundPanel": { "$ref": "#/definitions/colorValue" },
|
| 51 |
+
"backgroundElement": { "$ref": "#/definitions/colorValue" },
|
| 52 |
+
"border": { "$ref": "#/definitions/colorValue" },
|
| 53 |
+
"borderActive": { "$ref": "#/definitions/colorValue" },
|
| 54 |
+
"borderSubtle": { "$ref": "#/definitions/colorValue" },
|
| 55 |
+
"diffAdded": { "$ref": "#/definitions/colorValue" },
|
| 56 |
+
"diffRemoved": { "$ref": "#/definitions/colorValue" },
|
| 57 |
+
"diffContext": { "$ref": "#/definitions/colorValue" },
|
| 58 |
+
"diffHunkHeader": { "$ref": "#/definitions/colorValue" },
|
| 59 |
+
"diffHighlightAdded": { "$ref": "#/definitions/colorValue" },
|
| 60 |
+
"diffHighlightRemoved": { "$ref": "#/definitions/colorValue" },
|
| 61 |
+
"diffAddedBg": { "$ref": "#/definitions/colorValue" },
|
| 62 |
+
"diffRemovedBg": { "$ref": "#/definitions/colorValue" },
|
| 63 |
+
"diffContextBg": { "$ref": "#/definitions/colorValue" },
|
| 64 |
+
"diffLineNumber": { "$ref": "#/definitions/colorValue" },
|
| 65 |
+
"diffAddedLineNumberBg": { "$ref": "#/definitions/colorValue" },
|
| 66 |
+
"diffRemovedLineNumberBg": { "$ref": "#/definitions/colorValue" },
|
| 67 |
+
"markdownText": { "$ref": "#/definitions/colorValue" },
|
| 68 |
+
"markdownHeading": { "$ref": "#/definitions/colorValue" },
|
| 69 |
+
"markdownLink": { "$ref": "#/definitions/colorValue" },
|
| 70 |
+
"markdownLinkText": { "$ref": "#/definitions/colorValue" },
|
| 71 |
+
"markdownCode": { "$ref": "#/definitions/colorValue" },
|
| 72 |
+
"markdownBlockQuote": { "$ref": "#/definitions/colorValue" },
|
| 73 |
+
"markdownEmph": { "$ref": "#/definitions/colorValue" },
|
| 74 |
+
"markdownStrong": { "$ref": "#/definitions/colorValue" },
|
| 75 |
+
"markdownHorizontalRule": { "$ref": "#/definitions/colorValue" },
|
| 76 |
+
"markdownListItem": { "$ref": "#/definitions/colorValue" },
|
| 77 |
+
"markdownListEnumeration": { "$ref": "#/definitions/colorValue" },
|
| 78 |
+
"markdownImage": { "$ref": "#/definitions/colorValue" },
|
| 79 |
+
"markdownImageText": { "$ref": "#/definitions/colorValue" },
|
| 80 |
+
"markdownCodeBlock": { "$ref": "#/definitions/colorValue" },
|
| 81 |
+
"syntaxComment": { "$ref": "#/definitions/colorValue" },
|
| 82 |
+
"syntaxKeyword": { "$ref": "#/definitions/colorValue" },
|
| 83 |
+
"syntaxFunction": { "$ref": "#/definitions/colorValue" },
|
| 84 |
+
"syntaxVariable": { "$ref": "#/definitions/colorValue" },
|
| 85 |
+
"syntaxString": { "$ref": "#/definitions/colorValue" },
|
| 86 |
+
"syntaxNumber": { "$ref": "#/definitions/colorValue" },
|
| 87 |
+
"syntaxType": { "$ref": "#/definitions/colorValue" },
|
| 88 |
+
"syntaxOperator": { "$ref": "#/definitions/colorValue" },
|
| 89 |
+
"syntaxPunctuation": { "$ref": "#/definitions/colorValue" }
|
| 90 |
+
},
|
| 91 |
+
"required": ["primary", "secondary", "accent", "text", "textMuted", "background"],
|
| 92 |
+
"additionalProperties": false
|
| 93 |
+
}
|
| 94 |
+
},
|
| 95 |
+
"required": ["theme"],
|
| 96 |
+
"additionalProperties": false,
|
| 97 |
+
"definitions": {
|
| 98 |
+
"colorValue": {
|
| 99 |
+
"oneOf": [
|
| 100 |
+
{
|
| 101 |
+
"type": "string",
|
| 102 |
+
"pattern": "^#[0-9a-fA-F]{6}$",
|
| 103 |
+
"description": "Hex color value (same for dark and light)"
|
| 104 |
+
},
|
| 105 |
+
{
|
| 106 |
+
"type": "integer",
|
| 107 |
+
"minimum": 0,
|
| 108 |
+
"maximum": 255,
|
| 109 |
+
"description": "ANSI color code (0-255, same for dark and light)"
|
| 110 |
+
},
|
| 111 |
+
{
|
| 112 |
+
"type": "string",
|
| 113 |
+
"enum": ["none"],
|
| 114 |
+
"description": "No color (uses terminal default)"
|
| 115 |
+
},
|
| 116 |
+
{
|
| 117 |
+
"type": "string",
|
| 118 |
+
"pattern": "^[a-zA-Z][a-zA-Z0-9_]*$",
|
| 119 |
+
"description": "Reference to another color in the theme or defs"
|
| 120 |
+
},
|
| 121 |
+
{
|
| 122 |
+
"type": "object",
|
| 123 |
+
"properties": {
|
| 124 |
+
"dark": {
|
| 125 |
+
"oneOf": [
|
| 126 |
+
{
|
| 127 |
+
"type": "string",
|
| 128 |
+
"pattern": "^#[0-9a-fA-F]{6}$",
|
| 129 |
+
"description": "Hex color value for dark mode"
|
| 130 |
+
},
|
| 131 |
+
{
|
| 132 |
+
"type": "integer",
|
| 133 |
+
"minimum": 0,
|
| 134 |
+
"maximum": 255,
|
| 135 |
+
"description": "ANSI color code for dark mode"
|
| 136 |
+
},
|
| 137 |
+
{
|
| 138 |
+
"type": "string",
|
| 139 |
+
"enum": ["none"],
|
| 140 |
+
"description": "No color (uses terminal default)"
|
| 141 |
+
},
|
| 142 |
+
{
|
| 143 |
+
"type": "string",
|
| 144 |
+
"pattern": "^[a-zA-Z][a-zA-Z0-9_]*$",
|
| 145 |
+
"description": "Reference to another color for dark mode"
|
| 146 |
+
}
|
| 147 |
+
]
|
| 148 |
+
},
|
| 149 |
+
"light": {
|
| 150 |
+
"oneOf": [
|
| 151 |
+
{
|
| 152 |
+
"type": "string",
|
| 153 |
+
"pattern": "^#[0-9a-fA-F]{6}$",
|
| 154 |
+
"description": "Hex color value for light mode"
|
| 155 |
+
},
|
| 156 |
+
{
|
| 157 |
+
"type": "integer",
|
| 158 |
+
"minimum": 0,
|
| 159 |
+
"maximum": 255,
|
| 160 |
+
"description": "ANSI color code for light mode"
|
| 161 |
+
},
|
| 162 |
+
{
|
| 163 |
+
"type": "string",
|
| 164 |
+
"enum": ["none"],
|
| 165 |
+
"description": "No color (uses terminal default)"
|
| 166 |
+
},
|
| 167 |
+
{
|
| 168 |
+
"type": "string",
|
| 169 |
+
"pattern": "^[a-zA-Z][a-zA-Z0-9_]*$",
|
| 170 |
+
"description": "Reference to another color for light mode"
|
| 171 |
+
}
|
| 172 |
+
]
|
| 173 |
+
}
|
| 174 |
+
},
|
| 175 |
+
"required": ["dark", "light"],
|
| 176 |
+
"additionalProperties": false,
|
| 177 |
+
"description": "Separate colors for dark and light modes"
|
| 178 |
+
}
|
| 179 |
+
]
|
| 180 |
+
}
|
| 181 |
+
}
|
| 182 |
+
}
|
packages/console/app/public/web-app-manifest-192x192.png
ADDED
|
packages/console/app/public/web-app-manifest-512x512.png
ADDED
|
packages/console/app/script/generate-sitemap.ts
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env bun
|
| 2 |
+
import { readdir, writeFile } from "fs/promises"
|
| 3 |
+
import { join, dirname } from "path"
|
| 4 |
+
import { fileURLToPath } from "url"
|
| 5 |
+
import { config } from "../src/config.js"
|
| 6 |
+
import { LOCALES, route } from "../src/lib/language.js"
|
| 7 |
+
|
| 8 |
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
| 9 |
+
const BASE_URL = config.baseUrl
|
| 10 |
+
const PUBLIC_DIR = join(__dirname, "../public")
|
| 11 |
+
const DOCS_DIR = join(__dirname, "../../../web/src/content/docs")
|
| 12 |
+
|
| 13 |
+
interface SitemapEntry {
|
| 14 |
+
url: string
|
| 15 |
+
priority: number
|
| 16 |
+
changefreq: string
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
async function getMainRoutes(): Promise<SitemapEntry[]> {
|
| 20 |
+
const routes: SitemapEntry[] = []
|
| 21 |
+
|
| 22 |
+
// Add main static routes
|
| 23 |
+
const staticRoutes = [
|
| 24 |
+
{ path: "/", priority: 1.0, changefreq: "daily" },
|
| 25 |
+
{ path: "/enterprise", priority: 0.8, changefreq: "weekly" },
|
| 26 |
+
{ path: "/brand", priority: 0.6, changefreq: "monthly" },
|
| 27 |
+
{ path: "/zen", priority: 0.8, changefreq: "weekly" },
|
| 28 |
+
{ path: "/go", priority: 0.8, changefreq: "weekly" },
|
| 29 |
+
]
|
| 30 |
+
|
| 31 |
+
for (const item of staticRoutes) {
|
| 32 |
+
for (const locale of LOCALES) {
|
| 33 |
+
routes.push({
|
| 34 |
+
url: `${BASE_URL}${route(locale, item.path)}`,
|
| 35 |
+
priority: item.priority,
|
| 36 |
+
changefreq: item.changefreq,
|
| 37 |
+
})
|
| 38 |
+
}
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
return routes
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
async function getDocsRoutes(): Promise<SitemapEntry[]> {
|
| 45 |
+
const routes: SitemapEntry[] = []
|
| 46 |
+
|
| 47 |
+
try {
|
| 48 |
+
const files = await readdir(DOCS_DIR)
|
| 49 |
+
|
| 50 |
+
for (const file of files) {
|
| 51 |
+
if (!file.endsWith(".mdx")) continue
|
| 52 |
+
|
| 53 |
+
const slug = file.replace(".mdx", "")
|
| 54 |
+
const path = slug === "index" ? "/docs/" : `/docs/${slug}`
|
| 55 |
+
|
| 56 |
+
for (const locale of LOCALES) {
|
| 57 |
+
routes.push({
|
| 58 |
+
url: `${BASE_URL}${route(locale, path)}`,
|
| 59 |
+
priority: slug === "index" ? 0.9 : 0.7,
|
| 60 |
+
changefreq: "weekly",
|
| 61 |
+
})
|
| 62 |
+
}
|
| 63 |
+
}
|
| 64 |
+
} catch (error) {
|
| 65 |
+
console.error("Error reading docs directory:", error)
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
return routes
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
function generateSitemapXML(entries: SitemapEntry[]): string {
|
| 72 |
+
const urls = entries
|
| 73 |
+
.map(
|
| 74 |
+
(entry) => ` <url>
|
| 75 |
+
<loc>${entry.url}</loc>
|
| 76 |
+
<changefreq>${entry.changefreq}</changefreq>
|
| 77 |
+
<priority>${entry.priority}</priority>
|
| 78 |
+
</url>`,
|
| 79 |
+
)
|
| 80 |
+
.join("\n")
|
| 81 |
+
|
| 82 |
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
| 83 |
+
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
| 84 |
+
${urls}
|
| 85 |
+
</urlset>`
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
async function main() {
|
| 89 |
+
console.log("Generating sitemap...")
|
| 90 |
+
|
| 91 |
+
const mainRoutes = await getMainRoutes()
|
| 92 |
+
const docsRoutes = await getDocsRoutes()
|
| 93 |
+
|
| 94 |
+
const allRoutes = [...mainRoutes, ...docsRoutes]
|
| 95 |
+
|
| 96 |
+
console.log(`Found ${mainRoutes.length} main routes`)
|
| 97 |
+
console.log(`Found ${docsRoutes.length} docs routes`)
|
| 98 |
+
console.log(`Total: ${allRoutes.length} routes`)
|
| 99 |
+
|
| 100 |
+
const xml = generateSitemapXML(allRoutes)
|
| 101 |
+
|
| 102 |
+
const outputPath = join(PUBLIC_DIR, "sitemap.xml")
|
| 103 |
+
await writeFile(outputPath, xml, "utf-8")
|
| 104 |
+
|
| 105 |
+
console.log(`✓ Sitemap generated at ${outputPath}`)
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
void main()
|
packages/console/app/src/app.css
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
@import "./style/index.css";
|
packages/console/app/src/app.tsx
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { MetaProvider, Title, Meta } from "@solidjs/meta"
|
| 2 |
+
import { Router } from "@solidjs/router"
|
| 3 |
+
import { FileRoutes } from "@solidjs/start/router"
|
| 4 |
+
import { Suspense } from "solid-js"
|
| 5 |
+
import { Favicon } from "@opencode-ai/ui/favicon"
|
| 6 |
+
import { Font } from "@opencode-ai/ui/font"
|
| 7 |
+
import "@ibm/plex/css/ibm-plex.css"
|
| 8 |
+
import "./app.css"
|
| 9 |
+
import { LanguageProvider } from "~/context/language"
|
| 10 |
+
import { I18nProvider, useI18n } from "~/context/i18n"
|
| 11 |
+
import { strip } from "~/lib/language"
|
| 12 |
+
|
| 13 |
+
function AppMeta() {
|
| 14 |
+
const i18n = useI18n()
|
| 15 |
+
return (
|
| 16 |
+
<>
|
| 17 |
+
<Title>opencode</Title>
|
| 18 |
+
<Meta name="description" content={i18n.t("app.meta.description")} />
|
| 19 |
+
<Favicon />
|
| 20 |
+
<Font />
|
| 21 |
+
</>
|
| 22 |
+
)
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
export default function App() {
|
| 26 |
+
return (
|
| 27 |
+
<Router
|
| 28 |
+
explicitLinks={true}
|
| 29 |
+
transformUrl={strip}
|
| 30 |
+
root={(props) => (
|
| 31 |
+
<LanguageProvider>
|
| 32 |
+
<I18nProvider>
|
| 33 |
+
<MetaProvider>
|
| 34 |
+
<AppMeta />
|
| 35 |
+
<Suspense>{props.children}</Suspense>
|
| 36 |
+
</MetaProvider>
|
| 37 |
+
</I18nProvider>
|
| 38 |
+
</LanguageProvider>
|
| 39 |
+
)}
|
| 40 |
+
>
|
| 41 |
+
<FileRoutes />
|
| 42 |
+
</Router>
|
| 43 |
+
)
|
| 44 |
+
}
|
packages/console/app/src/asset/go-ornate-dark.svg
ADDED
|
|
packages/console/app/src/asset/go-ornate-light.svg
ADDED
|
|
packages/console/app/src/asset/logo-ornate-dark.svg
ADDED
|
|
packages/console/app/src/asset/logo-ornate-light.svg
ADDED
|
|
packages/console/app/src/asset/logo.svg
ADDED
|
|
packages/console/app/src/asset/zen-ornate-dark.svg
ADDED
|
|
packages/console/app/src/asset/zen-ornate-light.svg
ADDED
|
|
packages/console/app/src/component/desktop-promo.css
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[data-component="desktop-promo"] {
|
| 2 |
+
--promo-background: hsl(0, 20%, 99%);
|
| 3 |
+
--promo-background-weak: hsl(0, 8%, 97%);
|
| 4 |
+
--promo-text: hsl(0, 1%, 39%);
|
| 5 |
+
--promo-text-strong: hsl(0, 5%, 12%);
|
| 6 |
+
--promo-border: hsla(0, 100%, 3%, 0.12);
|
| 7 |
+
|
| 8 |
+
position: fixed;
|
| 9 |
+
z-index: 20;
|
| 10 |
+
right: 1.5rem;
|
| 11 |
+
bottom: 1.5rem;
|
| 12 |
+
width: min(28rem, calc(100vw - 2rem));
|
| 13 |
+
padding: 4px;
|
| 14 |
+
overflow: hidden;
|
| 15 |
+
color: var(--promo-text);
|
| 16 |
+
border: 1px solid var(--promo-border);
|
| 17 |
+
border-radius: 8px;
|
| 18 |
+
background: var(--promo-background);
|
| 19 |
+
box-shadow: 0 0.75rem 2rem rgb(0 0 0 / 15%);
|
| 20 |
+
font-family: var(--font-mono);
|
| 21 |
+
|
| 22 |
+
@media (prefers-color-scheme: dark) {
|
| 23 |
+
--promo-background: hsl(0, 9%, 7%);
|
| 24 |
+
--promo-background-weak: hsl(0, 6%, 10%);
|
| 25 |
+
--promo-text: hsl(0, 4%, 71%);
|
| 26 |
+
--promo-text-strong: hsl(0, 15%, 94%);
|
| 27 |
+
--promo-border: hsl(0, 4%, 23%);
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
@media (max-width: 40rem) {
|
| 31 |
+
right: 1rem;
|
| 32 |
+
bottom: 1rem;
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
[data-slot="desktop-promo-link"] {
|
| 36 |
+
display: block;
|
| 37 |
+
color: var(--promo-text);
|
| 38 |
+
text-decoration: none;
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
[data-slot="desktop-promo-link"]:focus-visible {
|
| 42 |
+
outline: 2px solid var(--promo-text-strong);
|
| 43 |
+
outline-offset: -3px;
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
video {
|
| 47 |
+
display: block;
|
| 48 |
+
width: 100%;
|
| 49 |
+
aspect-ratio: 16 / 9;
|
| 50 |
+
object-fit: cover;
|
| 51 |
+
border-radius: 4px;
|
| 52 |
+
background: var(--promo-background-weak);
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
[data-slot="desktop-promo-copy"] {
|
| 56 |
+
display: flex;
|
| 57 |
+
flex-direction: column;
|
| 58 |
+
gap: 0.35rem;
|
| 59 |
+
padding: 1rem;
|
| 60 |
+
font-size: 0.875rem;
|
| 61 |
+
line-height: 1.4;
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
[data-slot="desktop-promo-copy"] strong {
|
| 65 |
+
color: var(--promo-text-strong);
|
| 66 |
+
font-weight: 500;
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
[data-slot="desktop-promo-close"] {
|
| 70 |
+
position: absolute;
|
| 71 |
+
top: 0.5rem;
|
| 72 |
+
right: 0.5rem;
|
| 73 |
+
display: grid;
|
| 74 |
+
width: 2rem;
|
| 75 |
+
height: 2rem;
|
| 76 |
+
padding: 0;
|
| 77 |
+
place-items: center;
|
| 78 |
+
cursor: pointer;
|
| 79 |
+
color: white;
|
| 80 |
+
border: none;
|
| 81 |
+
border-radius: 0.25rem;
|
| 82 |
+
background: rgb(0 0 0 / 70%);
|
| 83 |
+
opacity: 0;
|
| 84 |
+
transition:
|
| 85 |
+
opacity 150ms ease,
|
| 86 |
+
background 150ms ease;
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
&:hover [data-slot="desktop-promo-close"],
|
| 90 |
+
[data-slot="desktop-promo-close"]:focus-visible {
|
| 91 |
+
opacity: 1;
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
[data-slot="desktop-promo-close"]:hover {
|
| 95 |
+
background: rgb(0 0 0 / 90%);
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
@media (hover: none) {
|
| 99 |
+
[data-slot="desktop-promo-close"] {
|
| 100 |
+
opacity: 1;
|
| 101 |
+
}
|
| 102 |
+
}
|
| 103 |
+
}
|
packages/console/app/src/component/desktop-promo.tsx
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import "./desktop-promo.css"
|
| 2 |
+
import { A, useLocation } from "@solidjs/router"
|
| 3 |
+
import { createSignal, Show } from "solid-js"
|
| 4 |
+
import { getRequestEvent } from "solid-js/web"
|
| 5 |
+
import desktopPromoVideo from "~/asset/lander/desktop-tabs-landscape.mp4"
|
| 6 |
+
import { useI18n } from "~/context/i18n"
|
| 7 |
+
import { useLanguage } from "~/context/language"
|
| 8 |
+
import { strip } from "~/lib/language"
|
| 9 |
+
|
| 10 |
+
const DISMISSED_COOKIE = "desktop_promo_dismissed"
|
| 11 |
+
|
| 12 |
+
export function DesktopPromo() {
|
| 13 |
+
const i18n = useI18n()
|
| 14 |
+
const language = useLanguage()
|
| 15 |
+
const location = useLocation()
|
| 16 |
+
const request = getRequestEvent()?.request
|
| 17 |
+
const cookie = request?.headers.get("cookie") ?? (typeof document === "object" ? document.cookie : "")
|
| 18 |
+
const [visible, setVisible] = createSignal(
|
| 19 |
+
!cookie.split(";").some((value) => value.trim() === `${DISMISSED_COOKIE}=1`),
|
| 20 |
+
)
|
| 21 |
+
const hostname = request ? new URL(request.url).hostname : typeof window === "object" ? window.location.hostname : ""
|
| 22 |
+
const primaryHost =
|
| 23 |
+
hostname === "opencode.ai" || hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1"
|
| 24 |
+
|
| 25 |
+
return (
|
| 26 |
+
<Show
|
| 27 |
+
when={
|
| 28 |
+
visible() &&
|
| 29 |
+
primaryHost &&
|
| 30 |
+
strip(location.pathname) !== "/download" &&
|
| 31 |
+
!strip(location.pathname).startsWith("/download/")
|
| 32 |
+
}
|
| 33 |
+
>
|
| 34 |
+
<aside data-component="desktop-promo">
|
| 35 |
+
<A href={language.route("/download")} data-slot="desktop-promo-link">
|
| 36 |
+
<video src={desktopPromoVideo} autoplay playsinline loop muted preload="metadata" aria-hidden="true" />
|
| 37 |
+
<span data-slot="desktop-promo-copy">
|
| 38 |
+
<strong>{i18n.t("home.promo.title")}</strong>
|
| 39 |
+
<span>
|
| 40 |
+
{i18n.t("home.promo.body")} {i18n.t("home.promo.cta")}
|
| 41 |
+
</span>
|
| 42 |
+
</span>
|
| 43 |
+
</A>
|
| 44 |
+
<button
|
| 45 |
+
type="button"
|
| 46 |
+
data-slot="desktop-promo-close"
|
| 47 |
+
onClick={() => {
|
| 48 |
+
document.cookie = `${DISMISSED_COOKIE}=1; Path=/; Max-Age=31536000; SameSite=Lax`
|
| 49 |
+
setVisible(false)
|
| 50 |
+
}}
|
| 51 |
+
>
|
| 52 |
+
<span class="sr-only">{i18n.t("home.promo.close")}</span>
|
| 53 |
+
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" aria-hidden="true">
|
| 54 |
+
<path d="M5 5L15 15M15 5L5 15" stroke="currentColor" stroke-width="1.5" />
|
| 55 |
+
</svg>
|
| 56 |
+
</button>
|
| 57 |
+
</aside>
|
| 58 |
+
</Show>
|
| 59 |
+
)
|
| 60 |
+
}
|
packages/console/app/src/component/dropdown.css
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[data-component="dropdown"] {
|
| 2 |
+
position: relative;
|
| 3 |
+
|
| 4 |
+
[data-slot="trigger"] {
|
| 5 |
+
display: flex;
|
| 6 |
+
align-items: center;
|
| 7 |
+
justify-content: space-between;
|
| 8 |
+
gap: var(--space-2);
|
| 9 |
+
padding: var(--space-2) var(--space-3);
|
| 10 |
+
border: none;
|
| 11 |
+
border-radius: var(--border-radius-sm);
|
| 12 |
+
background-color: transparent;
|
| 13 |
+
color: var(--color-text);
|
| 14 |
+
font-size: var(--font-size-sm);
|
| 15 |
+
font-family: var(--font-sans);
|
| 16 |
+
cursor: pointer;
|
| 17 |
+
transition: all 0.15s ease;
|
| 18 |
+
|
| 19 |
+
&:hover {
|
| 20 |
+
background-color: var(--color-surface-hover);
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
span {
|
| 24 |
+
flex: 1;
|
| 25 |
+
text-align: left;
|
| 26 |
+
font-weight: 500;
|
| 27 |
+
}
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
[data-slot="chevron"] {
|
| 31 |
+
flex-shrink: 0;
|
| 32 |
+
color: var(--color-text-secondary);
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
[data-slot="dropdown"] {
|
| 36 |
+
position: absolute;
|
| 37 |
+
top: 100%;
|
| 38 |
+
z-index: 1000;
|
| 39 |
+
margin-top: var(--space-1);
|
| 40 |
+
border: 1px solid var(--color-border);
|
| 41 |
+
border-radius: var(--border-radius-sm);
|
| 42 |
+
background-color: var(--color-bg);
|
| 43 |
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
| 44 |
+
min-width: 160px;
|
| 45 |
+
|
| 46 |
+
&[data-align="left"] {
|
| 47 |
+
left: 0;
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
&[data-align="right"] {
|
| 51 |
+
right: 0;
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
@media (prefers-color-scheme: dark) {
|
| 55 |
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
| 56 |
+
}
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
[data-slot="item"] {
|
| 60 |
+
display: block;
|
| 61 |
+
width: 100%;
|
| 62 |
+
padding: var(--space-2-5) var(--space-3);
|
| 63 |
+
border: none;
|
| 64 |
+
background: none;
|
| 65 |
+
color: var(--color-text);
|
| 66 |
+
font-size: var(--font-size-sm);
|
| 67 |
+
font-family: var(--font-sans);
|
| 68 |
+
text-align: left;
|
| 69 |
+
cursor: pointer;
|
| 70 |
+
transition: background-color 0.15s ease;
|
| 71 |
+
|
| 72 |
+
&:hover {
|
| 73 |
+
background-color: var(--color-bg-surface);
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
&[data-selected="true"] {
|
| 77 |
+
background-color: var(--color-accent-alpha);
|
| 78 |
+
}
|
| 79 |
+
}
|
| 80 |
+
}
|
packages/console/app/src/component/dropdown.tsx
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { JSX, Show, createEffect, onCleanup } from "solid-js"
|
| 2 |
+
import { createStore } from "solid-js/store"
|
| 3 |
+
import { IconChevron } from "./icon"
|
| 4 |
+
import "./dropdown.css"
|
| 5 |
+
|
| 6 |
+
interface DropdownProps {
|
| 7 |
+
trigger: JSX.Element | string
|
| 8 |
+
children: JSX.Element
|
| 9 |
+
open?: boolean
|
| 10 |
+
onOpenChange?: (open: boolean) => void
|
| 11 |
+
align?: "left" | "right"
|
| 12 |
+
class?: string
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
export function Dropdown(props: DropdownProps) {
|
| 16 |
+
const [store, setStore] = createStore({
|
| 17 |
+
isOpen: props.open ?? false,
|
| 18 |
+
})
|
| 19 |
+
let dropdownRef: HTMLDivElement | undefined
|
| 20 |
+
|
| 21 |
+
createEffect(() => {
|
| 22 |
+
if (props.open !== undefined) {
|
| 23 |
+
setStore("isOpen", props.open)
|
| 24 |
+
}
|
| 25 |
+
})
|
| 26 |
+
|
| 27 |
+
createEffect(() => {
|
| 28 |
+
const handleClickOutside = (event: MouseEvent) => {
|
| 29 |
+
if (dropdownRef && !dropdownRef.contains(event.target as Node)) {
|
| 30 |
+
setStore("isOpen", false)
|
| 31 |
+
props.onOpenChange?.(false)
|
| 32 |
+
}
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
document.addEventListener("click", handleClickOutside)
|
| 36 |
+
onCleanup(() => document.removeEventListener("click", handleClickOutside))
|
| 37 |
+
})
|
| 38 |
+
|
| 39 |
+
const toggle = () => {
|
| 40 |
+
const newValue = !store.isOpen
|
| 41 |
+
setStore("isOpen", newValue)
|
| 42 |
+
props.onOpenChange?.(newValue)
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
return (
|
| 46 |
+
<div data-component="dropdown" class={props.class} ref={dropdownRef}>
|
| 47 |
+
<button data-slot="trigger" type="button" onClick={toggle}>
|
| 48 |
+
{typeof props.trigger === "string" ? <span>{props.trigger}</span> : props.trigger}
|
| 49 |
+
<IconChevron data-slot="chevron" />
|
| 50 |
+
</button>
|
| 51 |
+
|
| 52 |
+
<Show when={store.isOpen}>
|
| 53 |
+
<div data-slot="dropdown" data-align={props.align ?? "left"}>
|
| 54 |
+
{props.children}
|
| 55 |
+
</div>
|
| 56 |
+
</Show>
|
| 57 |
+
</div>
|
| 58 |
+
)
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
interface DropdownItemProps {
|
| 62 |
+
children: JSX.Element
|
| 63 |
+
selected?: boolean
|
| 64 |
+
onClick?: () => void
|
| 65 |
+
type?: "button" | "submit" | "reset"
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
export function DropdownItem(props: DropdownItemProps) {
|
| 69 |
+
return (
|
| 70 |
+
<button
|
| 71 |
+
data-slot="item"
|
| 72 |
+
data-selected={props.selected ?? false}
|
| 73 |
+
type={props.type ?? "button"}
|
| 74 |
+
onClick={props.onClick}
|
| 75 |
+
>
|
| 76 |
+
{props.children}
|
| 77 |
+
</button>
|
| 78 |
+
)
|
| 79 |
+
}
|
packages/console/app/src/component/email-signup.tsx
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { action, useSubmission } from "@solidjs/router"
|
| 2 |
+
import { Resource } from "@opencode-ai/console-resource"
|
| 3 |
+
import { Show } from "solid-js"
|
| 4 |
+
import { useI18n } from "~/context/i18n"
|
| 5 |
+
|
| 6 |
+
const emailSignup = action(async (formData: FormData) => {
|
| 7 |
+
"use server"
|
| 8 |
+
const emailAddress = formData.get("email")!
|
| 9 |
+
const listId = "8b9bb82c-9d5f-11f0-975f-0df6fd1e4945"
|
| 10 |
+
const response = await fetch(`https://api.emailoctopus.com/lists/${listId}/contacts`, {
|
| 11 |
+
method: "PUT",
|
| 12 |
+
headers: {
|
| 13 |
+
Authorization: `Bearer ${Resource.EMAILOCTOPUS_API_KEY.value}`,
|
| 14 |
+
"Content-Type": "application/json",
|
| 15 |
+
},
|
| 16 |
+
body: JSON.stringify({
|
| 17 |
+
email_address: emailAddress,
|
| 18 |
+
}),
|
| 19 |
+
})
|
| 20 |
+
console.log(response)
|
| 21 |
+
return true
|
| 22 |
+
})
|
| 23 |
+
|
| 24 |
+
export function EmailSignup() {
|
| 25 |
+
const submission = useSubmission(emailSignup)
|
| 26 |
+
const i18n = useI18n()
|
| 27 |
+
return (
|
| 28 |
+
<section data-component="email">
|
| 29 |
+
<div data-slot="section-title">
|
| 30 |
+
<h3>{i18n.t("email.title")}</h3>
|
| 31 |
+
<p>{i18n.t("email.subtitle")}</p>
|
| 32 |
+
</div>
|
| 33 |
+
<form data-slot="form" action={emailSignup} method="post">
|
| 34 |
+
<input type="email" name="email" placeholder={i18n.t("email.placeholder")} required />
|
| 35 |
+
<button type="submit" disabled={submission.pending}>
|
| 36 |
+
{i18n.t("email.subscribe")}
|
| 37 |
+
</button>
|
| 38 |
+
</form>
|
| 39 |
+
<Show when={submission.result}>
|
| 40 |
+
<div style="color: #03B000; margin-top: 24px;">{i18n.t("email.success")}</div>
|
| 41 |
+
</Show>
|
| 42 |
+
<Show when={submission.error}>
|
| 43 |
+
<div style="color: #FF408F; margin-top: 24px;">{submission.error}</div>
|
| 44 |
+
</Show>
|
| 45 |
+
</section>
|
| 46 |
+
)
|
| 47 |
+
}
|
packages/console/app/src/component/faq.tsx
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Collapsible } from "@kobalte/core/collapsible"
|
| 2 |
+
import { ParentProps } from "solid-js"
|
| 3 |
+
|
| 4 |
+
export function Faq(props: ParentProps & { question: string }) {
|
| 5 |
+
return (
|
| 6 |
+
<Collapsible data-slot="faq-item">
|
| 7 |
+
<Collapsible.Trigger data-slot="faq-question">
|
| 8 |
+
<svg
|
| 9 |
+
data-slot="faq-icon-plus"
|
| 10 |
+
width="24"
|
| 11 |
+
height="24"
|
| 12 |
+
viewBox="0 0 24 24"
|
| 13 |
+
fill="currentColor"
|
| 14 |
+
xmlns="http://www.w3.org/2000/svg"
|
| 15 |
+
>
|
| 16 |
+
<path d="M12.5 11.5H19V12.5H12.5V19H11.5V12.5H5V11.5H11.5V5H12.5V11.5Z" fill="currentColor" />
|
| 17 |
+
</svg>
|
| 18 |
+
<svg
|
| 19 |
+
data-slot="faq-icon-minus"
|
| 20 |
+
width="24"
|
| 21 |
+
height="24"
|
| 22 |
+
viewBox="0 0 24 24"
|
| 23 |
+
fill="currentColor"
|
| 24 |
+
xmlns="http://www.w3.org/2000/svg"
|
| 25 |
+
>
|
| 26 |
+
<path d="M5 11.5H19V12.5H5Z" fill="currentColor" />
|
| 27 |
+
</svg>
|
| 28 |
+
<div data-slot="faq-question-text">{props.question}</div>
|
| 29 |
+
</Collapsible.Trigger>
|
| 30 |
+
<Collapsible.Content data-slot="faq-answer">{props.children}</Collapsible.Content>
|
| 31 |
+
</Collapsible>
|
| 32 |
+
)
|
| 33 |
+
}
|
packages/console/app/src/component/footer.tsx
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { createAsync } from "@solidjs/router"
|
| 2 |
+
import { createMemo } from "solid-js"
|
| 3 |
+
import { github } from "~/lib/github"
|
| 4 |
+
import { config } from "~/config"
|
| 5 |
+
import { useLanguage } from "~/context/language"
|
| 6 |
+
import { useI18n } from "~/context/i18n"
|
| 7 |
+
|
| 8 |
+
export function Footer() {
|
| 9 |
+
const language = useLanguage()
|
| 10 |
+
const i18n = useI18n()
|
| 11 |
+
const community = createMemo(() => {
|
| 12 |
+
const locale = language.locale()
|
| 13 |
+
return locale === "zh" || locale === "zht"
|
| 14 |
+
? ({ key: "footer.feishu", link: language.route("/feishu") } as const)
|
| 15 |
+
: ({ key: "footer.discord", link: language.route("/discord") } as const)
|
| 16 |
+
})
|
| 17 |
+
const githubData = createAsync(() => github())
|
| 18 |
+
const starCount = createMemo(() =>
|
| 19 |
+
githubData()?.stars
|
| 20 |
+
? new Intl.NumberFormat(language.tag(language.locale()), {
|
| 21 |
+
notation: "compact",
|
| 22 |
+
compactDisplay: "short",
|
| 23 |
+
}).format(githubData()!.stars!)
|
| 24 |
+
: config.github.starsFormatted.compact,
|
| 25 |
+
)
|
| 26 |
+
|
| 27 |
+
return (
|
| 28 |
+
<footer data-component="footer">
|
| 29 |
+
<div data-slot="cell">
|
| 30 |
+
<a href={config.github.repoUrl} target="_blank">
|
| 31 |
+
{i18n.t("footer.github")} <span>[{starCount()}]</span>
|
| 32 |
+
</a>
|
| 33 |
+
</div>
|
| 34 |
+
<div data-slot="cell">
|
| 35 |
+
<a href={language.route("/docs")}>{i18n.t("footer.docs")}</a>
|
| 36 |
+
</div>
|
| 37 |
+
<div data-slot="cell">
|
| 38 |
+
<a href={language.route("/changelog")}>{i18n.t("footer.changelog")}</a>
|
| 39 |
+
</div>
|
| 40 |
+
<div data-slot="cell">
|
| 41 |
+
<a href={community().link}>{i18n.t(community().key)}</a>
|
| 42 |
+
</div>
|
| 43 |
+
<div data-slot="cell">
|
| 44 |
+
<a href={config.social.twitter}>{i18n.t("footer.x")}</a>
|
| 45 |
+
</div>
|
| 46 |
+
</footer>
|
| 47 |
+
)
|
| 48 |
+
}
|
packages/console/app/src/component/go-models.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Requests are per 5 hours; allowances are monthly usage at official API prices.
|
| 2 |
+
export const goModels = [
|
| 3 |
+
{ id: "kimi-k3", name: "Kimi K3", requests: 110, allowance: 15, featured: true },
|
| 4 |
+
{ id: "qwen3.8-max", name: "Qwen3.8 Max", requests: 160, allowance: 15 },
|
| 5 |
+
{ id: "grok-4.6", name: "Grok 4.6", requests: 169, allowance: 15 },
|
| 6 |
+
{ id: "qwen3.7-max", name: "Qwen3.7 Max", requests: 170, allowance: 30 },
|
| 7 |
+
{ id: "glm-5.3", name: "GLM-5.3", requests: 220, allowance: 15 },
|
| 8 |
+
{ id: "glm-5.2", name: "GLM-5.2", requests: 880, allowance: 60 },
|
| 9 |
+
{ id: "glm-5.1", name: "GLM-5.1", requests: 880, allowance: 60 },
|
| 10 |
+
{ id: "deepseek-v4-pro", name: "DeepSeek V4 Pro", requests: 1050, allowance: 15 },
|
| 11 |
+
{ id: "kimi-k2.6", name: "Kimi K2.6", requests: 1150, allowance: 60 },
|
| 12 |
+
{ id: "kimi-k2.7-code", name: "Kimi K2.7 Code", requests: 1350, allowance: 60, featured: true },
|
| 13 |
+
{ id: "hy4-preview", name: "Hy4 preview", requests: 1350, allowance: 30 },
|
| 14 |
+
{ id: "gpt-5.6-luna", name: "GPT 5.6 Luna", requests: 2050, allowance: 15, featured: true },
|
| 15 |
+
{ id: "minimax-m3", name: "MiniMax M3", requests: 3200, allowance: 60, featured: true },
|
| 16 |
+
{ id: "mimo-v2.5-pro", name: "MiMo-V2.5-Pro", requests: 3250, allowance: 15 },
|
| 17 |
+
{ id: "qwen3.6-plus", name: "Qwen3.6 Plus", requests: 3300, allowance: 60 },
|
| 18 |
+
{ id: "minimax-m2.7", name: "MiniMax M2.7", requests: 3400, allowance: 60 },
|
| 19 |
+
{ id: "deepseek-v4-flash-vision-exp", name: "DeepSeek V4 Flash Vision Exp", requests: 6500, allowance: 15 },
|
| 20 |
+
{ id: "qwen3.7-plus", name: "Qwen3.7 Plus", requests: 4300, allowance: 60, featured: true },
|
| 21 |
+
{ id: "hy3", name: "Hy3", requests: 4300, allowance: 60 },
|
| 22 |
+
{ id: "qwen3.8-flash", name: "Qwen3.8 Flash", requests: 5400, allowance: 30 },
|
| 23 |
+
{ id: "glm-5.3-flash", name: "GLM-5.3-Flash", requests: 6320, allowance: 60, featured: true },
|
| 24 |
+
{ id: "deepseek-v4-flash", name: "DeepSeek V4 Flash", requests: 13000, allowance: 30, featured: true },
|
| 25 |
+
{
|
| 26 |
+
id: "deepseek-flash",
|
| 27 |
+
name: "DeepSeek V4.1 Flash",
|
| 28 |
+
requests: 26000,
|
| 29 |
+
baseRequests: 6500,
|
| 30 |
+
allowance: 60,
|
| 31 |
+
baseAllowance: 15,
|
| 32 |
+
bonus: 4,
|
| 33 |
+
featured: true,
|
| 34 |
+
fresh: true,
|
| 35 |
+
},
|
| 36 |
+
{ id: "longcat-2.0", name: "LongCat-2.0", requests: 11400, allowance: 60 },
|
| 37 |
+
{ id: "mimo-v2.5", name: "MiMo-V2.5", requests: 30100, allowance: 60, featured: true },
|
| 38 |
+
{
|
| 39 |
+
id: "muse-spark-1.3-contributor",
|
| 40 |
+
name: "Muse Spark 1.3 Contributor",
|
| 41 |
+
requests: 45300,
|
| 42 |
+
allowance: 60,
|
| 43 |
+
featured: true,
|
| 44 |
+
regions: true,
|
| 45 |
+
},
|
| 46 |
+
{
|
| 47 |
+
id: "muse-spark-1.2-contributor",
|
| 48 |
+
name: "Muse Spark 1.2 Contributor",
|
| 49 |
+
requests: 45300,
|
| 50 |
+
allowance: 60,
|
| 51 |
+
regions: true,
|
| 52 |
+
},
|
| 53 |
+
].sort((a, b) => a.requests - b.requests)
|
packages/console/app/src/component/go-referral.css
ADDED
|
@@ -0,0 +1,289 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[data-component="go-credit-confirm"] {
|
| 2 |
+
display: flex;
|
| 3 |
+
flex-direction: column;
|
| 4 |
+
gap: var(--space-4);
|
| 5 |
+
min-width: min(34rem, calc(100vw - var(--space-8)));
|
| 6 |
+
|
| 7 |
+
p {
|
| 8 |
+
margin: 0;
|
| 9 |
+
color: var(--color-text-secondary);
|
| 10 |
+
font-size: var(--font-size-sm);
|
| 11 |
+
line-height: 1.6;
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
[data-slot="usage-preview"] {
|
| 15 |
+
display: flex;
|
| 16 |
+
flex-direction: column;
|
| 17 |
+
gap: var(--space-5);
|
| 18 |
+
padding: var(--space-4);
|
| 19 |
+
border: 1px solid var(--color-border);
|
| 20 |
+
border-radius: var(--border-radius-sm);
|
| 21 |
+
background-color: var(--color-bg-surface);
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
[data-slot="usage-preview-item"] {
|
| 25 |
+
display: flex;
|
| 26 |
+
flex-direction: column;
|
| 27 |
+
gap: var(--space-2);
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
[data-slot="usage-preview-header"] {
|
| 31 |
+
display: flex;
|
| 32 |
+
align-items: baseline;
|
| 33 |
+
justify-content: space-between;
|
| 34 |
+
gap: var(--space-3);
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
[data-slot="usage-preview-label"] {
|
| 38 |
+
color: var(--color-text);
|
| 39 |
+
font-size: var(--font-size-sm);
|
| 40 |
+
font-weight: 500;
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
[data-slot="usage-preview-value"] {
|
| 44 |
+
display: inline-flex;
|
| 45 |
+
align-items: center;
|
| 46 |
+
gap: var(--space-1);
|
| 47 |
+
color: var(--color-text-muted);
|
| 48 |
+
font-family: var(--font-mono);
|
| 49 |
+
font-size: var(--font-size-xs);
|
| 50 |
+
white-space: nowrap;
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
[data-slot="usage-preview-after-value"] {
|
| 54 |
+
color: var(--color-accent);
|
| 55 |
+
font-weight: 600;
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
[data-slot="usage-preview-progress"] {
|
| 59 |
+
position: relative;
|
| 60 |
+
height: 8px;
|
| 61 |
+
overflow: hidden;
|
| 62 |
+
border-radius: var(--border-radius-sm);
|
| 63 |
+
background-color: var(--color-bg);
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
[data-slot="usage-preview-before"],
|
| 67 |
+
[data-slot="usage-preview-after"] {
|
| 68 |
+
position: absolute;
|
| 69 |
+
top: 0;
|
| 70 |
+
bottom: 0;
|
| 71 |
+
left: 0;
|
| 72 |
+
border-radius: var(--border-radius-sm);
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
[data-slot="usage-preview-before"] {
|
| 76 |
+
background-color: var(--color-border);
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
[data-slot="usage-preview-after"] {
|
| 80 |
+
background-color: var(--color-accent);
|
| 81 |
+
transition: width 0.35s ease;
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
[data-slot="usage-preview-reset"] {
|
| 85 |
+
color: var(--color-text-muted);
|
| 86 |
+
font-size: var(--font-size-xs);
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
[data-slot="modal-actions"] {
|
| 90 |
+
display: flex;
|
| 91 |
+
justify-content: flex-end;
|
| 92 |
+
gap: var(--space-3);
|
| 93 |
+
}
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
[data-slot="invite-link-box"] {
|
| 97 |
+
display: flex;
|
| 98 |
+
flex-direction: column;
|
| 99 |
+
gap: var(--space-3);
|
| 100 |
+
|
| 101 |
+
> div {
|
| 102 |
+
display: flex;
|
| 103 |
+
align-items: center;
|
| 104 |
+
gap: var(--space-3);
|
| 105 |
+
border-radius: var(--border-radius-sm);
|
| 106 |
+
|
| 107 |
+
@media (max-width: 40rem) {
|
| 108 |
+
align-items: stretch;
|
| 109 |
+
flex-direction: column;
|
| 110 |
+
}
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
code {
|
| 114 |
+
flex: 1;
|
| 115 |
+
min-width: 0;
|
| 116 |
+
padding: var(--space-3);
|
| 117 |
+
border: 1px solid var(--color-border);
|
| 118 |
+
border-radius: var(--border-radius-sm);
|
| 119 |
+
background-color: var(--color-bg);
|
| 120 |
+
color: var(--color-text);
|
| 121 |
+
font-family: var(--font-mono);
|
| 122 |
+
font-size: var(--font-size-sm);
|
| 123 |
+
line-height: 1.4;
|
| 124 |
+
overflow: hidden;
|
| 125 |
+
text-overflow: ellipsis;
|
| 126 |
+
white-space: nowrap;
|
| 127 |
+
|
| 128 |
+
@media (max-width: 40rem) {
|
| 129 |
+
padding: var(--space-2-5);
|
| 130 |
+
font-size: var(--font-size-xs);
|
| 131 |
+
}
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
button {
|
| 135 |
+
display: inline-flex;
|
| 136 |
+
align-items: center;
|
| 137 |
+
justify-content: center;
|
| 138 |
+
gap: var(--space-2);
|
| 139 |
+
min-width: 130px;
|
| 140 |
+
white-space: nowrap;
|
| 141 |
+
|
| 142 |
+
@media (max-width: 40rem) {
|
| 143 |
+
min-width: 96px;
|
| 144 |
+
padding: var(--space-2-5) var(--space-3);
|
| 145 |
+
font-size: var(--font-size-xs);
|
| 146 |
+
}
|
| 147 |
+
}
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
[data-slot="instructions"] {
|
| 151 |
+
display: flex;
|
| 152 |
+
flex-direction: column;
|
| 153 |
+
gap: var(--space-3);
|
| 154 |
+
|
| 155 |
+
ol {
|
| 156 |
+
display: flex;
|
| 157 |
+
flex-direction: column;
|
| 158 |
+
gap: var(--space-2);
|
| 159 |
+
margin: 0;
|
| 160 |
+
padding-left: 0;
|
| 161 |
+
color: var(--color-text-secondary);
|
| 162 |
+
font-size: var(--font-size-md);
|
| 163 |
+
list-style-position: inside;
|
| 164 |
+
line-height: 1.5;
|
| 165 |
+
}
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
[data-component="go-referral-section"] {
|
| 169 |
+
[data-component="go-referral-overview"] {
|
| 170 |
+
display: flex;
|
| 171 |
+
flex-direction: column;
|
| 172 |
+
gap: var(--space-8);
|
| 173 |
+
padding: var(--space-6);
|
| 174 |
+
border: 1px dashed var(--color-border);
|
| 175 |
+
border-radius: var(--border-radius-sm);
|
| 176 |
+
background-color: var(--color-bg-surface);
|
| 177 |
+
|
| 178 |
+
@media (max-width: 30rem) {
|
| 179 |
+
gap: var(--space-8);
|
| 180 |
+
padding: var(--space-4);
|
| 181 |
+
}
|
| 182 |
+
}
|
| 183 |
+
|
| 184 |
+
[data-component="go-referral-overview"] + [data-slot="section-title"] {
|
| 185 |
+
margin-top: var(--space-4);
|
| 186 |
+
}
|
| 187 |
+
|
| 188 |
+
[data-slot="referrals-table"] {
|
| 189 |
+
overflow-x: auto;
|
| 190 |
+
}
|
| 191 |
+
|
| 192 |
+
[data-component="empty-state"] {
|
| 193 |
+
padding: var(--space-4);
|
| 194 |
+
border: 1px solid var(--color-border);
|
| 195 |
+
border-radius: var(--border-radius-sm);
|
| 196 |
+
background-color: var(--color-bg-surface);
|
| 197 |
+
color: var(--color-text-muted);
|
| 198 |
+
font-size: var(--font-size-sm);
|
| 199 |
+
}
|
| 200 |
+
|
| 201 |
+
[data-slot="referrals-table-element"] {
|
| 202 |
+
width: 100%;
|
| 203 |
+
border-collapse: collapse;
|
| 204 |
+
font-size: var(--font-size-sm);
|
| 205 |
+
|
| 206 |
+
thead {
|
| 207 |
+
border-bottom: 1px solid var(--color-border);
|
| 208 |
+
}
|
| 209 |
+
|
| 210 |
+
th {
|
| 211 |
+
padding: var(--space-3) var(--space-4);
|
| 212 |
+
text-align: left;
|
| 213 |
+
font-weight: normal;
|
| 214 |
+
color: var(--color-text-muted);
|
| 215 |
+
text-transform: uppercase;
|
| 216 |
+
|
| 217 |
+
&:nth-child(1) {
|
| 218 |
+
width: 120px;
|
| 219 |
+
}
|
| 220 |
+
|
| 221 |
+
&:nth-child(3) {
|
| 222 |
+
width: 180px;
|
| 223 |
+
}
|
| 224 |
+
|
| 225 |
+
&:nth-child(4) {
|
| 226 |
+
width: 140px;
|
| 227 |
+
}
|
| 228 |
+
}
|
| 229 |
+
|
| 230 |
+
td {
|
| 231 |
+
padding: var(--space-3) var(--space-4);
|
| 232 |
+
border-bottom: 1px solid var(--color-border-muted);
|
| 233 |
+
color: var(--color-text-muted);
|
| 234 |
+
font-family: var(--font-mono);
|
| 235 |
+
|
| 236 |
+
&[data-slot="referral-amount"] {
|
| 237 |
+
color: var(--color-text);
|
| 238 |
+
font-weight: 500;
|
| 239 |
+
}
|
| 240 |
+
|
| 241 |
+
&[data-slot="referral-source"] {
|
| 242 |
+
color: var(--color-text-secondary);
|
| 243 |
+
font-family: var(--font-sans);
|
| 244 |
+
white-space: nowrap;
|
| 245 |
+
}
|
| 246 |
+
|
| 247 |
+
&[data-slot="referral-action"] {
|
| 248 |
+
text-align: right;
|
| 249 |
+
font-family: var(--font-sans);
|
| 250 |
+
white-space: nowrap;
|
| 251 |
+
|
| 252 |
+
button {
|
| 253 |
+
min-width: 96px;
|
| 254 |
+
}
|
| 255 |
+
}
|
| 256 |
+
}
|
| 257 |
+
|
| 258 |
+
tbody tr {
|
| 259 |
+
&[data-status="applied"] {
|
| 260 |
+
td:not([data-slot="referral-action"]) {
|
| 261 |
+
opacity: 0.68;
|
| 262 |
+
}
|
| 263 |
+
}
|
| 264 |
+
|
| 265 |
+
&[data-status="pending"] {
|
| 266 |
+
td[data-slot="referral-amount"],
|
| 267 |
+
td[data-slot="referral-date"] {
|
| 268 |
+
color: var(--color-text-muted);
|
| 269 |
+
}
|
| 270 |
+
|
| 271 |
+
td[data-slot="referral-source"] {
|
| 272 |
+
color: var(--color-text);
|
| 273 |
+
}
|
| 274 |
+
}
|
| 275 |
+
|
| 276 |
+
&:last-child td {
|
| 277 |
+
border-bottom: none;
|
| 278 |
+
}
|
| 279 |
+
}
|
| 280 |
+
|
| 281 |
+
@media (max-width: 40rem) {
|
| 282 |
+
th,
|
| 283 |
+
td {
|
| 284 |
+
padding: var(--space-2) var(--space-3);
|
| 285 |
+
font-size: var(--font-size-xs);
|
| 286 |
+
}
|
| 287 |
+
}
|
| 288 |
+
}
|
| 289 |
+
}
|
packages/console/app/src/component/go-referral.tsx
ADDED
|
@@ -0,0 +1,306 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { action, json, query, useAction, useSubmission } from "@solidjs/router"
|
| 2 |
+
import { createEffect, createMemo, createSignal, For, onCleanup, Show } from "solid-js"
|
| 3 |
+
import { getRequestEvent } from "solid-js/web"
|
| 4 |
+
import { Referral } from "@opencode-ai/console-core/referral.js"
|
| 5 |
+
import { withActor } from "~/context/auth.withActor"
|
| 6 |
+
import { Modal } from "~/component/modal"
|
| 7 |
+
import { IconCheck, IconCopy } from "~/component/icon"
|
| 8 |
+
import { useI18n } from "~/context/i18n"
|
| 9 |
+
import { useLanguage } from "~/context/language"
|
| 10 |
+
import { formatResetTime, liteResetTimeKeys } from "~/lib/format-reset-time"
|
| 11 |
+
import { queryLiteSubscription } from "~/routes/workspace/[id]/go/lite-section"
|
| 12 |
+
import { createReferralFromCookie } from "~/lib/referral-invite"
|
| 13 |
+
import "./go-referral.css"
|
| 14 |
+
|
| 15 |
+
type GoReferralSummary = Awaited<ReturnType<typeof Referral.summary>>
|
| 16 |
+
type GoReferralReward = GoReferralSummary["rewards"][number]
|
| 17 |
+
type GoLiteSubscription = Awaited<ReturnType<typeof queryLiteSubscription>>
|
| 18 |
+
type GoReferralUsagePreview = NonNullable<Awaited<ReturnType<typeof Referral.usagePreview>>>
|
| 19 |
+
type GoReferralUsagePreviewItem = GoReferralUsagePreview["rollingUsage"]
|
| 20 |
+
|
| 21 |
+
const emptyUsagePreview = {
|
| 22 |
+
rollingUsage: { beforePercent: 0, afterPercent: 0, resetInSec: 0 },
|
| 23 |
+
weeklyUsage: { beforePercent: 0, afterPercent: 0, resetInSec: 0 },
|
| 24 |
+
monthlyUsage: { beforePercent: 0, afterPercent: 0, resetInSec: 0 },
|
| 25 |
+
} satisfies GoReferralUsagePreview
|
| 26 |
+
|
| 27 |
+
export const queryGoReferral = query(async (workspaceID: string) => {
|
| 28 |
+
"use server"
|
| 29 |
+
return withActor(async () => {
|
| 30 |
+
await createReferralFromCookie()
|
| 31 |
+
return Referral.summary()
|
| 32 |
+
}, workspaceID)
|
| 33 |
+
}, "go.referral.get")
|
| 34 |
+
|
| 35 |
+
export const queryGoReferralUsagePreview = query(async (workspaceID: string, referralID?: string) => {
|
| 36 |
+
"use server"
|
| 37 |
+
if (!referralID) return null
|
| 38 |
+
return withActor(() => Referral.usagePreview({ referralID }), workspaceID)
|
| 39 |
+
}, "go.referral.usagePreview")
|
| 40 |
+
|
| 41 |
+
export const applyGoReferralReward = action(async (workspaceID: string, referralID: string) => {
|
| 42 |
+
"use server"
|
| 43 |
+
return json(await withActor(() => Referral.applyReward({ referralID }), workspaceID), {
|
| 44 |
+
revalidate: [queryGoReferral.key, queryGoReferralUsagePreview.key, queryLiteSubscription.key],
|
| 45 |
+
})
|
| 46 |
+
}, "go.referral.reward.apply")
|
| 47 |
+
|
| 48 |
+
function currentUsagePreview(usage: { resetInSec: number; usagePercent: number }) {
|
| 49 |
+
return {
|
| 50 |
+
beforePercent: usage.usagePercent,
|
| 51 |
+
afterPercent: usage.usagePercent,
|
| 52 |
+
resetInSec: usage.resetInSec,
|
| 53 |
+
}
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
function formatCurrency(amount: number) {
|
| 57 |
+
if (amount % 100 === 0) return `$${amount / 100}`
|
| 58 |
+
return `$${(amount / 100).toFixed(2)}`
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
function formatDate(value: string | Date, locale: string) {
|
| 62 |
+
return new Intl.DateTimeFormat(locale, { month: "short", day: "numeric", year: "numeric" }).format(new Date(value))
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
function rewardDescriptionKey(source: GoReferralReward["source"]) {
|
| 66 |
+
if (source === "invitee") return "workspace.referral.reward.description.invitee" as const
|
| 67 |
+
return "workspace.referral.reward.description.inviter" as const
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
function rewardActionKey(reward: GoReferralReward, hasActiveGo: boolean) {
|
| 71 |
+
if (reward.status === "applied") return "workspace.referral.reward.action.applied" as const
|
| 72 |
+
if (reward.status === "pending" && reward.source === "inviter")
|
| 73 |
+
return "workspace.referral.reward.source.pendingInviter" as const
|
| 74 |
+
if (reward.status === "pending" || !hasActiveGo) return "workspace.referral.reward.action.subscribeUnlock" as const
|
| 75 |
+
return "workspace.referral.reward.action.view" as const
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
function CopyInviteLink(props: { summary: GoReferralSummary }) {
|
| 79 |
+
const i18n = useI18n()
|
| 80 |
+
const [copied, setCopied] = createSignal(false)
|
| 81 |
+
const event = getRequestEvent()
|
| 82 |
+
const origin = event
|
| 83 |
+
? new URL(event.request.url).origin
|
| 84 |
+
: typeof window === "object"
|
| 85 |
+
? window.location.origin
|
| 86 |
+
: undefined
|
| 87 |
+
const inviteUrl = createMemo(() => {
|
| 88 |
+
const path = `/go?ref=${props.summary.referralCode}`
|
| 89 |
+
if (!origin) return path
|
| 90 |
+
return new URL(path, origin).toString()
|
| 91 |
+
})
|
| 92 |
+
|
| 93 |
+
async function copy() {
|
| 94 |
+
if (typeof navigator !== "object") return
|
| 95 |
+
await navigator.clipboard.writeText(inviteUrl())
|
| 96 |
+
setCopied(true)
|
| 97 |
+
window.setTimeout(() => setCopied(false), 1600)
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
return (
|
| 101 |
+
<div data-slot="invite-link-box">
|
| 102 |
+
<div>
|
| 103 |
+
<code title={inviteUrl()}>{inviteUrl()}</code>
|
| 104 |
+
<button type="button" onClick={copy}>
|
| 105 |
+
<Show
|
| 106 |
+
when={copied()}
|
| 107 |
+
fallback={
|
| 108 |
+
<>
|
| 109 |
+
<IconCopy style={{ width: "16px", height: "16px" }} /> {i18n.t("workspace.referral.copyLink")}
|
| 110 |
+
</>
|
| 111 |
+
}
|
| 112 |
+
>
|
| 113 |
+
<IconCheck style={{ width: "16px", height: "16px" }} /> {i18n.t("workspace.referral.copied")}
|
| 114 |
+
</Show>
|
| 115 |
+
</button>
|
| 116 |
+
</div>
|
| 117 |
+
</div>
|
| 118 |
+
)
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
export function GoReferralSection(props: {
|
| 122 |
+
workspaceID: string
|
| 123 |
+
summary: GoReferralSummary
|
| 124 |
+
lite: GoLiteSubscription | undefined
|
| 125 |
+
}) {
|
| 126 |
+
const i18n = useI18n()
|
| 127 |
+
const language = useLanguage()
|
| 128 |
+
const apply = useAction(applyGoReferralReward)
|
| 129 |
+
const submission = useSubmission(applyGoReferralReward)
|
| 130 |
+
const [selected, setSelected] = createSignal<GoReferralReward>()
|
| 131 |
+
const [preview, setPreview] = createSignal<GoReferralUsagePreview | null>()
|
| 132 |
+
const displayPreview = createMemo(() => {
|
| 133 |
+
const loaded = preview()
|
| 134 |
+
if (loaded) return loaded
|
| 135 |
+
const current = props.lite
|
| 136 |
+
if (!current) return emptyUsagePreview
|
| 137 |
+
return {
|
| 138 |
+
rollingUsage: currentUsagePreview(current.rollingUsage),
|
| 139 |
+
weeklyUsage: currentUsagePreview(current.weeklyUsage),
|
| 140 |
+
monthlyUsage: currentUsagePreview(current.monthlyUsage),
|
| 141 |
+
} satisfies GoReferralUsagePreview
|
| 142 |
+
})
|
| 143 |
+
createEffect(() => {
|
| 144 |
+
const reward = selected()
|
| 145 |
+
if (!reward) {
|
| 146 |
+
setPreview(undefined)
|
| 147 |
+
return
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
const request = { cancelled: false }
|
| 151 |
+
setPreview(undefined)
|
| 152 |
+
queryGoReferralUsagePreview(props.workspaceID, reward.id).then((result) => {
|
| 153 |
+
if (request.cancelled) return
|
| 154 |
+
setPreview(result)
|
| 155 |
+
})
|
| 156 |
+
onCleanup(() => {
|
| 157 |
+
request.cancelled = true
|
| 158 |
+
})
|
| 159 |
+
})
|
| 160 |
+
|
| 161 |
+
async function onApply() {
|
| 162 |
+
const reward = selected()
|
| 163 |
+
if (!reward) return
|
| 164 |
+
await apply(props.workspaceID, reward.id)
|
| 165 |
+
setSelected(undefined)
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
return (
|
| 169 |
+
<>
|
| 170 |
+
<Show when={props.lite || props.summary.hasReferral}>
|
| 171 |
+
<section data-component="go-referral-section">
|
| 172 |
+
<Show when={props.lite}>
|
| 173 |
+
<div data-slot="section-title">
|
| 174 |
+
<h2>{i18n.t("workspace.referral.overview.title")}</h2>
|
| 175 |
+
<p>{i18n.t("workspace.referral.overview.subtitle")}</p>
|
| 176 |
+
</div>
|
| 177 |
+
<div data-component="go-referral-overview">
|
| 178 |
+
<CopyInviteLink summary={props.summary} />
|
| 179 |
+
<div data-slot="instructions">
|
| 180 |
+
<ol>
|
| 181 |
+
<li>{i18n.t("workspace.referral.instructions.share")}</li>
|
| 182 |
+
<li>{i18n.t("workspace.referral.instructions.subscribe")}</li>
|
| 183 |
+
<li>{i18n.t("workspace.referral.instructions.claim")}</li>
|
| 184 |
+
</ol>
|
| 185 |
+
</div>
|
| 186 |
+
</div>
|
| 187 |
+
</Show>
|
| 188 |
+
<Show when={props.summary.hasReferral}>
|
| 189 |
+
<div data-slot="section-title">
|
| 190 |
+
<h2>{i18n.t("workspace.referral.rewards.title")}</h2>
|
| 191 |
+
<p>{i18n.t("workspace.referral.rewards.description")}</p>
|
| 192 |
+
</div>
|
| 193 |
+
<div data-slot="referrals-table">
|
| 194 |
+
<table data-slot="referrals-table-element">
|
| 195 |
+
<thead>
|
| 196 |
+
<tr>
|
| 197 |
+
<th>{i18n.t("workspace.referral.table.reward")}</th>
|
| 198 |
+
<th>{i18n.t("workspace.referral.table.referral")}</th>
|
| 199 |
+
<th>{i18n.t("workspace.referral.table.date")}</th>
|
| 200 |
+
<th></th>
|
| 201 |
+
</tr>
|
| 202 |
+
</thead>
|
| 203 |
+
<tbody>
|
| 204 |
+
<For each={props.summary.rewards}>
|
| 205 |
+
{(reward) => {
|
| 206 |
+
const earnedAt = () => formatDate(reward.timeCreated, language.tag(language.locale()))
|
| 207 |
+
return (
|
| 208 |
+
<tr data-status={reward.status} data-source={reward.source}>
|
| 209 |
+
<td data-slot="referral-amount">{formatCurrency(reward.amount)}</td>
|
| 210 |
+
<td data-slot="referral-source">
|
| 211 |
+
{i18n.t(rewardDescriptionKey(reward.source), { email: reward.email ?? "" })}
|
| 212 |
+
</td>
|
| 213 |
+
<td data-slot="referral-date" title={earnedAt()}>
|
| 214 |
+
{earnedAt()}
|
| 215 |
+
</td>
|
| 216 |
+
<td data-slot="referral-action">
|
| 217 |
+
<button
|
| 218 |
+
type="button"
|
| 219 |
+
disabled={reward.status !== "available" || !props.lite || submission.pending}
|
| 220 |
+
onClick={() => setSelected(reward)}
|
| 221 |
+
>
|
| 222 |
+
{i18n.t(rewardActionKey(reward, !!props.lite))}
|
| 223 |
+
</button>
|
| 224 |
+
</td>
|
| 225 |
+
</tr>
|
| 226 |
+
)
|
| 227 |
+
}}
|
| 228 |
+
</For>
|
| 229 |
+
</tbody>
|
| 230 |
+
</table>
|
| 231 |
+
</div>
|
| 232 |
+
</Show>
|
| 233 |
+
</section>
|
| 234 |
+
</Show>
|
| 235 |
+
|
| 236 |
+
<Modal
|
| 237 |
+
open={!!selected()}
|
| 238 |
+
onClose={() => setSelected(undefined)}
|
| 239 |
+
title={i18n.t("workspace.referral.apply.confirmTitle")}
|
| 240 |
+
>
|
| 241 |
+
<div data-component="go-credit-confirm">
|
| 242 |
+
<p>
|
| 243 |
+
{i18n.t("workspace.referral.apply.confirmBody", {
|
| 244 |
+
amount: formatCurrency(selected()?.amount ?? 0),
|
| 245 |
+
})}
|
| 246 |
+
</p>
|
| 247 |
+
<GoReferralUsagePreview preview={displayPreview()} />
|
| 248 |
+
<div data-slot="modal-actions">
|
| 249 |
+
<button type="button" onClick={() => setSelected(undefined)}>
|
| 250 |
+
{i18n.t("common.cancel")}
|
| 251 |
+
</button>
|
| 252 |
+
<button type="button" data-color="primary" disabled={submission.pending} onClick={onApply}>
|
| 253 |
+
{submission.pending ? i18n.t("workspace.lite.loading") : i18n.t("workspace.referral.apply.confirmAction")}
|
| 254 |
+
</button>
|
| 255 |
+
</div>
|
| 256 |
+
</div>
|
| 257 |
+
</Modal>
|
| 258 |
+
</>
|
| 259 |
+
)
|
| 260 |
+
}
|
| 261 |
+
|
| 262 |
+
function GoReferralUsagePreview(props: { preview: GoReferralUsagePreview }) {
|
| 263 |
+
const i18n = useI18n()
|
| 264 |
+
|
| 265 |
+
return (
|
| 266 |
+
<div data-slot="usage-preview">
|
| 267 |
+
<GoReferralUsagePreviewRow
|
| 268 |
+
label={i18n.t("workspace.lite.subscription.rollingUsage")}
|
| 269 |
+
usage={props.preview.rollingUsage}
|
| 270 |
+
/>
|
| 271 |
+
<GoReferralUsagePreviewRow
|
| 272 |
+
label={i18n.t("workspace.lite.subscription.weeklyUsage")}
|
| 273 |
+
usage={props.preview.weeklyUsage}
|
| 274 |
+
/>
|
| 275 |
+
<GoReferralUsagePreviewRow
|
| 276 |
+
label={i18n.t("workspace.lite.subscription.monthlyUsage")}
|
| 277 |
+
usage={props.preview.monthlyUsage}
|
| 278 |
+
/>
|
| 279 |
+
</div>
|
| 280 |
+
)
|
| 281 |
+
}
|
| 282 |
+
|
| 283 |
+
function GoReferralUsagePreviewRow(props: { label: string; usage: GoReferralUsagePreviewItem }) {
|
| 284 |
+
const i18n = useI18n()
|
| 285 |
+
|
| 286 |
+
return (
|
| 287 |
+
<div data-slot="usage-preview-item">
|
| 288 |
+
<div data-slot="usage-preview-header">
|
| 289 |
+
<span data-slot="usage-preview-label">{props.label}</span>
|
| 290 |
+
<span data-slot="usage-preview-value">
|
| 291 |
+
<span>{props.usage.beforePercent}%</span>
|
| 292 |
+
<span aria-hidden="true">-></span>
|
| 293 |
+
<span data-slot="usage-preview-after-value">{props.usage.afterPercent}%</span>
|
| 294 |
+
</span>
|
| 295 |
+
</div>
|
| 296 |
+
<div data-slot="usage-preview-progress">
|
| 297 |
+
<div data-slot="usage-preview-before" style={{ width: `${props.usage.beforePercent}%` }} />
|
| 298 |
+
<div data-slot="usage-preview-after" style={{ width: `${props.usage.afterPercent}%` }} />
|
| 299 |
+
</div>
|
| 300 |
+
<span data-slot="usage-preview-reset">
|
| 301 |
+
{i18n.t("workspace.lite.subscription.resetsIn")}{" "}
|
| 302 |
+
{formatResetTime(props.usage.resetInSec, i18n, liteResetTimeKeys)}
|
| 303 |
+
</span>
|
| 304 |
+
</div>
|
| 305 |
+
)
|
| 306 |
+
}
|
packages/console/app/src/component/header-context-menu.css
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.context-menu {
|
| 2 |
+
position: fixed;
|
| 3 |
+
z-index: 1000;
|
| 4 |
+
min-width: 160px;
|
| 5 |
+
border-radius: 8px;
|
| 6 |
+
background-color: var(--color-background);
|
| 7 |
+
box-shadow:
|
| 8 |
+
0 0 0 1px rgba(19, 16, 16, 0.08),
|
| 9 |
+
0 6px 8px -4px rgba(19, 16, 16, 0.12),
|
| 10 |
+
0 4px 3px -2px rgba(19, 16, 16, 0.12),
|
| 11 |
+
0 1px 2px -1px rgba(19, 16, 16, 0.12);
|
| 12 |
+
padding: 6px;
|
| 13 |
+
|
| 14 |
+
@media (prefers-color-scheme: dark) {
|
| 15 |
+
box-shadow: 0 0 0 1px rgba(247, 237, 237, 0.1);
|
| 16 |
+
}
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
.context-menu-item {
|
| 20 |
+
display: flex;
|
| 21 |
+
gap: 12px;
|
| 22 |
+
width: 100%;
|
| 23 |
+
padding: 8px 16px 8px 8px;
|
| 24 |
+
font-weight: 500;
|
| 25 |
+
cursor: pointer;
|
| 26 |
+
background: none;
|
| 27 |
+
border: none;
|
| 28 |
+
align-items: center;
|
| 29 |
+
color: var(--color-text);
|
| 30 |
+
font-size: var(--font-size-sm);
|
| 31 |
+
text-align: left;
|
| 32 |
+
border-radius: 2px;
|
| 33 |
+
transition: background-color 0.2s ease;
|
| 34 |
+
|
| 35 |
+
[data-slot="copy dark"] {
|
| 36 |
+
display: none;
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
@media (prefers-color-scheme: dark) {
|
| 40 |
+
[data-slot="copy light"] {
|
| 41 |
+
display: none;
|
| 42 |
+
}
|
| 43 |
+
[data-slot="copy dark"] {
|
| 44 |
+
display: block;
|
| 45 |
+
}
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
&:hover {
|
| 49 |
+
background-color: var(--color-background-weak-hover);
|
| 50 |
+
color: var(--color-text-strong);
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
img {
|
| 54 |
+
width: 22px;
|
| 55 |
+
height: 26px;
|
| 56 |
+
}
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
.context-menu-divider {
|
| 60 |
+
border: none;
|
| 61 |
+
border-top: 1px solid var(--color-border);
|
| 62 |
+
margin: var(--space-1) 0;
|
| 63 |
+
}
|
packages/console/app/src/component/header.tsx
ADDED
|
@@ -0,0 +1,288 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import logoLight from "../asset/logo-ornate-light.svg"
|
| 2 |
+
import logoDark from "../asset/logo-ornate-dark.svg"
|
| 3 |
+
import copyLogoLight from "../asset/lander/logo-light.svg"
|
| 4 |
+
import copyLogoDark from "../asset/lander/logo-dark.svg"
|
| 5 |
+
import copyWordmarkLight from "../asset/lander/wordmark-light.svg"
|
| 6 |
+
import copyWordmarkDark from "../asset/lander/wordmark-dark.svg"
|
| 7 |
+
import copyBrandAssetsLight from "../asset/lander/brand-assets-light.svg"
|
| 8 |
+
import copyBrandAssetsDark from "../asset/lander/brand-assets-dark.svg"
|
| 9 |
+
|
| 10 |
+
// SVG files for copying (separate from button icons)
|
| 11 |
+
// Replace these with your actual SVG files for copying
|
| 12 |
+
import copyLogoSvgLight from "../asset/lander/opencode-logo-light.svg"
|
| 13 |
+
import copyLogoSvgDark from "../asset/lander/opencode-logo-dark.svg"
|
| 14 |
+
import copyWordmarkSvgLight from "../asset/lander/opencode-wordmark-light.svg"
|
| 15 |
+
import copyWordmarkSvgDark from "../asset/lander/opencode-wordmark-dark.svg"
|
| 16 |
+
import { A, useNavigate } from "@solidjs/router"
|
| 17 |
+
import { createMemo, Match, Show, Switch } from "solid-js"
|
| 18 |
+
import { createStore } from "solid-js/store"
|
| 19 |
+
import { createEffect, onCleanup } from "solid-js"
|
| 20 |
+
import { config } from "~/config"
|
| 21 |
+
import { useI18n } from "~/context/i18n"
|
| 22 |
+
import { useLanguage } from "~/context/language"
|
| 23 |
+
import "./header-context-menu.css"
|
| 24 |
+
|
| 25 |
+
const isDarkMode = () => window.matchMedia("(prefers-color-scheme: dark)").matches
|
| 26 |
+
|
| 27 |
+
const fetchSvgContent = async (svgPath: string): Promise<string> => {
|
| 28 |
+
try {
|
| 29 |
+
const response = await fetch(svgPath)
|
| 30 |
+
const svgText = await response.text()
|
| 31 |
+
return svgText
|
| 32 |
+
} catch (err) {
|
| 33 |
+
console.error("Failed to fetch SVG content:", err)
|
| 34 |
+
throw err
|
| 35 |
+
}
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
export function Header(props: { zen?: boolean; go?: boolean; hideGetStarted?: boolean }) {
|
| 39 |
+
const navigate = useNavigate()
|
| 40 |
+
const i18n = useI18n()
|
| 41 |
+
const language = useLanguage()
|
| 42 |
+
|
| 43 |
+
const [store, setStore] = createStore({
|
| 44 |
+
mobileMenuOpen: false,
|
| 45 |
+
contextMenuOpen: false,
|
| 46 |
+
contextMenuPosition: { x: 0, y: 0 },
|
| 47 |
+
})
|
| 48 |
+
|
| 49 |
+
createEffect(() => {
|
| 50 |
+
const handleClickOutside = () => {
|
| 51 |
+
setStore("contextMenuOpen", false)
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
const handleContextMenu = (event: MouseEvent) => {
|
| 55 |
+
event.preventDefault()
|
| 56 |
+
setStore("contextMenuOpen", false)
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
const handleKeyDown = (event: KeyboardEvent) => {
|
| 60 |
+
if (event.key === "Escape") {
|
| 61 |
+
setStore("contextMenuOpen", false)
|
| 62 |
+
}
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
if (store.contextMenuOpen) {
|
| 66 |
+
document.addEventListener("click", handleClickOutside)
|
| 67 |
+
document.addEventListener("contextmenu", handleContextMenu)
|
| 68 |
+
document.addEventListener("keydown", handleKeyDown)
|
| 69 |
+
onCleanup(() => {
|
| 70 |
+
document.removeEventListener("click", handleClickOutside)
|
| 71 |
+
document.removeEventListener("contextmenu", handleContextMenu)
|
| 72 |
+
document.removeEventListener("keydown", handleKeyDown)
|
| 73 |
+
})
|
| 74 |
+
}
|
| 75 |
+
})
|
| 76 |
+
|
| 77 |
+
const handleLogoContextMenu = (event: MouseEvent) => {
|
| 78 |
+
event.preventDefault()
|
| 79 |
+
const logoElement = (event.currentTarget as HTMLElement).querySelector("a")
|
| 80 |
+
if (logoElement) {
|
| 81 |
+
const rect = logoElement.getBoundingClientRect()
|
| 82 |
+
setStore("contextMenuPosition", {
|
| 83 |
+
x: rect.left - 16,
|
| 84 |
+
y: rect.bottom + 8,
|
| 85 |
+
})
|
| 86 |
+
}
|
| 87 |
+
setStore("contextMenuOpen", true)
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
const copyWordmarkToClipboard = async () => {
|
| 91 |
+
try {
|
| 92 |
+
const isDark = isDarkMode()
|
| 93 |
+
const wordmarkSvgPath = isDark ? copyWordmarkSvgDark : copyWordmarkSvgLight
|
| 94 |
+
const wordmarkSvg = await fetchSvgContent(wordmarkSvgPath)
|
| 95 |
+
await navigator.clipboard.writeText(wordmarkSvg)
|
| 96 |
+
} catch (err) {
|
| 97 |
+
console.error("Failed to copy wordmark to clipboard:", err)
|
| 98 |
+
}
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
const copyLogoToClipboard = async () => {
|
| 102 |
+
try {
|
| 103 |
+
const isDark = isDarkMode()
|
| 104 |
+
const logoSvgPath = isDark ? copyLogoSvgDark : copyLogoSvgLight
|
| 105 |
+
const logoSvg = await fetchSvgContent(logoSvgPath)
|
| 106 |
+
await navigator.clipboard.writeText(logoSvg)
|
| 107 |
+
} catch (err) {
|
| 108 |
+
console.error("Failed to copy logo to clipboard:", err)
|
| 109 |
+
}
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
return (
|
| 113 |
+
<section data-component="top">
|
| 114 |
+
<div onContextMenu={handleLogoContextMenu}>
|
| 115 |
+
<A href={language.route("/")}>
|
| 116 |
+
<img data-slot="logo light" src={logoLight} alt={i18n.t("nav.logoAlt")} width="189" height="34" />
|
| 117 |
+
<img data-slot="logo dark" src={logoDark} alt={i18n.t("nav.logoAlt")} width="189" height="34" />
|
| 118 |
+
</A>
|
| 119 |
+
</div>
|
| 120 |
+
|
| 121 |
+
<Show when={store.contextMenuOpen}>
|
| 122 |
+
<div
|
| 123 |
+
class="context-menu"
|
| 124 |
+
style={`left: ${store.contextMenuPosition.x}px; top: ${store.contextMenuPosition.y}px;`}
|
| 125 |
+
>
|
| 126 |
+
<button class="context-menu-item" onClick={copyLogoToClipboard}>
|
| 127 |
+
<img data-slot="copy light" src={copyLogoLight} alt="" />
|
| 128 |
+
<img data-slot="copy dark" src={copyLogoDark} alt="" />
|
| 129 |
+
{i18n.t("nav.context.copyLogo")}
|
| 130 |
+
</button>
|
| 131 |
+
<button class="context-menu-item" onClick={copyWordmarkToClipboard}>
|
| 132 |
+
<img data-slot="copy light" src={copyWordmarkLight} alt="" />
|
| 133 |
+
<img data-slot="copy dark" src={copyWordmarkDark} alt="" />
|
| 134 |
+
{i18n.t("nav.context.copyWordmark")}
|
| 135 |
+
</button>
|
| 136 |
+
<button class="context-menu-item" onClick={() => navigate(language.route("/brand"))}>
|
| 137 |
+
<img data-slot="copy light" src={copyBrandAssetsLight} alt="" />
|
| 138 |
+
<img data-slot="copy dark" src={copyBrandAssetsDark} alt="" />
|
| 139 |
+
{i18n.t("nav.context.brandAssets")}
|
| 140 |
+
</button>
|
| 141 |
+
</div>
|
| 142 |
+
</Show>
|
| 143 |
+
<nav data-component="nav-desktop">
|
| 144 |
+
<ul>
|
| 145 |
+
<li>
|
| 146 |
+
<a href={config.github.repoUrl} target="_blank" style="white-space: nowrap;">
|
| 147 |
+
{i18n.t("nav.github")}
|
| 148 |
+
</a>
|
| 149 |
+
</li>
|
| 150 |
+
<li>
|
| 151 |
+
<a href={language.route("/docs")}>{i18n.t("nav.docs")}</a>
|
| 152 |
+
</li>
|
| 153 |
+
<li>
|
| 154 |
+
<a href={language.route("/data")}>{i18n.t("nav.data")}</a>
|
| 155 |
+
</li>
|
| 156 |
+
<li>
|
| 157 |
+
<A href={language.route("/zen")}>{i18n.t("nav.zen")}</A>
|
| 158 |
+
</li>
|
| 159 |
+
<li>
|
| 160 |
+
<A href={language.route("/go")}>{i18n.t("nav.go")}</A>
|
| 161 |
+
</li>
|
| 162 |
+
<li>
|
| 163 |
+
<A href={language.route("/enterprise")}>{i18n.t("nav.enterprise")}</A>
|
| 164 |
+
</li>
|
| 165 |
+
<Show when={props.zen || props.go}>
|
| 166 |
+
<li>
|
| 167 |
+
<a href="/auth">{i18n.t("nav.login")}</a>
|
| 168 |
+
</li>
|
| 169 |
+
</Show>
|
| 170 |
+
<Show when={!props.hideGetStarted}>
|
| 171 |
+
<li>
|
| 172 |
+
<A href={language.route("/download")} data-slot="cta-button">
|
| 173 |
+
<svg
|
| 174 |
+
width="18"
|
| 175 |
+
height="18"
|
| 176 |
+
viewBox="0 0 18 18"
|
| 177 |
+
fill="none"
|
| 178 |
+
xmlns="http://www.w3.org/2000/svg"
|
| 179 |
+
style="flex-shrink: 0;"
|
| 180 |
+
>
|
| 181 |
+
<path
|
| 182 |
+
d="M12.1875 9.75L9.00001 12.9375L5.8125 9.75M9.00001 2.0625L9 12.375M14.4375 15.9375H3.5625"
|
| 183 |
+
stroke="currentColor"
|
| 184 |
+
stroke-width="1.5"
|
| 185 |
+
stroke-linecap="square"
|
| 186 |
+
/>
|
| 187 |
+
</svg>
|
| 188 |
+
{i18n.t("nav.free")}
|
| 189 |
+
</A>
|
| 190 |
+
</li>
|
| 191 |
+
</Show>
|
| 192 |
+
</ul>
|
| 193 |
+
</nav>
|
| 194 |
+
<nav data-component="nav-mobile">
|
| 195 |
+
<button
|
| 196 |
+
type="button"
|
| 197 |
+
data-component="nav-mobile-toggle"
|
| 198 |
+
aria-expanded="false"
|
| 199 |
+
aria-controls="nav-mobile-menu"
|
| 200 |
+
class="nav-toggle"
|
| 201 |
+
onClick={() => setStore("mobileMenuOpen", !store.mobileMenuOpen)}
|
| 202 |
+
>
|
| 203 |
+
<span class="sr-only">{i18n.t("nav.openMenu")}</span>
|
| 204 |
+
<Switch>
|
| 205 |
+
<Match when={store.mobileMenuOpen}>
|
| 206 |
+
<svg
|
| 207 |
+
class="icon icon-close"
|
| 208 |
+
width="24"
|
| 209 |
+
height="24"
|
| 210 |
+
viewBox="0 0 24 24"
|
| 211 |
+
fill="none"
|
| 212 |
+
aria-hidden="true"
|
| 213 |
+
xmlns="http://www.w3.org/2000/svg"
|
| 214 |
+
>
|
| 215 |
+
<path
|
| 216 |
+
d="M12.7071 11.9993L18.0104 17.3026L17.3033 18.0097L12 12.7064L6.6967 18.0097L5.98959 17.3026L11.2929 11.9993L5.98959 6.69595L6.6967 5.98885L12 11.2921L17.3033 5.98885L18.0104 6.69595L12.7071 11.9993Z"
|
| 217 |
+
fill="currentColor"
|
| 218 |
+
/>
|
| 219 |
+
</svg>
|
| 220 |
+
</Match>
|
| 221 |
+
<Match when={!store.mobileMenuOpen}>
|
| 222 |
+
<svg
|
| 223 |
+
class="icon icon-hamburger"
|
| 224 |
+
width="24"
|
| 225 |
+
height="24"
|
| 226 |
+
viewBox="0 0 24 24"
|
| 227 |
+
fill="none"
|
| 228 |
+
aria-hidden="true"
|
| 229 |
+
xmlns="http://www.w3.org/2000/svg"
|
| 230 |
+
>
|
| 231 |
+
<path d="M19 17H5V16H19V17Z" fill="currentColor" />
|
| 232 |
+
<path d="M19 8H5V7H19V8Z" fill="currentColor" />
|
| 233 |
+
</svg>
|
| 234 |
+
</Match>
|
| 235 |
+
</Switch>
|
| 236 |
+
</button>
|
| 237 |
+
|
| 238 |
+
<Show when={store.mobileMenuOpen}>
|
| 239 |
+
<div id="nav-mobile-menu" data-component="nav-mobile">
|
| 240 |
+
<nav data-component="nav-mobile-menu-list">
|
| 241 |
+
<ul>
|
| 242 |
+
<li>
|
| 243 |
+
<A href={language.route("/")}>{i18n.t("nav.home")}</A>
|
| 244 |
+
</li>
|
| 245 |
+
<li>
|
| 246 |
+
<a href={config.github.repoUrl} target="_blank" style="white-space: nowrap;">
|
| 247 |
+
{i18n.t("nav.github")}
|
| 248 |
+
</a>
|
| 249 |
+
</li>
|
| 250 |
+
<li>
|
| 251 |
+
<a href={language.route("/docs")}>{i18n.t("nav.docs")}</a>
|
| 252 |
+
</li>
|
| 253 |
+
<li>
|
| 254 |
+
<a href={language.route("/data")}>{i18n.t("nav.data")}</a>
|
| 255 |
+
</li>
|
| 256 |
+
<Show when={!props.zen}>
|
| 257 |
+
<li>
|
| 258 |
+
<A href={language.route("/zen")}>{i18n.t("nav.zen")}</A>
|
| 259 |
+
</li>
|
| 260 |
+
</Show>
|
| 261 |
+
<Show when={!props.go}>
|
| 262 |
+
<li>
|
| 263 |
+
<A href={language.route("/go")}>{i18n.t("nav.go")}</A>
|
| 264 |
+
</li>
|
| 265 |
+
</Show>
|
| 266 |
+
<li>
|
| 267 |
+
<A href={language.route("/enterprise")}>{i18n.t("nav.enterprise")}</A>
|
| 268 |
+
</li>
|
| 269 |
+
<Show when={props.zen || props.go}>
|
| 270 |
+
<li>
|
| 271 |
+
<a href="/auth">{i18n.t("nav.login")}</a>
|
| 272 |
+
</li>
|
| 273 |
+
</Show>
|
| 274 |
+
<Show when={!props.hideGetStarted}>
|
| 275 |
+
<li>
|
| 276 |
+
<A href={language.route("/download")} data-slot="cta-button">
|
| 277 |
+
{i18n.t("nav.getStartedFree")}
|
| 278 |
+
</A>
|
| 279 |
+
</li>
|
| 280 |
+
</Show>
|
| 281 |
+
</ul>
|
| 282 |
+
</nav>
|
| 283 |
+
</div>
|
| 284 |
+
</Show>
|
| 285 |
+
</nav>
|
| 286 |
+
</section>
|
| 287 |
+
)
|
| 288 |
+
}
|
packages/console/app/src/component/icon.tsx
ADDED
|
@@ -0,0 +1,286 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { JSX } from "solid-js"
|
| 2 |
+
|
| 3 |
+
export function IconZen(_props: JSX.SvgSVGAttributes<SVGSVGElement>) {
|
| 4 |
+
return (
|
| 5 |
+
<svg width="84" height="30" viewBox="0 0 84 30" fill="none" xmlns="http://www.w3.org/2000/svg">
|
| 6 |
+
<path d="M24 24H6V18H18V12H24V24ZM6 18H0V12H6V18Z" fill="currentColor" fill-opacity="0.2" />
|
| 7 |
+
<path d="M6 24H24V30H0V18H6V24ZM18 18H6V12H18V18ZM24 12H18V6H0V0H24V12Z" fill="currentColor" />
|
| 8 |
+
<path d="M54 18V24H36V18H54Z" fill="currentColor" fill-opacity="0.2" />
|
| 9 |
+
<path d="M54 18H36V24H54V30H30V0H54V18ZM36 12H48V6H36V12Z" fill="currentColor" />
|
| 10 |
+
<path d="M78 30H66V12H78V30Z" fill="currentColor" fill-opacity="0.2" />
|
| 11 |
+
<path d="M78 6H66V30H60V0H78V6ZM84 30H78V6H84V30Z" fill="currentColor" />
|
| 12 |
+
</svg>
|
| 13 |
+
)
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
export function IconGo(_props: JSX.SvgSVGAttributes<SVGSVGElement>) {
|
| 17 |
+
return (
|
| 18 |
+
<svg width="54" height="30" viewBox="0 0 54 30" fill="none" xmlns="http://www.w3.org/2000/svg">
|
| 19 |
+
<path d="M24 30H0V0H24V6H6V24H18V18H12V12H24V30Z" fill="currentColor" />
|
| 20 |
+
<path d="M12 18H18V24H6V12H12V18Z" fill="currentColor" fill-opacity="0.2" />
|
| 21 |
+
<path d="M48 12V24H36V12H48Z" fill="currentColor" fill-opacity="0.2" />
|
| 22 |
+
<path d="M54 30H30V0H54V30ZM36 24H48V6H36V24Z" fill="currentColor" />
|
| 23 |
+
</svg>
|
| 24 |
+
)
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
export function IconCopy(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
|
| 28 |
+
return (
|
| 29 |
+
<svg {...props} width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
| 30 |
+
<path
|
| 31 |
+
d="M8.75 8.75V2.75H21.25V15.25H15.25M15.25 8.75H2.75V21.25H15.25V8.75Z"
|
| 32 |
+
stroke="currentColor"
|
| 33 |
+
stroke-width="1.5"
|
| 34 |
+
stroke-linecap="square"
|
| 35 |
+
/>
|
| 36 |
+
</svg>
|
| 37 |
+
)
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
export function IconCheck(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
|
| 41 |
+
return (
|
| 42 |
+
<svg {...props} width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
| 43 |
+
<path d="M2.75 15.0938L9 20.25L21.25 3.75" stroke="#03B000" stroke-width="2" stroke-linecap="square" />
|
| 44 |
+
</svg>
|
| 45 |
+
)
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
export function IconCreditCard(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
|
| 49 |
+
return (
|
| 50 |
+
<svg {...props} viewBox="0 0 24 24">
|
| 51 |
+
<path
|
| 52 |
+
fill="currentColor"
|
| 53 |
+
d="M20 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 14H4V8h16v10z"
|
| 54 |
+
/>
|
| 55 |
+
</svg>
|
| 56 |
+
)
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
export function IconStripe(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
|
| 60 |
+
return (
|
| 61 |
+
<svg {...props} viewBox="0 0 24 24">
|
| 62 |
+
<path
|
| 63 |
+
fill="currentColor"
|
| 64 |
+
d="M15.827 12.506c0 .672-.31 1.175-.771 1.175-.293 0-.468-.106-.589-.237l-.007-1.855c.13-.143.31-.247.596-.247.456-.001.771.51.771 1.164zm3.36-1.253c-.312 0-.659.236-.659.798h1.291c0-.562-.325-.798-.632-.798zm4.813-5.253v12c0 1.104-.896 2-2 2h-20c-1.104 0-2-.896-2-2v-12c0-1.104.896-2 2-2h20c1.104 0 2 .896 2 2zm-17.829 7.372c0-1.489-1.909-1.222-1.909-1.784 0-.195.162-.271.424-.271.38 0 .862.116 1.242.321v-1.176c-.414-.165-.827-.228-1.241-.228-1.012.001-1.687.53-1.687 1.414 0 1.382 1.898 1.158 1.898 1.754 0 .231-.201.305-.479.305-.414 0-.947-.171-1.366-.399v1.192c.464.2.935.283 1.365.283 1.038.001 1.753-.512 1.753-1.411zm2.422-3.054h-.949l-.001-1.084-1.219.259-.005 4.006c0 .739.556 1.285 1.297 1.285.408 0 .71-.074.876-.165v-1.016c-.16.064-.948.293-.948-.443v-1.776h.948v-1.066zm2.596 0c-.166-.06-.75-.169-1.042.369l-.078-.369h-1.079v4.377h1.248v-2.967c.295-.388.793-.313.952-.262v-1.148zm1.554 0h-1.253v4.377h1.253v-4.377zm0-1.664l-1.253.266v1.017l1.253-.266v-1.017zm4.314 3.824c0-1.454-.826-2.244-1.703-2.243-.489 0-.805.23-.978.392l-.065-.309h-1.099v5.828l1.249-.265.003-1.413c.179.131.446.316.883.316.893 0 1.71-.719 1.71-2.306zm3.943.045c0-1.279-.619-2.288-1.805-2.288-1.188 0-1.911 1.01-1.911 2.281 0 1.506.852 2.267 2.068 2.267.597 0 1.045-.136 1.384-.324v-1.006c-.34.172-.731.276-1.227.276-.487 0-.915-.172-.971-.758h2.444l.018-.448z"
|
| 65 |
+
/>
|
| 66 |
+
</svg>
|
| 67 |
+
)
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
export function IconAlipay(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
|
| 71 |
+
return (
|
| 72 |
+
<svg {...props} viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
| 73 |
+
<path d="M2.541 0H13.5a2.55 2.55 0 0 1 2.54 2.563v8.297c-.006 0-.531-.046-2.978-.813-.412-.14-.916-.327-1.479-.536q-.456-.17-.957-.353a13 13 0 0 0 1.325-3.373H8.822V4.649h3.831v-.634h-3.83V2.121H7.26c-.274 0-.274.273-.274.273v1.621H3.11v.634h3.875v1.136h-3.2v.634H9.99c-.227.789-.532 1.53-.894 2.202-2.013-.67-4.161-1.212-5.51-.878-.864.214-1.42.597-1.746.998-1.499 1.84-.424 4.633 2.741 4.633 1.872 0 3.675-1.053 5.072-2.787 2.08 1.008 6.37 2.738 6.387 2.745v.105A2.55 2.55 0 0 1 13.5 16H2.541A2.55 2.55 0 0 1 0 13.437V2.563A2.55 2.55 0 0 1 2.541 0" />
|
| 74 |
+
<path d="M2.309 9.27c-1.22 1.073-.49 3.034 1.978 3.034 1.434 0 2.868-.925 3.994-2.406-1.602-.789-2.959-1.353-4.425-1.207-.397.04-1.14.217-1.547.58Z" />
|
| 75 |
+
</svg>
|
| 76 |
+
)
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
export function IconUpi(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
|
| 80 |
+
return (
|
| 81 |
+
<svg {...props} viewBox="10 16 100 28" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
| 82 |
+
<path d="M95.678 42.9 110 29.835l-6.784-13.516Z" />
|
| 83 |
+
<path d="M90.854 42.9 105.176 29.835l-6.784-13.516Z" />
|
| 84 |
+
<path
|
| 85 |
+
d="M22.41 16.47 16.38 37.945l21.407.15 5.88-21.625h5.427l-7.05 25.14c-.27.96-1.298 1.74-2.295 1.74H12.31c-1.664 0-2.65-1.3-2.2-2.9l6.724-23.98Zm66.182-.15h5.427l-7.538 27.03h-5.58ZM49.698 27.582l27.136-.15 1.81-5.707H51.054l1.658-5.256 29.4-.27c1.83-.017 2.92 1.4 2.438 3.167L81.78 29.49c-.483 1.766-2.36 3.197-4.19 3.197H53.316L50.454 43.8h-5.28Z"
|
| 86 |
+
fill-rule="evenodd"
|
| 87 |
+
/>
|
| 88 |
+
</svg>
|
| 89 |
+
)
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
export function IconWechat(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
|
| 93 |
+
return (
|
| 94 |
+
<svg {...props} viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
| 95 |
+
<path d="M9.27099 14.6689C8.9532 14.8312 8.56403 14.7122 8.39132 14.4L8.3477 14.3054L6.53019 10.3069C6.52269 10.2588 6.52269 10.2097 6.53019 10.1615C6.53017 10.0735 6.56564 9.98916 6.62857 9.9276C6.6915 9.86603 6.7766 9.83243 6.86462 9.83438C6.93567 9.83269 7.00508 9.85582 7.06091 9.89981L9.24191 11.4265C9.40329 11.5346 9.59293 11.5928 9.78716 11.5937C9.90424 11.5945 10.0203 11.5723 10.1289 11.5283L20.176 7.02816C18.091 4.72544 15.1103 3.43931 12.0045 3.5022C6.4793 3.5022 2.00098 7.23172 2.00098 11.87C2.06681 14.4052 3.35646 16.7515 5.4615 18.1658C5.6878 18.3326 5.78402 18.6241 5.70141 18.8928L5.25067 20.594C5.22336 20.6714 5.20625 20.7521 5.19978 20.8339C5.19777 20.9232 5.23236 21.0094 5.29552 21.0726C5.35868 21.1358 5.44491 21.1703 5.5342 21.1684C5.60098 21.1645 5.66583 21.1445 5.72322 21.1102L7.90423 19.8452C8.06383 19.7467 8.2474 19.6939 8.43494 19.6925C8.53352 19.6923 8.63157 19.707 8.72574 19.7361C9.78781 20.0363 10.8863 20.188 11.99 20.1869C17.5152 20.1869 22.001 16.4574 22.001 11.8554C22.0108 10.4834 21.6301 9.13687 20.903 7.97326L9.35096 14.6253L9.27099 14.6689Z" />
|
| 96 |
+
</svg>
|
| 97 |
+
)
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
export function IconChevron(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
|
| 101 |
+
return (
|
| 102 |
+
<svg {...props} width="8" height="6" viewBox="0 0 8 6" fill="none" xmlns="http://www.w3.org/2000/svg">
|
| 103 |
+
<path
|
| 104 |
+
fill="currentColor"
|
| 105 |
+
d="M4.00024 5.04041L7.37401 1.66663L6.66691 0.959525L4.00024 3.62619L1.33357 0.959525L0.626465 1.66663L4.00024 5.04041Z"
|
| 106 |
+
/>
|
| 107 |
+
</svg>
|
| 108 |
+
)
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
export function IconWorkspaceLogo(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
|
| 112 |
+
return (
|
| 113 |
+
<svg {...props} width="24" height="30" viewBox="0 0 24 30" fill="none" xmlns="http://www.w3.org/2000/svg">
|
| 114 |
+
<path d="M18 6H6V24H18V6ZM24 30H0V0H24V30Z" fill="currentColor" />
|
| 115 |
+
</svg>
|
| 116 |
+
)
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
export function IconOpenAI(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
|
| 120 |
+
return (
|
| 121 |
+
<svg {...props} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22" fill="none">
|
| 122 |
+
<path
|
| 123 |
+
d="M8.43799 8.06943V6.09387C8.43799 5.92749 8.50347 5.80267 8.65601 5.71959L12.8206 3.43211C13.3875 3.1202 14.0635 2.9747 14.7611 2.9747C17.3775 2.9747 19.0347 4.9087 19.0347 6.96734C19.0347 7.11288 19.0347 7.27926 19.0128 7.44564L14.6956 5.03335C14.434 4.88785 14.1723 4.88785 13.9107 5.03335L8.43799 8.06943ZM18.1624 15.7637V11.0431C18.1624 10.7519 18.0315 10.544 17.7699 10.3984L12.2972 7.36234L14.0851 6.3849C14.2377 6.30182 14.3686 6.30182 14.5212 6.3849L18.6858 8.67238C19.8851 9.3379 20.6917 10.7519 20.6917 12.1243C20.6917 13.7047 19.7106 15.1604 18.1624 15.7636V15.7637ZM7.15158 11.6047L5.36369 10.6066C5.21114 10.5235 5.14566 10.3986 5.14566 10.2323V5.65735C5.14566 3.43233 6.93355 1.7478 9.35381 1.7478C10.2697 1.7478 11.1199 2.039 11.8396 2.55886L7.54424 4.92959C7.28268 5.07508 7.15181 5.28303 7.15181 5.57427V11.6049L7.15158 11.6047ZM11 13.7258L8.43799 12.3533V9.44209L11 8.06965L13.5618 9.44209V12.3533L11 13.7258ZM12.6461 20.0476C11.7303 20.0476 10.8801 19.7564 10.1604 19.2366L14.4557 16.8658C14.7173 16.7203 14.8482 16.5124 14.8482 16.2211V10.1905L16.658 11.1886C16.8105 11.2717 16.876 11.3965 16.876 11.563V16.1379C16.876 18.3629 15.0662 20.0474 12.6461 20.0474V20.0476ZM7.47863 15.4103L3.314 13.1229C2.11471 12.4573 1.30808 11.0433 1.30808 9.67088C1.30808 8.06965 2.31106 6.6348 3.85903 6.03168V10.773C3.85903 11.0642 3.98995 11.2721 4.25151 11.4177L9.70253 14.4328L7.91464 15.4103C7.76209 15.4934 7.63117 15.4934 7.47863 15.4103ZM7.23892 18.8207C4.77508 18.8207 2.96533 17.0531 2.96533 14.8696C2.96533 14.7032 2.98719 14.5368 3.00886 14.3704L7.30418 16.7412C7.56574 16.8867 7.82752 16.8867 8.08909 16.7412L13.5618 13.726V15.7015C13.5618 15.8679 13.4964 15.9927 13.3438 16.0758L9.17918 18.3633C8.61225 18.6752 7.93631 18.8207 7.23869 18.8207H7.23892ZM12.6461 21.2952C15.2844 21.2952 17.4865 19.5069 17.9882 17.1362C20.4301 16.5331 22 14.3495 22 12.1245C22 10.6688 21.346 9.25482 20.1685 8.23581C20.2775 7.79908 20.343 7.36234 20.343 6.92582C20.343 3.95215 17.8137 1.72691 14.892 1.72691C14.3034 1.72691 13.7365 1.80999 13.1695 1.99726C12.1882 1.08223 10.8364 0.5 9.35381 0.5C6.71557 0.5 4.51352 2.28829 4.01185 4.65902C1.56987 5.26214 0 7.44564 0 9.67067C0 11.1264 0.654039 12.5404 1.83147 13.5594C1.72246 13.9961 1.65702 14.4328 1.65702 14.8694C1.65702 17.8431 4.1863 20.0683 7.108 20.0683C7.69661 20.0683 8.26354 19.9852 8.83046 19.7979C9.81155 20.713 11.1634 21.2952 12.6461 21.2952Z"
|
| 124 |
+
fill="currentColor"
|
| 125 |
+
/>
|
| 126 |
+
</svg>
|
| 127 |
+
)
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
export function IconAnthropic(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
|
| 131 |
+
return (
|
| 132 |
+
<svg {...props} viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
| 133 |
+
<path fill="currentColor" d="M13.7891 3.93188L20.2223 20.068H23.7502L17.317 3.93188H13.7891Z" />
|
| 134 |
+
<path
|
| 135 |
+
fill="currentColor"
|
| 136 |
+
d="M6.32538 13.6827L8.52662 8.01201L10.7279 13.6827H6.32538ZM6.68225 3.93188L0.25 20.068H3.84652L5.16202 16.6794H11.8914L13.2067 20.068H16.8033L10.371 3.93188H6.68225Z"
|
| 137 |
+
/>
|
| 138 |
+
</svg>
|
| 139 |
+
)
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
export function IconXai(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
|
| 143 |
+
return (
|
| 144 |
+
<svg {...props} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
|
| 145 |
+
<path
|
| 146 |
+
d="M9.16861 16.0529L17.2018 9.85156C17.5957 9.54755 18.1586 9.66612 18.3463 10.1384C19.3339 12.6288 18.8926 15.6217 16.9276 17.6766C14.9626 19.7314 12.2285 20.1821 9.72948 19.1557L6.9995 20.4775C10.9151 23.2763 15.6699 22.5841 18.6411 19.4749C20.9979 17.0103 21.7278 13.6508 21.0453 10.6214L21.0515 10.6278C20.0617 6.17736 21.2948 4.39847 23.8207 0.760904C23.8804 0.674655 23.9402 0.588405 24 0.5L20.6762 3.97585V3.96506L9.16658 16.0551"
|
| 147 |
+
fill="currentColor"
|
| 148 |
+
/>
|
| 149 |
+
<path
|
| 150 |
+
d="M7.37742 16.7017C4.67579 14.0395 5.14158 9.91963 7.44676 7.54383C9.15135 5.78544 11.9442 5.06779 14.3821 6.12281L17.0005 4.87559C16.5288 4.52392 15.9242 4.14566 15.2305 3.87986C12.0948 2.54882 8.34069 3.21127 5.79171 5.8386C3.33985 8.36779 2.56881 12.2567 3.89286 15.5751C4.88192 18.0552 3.26056 19.8094 1.62731 21.5801C1.04853 22.2078 0.467774 22.8355 0 23.5L7.3754 16.7037"
|
| 151 |
+
fill="currentColor"
|
| 152 |
+
/>
|
| 153 |
+
</svg>
|
| 154 |
+
)
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
export function IconAlibaba(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
|
| 158 |
+
return (
|
| 159 |
+
<svg {...props} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22" fill="none">
|
| 160 |
+
<path
|
| 161 |
+
fill-rule="evenodd"
|
| 162 |
+
clip-rule="evenodd"
|
| 163 |
+
fill="currentColor"
|
| 164 |
+
d="M11.6043 0.340162C11.9973 1.03016 12.3883 1.72215 12.7783 2.41514C12.7941 2.44286 12.8169 2.46589 12.8445 2.48187C12.8721 2.49786 12.9034 2.50624 12.9353 2.50614H18.4873C18.6612 2.50614 18.8092 2.61614 18.9332 2.83314L20.3872 5.40311C20.5772 5.74011 20.6272 5.88111 20.4112 6.24011C20.1512 6.6701 19.8982 7.1041 19.6512 7.54009L19.2842 8.19809C19.1782 8.39409 19.0612 8.47809 19.2442 8.71008L21.8962 13.347C22.0682 13.648 22.0072 13.841 21.8532 14.117C21.4162 14.902 20.9712 15.681 20.5182 16.457C20.3592 16.729 20.1662 16.832 19.8382 16.827C19.0612 16.811 18.2863 16.817 17.5113 16.843C17.4946 16.8439 17.4785 16.8489 17.4644 16.8576C17.4502 16.8664 17.4385 16.8785 17.4303 16.893C16.5361 18.4773 15.6344 20.0573 14.7253 21.633C14.5563 21.926 14.3453 21.996 14.0003 21.997C13.0033 22 11.9983 22.001 10.9833 21.999C10.8889 21.9987 10.7961 21.9735 10.7145 21.9259C10.6328 21.8783 10.5652 21.8101 10.5184 21.728L9.18337 19.405C9.1756 19.3898 9.16368 19.3771 9.14898 19.3684C9.13429 19.3598 9.11743 19.3554 9.10037 19.356H3.98244C3.69744 19.386 3.42944 19.355 3.17745 19.264L1.57447 16.494C1.52706 16.412 1.50193 16.319 1.50158 16.2243C1.50123 16.1296 1.52567 16.0364 1.57247 15.954L2.77945 13.834C2.79665 13.8041 2.80569 13.7701 2.80569 13.7355C2.80569 13.701 2.79665 13.667 2.77945 13.637C2.15073 12.5485 1.52573 11.4579 0.904476 10.3651L0.114486 8.97008C-0.0455115 8.66008 -0.0585113 8.47409 0.209485 8.00509C0.674479 7.1921 1.13647 6.38011 1.59647 5.56911C1.72847 5.33512 1.90046 5.23512 2.18046 5.23412C3.04344 5.23048 3.90644 5.23015 4.76943 5.23312C4.79123 5.23295 4.81259 5.22704 4.83138 5.21597C4.85016 5.20491 4.8657 5.1891 4.87643 5.17012L7.68239 0.275163C7.72491 0.200697 7.78631 0.138751 7.86039 0.0955646C7.93448 0.0523783 8.01863 0.0294762 8.10439 0.0291651C8.62838 0.0281651 9.15737 0.029165 9.68736 0.0231651L10.7044 0.000165317C11.0453 -0.00283466 11.4283 0.032165 11.6043 0.340162ZM8.17238 0.743158C8.16185 0.743152 8.15149 0.745921 8.14236 0.751187C8.13323 0.756454 8.12565 0.764031 8.12038 0.773158L5.25442 5.78811C5.24066 5.81174 5.22097 5.83137 5.19729 5.84505C5.17361 5.85873 5.14677 5.86599 5.11942 5.86611H2.25346C2.19746 5.86611 2.18346 5.89111 2.21246 5.94011L8.02239 16.096C8.04739 16.138 8.03539 16.158 7.98839 16.159L5.19342 16.174C5.15256 16.1727 5.11214 16.1828 5.07678 16.2033C5.04141 16.2238 5.01253 16.2539 4.99342 16.29L3.67344 18.6C3.62944 18.678 3.65244 18.718 3.74144 18.718L9.45737 18.726C9.50337 18.726 9.53737 18.746 9.56137 18.787L10.9643 21.241C11.0103 21.322 11.0563 21.323 11.1033 21.241L16.1093 12.481L16.8923 11.0991C16.897 11.0905 16.904 11.0834 16.9125 11.0785C16.9209 11.0735 16.9305 11.0709 16.9403 11.0709C16.9501 11.0709 16.9597 11.0735 16.9681 11.0785C16.9765 11.0834 16.9835 11.0905 16.9883 11.0991L18.4123 13.629C18.4229 13.648 18.4385 13.6637 18.4573 13.6746C18.4761 13.6855 18.4975 13.6912 18.5193 13.691L21.2822 13.671C21.2893 13.6711 21.2963 13.6693 21.3024 13.6658C21.3086 13.6623 21.3137 13.6572 21.3172 13.651C21.3206 13.6449 21.3224 13.638 21.3224 13.631C21.3224 13.624 21.3206 13.6172 21.3172 13.611L18.4173 8.52508C18.4068 8.50809 18.4013 8.48853 18.4013 8.46859C18.4013 8.44864 18.4068 8.42908 18.4173 8.41209L18.7102 7.90509L19.8302 5.92811C19.8542 5.88711 19.8422 5.86611 19.7952 5.86611H8.20038C8.14138 5.86611 8.12738 5.84011 8.15738 5.78911L9.59137 3.28413C9.60211 3.26706 9.60781 3.24731 9.60781 3.22714C9.60781 3.20697 9.60211 3.18721 9.59137 3.17014L8.22538 0.774158C8.22016 0.764697 8.21248 0.756822 8.20315 0.751365C8.19382 0.745909 8.18319 0.743073 8.17238 0.743158ZM14.4623 8.76308C14.5083 8.76308 14.5203 8.78308 14.4963 8.82308L13.6643 10.2881L11.0513 14.873C11.0464 14.8819 11.0392 14.8894 11.0304 14.8945C11.0216 14.8996 11.0115 14.9022 11.0013 14.902C10.9912 14.902 10.9813 14.8993 10.9725 14.8942C10.9637 14.8891 10.9564 14.8818 10.9513 14.873L7.49839 8.84108C7.47839 8.80708 7.48839 8.78908 7.52639 8.78708L7.74239 8.77508L14.4643 8.76308H14.4623Z"
|
| 165 |
+
/>
|
| 166 |
+
</svg>
|
| 167 |
+
)
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
export function IconMoonshotAI(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
|
| 171 |
+
return (
|
| 172 |
+
<svg {...props} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
|
| 173 |
+
<path
|
| 174 |
+
fill-rule="evenodd"
|
| 175 |
+
clip-rule="evenodd"
|
| 176 |
+
d="M20.052 6.0364C18.7527 4.28846 16.8758 2.94985 14.6066 2.34296C12.3374 1.73606 10.0411 1.95816 8.04092 2.82331L20.052 6.0364ZM3.75774 6.34071C4.64576 5.04902 5.81872 3.99836 7.16325 3.25459L16.0115 5.62191C15.4308 5.95635 14.8088 6.53268 14.3273 7.16626L21.6025 9.11263C21.809 9.79398 21.9435 10.5003 22 11.2213L3.75774 6.34071ZM21.6866 14.5876C21.584 14.9707 21.4603 15.3425 21.3172 15.7019L2.10543 10.5623C2.16147 10.1792 2.24079 9.7957 2.34339 9.41263C2.58479 8.51262 2.94172 7.67459 3.39435 6.91016L12.7957 9.42554C12.4194 9.96271 12.0766 10.5464 11.7749 11.1709L21.8517 13.8671C21.8056 14.1077 21.7504 14.3478 21.6862 14.5884L21.6866 14.5876ZM2.58134 15.3529C2.11535 14.05 1.91662 12.6408 2.03215 11.2088L10.8985 13.5808C10.8347 13.7823 10.7748 13.9863 10.7192 14.1933C10.6062 14.6147 10.514 15.0352 10.4424 15.4527L20.0166 18.0142C19.5709 18.6051 19.0631 19.1406 18.5057 19.6132L2.58134 15.3529ZM9.42338 21.6568C6.40111 20.8481 4.07415 18.7424 2.88266 16.1001L10.0976 18.0305C10.0859 18.7024 10.1278 19.3571 10.2196 19.9851L15.4619 21.3874C13.5906 22.0743 11.496 22.2112 9.42338 21.6568Z"
|
| 177 |
+
fill="currentColor"
|
| 178 |
+
/>
|
| 179 |
+
</svg>
|
| 180 |
+
)
|
| 181 |
+
}
|
| 182 |
+
|
| 183 |
+
export function IconZai(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
|
| 184 |
+
return (
|
| 185 |
+
<svg {...props} fill="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
| 186 |
+
<path d="M12.105 2L9.927 4.953H.653L2.83 2h9.276zM23.254 19.048L21.078 22h-9.242l2.174-2.952h9.244zM24 2L9.264 22H0L14.736 2H24z"></path>
|
| 187 |
+
</svg>
|
| 188 |
+
)
|
| 189 |
+
}
|
| 190 |
+
|
| 191 |
+
export function IconMiniMax(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
|
| 192 |
+
return (
|
| 193 |
+
<svg {...props} fill="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
| 194 |
+
<path d="M16.278 2c1.156 0 2.093.927 2.093 2.07v12.501a.74.74 0 00.744.709.74.74 0 00.743-.709V9.099a2.06 2.06 0 012.071-2.049A2.06 2.06 0 0124 9.1v6.561a.649.649 0 01-.652.645.649.649 0 01-.653-.645V9.1a.762.762 0 00-.766-.758.762.762 0 00-.766.758v7.472a2.037 2.037 0 01-2.048 2.026 2.037 2.037 0 01-2.048-2.026v-12.5a.785.785 0 00-.788-.753.785.785 0 00-.789.752l-.001 15.904A2.037 2.037 0 0113.441 22a2.037 2.037 0 01-2.048-2.026V18.04c0-.356.292-.645.652-.645.36 0 .652.289.652.645v1.934c0 .263.142.506.372.638.23.131.514.131.744 0a.734.734 0 00.372-.638V4.07c0-1.143.937-2.07 2.093-2.07zm-5.674 0c1.156 0 2.093.927 2.093 2.07v11.523a.648.648 0 01-.652.645.648.648 0 01-.652-.645V4.07a.785.785 0 00-.789-.78.785.785 0 00-.789.78v14.013a2.06 2.06 0 01-2.07 2.048 2.06 2.06 0 01-2.071-2.048V9.1a.762.762 0 00-.766-.758.762.762 0 00-.766.758v3.8a2.06 2.06 0 01-2.071 2.049A2.06 2.06 0 010 12.9v-1.378c0-.357.292-.646.652-.646.36 0 .653.29.653.646V12.9c0 .418.343.757.766.757s.766-.339.766-.757V9.099a2.06 2.06 0 012.07-2.048 2.06 2.06 0 012.071 2.048v8.984c0 .419.343.758.767.758.423 0 .766-.339.766-.758V4.07c0-1.143.937-2.07 2.093-2.07z" />
|
| 195 |
+
</svg>
|
| 196 |
+
)
|
| 197 |
+
}
|
| 198 |
+
|
| 199 |
+
export function IconGemini(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
|
| 200 |
+
return (
|
| 201 |
+
<svg {...props} viewBox="0 0 50 50" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
| 202 |
+
<path d="M49.04,24.001l-1.082-0.043h-0.001C36.134,23.492,26.508,13.866,26.042,2.043L25.999,0.96C25.978,0.424,25.537,0,25,0 s-0.978,0.424-0.999,0.96l-0.043,1.083C23.492,13.866,13.866,23.492,2.042,23.958L0.96,24.001C0.424,24.022,0,24.463,0,25 c0,0.537,0.424,0.978,0.961,0.999l1.082,0.042c11.823,0.467,21.449,10.093,21.915,21.916l0.043,1.083C24.022,49.576,24.463,50,25,50 s0.978-0.424,0.999-0.96l0.043-1.083c0.466-11.823,10.092-21.449,21.915-21.916l1.082-0.042C49.576,25.978,50,25.537,50,25 C50,24.463,49.576,24.022,49.04,24.001z"></path>
|
| 203 |
+
</svg>
|
| 204 |
+
)
|
| 205 |
+
}
|
| 206 |
+
|
| 207 |
+
export function IconDeepSeek(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
|
| 208 |
+
return (
|
| 209 |
+
<svg {...props} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
| 210 |
+
<path d="M23.748 4.482c-.254-.124-.364.113-.512.234-.051.039-.094.09-.137.136-.372.397-.806.657-1.373.626-.829-.046-1.537.214-2.163.848-.133-.782-.575-1.249-1.247-1.548-.352-.156-.708-.311-.955-.65-.172-.241-.219-.51-.305-.774-.055-.16-.11-.323-.293-.35-.2-.031-.278.136-.356.276-.313.572-.434 1.202-.422 1.84.027 1.436.633 2.58 1.838 3.393.137.093.172.187.129.323-.082.28-.18.552-.266.833-.055.179-.137.217-.329.14a5.526 5.526 0 0 1-1.736-1.18c-.857-.828-1.631-1.742-2.597-2.458a11.365 11.365 0 0 0-.689-.471c-.985-.957.13-1.743.388-1.836.27-.098.093-.432-.779-.428-.872.004-1.67.295-2.687.684a3.055 3.055 0 0 1-.465.137 9.597 9.597 0 0 0-2.883-.102c-1.885.21-3.39 1.102-4.497 2.623C.082 8.606-.231 10.684.152 12.85c.403 2.284 1.569 4.175 3.36 5.653 1.858 1.533 3.997 2.284 6.438 2.14 1.482-.085 3.133-.284 4.994-1.86.47.234.962.327 1.78.397.63.059 1.236-.03 1.705-.128.735-.156.684-.837.419-.961-2.155-1.004-1.682-.595-2.113-.926 1.096-1.296 2.746-2.642 3.392-7.003.05-.347.007-.565 0-.845-.004-.17.035-.237.23-.256a4.173 4.173 0 0 0 1.545-.475c1.396-.763 1.96-2.015 2.093-3.517.02-.23-.004-.467-.247-.588zM11.581 18c-2.089-1.642-3.102-2.183-3.52-2.16-.392.024-.321.471-.235.763.09.288.207.486.371.739.114.167.192.416-.113.603-.673.416-1.842-.14-1.897-.167-1.361-.802-2.5-1.86-3.301-3.307-.774-1.393-1.224-2.887-1.298-4.482-.02-.386.093-.522.477-.592a4.696 4.696 0 0 1 1.529-.039c2.132.312 3.946 1.265 5.468 2.774.868.86 1.525 1.887 2.202 2.891.72 1.066 1.494 2.082 2.48 2.914.348.292.625.514.891.677-.802.09-2.14.11-3.054-.614zm1-6.44a.306.306 0 0 1 .415-.287.302.302 0 0 1 .2.288.306.306 0 0 1-.31.307.303.303 0 0 1-.304-.308zm3.11 1.596c-.2.081-.399.151-.59.16a1.245 1.245 0 0 1-.798-.254c-.274-.23-.47-.358-.552-.758a1.73 1.73 0 0 1 .016-.588c.07-.327-.008-.537-.239-.727-.187-.156-.426-.199-.688-.199a.559.559 0 0 1-.254-.078c-.11-.054-.2-.19-.114-.358.028-.054.16-.186.192-.21.356-.202.767-.136 1.146.016.352.144.618.408 1.001.782.391.451.462.576.685.914.176.265.336.537.445.848.067.195-.019.354-.25.452z" />
|
| 211 |
+
</svg>
|
| 212 |
+
)
|
| 213 |
+
}
|
| 214 |
+
|
| 215 |
+
export function IconMiMo(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
|
| 216 |
+
return (
|
| 217 |
+
<svg {...props} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
| 218 |
+
<path d="M12 0C8.016 0 4.756.255 2.493 2.516.23 4.776 0 8.033 0 12.012c0 3.98.23 7.235 2.494 9.497C4.757 23.77 8.017 24 12 24c3.983 0 7.243-.23 9.506-2.491C23.77 19.247 24 15.99 24 12.012c0-3.984-.233-7.243-2.502-9.504C19.234.252 15.978 0 12 0zM4.906 7.405h5.624c1.47 0 3.007.068 3.764.827.746.746.827 2.233.83 3.676v4.54a.15.15 0 0 1-.152.147h-1.947a.15.15 0 0 1-.152-.148V11.83c-.002-.806-.048-1.634-.464-2.051-.358-.36-1.026-.441-1.72-.458H7.158a.15.15 0 0 0-.151.147v6.98a.15.15 0 0 1-.152.148H4.906a.15.15 0 0 1-.15-.148V7.554a.15.15 0 0 1 .15-.149zm12.131 0h1.949a.15.15 0 0 1 .15.15v8.892a.15.15 0 0 1-.15.148h-1.949a.15.15 0 0 1-.151-.148V7.554a.15.15 0 0 1 .151-.149zM8.92 10.948h2.046c.083 0 .15.066.15.147v5.352a.15.15 0 0 1-.15.148H8.92a.15.15 0 0 1-.152-.148v-5.352a.15.15 0 0 1 .152-.147Z" />
|
| 219 |
+
</svg>
|
| 220 |
+
)
|
| 221 |
+
}
|
| 222 |
+
|
| 223 |
+
export function IconXiaomi(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
|
| 224 |
+
return (
|
| 225 |
+
<svg {...props} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
| 226 |
+
<path d="M12 0C8.016 0 4.756.255 2.493 2.516.23 4.776 0 8.033 0 12.012c0 3.98.23 7.235 2.494 9.497C4.757 23.77 8.017 24 12 24c3.983 0 7.243-.23 9.506-2.491C23.77 19.247 24 15.99 24 12.012c0-3.984-.233-7.243-2.502-9.504C19.234.252 15.978 0 12 0zM4.906 7.405h5.624c1.47 0 3.007.068 3.764.827.746.746.827 2.233.83 3.676v4.54a.15.15 0 0 1-.152.147h-1.947a.15.15 0 0 1-.152-.148V11.83c-.002-.806-.048-1.634-.464-2.051-.358-.36-1.026-.441-1.72-.458H7.158a.15.15 0 0 0-.151.147v6.98a.15.15 0 0 1-.152.148H4.906a.15.15 0 0 1-.15-.148V7.554a.15.15 0 0 1 .15-.149zm12.131 0h1.949a.15.15 0 0 1 .15.15v8.892a.15.15 0 0 1-.15.148h-1.949a.15.15 0 0 1-.151-.148V7.554a.15.15 0 0 1 .151-.149zM8.92 10.948h2.046c.083 0 .15.066.15.147v5.352a.15.15 0 0 1-.15.148H8.92a.15.15 0 0 1-.152-.148v-5.352a.15.15 0 0 1 .152-.147Z" />
|
| 227 |
+
</svg>
|
| 228 |
+
)
|
| 229 |
+
}
|
| 230 |
+
|
| 231 |
+
export function IconNvidia(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
|
| 232 |
+
return (
|
| 233 |
+
<svg {...props} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
| 234 |
+
<path d="M8.948 8.798v-1.43a6.7 6.7 0 0 1 .424-.018c3.922-.124 6.493 3.374 6.493 3.374s-2.774 3.851-5.75 3.851c-.398 0-.787-.062-1.158-.185v-4.346c1.528.185 1.837.857 2.747 2.385l2.04-1.714s-1.492-1.952-4-1.952a6.016 6.016 0 0 0-.796.035m0-4.735v2.138l.424-.027c5.45-.185 9.01 4.47 9.01 4.47s-4.08 4.964-8.33 4.964c-.37 0-.733-.035-1.095-.097v1.325c.3.035.61.062.91.062 3.957 0 6.82-2.023 9.593-4.408.459.371 2.34 1.263 2.73 1.652-2.633 2.208-8.772 3.984-12.253 3.984-.335 0-.653-.018-.971-.053v1.864H24V4.063zm0 10.326v1.131c-3.657-.654-4.673-4.46-4.673-4.46s1.758-1.944 4.673-2.262v1.237H8.94c-1.528-.186-2.73 1.245-2.73 1.245s.68 2.412 2.739 3.11M2.456 10.9s2.164-3.197 6.5-3.533V6.201C4.153 6.59 0 10.653 0 10.653s2.35 6.802 8.948 7.42v-1.237c-4.84-.6-6.492-5.936-6.492-5.936z" />
|
| 235 |
+
</svg>
|
| 236 |
+
)
|
| 237 |
+
}
|
| 238 |
+
|
| 239 |
+
export function IconArcee(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
|
| 240 |
+
return (
|
| 241 |
+
<svg {...props} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 37 32" fill="currentColor">
|
| 242 |
+
<path d="M20.4062 3.66602L4.24121 31.5928H0L18.2881 0L20.4062 3.66602Z" />
|
| 243 |
+
<path d="M25.8838 13.1553L11.0752 31.5928H6.36719L23.9131 9.74512L25.8838 13.1553Z" />
|
| 244 |
+
<path d="M36.5352 31.5928H21.6611L34.6191 28.2783L36.5352 31.5928Z" />
|
| 245 |
+
<path d="M31.2627 22.4648L19.1699 31.5898H13.0762L29.4131 19.2617L31.2627 22.4648Z" />
|
| 246 |
+
</svg>
|
| 247 |
+
)
|
| 248 |
+
}
|
| 249 |
+
|
| 250 |
+
export function IconStealth(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
|
| 251 |
+
return (
|
| 252 |
+
<svg {...props} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 18" fill="none">
|
| 253 |
+
<path
|
| 254 |
+
d="M24 15.5L18.1816 14.2871L18.1328 14.3115C16.9036 11.7879 15.6135 9.29301 14.2607 6.82812C14.0172 6.38435 13.771 5.94188 13.5234 5.5C13.6911 5.97998 13.8606 6.45942 14.0322 6.9375C14.9902 9.60529 16.012 12.2429 17.0947 14.8516L12 17.5L6.9043 14.8516C7.98712 12.2428 9.00977 9.6054 9.96777 6.9375C10.1394 6.45942 10.3089 5.97998 10.4766 5.5C10.229 5.94188 9.98281 6.38435 9.73926 6.82812C8.38629 9.29339 7.09557 11.7884 5.86621 14.3125L5.81738 14.2871L0 15.5L12 0.5L24 15.5Z"
|
| 255 |
+
fill="currentColor"
|
| 256 |
+
/>
|
| 257 |
+
</svg>
|
| 258 |
+
)
|
| 259 |
+
}
|
| 260 |
+
|
| 261 |
+
export function IconChevronLeft(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
|
| 262 |
+
return (
|
| 263 |
+
<svg {...props} viewBox="0 0 20 20" fill="none">
|
| 264 |
+
<path d="M12 15L7 10L12 5" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" />
|
| 265 |
+
</svg>
|
| 266 |
+
)
|
| 267 |
+
}
|
| 268 |
+
|
| 269 |
+
export function IconChevronRight(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
|
| 270 |
+
return (
|
| 271 |
+
<svg {...props} viewBox="0 0 20 20" fill="none">
|
| 272 |
+
<path d="M8 5L13 10L8 15" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" />
|
| 273 |
+
</svg>
|
| 274 |
+
)
|
| 275 |
+
}
|
| 276 |
+
|
| 277 |
+
export function IconBreakdown(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
|
| 278 |
+
return (
|
| 279 |
+
<svg {...props} width="16" height="16" viewBox="0 0 16 16" fill="none">
|
| 280 |
+
<path d="M2 12L2 8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
|
| 281 |
+
<path d="M6 12L6 4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
|
| 282 |
+
<path d="M10 12L10 6" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
|
| 283 |
+
<path d="M14 12L14 9" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
|
| 284 |
+
</svg>
|
| 285 |
+
)
|
| 286 |
+
}
|
packages/console/app/src/component/language-picker.css
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[data-component="language-picker"] {
|
| 2 |
+
width: auto;
|
| 3 |
+
}
|
| 4 |
+
|
| 5 |
+
[data-component="footer"] [data-component="language-picker"] {
|
| 6 |
+
width: 100%;
|
| 7 |
+
}
|
| 8 |
+
|
| 9 |
+
[data-component="footer"] [data-component="language-picker"] [data-component="dropdown"] {
|
| 10 |
+
width: 100%;
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
/* Standard site footer (grid of cells) */
|
| 14 |
+
[data-component="footer"] [data-slot="cell"] [data-component="language-picker"] {
|
| 15 |
+
height: 100%;
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
[data-component="footer"] [data-slot="cell"] [data-component="language-picker"] [data-slot="trigger"] {
|
| 19 |
+
width: 100%;
|
| 20 |
+
padding: 2rem 0;
|
| 21 |
+
border-radius: 0;
|
| 22 |
+
justify-content: center;
|
| 23 |
+
gap: var(--space-2);
|
| 24 |
+
color: inherit;
|
| 25 |
+
font: inherit;
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
[data-component="footer"] [data-slot="cell"] [data-component="language-picker"] [data-slot="trigger"] span {
|
| 29 |
+
flex: 0 0 auto;
|
| 30 |
+
text-align: center;
|
| 31 |
+
font-weight: inherit;
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
[data-component="footer"] [data-slot="cell"] [data-component="language-picker"] [data-slot="trigger"]:hover {
|
| 35 |
+
background: var(--color-background-weak);
|
| 36 |
+
text-decoration: underline;
|
| 37 |
+
text-underline-offset: var(--space-1);
|
| 38 |
+
text-decoration-thickness: 1px;
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
/* Footer dropdown should open upward */
|
| 42 |
+
[data-component="footer"] [data-component="language-picker"] [data-slot="dropdown"] {
|
| 43 |
+
top: auto;
|
| 44 |
+
bottom: 100%;
|
| 45 |
+
margin-top: 0;
|
| 46 |
+
margin-bottom: var(--space-2);
|
| 47 |
+
max-height: min(60vh, 420px);
|
| 48 |
+
overflow: auto;
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
[data-component="legal"] {
|
| 52 |
+
flex-wrap: wrap;
|
| 53 |
+
row-gap: var(--space-2);
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
[data-component="legal"] [data-component="language-picker"] {
|
| 57 |
+
width: auto;
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
[data-component="legal"] [data-component="language-picker"] [data-slot="trigger"] {
|
| 61 |
+
padding: 0;
|
| 62 |
+
border-radius: 0;
|
| 63 |
+
background: transparent;
|
| 64 |
+
font: inherit;
|
| 65 |
+
color: var(--color-text-weak);
|
| 66 |
+
white-space: nowrap;
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
[data-component="legal"] [data-component="language-picker"] [data-slot="trigger"] span {
|
| 70 |
+
font-weight: inherit;
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
[data-component="legal"] [data-component="language-picker"] [data-slot="trigger"]:hover {
|
| 74 |
+
background: transparent;
|
| 75 |
+
color: var(--color-text);
|
| 76 |
+
text-decoration: underline;
|
| 77 |
+
text-underline-offset: var(--space-1);
|
| 78 |
+
text-decoration-thickness: 1px;
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
[data-component="legal"] [data-component="language-picker"] [data-slot="dropdown"] {
|
| 82 |
+
top: auto;
|
| 83 |
+
bottom: 100%;
|
| 84 |
+
margin-top: 0;
|
| 85 |
+
margin-bottom: var(--space-2);
|
| 86 |
+
max-height: min(60vh, 420px);
|
| 87 |
+
overflow: auto;
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
/* Black pages footer */
|
| 91 |
+
[data-page="black"] [data-component="language-picker"] {
|
| 92 |
+
width: auto;
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
[data-page="black"] [data-component="language-picker"] [data-component="dropdown"] {
|
| 96 |
+
width: auto;
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
[data-page="black"] [data-component="language-picker"] [data-slot="trigger"] {
|
| 100 |
+
padding: 0;
|
| 101 |
+
border-radius: 0;
|
| 102 |
+
background: transparent;
|
| 103 |
+
font: inherit;
|
| 104 |
+
color: rgba(255, 255, 255, 0.39);
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
[data-page="black"] [data-component="language-picker"] [data-slot="trigger"]:hover {
|
| 108 |
+
background: transparent;
|
| 109 |
+
text-decoration: underline;
|
| 110 |
+
text-underline-offset: 4px;
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
[data-page="black"] [data-component="language-picker"] [data-slot="dropdown"] {
|
| 114 |
+
top: auto;
|
| 115 |
+
bottom: 100%;
|
| 116 |
+
margin-top: 0;
|
| 117 |
+
margin-bottom: 12px;
|
| 118 |
+
background-color: #0e0e10;
|
| 119 |
+
border-color: rgba(255, 255, 255, 0.14);
|
| 120 |
+
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.6);
|
| 121 |
+
max-height: min(60vh, 420px);
|
| 122 |
+
overflow: auto;
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
[data-page="black"] [data-component="language-picker"] [data-slot="item"] {
|
| 126 |
+
color: rgba(255, 255, 255, 0.86);
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
[data-page="black"] [data-component="language-picker"] [data-slot="item"]:hover {
|
| 130 |
+
background-color: rgba(255, 255, 255, 0.06);
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
[data-page="black"] [data-component="language-picker"] [data-slot="item"][data-selected="true"] {
|
| 134 |
+
background-color: rgba(255, 255, 255, 0.1);
|
| 135 |
+
}
|
packages/console/app/src/component/language-picker.tsx
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { For, createSignal } from "solid-js"
|
| 2 |
+
import { useLocation, useNavigate } from "@solidjs/router"
|
| 3 |
+
import { Dropdown, DropdownItem } from "~/component/dropdown"
|
| 4 |
+
import { useLanguage } from "~/context/language"
|
| 5 |
+
import { route, strip } from "~/lib/language"
|
| 6 |
+
import "./language-picker.css"
|
| 7 |
+
|
| 8 |
+
export function LanguagePicker(props: { align?: "left" | "right" } = {}) {
|
| 9 |
+
const language = useLanguage()
|
| 10 |
+
const navigate = useNavigate()
|
| 11 |
+
const location = useLocation()
|
| 12 |
+
const [open, setOpen] = createSignal(false)
|
| 13 |
+
|
| 14 |
+
return (
|
| 15 |
+
<div data-component="language-picker">
|
| 16 |
+
<Dropdown
|
| 17 |
+
trigger={language.label(language.locale())}
|
| 18 |
+
align={props.align ?? "left"}
|
| 19 |
+
open={open()}
|
| 20 |
+
onOpenChange={setOpen}
|
| 21 |
+
>
|
| 22 |
+
<For each={language.locales}>
|
| 23 |
+
{(locale) => (
|
| 24 |
+
<DropdownItem
|
| 25 |
+
selected={locale === language.locale()}
|
| 26 |
+
onClick={() => {
|
| 27 |
+
language.setLocale(locale)
|
| 28 |
+
const href = `${route(locale, strip(location.pathname))}${location.search}${location.hash}`
|
| 29 |
+
if (href !== `${location.pathname}${location.search}${location.hash}`) navigate(href)
|
| 30 |
+
setOpen(false)
|
| 31 |
+
}}
|
| 32 |
+
>
|
| 33 |
+
{language.label(locale)}
|
| 34 |
+
</DropdownItem>
|
| 35 |
+
)}
|
| 36 |
+
</For>
|
| 37 |
+
</Dropdown>
|
| 38 |
+
</div>
|
| 39 |
+
)
|
| 40 |
+
}
|
packages/console/app/src/component/legal.tsx
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { A } from "@solidjs/router"
|
| 2 |
+
import { LanguagePicker } from "~/component/language-picker"
|
| 3 |
+
import { useI18n } from "~/context/i18n"
|
| 4 |
+
import { useLanguage } from "~/context/language"
|
| 5 |
+
|
| 6 |
+
export function Legal() {
|
| 7 |
+
const i18n = useI18n()
|
| 8 |
+
const language = useLanguage()
|
| 9 |
+
return (
|
| 10 |
+
<div data-component="legal">
|
| 11 |
+
<span>
|
| 12 |
+
©{new Date().getFullYear()} <a href="https://anoma.ly">Anomaly</a>
|
| 13 |
+
</span>
|
| 14 |
+
<span>
|
| 15 |
+
<A href={language.route("/brand")}>{i18n.t("legal.brand")}</A>
|
| 16 |
+
</span>
|
| 17 |
+
<span>
|
| 18 |
+
<A href={language.route("/legal/privacy-policy")}>{i18n.t("legal.privacy")}</A>
|
| 19 |
+
</span>
|
| 20 |
+
<span>
|
| 21 |
+
<A href={language.route("/legal/terms-of-service")}>{i18n.t("legal.terms")}</A>
|
| 22 |
+
</span>
|
| 23 |
+
<span>
|
| 24 |
+
<LanguagePicker align="right" />
|
| 25 |
+
</span>
|
| 26 |
+
</div>
|
| 27 |
+
)
|
| 28 |
+
}
|
packages/console/app/src/component/limits-graph.css
ADDED
|
@@ -0,0 +1,300 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[data-component="go-usage"] {
|
| 2 |
+
--model-width: 16.5rem;
|
| 3 |
+
--request-width: 5.25rem;
|
| 4 |
+
--allowance-width: 6.75rem;
|
| 5 |
+
--column-gap: 1.5rem;
|
| 6 |
+
--bar-origin: left;
|
| 7 |
+
container-type: inline-size;
|
| 8 |
+
margin: 0;
|
| 9 |
+
padding: 3rem var(--padding) 2.5rem;
|
| 10 |
+
font-variant-numeric: tabular-nums;
|
| 11 |
+
scroll-margin-top: 6rem;
|
| 12 |
+
-webkit-font-smoothing: antialiased;
|
| 13 |
+
|
| 14 |
+
&:dir(rtl) {
|
| 15 |
+
--bar-origin: right;
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
[data-slot="heading"] {
|
| 19 |
+
padding-bottom: 1.5rem;
|
| 20 |
+
|
| 21 |
+
h2 {
|
| 22 |
+
color: var(--color-text-strong);
|
| 23 |
+
font-size: 1rem;
|
| 24 |
+
font-weight: 600;
|
| 25 |
+
}
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
s {
|
| 29 |
+
display: block;
|
| 30 |
+
color: var(--color-text-weak);
|
| 31 |
+
font-size: 0.6875rem;
|
| 32 |
+
font-weight: 400;
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
[data-slot="columns"],
|
| 36 |
+
[data-slot="model-row"] {
|
| 37 |
+
display: grid;
|
| 38 |
+
grid-template-columns: var(--model-width) minmax(0, 1fr) var(--allowance-width);
|
| 39 |
+
align-items: center;
|
| 40 |
+
column-gap: var(--column-gap);
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
[data-slot="columns"] {
|
| 44 |
+
padding-bottom: 0.875rem;
|
| 45 |
+
border-bottom: 1px solid var(--color-border-weak);
|
| 46 |
+
font-size: 0.6875rem;
|
| 47 |
+
color: var(--color-text);
|
| 48 |
+
overflow-wrap: anywhere;
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
[data-slot="requests-heading"],
|
| 52 |
+
[data-slot="allowance-heading"] {
|
| 53 |
+
text-align: end;
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
[data-slot="rows"] {
|
| 57 |
+
padding-top: 0.375rem;
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
[data-slot="model-row"] {
|
| 61 |
+
min-height: 2.5rem;
|
| 62 |
+
font-size: 0.8125rem;
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
[data-slot="model"] {
|
| 66 |
+
display: flex;
|
| 67 |
+
align-items: baseline;
|
| 68 |
+
flex-wrap: wrap;
|
| 69 |
+
gap: 0.375rem 0.5rem;
|
| 70 |
+
min-width: 0;
|
| 71 |
+
line-height: 1.5;
|
| 72 |
+
color: var(--color-text);
|
| 73 |
+
font-weight: 400;
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
[data-slot="badge"] {
|
| 77 |
+
color: var(--color-text-strong);
|
| 78 |
+
font-size: 0.625rem;
|
| 79 |
+
line-height: 1.3;
|
| 80 |
+
border: 1px solid var(--color-border);
|
| 81 |
+
padding: 0.0625rem 0.25rem;
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
[data-slot="region"] {
|
| 85 |
+
display: inline-flex;
|
| 86 |
+
align-self: center;
|
| 87 |
+
padding: 0.25rem;
|
| 88 |
+
color: var(--color-text);
|
| 89 |
+
text-decoration: none;
|
| 90 |
+
|
| 91 |
+
&:hover {
|
| 92 |
+
color: var(--color-text-strong);
|
| 93 |
+
}
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
[data-slot="usage-value"] {
|
| 97 |
+
display: grid;
|
| 98 |
+
grid-template-columns: minmax(0, 1fr) var(--request-width);
|
| 99 |
+
align-items: center;
|
| 100 |
+
gap: var(--column-gap);
|
| 101 |
+
align-self: stretch;
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
[data-slot="track"] {
|
| 105 |
+
display: flex;
|
| 106 |
+
align-items: center;
|
| 107 |
+
align-self: stretch;
|
| 108 |
+
position: relative;
|
| 109 |
+
isolation: isolate;
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
[data-slot="gridline"] {
|
| 113 |
+
position: absolute;
|
| 114 |
+
inset-block: 0;
|
| 115 |
+
inset-inline-start: var(--position);
|
| 116 |
+
border-inline-start: 1px dotted var(--color-border-weak);
|
| 117 |
+
z-index: -1;
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
[data-slot="bar"] {
|
| 121 |
+
width: var(--width);
|
| 122 |
+
height: 0.375rem;
|
| 123 |
+
background: var(--color-go-2);
|
| 124 |
+
transform-origin: var(--bar-origin);
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
&[data-visible] [data-slot="bar"] {
|
| 128 |
+
animation: go-usage-reveal 700ms cubic-bezier(0.22, 1, 0.36, 1) both;
|
| 129 |
+
animation-delay: var(--delay);
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
[data-slot="requests"] {
|
| 133 |
+
text-align: end;
|
| 134 |
+
font-weight: 600;
|
| 135 |
+
color: var(--color-text-strong);
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
[data-slot="allowance"] {
|
| 139 |
+
text-align: end;
|
| 140 |
+
color: var(--color-text);
|
| 141 |
+
|
| 142 |
+
&[data-high] {
|
| 143 |
+
color: var(--color-text-strong);
|
| 144 |
+
}
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
[data-slot="currency"] {
|
| 148 |
+
color: var(--color-text-weak);
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
[data-slot="axis"] {
|
| 152 |
+
padding-inline-start: calc(var(--model-width) + var(--column-gap));
|
| 153 |
+
padding-inline-end: calc(var(--request-width) + var(--allowance-width) + 2 * var(--column-gap));
|
| 154 |
+
height: 2.5rem;
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
[data-slot="ticks"] {
|
| 158 |
+
position: relative;
|
| 159 |
+
height: 100%;
|
| 160 |
+
font-size: 0.625rem;
|
| 161 |
+
color: var(--color-text-weak);
|
| 162 |
+
|
| 163 |
+
span {
|
| 164 |
+
position: absolute;
|
| 165 |
+
inset-inline-start: var(--position);
|
| 166 |
+
top: 0.75rem;
|
| 167 |
+
transform: translateX(-50%);
|
| 168 |
+
|
| 169 |
+
&:dir(rtl) {
|
| 170 |
+
transform: translateX(50%);
|
| 171 |
+
}
|
| 172 |
+
}
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
[data-slot="expand"] {
|
| 176 |
+
display: flex;
|
| 177 |
+
justify-content: center;
|
| 178 |
+
align-items: center;
|
| 179 |
+
gap: 0.5rem;
|
| 180 |
+
min-height: 2.75rem;
|
| 181 |
+
padding: 0.75rem 0;
|
| 182 |
+
background: none;
|
| 183 |
+
border: none;
|
| 184 |
+
color: var(--color-text);
|
| 185 |
+
font-family: inherit;
|
| 186 |
+
font-size: 0.75rem;
|
| 187 |
+
cursor: pointer;
|
| 188 |
+
|
| 189 |
+
&:hover {
|
| 190 |
+
color: var(--color-text-strong);
|
| 191 |
+
}
|
| 192 |
+
&[aria-expanded="true"] svg {
|
| 193 |
+
transform: rotate(180deg);
|
| 194 |
+
}
|
| 195 |
+
}
|
| 196 |
+
|
| 197 |
+
:is(button, a):focus-visible {
|
| 198 |
+
outline: 1px solid var(--color-text-strong);
|
| 199 |
+
outline-offset: 4px;
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
figcaption {
|
| 203 |
+
display: flex;
|
| 204 |
+
flex-wrap: wrap;
|
| 205 |
+
justify-content: space-between;
|
| 206 |
+
align-items: center;
|
| 207 |
+
gap: 1rem;
|
| 208 |
+
border-top: 1px solid var(--color-border-weak);
|
| 209 |
+
font-size: 0.75rem;
|
| 210 |
+
color: var(--color-text);
|
| 211 |
+
|
| 212 |
+
a {
|
| 213 |
+
color: inherit;
|
| 214 |
+
white-space: nowrap;
|
| 215 |
+
}
|
| 216 |
+
}
|
| 217 |
+
|
| 218 |
+
@container (max-width: 48rem) {
|
| 219 |
+
[data-slot="columns"],
|
| 220 |
+
[data-slot="model-row"],
|
| 221 |
+
[data-slot="axis"] {
|
| 222 |
+
--model-width: 13rem;
|
| 223 |
+
--request-width: 4rem;
|
| 224 |
+
--allowance-width: 6rem;
|
| 225 |
+
--column-gap: 1rem;
|
| 226 |
+
}
|
| 227 |
+
}
|
| 228 |
+
|
| 229 |
+
@container (max-width: 38rem) {
|
| 230 |
+
[data-slot="columns"],
|
| 231 |
+
[data-slot="model-row"] {
|
| 232 |
+
--request-width: 5rem;
|
| 233 |
+
--allowance-width: 4.5rem;
|
| 234 |
+
--column-gap: 0.75rem;
|
| 235 |
+
grid-template-columns: minmax(0, 1fr) var(--request-width) var(--allowance-width);
|
| 236 |
+
}
|
| 237 |
+
|
| 238 |
+
[data-slot="columns"] {
|
| 239 |
+
font-size: 0.625rem;
|
| 240 |
+
}
|
| 241 |
+
|
| 242 |
+
[data-slot="model-row"] {
|
| 243 |
+
min-height: 4rem;
|
| 244 |
+
padding-block: 0.375rem;
|
| 245 |
+
row-gap: 0.25rem;
|
| 246 |
+
font-size: 1rem;
|
| 247 |
+
}
|
| 248 |
+
|
| 249 |
+
[data-slot="model"] {
|
| 250 |
+
grid-column: 1 / -1;
|
| 251 |
+
}
|
| 252 |
+
[data-slot="usage-value"] {
|
| 253 |
+
display: contents;
|
| 254 |
+
}
|
| 255 |
+
|
| 256 |
+
[data-slot="track"] {
|
| 257 |
+
grid-column: 1;
|
| 258 |
+
grid-row: 2;
|
| 259 |
+
align-self: center;
|
| 260 |
+
height: 4px;
|
| 261 |
+
}
|
| 262 |
+
|
| 263 |
+
[data-slot="bar"] {
|
| 264 |
+
height: 4px;
|
| 265 |
+
}
|
| 266 |
+
[data-slot="requests"] {
|
| 267 |
+
grid-column: 2;
|
| 268 |
+
grid-row: 2;
|
| 269 |
+
}
|
| 270 |
+
[data-slot="allowance"] {
|
| 271 |
+
grid-column: 3;
|
| 272 |
+
grid-row: 2;
|
| 273 |
+
}
|
| 274 |
+
[data-slot="gridline"],
|
| 275 |
+
[data-slot="axis"] {
|
| 276 |
+
display: none;
|
| 277 |
+
}
|
| 278 |
+
[data-slot="expand"] {
|
| 279 |
+
font-size: 0.8125rem;
|
| 280 |
+
}
|
| 281 |
+
figcaption {
|
| 282 |
+
margin-top: 1rem;
|
| 283 |
+
}
|
| 284 |
+
}
|
| 285 |
+
|
| 286 |
+
@media (prefers-reduced-motion: reduce) {
|
| 287 |
+
&[data-visible] [data-slot="bar"] {
|
| 288 |
+
animation: none;
|
| 289 |
+
}
|
| 290 |
+
}
|
| 291 |
+
}
|
| 292 |
+
|
| 293 |
+
@keyframes go-usage-reveal {
|
| 294 |
+
from {
|
| 295 |
+
transform: scaleX(0);
|
| 296 |
+
}
|
| 297 |
+
to {
|
| 298 |
+
transform: scaleX(1);
|
| 299 |
+
}
|
| 300 |
+
}
|
packages/console/app/src/component/limits-graph.tsx
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import "./limits-graph.css"
|
| 2 |
+
import { For, Show, createMemo, createSignal, createUniqueId, onCleanup, onMount } from "solid-js"
|
| 3 |
+
import { useI18n } from "~/context/i18n"
|
| 4 |
+
import { useLanguage } from "~/context/language"
|
| 5 |
+
import { goModels } from "./go-models"
|
| 6 |
+
|
| 7 |
+
// Compress the request range, with the same domain in both views.
|
| 8 |
+
const max = Math.max(...goModels.map((model) => model.requests))
|
| 9 |
+
const position = (requests: number) =>
|
| 10 |
+
4 + Math.pow(Math.log10(Math.max(requests / 100, 1)) / Math.log10(max / 100), 2.2) * 96
|
| 11 |
+
const ticks = [100, 1000, 10000, 40000]
|
| 12 |
+
|
| 13 |
+
export function LimitsGraph(props: { href: string }) {
|
| 14 |
+
const i18n = useI18n()
|
| 15 |
+
const language = useLanguage()
|
| 16 |
+
const id = createUniqueId()
|
| 17 |
+
const [expanded, setExpanded] = createSignal(false)
|
| 18 |
+
const [visible, setVisible] = createSignal(false)
|
| 19 |
+
const models = createMemo(() => goModels.filter((model) => expanded() || model.featured || model.fresh))
|
| 20 |
+
const format = createMemo(() => new Intl.NumberFormat(language.tag(language.locale())))
|
| 21 |
+
const compact = createMemo(
|
| 22 |
+
() => new Intl.NumberFormat(language.tag(language.locale()), { notation: "compact", maximumFractionDigits: 1 }),
|
| 23 |
+
)
|
| 24 |
+
const currency = createMemo(
|
| 25 |
+
() =>
|
| 26 |
+
new Intl.NumberFormat(language.tag(language.locale()), {
|
| 27 |
+
style: "currency",
|
| 28 |
+
currency: "USD",
|
| 29 |
+
currencyDisplay: "narrowSymbol",
|
| 30 |
+
maximumFractionDigits: 0,
|
| 31 |
+
}),
|
| 32 |
+
)
|
| 33 |
+
let root!: HTMLElement
|
| 34 |
+
|
| 35 |
+
onMount(() => {
|
| 36 |
+
if (!window.IntersectionObserver) return setVisible(true)
|
| 37 |
+
const observer = new IntersectionObserver(
|
| 38 |
+
(entries) => {
|
| 39 |
+
if (!entries.some((entry) => entry.isIntersecting)) return
|
| 40 |
+
setVisible(true)
|
| 41 |
+
observer.disconnect()
|
| 42 |
+
},
|
| 43 |
+
{ threshold: 0.1 },
|
| 44 |
+
)
|
| 45 |
+
observer.observe(root)
|
| 46 |
+
onCleanup(() => observer.disconnect())
|
| 47 |
+
})
|
| 48 |
+
|
| 49 |
+
return (
|
| 50 |
+
<figure
|
| 51 |
+
ref={root}
|
| 52 |
+
id="usage"
|
| 53 |
+
dir={language.dir(language.locale())}
|
| 54 |
+
data-component="go-usage"
|
| 55 |
+
data-visible={visible() ? "" : undefined}
|
| 56 |
+
aria-labelledby={`${id}-title`}
|
| 57 |
+
>
|
| 58 |
+
<div data-slot="heading">
|
| 59 |
+
<h2 id={`${id}-title`}>{i18n.t("go.graph.period")}</h2>
|
| 60 |
+
</div>
|
| 61 |
+
|
| 62 |
+
<div role="table" aria-labelledby={`${id}-title`} id={`${id}-models`}>
|
| 63 |
+
<div role="row" data-slot="columns">
|
| 64 |
+
<div role="columnheader" data-slot="model-heading">
|
| 65 |
+
{i18n.t("go.graph.model")}
|
| 66 |
+
</div>
|
| 67 |
+
<div role="columnheader" data-slot="requests-heading">
|
| 68 |
+
{i18n.t("go.graph.requests")}
|
| 69 |
+
</div>
|
| 70 |
+
<div role="columnheader" data-slot="allowance-heading">
|
| 71 |
+
{i18n.t("go.graph.allowance")}
|
| 72 |
+
</div>
|
| 73 |
+
</div>
|
| 74 |
+
<div role="rowgroup" data-slot="rows">
|
| 75 |
+
<For each={models()}>
|
| 76 |
+
{(model, index) => (
|
| 77 |
+
<div role="row" data-slot="model-row" data-model={model.id} style={{ "--delay": `${index() * 30}ms` }}>
|
| 78 |
+
<div role="rowheader" data-slot="model">
|
| 79 |
+
<bdi>{model.name}</bdi>
|
| 80 |
+
<Show when={model.fresh}>
|
| 81 |
+
<span data-slot="badge">{i18n.t("go.graph.new")}</span>
|
| 82 |
+
</Show>
|
| 83 |
+
<Show when={model.bonus}>
|
| 84 |
+
<span data-slot="badge">{i18n.t("go.graph.bonus", { count: model.bonus! })}</span>
|
| 85 |
+
</Show>
|
| 86 |
+
<Show when={model.regions}>
|
| 87 |
+
<a
|
| 88 |
+
data-slot="region"
|
| 89 |
+
href="https://ai.developer.meta.com/legal/geographic-use-policy"
|
| 90 |
+
aria-label={`${model.name}: ${i18n.t("go.graph.limitedRegions")}`}
|
| 91 |
+
title={i18n.t("go.graph.limitedRegions")}
|
| 92 |
+
>
|
| 93 |
+
<svg viewBox="0 0 16 16" width="13" height="13" fill="none" aria-hidden="true">
|
| 94 |
+
<circle cx="8" cy="8" r="6" stroke="currentColor" />
|
| 95 |
+
<ellipse cx="8" cy="8" rx="2.5" ry="6" stroke="currentColor" />
|
| 96 |
+
<path d="M2 8h12" stroke="currentColor" />
|
| 97 |
+
</svg>
|
| 98 |
+
</a>
|
| 99 |
+
</Show>
|
| 100 |
+
</div>
|
| 101 |
+
<div role="cell" data-slot="usage-value">
|
| 102 |
+
<div data-slot="track" aria-hidden="true">
|
| 103 |
+
<For each={ticks}>
|
| 104 |
+
{(tick) => <span data-slot="gridline" style={{ "--position": `${position(tick)}%` }} />}
|
| 105 |
+
</For>
|
| 106 |
+
<div data-slot="bar" style={{ "--width": `${position(model.requests)}%` }} />
|
| 107 |
+
</div>
|
| 108 |
+
<div data-slot="requests">
|
| 109 |
+
<Show when={model.baseRequests}>
|
| 110 |
+
<s>{format().format(model.baseRequests!)}</s>{" "}
|
| 111 |
+
</Show>
|
| 112 |
+
<bdi>{format().format(model.requests)}</bdi>
|
| 113 |
+
</div>
|
| 114 |
+
</div>
|
| 115 |
+
<div role="cell" data-slot="allowance" data-high={model.allowance >= 60 ? "" : undefined}>
|
| 116 |
+
<Show when={model.baseAllowance}>
|
| 117 |
+
<s>{currency().format(model.baseAllowance!)}</s>{" "}
|
| 118 |
+
</Show>
|
| 119 |
+
<bdi>
|
| 120 |
+
<For each={currency().formatToParts(model.allowance)}>
|
| 121 |
+
{(part) => (
|
| 122 |
+
<span data-slot={part.type === "currency" ? "currency" : undefined}>{part.value}</span>
|
| 123 |
+
)}
|
| 124 |
+
</For>
|
| 125 |
+
</bdi>
|
| 126 |
+
</div>
|
| 127 |
+
</div>
|
| 128 |
+
)}
|
| 129 |
+
</For>
|
| 130 |
+
</div>
|
| 131 |
+
</div>
|
| 132 |
+
|
| 133 |
+
<div data-slot="axis" aria-hidden="true" title={i18n.t("go.graph.scale")}>
|
| 134 |
+
<div data-slot="ticks">
|
| 135 |
+
<For each={ticks}>
|
| 136 |
+
{(tick) => <span style={{ "--position": `${position(tick)}%` }}>{compact().format(tick)}</span>}
|
| 137 |
+
</For>
|
| 138 |
+
</div>
|
| 139 |
+
</div>
|
| 140 |
+
|
| 141 |
+
<figcaption>
|
| 142 |
+
<button
|
| 143 |
+
type="button"
|
| 144 |
+
data-slot="expand"
|
| 145 |
+
aria-expanded={expanded()}
|
| 146 |
+
aria-controls={`${id}-models`}
|
| 147 |
+
onClick={() => {
|
| 148 |
+
if (expanded()) {
|
| 149 |
+
const top = root.getBoundingClientRect().top
|
| 150 |
+
if (top < 80) window.scrollTo({ top: window.scrollY + top - 96, behavior: "instant" })
|
| 151 |
+
}
|
| 152 |
+
setExpanded(!expanded())
|
| 153 |
+
}}
|
| 154 |
+
>
|
| 155 |
+
{expanded()
|
| 156 |
+
? i18n.t("go.graph.showLess")
|
| 157 |
+
: i18n.t("go.graph.showAll", { count: format().format(goModels.length) })}
|
| 158 |
+
<svg width="14" height="14" viewBox="0 0 16 16" fill="none" aria-hidden="true">
|
| 159 |
+
<path d="m4 6 4 4 4-4" stroke="currentColor" stroke-width="1.25" />
|
| 160 |
+
</svg>
|
| 161 |
+
</button>
|
| 162 |
+
<a href={props.href}>{i18n.t("go.graph.usageLimits")}</a>
|
| 163 |
+
</figcaption>
|
| 164 |
+
</figure>
|
| 165 |
+
)
|
| 166 |
+
}
|
packages/console/app/src/component/locale-links.tsx
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Link } from "@solidjs/meta"
|
| 2 |
+
import { For } from "solid-js"
|
| 3 |
+
import { getRequestEvent } from "solid-js/web"
|
| 4 |
+
import { config } from "~/config"
|
| 5 |
+
import { useLanguage } from "~/context/language"
|
| 6 |
+
import { LOCALES, route, tag } from "~/lib/language"
|
| 7 |
+
|
| 8 |
+
function skip(path: string) {
|
| 9 |
+
const evt = getRequestEvent()
|
| 10 |
+
if (!evt) return false
|
| 11 |
+
|
| 12 |
+
const key = "__locale_links_seen"
|
| 13 |
+
const locals = evt.locals as Record<string, unknown>
|
| 14 |
+
const seen = locals[key] instanceof Set ? (locals[key] as Set<string>) : new Set<string>()
|
| 15 |
+
locals[key] = seen
|
| 16 |
+
if (seen.has(path)) return true
|
| 17 |
+
seen.add(path)
|
| 18 |
+
return false
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
export function LocaleLinks(props: { path: string }) {
|
| 22 |
+
const language = useLanguage()
|
| 23 |
+
if (skip(props.path)) return null
|
| 24 |
+
|
| 25 |
+
return (
|
| 26 |
+
<>
|
| 27 |
+
<Link rel="canonical" href={`${config.baseUrl}${route(language.locale(), props.path)}`} />
|
| 28 |
+
<For each={LOCALES}>
|
| 29 |
+
{(locale) => (
|
| 30 |
+
<Link rel="alternate" hreflang={tag(locale)} href={`${config.baseUrl}${route(locale, props.path)}`} />
|
| 31 |
+
)}
|
| 32 |
+
</For>
|
| 33 |
+
<Link rel="alternate" hreflang="x-default" href={`${config.baseUrl}${props.path}`} />
|
| 34 |
+
</>
|
| 35 |
+
)
|
| 36 |
+
}
|
packages/console/app/src/component/modal.css
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
@keyframes fadeIn {
|
| 2 |
+
from {
|
| 3 |
+
opacity: 0;
|
| 4 |
+
}
|
| 5 |
+
|
| 6 |
+
to {
|
| 7 |
+
opacity: 1;
|
| 8 |
+
}
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
@keyframes slideUp {
|
| 12 |
+
from {
|
| 13 |
+
opacity: 0;
|
| 14 |
+
transform: translateY(20px);
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
to {
|
| 18 |
+
opacity: 1;
|
| 19 |
+
transform: translateY(0);
|
| 20 |
+
}
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
[data-component="modal"][data-slot="overlay"] {
|
| 24 |
+
position: fixed;
|
| 25 |
+
top: 0;
|
| 26 |
+
left: 0;
|
| 27 |
+
right: 0;
|
| 28 |
+
bottom: 0;
|
| 29 |
+
z-index: 9999;
|
| 30 |
+
display: flex;
|
| 31 |
+
align-items: center;
|
| 32 |
+
justify-content: center;
|
| 33 |
+
background-color: rgba(0, 0, 0, 0.5);
|
| 34 |
+
animation: fadeIn 0.2s ease;
|
| 35 |
+
|
| 36 |
+
@media (prefers-color-scheme: dark) {
|
| 37 |
+
background-color: rgba(0, 0, 0, 0.7);
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
[data-slot="content"] {
|
| 41 |
+
background-color: var(--color-bg);
|
| 42 |
+
border: 1px solid var(--color-border);
|
| 43 |
+
border-radius: var(--border-radius-md);
|
| 44 |
+
padding: var(--space-6);
|
| 45 |
+
min-width: 400px;
|
| 46 |
+
max-width: 90vw;
|
| 47 |
+
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
|
| 48 |
+
animation: slideUp 0.2s ease;
|
| 49 |
+
|
| 50 |
+
@media (max-width: 30rem) {
|
| 51 |
+
min-width: 300px;
|
| 52 |
+
padding: var(--space-4);
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
@media (prefers-color-scheme: dark) {
|
| 56 |
+
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5);
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
button {
|
| 60 |
+
display: inline-flex;
|
| 61 |
+
align-items: center;
|
| 62 |
+
justify-content: center;
|
| 63 |
+
padding: var(--space-3) var(--space-4);
|
| 64 |
+
border: 1px solid var(--color-border);
|
| 65 |
+
border-radius: var(--border-radius-sm);
|
| 66 |
+
background-color: var(--color-bg);
|
| 67 |
+
color: var(--color-text);
|
| 68 |
+
font-size: var(--font-size-sm);
|
| 69 |
+
font-family: var(--font-sans);
|
| 70 |
+
font-weight: 500;
|
| 71 |
+
line-height: 1;
|
| 72 |
+
cursor: pointer;
|
| 73 |
+
transition: all 0.15s ease;
|
| 74 |
+
|
| 75 |
+
&:hover:not(:disabled) {
|
| 76 |
+
background-color: var(--color-surface-hover);
|
| 77 |
+
border-color: var(--color-accent);
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
&:active:not(:disabled) {
|
| 81 |
+
transform: translateY(1px);
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
&:disabled {
|
| 85 |
+
opacity: 0.5;
|
| 86 |
+
cursor: not-allowed;
|
| 87 |
+
transform: none;
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
&[data-color="primary"] {
|
| 91 |
+
background-color: var(--color-primary);
|
| 92 |
+
border-color: var(--color-primary);
|
| 93 |
+
color: var(--color-primary-text);
|
| 94 |
+
|
| 95 |
+
&:hover:not(:disabled) {
|
| 96 |
+
background-color: var(--color-primary-hover);
|
| 97 |
+
border-color: var(--color-primary-hover);
|
| 98 |
+
}
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
&[data-color="ghost"] {
|
| 102 |
+
background-color: transparent;
|
| 103 |
+
border-color: transparent;
|
| 104 |
+
color: var(--color-text-muted);
|
| 105 |
+
|
| 106 |
+
&:hover:not(:disabled) {
|
| 107 |
+
background-color: var(--color-surface-hover);
|
| 108 |
+
border-color: var(--color-border);
|
| 109 |
+
color: var(--color-text);
|
| 110 |
+
}
|
| 111 |
+
}
|
| 112 |
+
}
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
[data-slot="title"] {
|
| 116 |
+
margin: 0 0 var(--space-4) 0;
|
| 117 |
+
font-size: var(--font-size-lg);
|
| 118 |
+
font-weight: 600;
|
| 119 |
+
color: var(--color-text);
|
| 120 |
+
text-align: center;
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
[data-slot="content"][data-variant="black"] {
|
| 124 |
+
background-color: #000;
|
| 125 |
+
border-color: rgba(255, 255, 255, 0.17);
|
| 126 |
+
color: rgba(255, 255, 255, 0.92);
|
| 127 |
+
font-family: var(--font-mono);
|
| 128 |
+
|
| 129 |
+
[data-slot="title"] {
|
| 130 |
+
color: rgba(255, 255, 255, 0.92);
|
| 131 |
+
font-family: var(--font-mono);
|
| 132 |
+
}
|
| 133 |
+
}
|
| 134 |
+
}
|
packages/console/app/src/component/modal.tsx
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Dialog as Kobalte } from "@kobalte/core/dialog"
|
| 2 |
+
import { JSX, Show } from "solid-js"
|
| 3 |
+
import "./modal.css"
|
| 4 |
+
|
| 5 |
+
interface ModalProps {
|
| 6 |
+
open: boolean
|
| 7 |
+
onClose: () => void
|
| 8 |
+
title?: string
|
| 9 |
+
variant?: "black"
|
| 10 |
+
children: JSX.Element
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
export function Modal(props: ModalProps) {
|
| 14 |
+
return (
|
| 15 |
+
<Show when={props.open}>
|
| 16 |
+
<Kobalte
|
| 17 |
+
modal
|
| 18 |
+
open={props.open}
|
| 19 |
+
preventScroll={false}
|
| 20 |
+
onOpenChange={(open) => {
|
| 21 |
+
if (!open) props.onClose()
|
| 22 |
+
}}
|
| 23 |
+
>
|
| 24 |
+
<Kobalte.Portal>
|
| 25 |
+
<Kobalte.Overlay data-component="modal" data-slot="overlay" onClick={props.onClose}>
|
| 26 |
+
<Kobalte.Content
|
| 27 |
+
data-slot="content"
|
| 28 |
+
data-variant={props.variant}
|
| 29 |
+
onClick={(e) => e.stopPropagation()}
|
| 30 |
+
onOpenAutoFocus={(e) => {
|
| 31 |
+
e.preventDefault()
|
| 32 |
+
const target = e.currentTarget as HTMLElement | null
|
| 33 |
+
target?.focus({ preventScroll: true })
|
| 34 |
+
}}
|
| 35 |
+
>
|
| 36 |
+
<Show when={props.title}>
|
| 37 |
+
<Kobalte.Title data-slot="title">{props.title}</Kobalte.Title>
|
| 38 |
+
</Show>
|
| 39 |
+
{props.children}
|
| 40 |
+
</Kobalte.Content>
|
| 41 |
+
</Kobalte.Overlay>
|
| 42 |
+
</Kobalte.Portal>
|
| 43 |
+
</Kobalte>
|
| 44 |
+
</Show>
|
| 45 |
+
)
|
| 46 |
+
}
|
packages/console/app/src/component/rolling-number.tsx
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import NumberFlow from "number-flow"
|
| 2 |
+
import { continuous } from "number-flow/plugins"
|
| 3 |
+
import { createEffect, onCleanup, onMount } from "solid-js"
|
| 4 |
+
|
| 5 |
+
export function RollingNumber(props: {
|
| 6 |
+
value: number
|
| 7 |
+
target: number
|
| 8 |
+
timing: () => { duration: number; easing: string; spinEasing: string }
|
| 9 |
+
}) {
|
| 10 |
+
let root!: HTMLSpanElement
|
| 11 |
+
// Keep the server-rendered text stable while the custom element owns its contents.
|
| 12 |
+
const initial = props.value.toLocaleString()
|
| 13 |
+
const growing = props.target.toLocaleString().length > initial.length
|
| 14 |
+
|
| 15 |
+
onMount(() => {
|
| 16 |
+
if (new Intl.NumberFormat().resolvedOptions().numberingSystem !== "latn") {
|
| 17 |
+
createEffect(() => {
|
| 18 |
+
root.textContent = props.value.toLocaleString()
|
| 19 |
+
})
|
| 20 |
+
return
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
const flow = new NumberFlow()
|
| 24 |
+
flow.format = { useGrouping: true, maximumFractionDigits: 0 }
|
| 25 |
+
flow.trend = 1
|
| 26 |
+
if (growing) flow.plugins = [continuous]
|
| 27 |
+
flow.opacityTiming = { duration: 180, easing: "ease-out" }
|
| 28 |
+
const motion = window.matchMedia("(prefers-reduced-motion: reduce)")
|
| 29 |
+
const updateMotion = () => {
|
| 30 |
+
flow.animated = !motion.matches
|
| 31 |
+
}
|
| 32 |
+
updateMotion()
|
| 33 |
+
motion.addEventListener("change", updateMotion)
|
| 34 |
+
onCleanup(() => motion.removeEventListener("change", updateMotion))
|
| 35 |
+
root.replaceChildren(flow)
|
| 36 |
+
createEffect(() => {
|
| 37 |
+
const value = props.value
|
| 38 |
+
if (flow.value === value) return
|
| 39 |
+
const timing = props.timing()
|
| 40 |
+
flow.transformTiming = { duration: timing.duration, easing: timing.easing }
|
| 41 |
+
flow.spinTiming = {
|
| 42 |
+
duration: timing.duration,
|
| 43 |
+
easing: growing ? "cubic-bezier(0.45, 0, 0.55, 1)" : timing.spinEasing,
|
| 44 |
+
}
|
| 45 |
+
flow.update(value)
|
| 46 |
+
})
|
| 47 |
+
})
|
| 48 |
+
|
| 49 |
+
return (
|
| 50 |
+
<span
|
| 51 |
+
data-value
|
| 52 |
+
ref={root}
|
| 53 |
+
style={{ "min-width": `${props.target.toLocaleString().length}ch`, "text-align": "right" }}
|
| 54 |
+
>
|
| 55 |
+
{initial}
|
| 56 |
+
</span>
|
| 57 |
+
)
|
| 58 |
+
}
|
packages/console/app/src/component/spotlight.css
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.spotlight-container {
|
| 2 |
+
position: absolute;
|
| 3 |
+
top: 0;
|
| 4 |
+
left: 0;
|
| 5 |
+
width: 100%;
|
| 6 |
+
height: 50dvh;
|
| 7 |
+
pointer-events: none;
|
| 8 |
+
overflow: hidden;
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
.spotlight-container canvas {
|
| 12 |
+
display: block;
|
| 13 |
+
width: 100%;
|
| 14 |
+
height: 100%;
|
| 15 |
+
}
|
packages/console/app/src/component/spotlight.tsx
ADDED
|
@@ -0,0 +1,820 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { createSignal, createEffect, onMount, onCleanup, Accessor } from "solid-js"
|
| 2 |
+
import "./spotlight.css"
|
| 3 |
+
|
| 4 |
+
export interface ParticlesConfig {
|
| 5 |
+
enabled: boolean
|
| 6 |
+
amount: number
|
| 7 |
+
size: [number, number]
|
| 8 |
+
speed: number
|
| 9 |
+
opacity: number
|
| 10 |
+
drift: number
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
export interface SpotlightConfig {
|
| 14 |
+
placement: [number, number]
|
| 15 |
+
color: string
|
| 16 |
+
speed: number
|
| 17 |
+
spread: number
|
| 18 |
+
length: number
|
| 19 |
+
width: number
|
| 20 |
+
pulsating: false | [number, number]
|
| 21 |
+
distance: number
|
| 22 |
+
saturation: number
|
| 23 |
+
noiseAmount: number
|
| 24 |
+
distortion: number
|
| 25 |
+
opacity: number
|
| 26 |
+
particles: ParticlesConfig
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
export const defaultConfig: SpotlightConfig = {
|
| 30 |
+
placement: [0.5, -0.15],
|
| 31 |
+
color: "#ffffff",
|
| 32 |
+
speed: 0.8,
|
| 33 |
+
spread: 0.5,
|
| 34 |
+
length: 4.0,
|
| 35 |
+
width: 0.15,
|
| 36 |
+
pulsating: [0.95, 1.1],
|
| 37 |
+
distance: 3.5,
|
| 38 |
+
saturation: 0.35,
|
| 39 |
+
noiseAmount: 0.15,
|
| 40 |
+
distortion: 0.05,
|
| 41 |
+
opacity: 0.325,
|
| 42 |
+
particles: {
|
| 43 |
+
enabled: true,
|
| 44 |
+
amount: 70,
|
| 45 |
+
size: [1.25, 1.5],
|
| 46 |
+
speed: 0.75,
|
| 47 |
+
opacity: 0.9,
|
| 48 |
+
drift: 1.5,
|
| 49 |
+
},
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
export interface SpotlightAnimationState {
|
| 53 |
+
time: number
|
| 54 |
+
intensity: number
|
| 55 |
+
pulseValue: number
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
interface SpotlightProps {
|
| 59 |
+
config: Accessor<SpotlightConfig>
|
| 60 |
+
class?: string
|
| 61 |
+
onAnimationFrame?: (state: SpotlightAnimationState) => void
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
const hexToRgb = (hex: string): [number, number, number] => {
|
| 65 |
+
const m = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
|
| 66 |
+
return m ? [parseInt(m[1], 16) / 255, parseInt(m[2], 16) / 255, parseInt(m[3], 16) / 255] : [1, 1, 1]
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
const getAnchorAndDir = (
|
| 70 |
+
placement: [number, number],
|
| 71 |
+
w: number,
|
| 72 |
+
h: number,
|
| 73 |
+
): { anchor: [number, number]; dir: [number, number] } => {
|
| 74 |
+
const [px, py] = placement
|
| 75 |
+
const outside = 0.2
|
| 76 |
+
|
| 77 |
+
let anchorX = px * w
|
| 78 |
+
let anchorY = py * h
|
| 79 |
+
let dirX = 0
|
| 80 |
+
let dirY = 0
|
| 81 |
+
|
| 82 |
+
const centerX = 0.5
|
| 83 |
+
const centerY = 0.5
|
| 84 |
+
|
| 85 |
+
if (py <= 0.25) {
|
| 86 |
+
anchorY = -outside * h + py * h
|
| 87 |
+
dirY = 1
|
| 88 |
+
dirX = (centerX - px) * 0.5
|
| 89 |
+
} else if (py >= 0.75) {
|
| 90 |
+
anchorY = (1 + outside) * h - (1 - py) * h
|
| 91 |
+
dirY = -1
|
| 92 |
+
dirX = (centerX - px) * 0.5
|
| 93 |
+
} else if (px <= 0.25) {
|
| 94 |
+
anchorX = -outside * w + px * w
|
| 95 |
+
dirX = 1
|
| 96 |
+
dirY = (centerY - py) * 0.5
|
| 97 |
+
} else if (px >= 0.75) {
|
| 98 |
+
anchorX = (1 + outside) * w - (1 - px) * w
|
| 99 |
+
dirX = -1
|
| 100 |
+
dirY = (centerY - py) * 0.5
|
| 101 |
+
} else {
|
| 102 |
+
dirY = 1
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
const len = Math.sqrt(dirX * dirX + dirY * dirY)
|
| 106 |
+
if (len > 0) {
|
| 107 |
+
dirX /= len
|
| 108 |
+
dirY /= len
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
return { anchor: [anchorX, anchorY], dir: [dirX, dirY] }
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
interface UniformData {
|
| 115 |
+
iTime: number
|
| 116 |
+
iResolution: [number, number]
|
| 117 |
+
lightPos: [number, number]
|
| 118 |
+
lightDir: [number, number]
|
| 119 |
+
color: [number, number, number]
|
| 120 |
+
speed: number
|
| 121 |
+
lightSpread: number
|
| 122 |
+
lightLength: number
|
| 123 |
+
sourceWidth: number
|
| 124 |
+
pulsating: number
|
| 125 |
+
pulsatingMin: number
|
| 126 |
+
pulsatingMax: number
|
| 127 |
+
fadeDistance: number
|
| 128 |
+
saturation: number
|
| 129 |
+
noiseAmount: number
|
| 130 |
+
distortion: number
|
| 131 |
+
particlesEnabled: number
|
| 132 |
+
particleAmount: number
|
| 133 |
+
particleSizeMin: number
|
| 134 |
+
particleSizeMax: number
|
| 135 |
+
particleSpeed: number
|
| 136 |
+
particleOpacity: number
|
| 137 |
+
particleDrift: number
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
const WGSL_SHADER = `
|
| 141 |
+
struct Uniforms {
|
| 142 |
+
iTime: f32,
|
| 143 |
+
_pad0: f32,
|
| 144 |
+
iResolution: vec2<f32>,
|
| 145 |
+
lightPos: vec2<f32>,
|
| 146 |
+
lightDir: vec2<f32>,
|
| 147 |
+
color: vec3<f32>,
|
| 148 |
+
speed: f32,
|
| 149 |
+
lightSpread: f32,
|
| 150 |
+
lightLength: f32,
|
| 151 |
+
sourceWidth: f32,
|
| 152 |
+
pulsating: f32,
|
| 153 |
+
pulsatingMin: f32,
|
| 154 |
+
pulsatingMax: f32,
|
| 155 |
+
fadeDistance: f32,
|
| 156 |
+
saturation: f32,
|
| 157 |
+
noiseAmount: f32,
|
| 158 |
+
distortion: f32,
|
| 159 |
+
particlesEnabled: f32,
|
| 160 |
+
particleAmount: f32,
|
| 161 |
+
particleSizeMin: f32,
|
| 162 |
+
particleSizeMax: f32,
|
| 163 |
+
particleSpeed: f32,
|
| 164 |
+
particleOpacity: f32,
|
| 165 |
+
particleDrift: f32,
|
| 166 |
+
_pad1: f32,
|
| 167 |
+
_pad2: f32,
|
| 168 |
+
};
|
| 169 |
+
|
| 170 |
+
@group(0) @binding(0) var<uniform> uniforms: Uniforms;
|
| 171 |
+
|
| 172 |
+
struct VertexOutput {
|
| 173 |
+
@builtin(position) position: vec4<f32>,
|
| 174 |
+
@location(0) vUv: vec2<f32>,
|
| 175 |
+
};
|
| 176 |
+
|
| 177 |
+
@vertex
|
| 178 |
+
fn vertexMain(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
|
| 179 |
+
var positions = array<vec2<f32>, 3>(
|
| 180 |
+
vec2<f32>(-1.0, -1.0),
|
| 181 |
+
vec2<f32>(3.0, -1.0),
|
| 182 |
+
vec2<f32>(-1.0, 3.0)
|
| 183 |
+
);
|
| 184 |
+
|
| 185 |
+
var output: VertexOutput;
|
| 186 |
+
let pos = positions[vertexIndex];
|
| 187 |
+
output.position = vec4<f32>(pos, 0.0, 1.0);
|
| 188 |
+
output.vUv = pos * 0.5 + 0.5;
|
| 189 |
+
return output;
|
| 190 |
+
}
|
| 191 |
+
|
| 192 |
+
fn hash(p: vec2<f32>) -> f32 {
|
| 193 |
+
let p3 = fract(p.xyx * 0.1031);
|
| 194 |
+
return fract((p3.x + p3.y) * p3.z + dot(p3, p3.yzx + 33.33));
|
| 195 |
+
}
|
| 196 |
+
|
| 197 |
+
fn hash2(p: vec2<f32>) -> vec2<f32> {
|
| 198 |
+
let n = sin(dot(p, vec2<f32>(41.0, 289.0)));
|
| 199 |
+
return fract(vec2<f32>(n * 262144.0, n * 32768.0));
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
fn fastNoise(st: vec2<f32>) -> f32 {
|
| 203 |
+
return fract(sin(dot(st, vec2<f32>(12.9898, 78.233))) * 43758.5453);
|
| 204 |
+
}
|
| 205 |
+
|
| 206 |
+
fn lightStrengthCombined(lightSource: vec2<f32>, lightRefDirection: vec2<f32>, coord: vec2<f32>) -> f32 {
|
| 207 |
+
let sourceToCoord = coord - lightSource;
|
| 208 |
+
let distSq = dot(sourceToCoord, sourceToCoord);
|
| 209 |
+
let distance = sqrt(distSq);
|
| 210 |
+
|
| 211 |
+
let baseSize = min(uniforms.iResolution.x, uniforms.iResolution.y);
|
| 212 |
+
let maxDistance = max(baseSize * uniforms.lightLength, 0.001);
|
| 213 |
+
if (distance > maxDistance) {
|
| 214 |
+
return 0.0;
|
| 215 |
+
}
|
| 216 |
+
|
| 217 |
+
let invDist = 1.0 / max(distance, 0.001);
|
| 218 |
+
let dirNorm = sourceToCoord * invDist;
|
| 219 |
+
let cosAngle = dot(dirNorm, lightRefDirection);
|
| 220 |
+
|
| 221 |
+
if (cosAngle < 0.0) {
|
| 222 |
+
return 0.0;
|
| 223 |
+
}
|
| 224 |
+
|
| 225 |
+
let side = dot(dirNorm, vec2<f32>(-lightRefDirection.y, lightRefDirection.x));
|
| 226 |
+
let time = uniforms.iTime;
|
| 227 |
+
let speed = uniforms.speed;
|
| 228 |
+
|
| 229 |
+
let asymNoise = fastNoise(vec2<f32>(side * 6.0 + time * 0.12, distance * 0.004 + cosAngle * 2.0));
|
| 230 |
+
let asymShift = (asymNoise - 0.5) * uniforms.distortion * 0.6;
|
| 231 |
+
|
| 232 |
+
let distortPhase = time * 1.4 + distance * 0.006 + cosAngle * 4.5 + side * 1.7;
|
| 233 |
+
let distortedAngle = cosAngle + uniforms.distortion * sin(distortPhase) * 0.22 + asymShift;
|
| 234 |
+
|
| 235 |
+
let flickerSeed = cosAngle * 9.0 + side * 4.0 + time * speed * 0.35;
|
| 236 |
+
let flicker = 0.86 + fastNoise(vec2<f32>(flickerSeed, distance * 0.01)) * 0.28;
|
| 237 |
+
|
| 238 |
+
let asymSpread = max(uniforms.lightSpread * (0.9 + (asymNoise - 0.5) * 0.25), 0.001);
|
| 239 |
+
let spreadFactor = pow(max(distortedAngle, 0.0), 1.0 / asymSpread);
|
| 240 |
+
let lengthFalloff = clamp(1.0 - distance / maxDistance, 0.0, 1.0);
|
| 241 |
+
|
| 242 |
+
let fadeMaxDist = max(baseSize * uniforms.fadeDistance, 0.001);
|
| 243 |
+
let fadeFalloff = clamp((fadeMaxDist - distance) / fadeMaxDist, 0.0, 1.0);
|
| 244 |
+
|
| 245 |
+
var pulse: f32 = 1.0;
|
| 246 |
+
if (uniforms.pulsating > 0.5) {
|
| 247 |
+
let pulseCenter = (uniforms.pulsatingMin + uniforms.pulsatingMax) * 0.5;
|
| 248 |
+
let pulseAmplitude = (uniforms.pulsatingMax - uniforms.pulsatingMin) * 0.5;
|
| 249 |
+
pulse = pulseCenter + pulseAmplitude * sin(time * speed * 3.0);
|
| 250 |
+
}
|
| 251 |
+
|
| 252 |
+
let timeSpeed = time * speed;
|
| 253 |
+
let wave = 0.5
|
| 254 |
+
+ 0.25 * sin(cosAngle * 28.0 + side * 8.0 + timeSpeed * 1.2)
|
| 255 |
+
+ 0.18 * cos(cosAngle * 22.0 - timeSpeed * 0.95 + side * 6.0)
|
| 256 |
+
+ 0.12 * sin(cosAngle * 35.0 + timeSpeed * 1.6 + asymNoise * 3.0);
|
| 257 |
+
let minStrength = 0.14 + asymNoise * 0.06;
|
| 258 |
+
let baseStrength = max(clamp(wave * (0.85 + asymNoise * 0.3), 0.0, 1.0), minStrength);
|
| 259 |
+
|
| 260 |
+
let lightStrength = baseStrength * lengthFalloff * fadeFalloff * spreadFactor * pulse * flicker;
|
| 261 |
+
let ambientLight = (0.06 + asymNoise * 0.04) * lengthFalloff * fadeFalloff * spreadFactor;
|
| 262 |
+
|
| 263 |
+
return max(lightStrength, ambientLight);
|
| 264 |
+
}
|
| 265 |
+
|
| 266 |
+
fn particle(coord: vec2<f32>, particlePos: vec2<f32>, size: f32) -> f32 {
|
| 267 |
+
let delta = coord - particlePos;
|
| 268 |
+
let distSq = dot(delta, delta);
|
| 269 |
+
let sizeSq = size * size;
|
| 270 |
+
|
| 271 |
+
if (distSq > sizeSq * 9.0) {
|
| 272 |
+
return 0.0;
|
| 273 |
+
}
|
| 274 |
+
|
| 275 |
+
let d = sqrt(distSq);
|
| 276 |
+
let core = smoothstep(size, size * 0.35, d);
|
| 277 |
+
let glow = smoothstep(size * 3.0, 0.0, d) * 0.55;
|
| 278 |
+
return core + glow;
|
| 279 |
+
}
|
| 280 |
+
|
| 281 |
+
fn renderParticles(coord: vec2<f32>, lightSource: vec2<f32>, lightDir: vec2<f32>) -> f32 {
|
| 282 |
+
if (uniforms.particlesEnabled < 0.5 || uniforms.particleAmount < 1.0) {
|
| 283 |
+
return 0.0;
|
| 284 |
+
}
|
| 285 |
+
|
| 286 |
+
var particleSum: f32 = 0.0;
|
| 287 |
+
let particleCount = i32(uniforms.particleAmount);
|
| 288 |
+
let time = uniforms.iTime * uniforms.particleSpeed;
|
| 289 |
+
let perpDir = vec2<f32>(-lightDir.y, lightDir.x);
|
| 290 |
+
let baseSize = min(uniforms.iResolution.x, uniforms.iResolution.y);
|
| 291 |
+
let maxDist = max(baseSize * uniforms.lightLength, 1.0);
|
| 292 |
+
let spreadScale = uniforms.lightSpread * baseSize * 0.65;
|
| 293 |
+
let coneHalfWidth = uniforms.lightSpread * baseSize * 0.55;
|
| 294 |
+
|
| 295 |
+
for (var i: i32 = 0; i < particleCount; i = i + 1) {
|
| 296 |
+
let fi = f32(i);
|
| 297 |
+
let seed = vec2<f32>(fi * 127.1, fi * 311.7);
|
| 298 |
+
let rnd = hash2(seed);
|
| 299 |
+
|
| 300 |
+
let lifeDuration = 2.0 + hash(seed + vec2<f32>(19.0, 73.0)) * 3.0;
|
| 301 |
+
let lifeOffset = hash(seed + vec2<f32>(91.0, 37.0)) * lifeDuration;
|
| 302 |
+
let lifeProgress = fract((time + lifeOffset) / lifeDuration);
|
| 303 |
+
|
| 304 |
+
let fadeIn = smoothstep(0.0, 0.2, lifeProgress);
|
| 305 |
+
let fadeOut = 1.0 - smoothstep(0.8, 1.0, lifeProgress);
|
| 306 |
+
let lifeFade = fadeIn * fadeOut;
|
| 307 |
+
if (lifeFade < 0.01) {
|
| 308 |
+
continue;
|
| 309 |
+
}
|
| 310 |
+
|
| 311 |
+
let alongLight = rnd.x * maxDist * 0.8;
|
| 312 |
+
let perpOffset = (rnd.y - 0.5) * spreadScale;
|
| 313 |
+
|
| 314 |
+
let floatPhase = rnd.y * 6.28318 + fi * 0.37;
|
| 315 |
+
let floatSpeed = 0.35 + rnd.x * 0.9;
|
| 316 |
+
let drift = vec2<f32>(
|
| 317 |
+
sin(time * floatSpeed + floatPhase),
|
| 318 |
+
cos(time * floatSpeed * 0.85 + floatPhase * 1.3)
|
| 319 |
+
) * uniforms.particleDrift * baseSize * 0.08;
|
| 320 |
+
|
| 321 |
+
let wobble = vec2<f32>(
|
| 322 |
+
sin(time * 1.4 + floatPhase * 2.1),
|
| 323 |
+
cos(time * 1.1 + floatPhase * 1.6)
|
| 324 |
+
) * uniforms.particleDrift * baseSize * 0.03;
|
| 325 |
+
|
| 326 |
+
let flowOffset = (rnd.x - 0.5) * baseSize * 0.12 + fract(time * 0.06 + rnd.y) * baseSize * 0.1;
|
| 327 |
+
|
| 328 |
+
let basePos = lightSource + lightDir * (alongLight + flowOffset) + perpDir * perpOffset + drift + wobble;
|
| 329 |
+
|
| 330 |
+
let toParticle = basePos - lightSource;
|
| 331 |
+
let projLen = dot(toParticle, lightDir);
|
| 332 |
+
if (projLen < 0.0 || projLen > maxDist) {
|
| 333 |
+
continue;
|
| 334 |
+
}
|
| 335 |
+
|
| 336 |
+
let sideDist = abs(dot(toParticle, perpDir));
|
| 337 |
+
if (sideDist > coneHalfWidth) {
|
| 338 |
+
continue;
|
| 339 |
+
}
|
| 340 |
+
|
| 341 |
+
let size = mix(uniforms.particleSizeMin, uniforms.particleSizeMax, rnd.x);
|
| 342 |
+
let twinkle = 0.7 + 0.3 * sin(time * (1.5 + rnd.y * 2.0) + floatPhase);
|
| 343 |
+
let distFade = 1.0 - smoothstep(maxDist * 0.2, maxDist * 0.95, projLen);
|
| 344 |
+
if (distFade < 0.01) {
|
| 345 |
+
continue;
|
| 346 |
+
}
|
| 347 |
+
|
| 348 |
+
let p = particle(coord, basePos, size);
|
| 349 |
+
if (p > 0.0) {
|
| 350 |
+
particleSum = particleSum + p * lifeFade * twinkle * distFade * uniforms.particleOpacity;
|
| 351 |
+
if (particleSum >= 1.0) {
|
| 352 |
+
break;
|
| 353 |
+
}
|
| 354 |
+
}
|
| 355 |
+
}
|
| 356 |
+
|
| 357 |
+
return min(particleSum, 1.0);
|
| 358 |
+
}
|
| 359 |
+
|
| 360 |
+
@fragment
|
| 361 |
+
fn fragmentMain(@builtin(position) fragCoord: vec4<f32>, @location(0) vUv: vec2<f32>) -> @location(0) vec4<f32> {
|
| 362 |
+
let coord = vec2<f32>(fragCoord.x, fragCoord.y);
|
| 363 |
+
|
| 364 |
+
let normalizedX = (coord.x / uniforms.iResolution.x) - 0.5;
|
| 365 |
+
let widthOffset = -normalizedX * uniforms.sourceWidth * uniforms.iResolution.x;
|
| 366 |
+
|
| 367 |
+
let perpDir = vec2<f32>(-uniforms.lightDir.y, uniforms.lightDir.x);
|
| 368 |
+
let adjustedLightPos = uniforms.lightPos + perpDir * widthOffset;
|
| 369 |
+
|
| 370 |
+
let lightValue = lightStrengthCombined(adjustedLightPos, uniforms.lightDir, coord);
|
| 371 |
+
|
| 372 |
+
if (lightValue < 0.001) {
|
| 373 |
+
let particles = renderParticles(coord, adjustedLightPos, uniforms.lightDir);
|
| 374 |
+
if (particles < 0.001) {
|
| 375 |
+
return vec4<f32>(0.0, 0.0, 0.0, 0.0);
|
| 376 |
+
}
|
| 377 |
+
let particleBrightness = particles * 1.8;
|
| 378 |
+
return vec4<f32>(uniforms.color * particleBrightness, particles * 0.9);
|
| 379 |
+
}
|
| 380 |
+
|
| 381 |
+
var fragColor = vec4<f32>(lightValue, lightValue, lightValue, lightValue);
|
| 382 |
+
|
| 383 |
+
if (uniforms.noiseAmount > 0.01) {
|
| 384 |
+
let n = fastNoise(coord * 0.5 + uniforms.iTime * 0.5);
|
| 385 |
+
let grain = mix(1.0, n, uniforms.noiseAmount * 0.5);
|
| 386 |
+
fragColor = vec4<f32>(fragColor.rgb * grain, fragColor.a);
|
| 387 |
+
}
|
| 388 |
+
|
| 389 |
+
let brightness = 1.0 - (coord.y / uniforms.iResolution.y);
|
| 390 |
+
fragColor = vec4<f32>(
|
| 391 |
+
fragColor.x * (0.15 + brightness * 0.85),
|
| 392 |
+
fragColor.y * (0.35 + brightness * 0.65),
|
| 393 |
+
fragColor.z * (0.55 + brightness * 0.45),
|
| 394 |
+
fragColor.a
|
| 395 |
+
);
|
| 396 |
+
|
| 397 |
+
if (abs(uniforms.saturation - 1.0) > 0.01) {
|
| 398 |
+
let gray = dot(fragColor.rgb, vec3<f32>(0.299, 0.587, 0.114));
|
| 399 |
+
fragColor = vec4<f32>(mix(vec3<f32>(gray), fragColor.rgb, uniforms.saturation), fragColor.a);
|
| 400 |
+
}
|
| 401 |
+
|
| 402 |
+
fragColor = vec4<f32>(fragColor.rgb * uniforms.color, fragColor.a);
|
| 403 |
+
|
| 404 |
+
let particles = renderParticles(coord, adjustedLightPos, uniforms.lightDir);
|
| 405 |
+
if (particles > 0.001) {
|
| 406 |
+
let particleBrightness = particles * 1.8;
|
| 407 |
+
fragColor = vec4<f32>(fragColor.rgb + uniforms.color * particleBrightness, max(fragColor.a, particles * 0.9));
|
| 408 |
+
}
|
| 409 |
+
|
| 410 |
+
return fragColor;
|
| 411 |
+
}
|
| 412 |
+
`
|
| 413 |
+
|
| 414 |
+
const UNIFORM_BUFFER_SIZE = 144
|
| 415 |
+
|
| 416 |
+
function updateUniformBuffer(buffer: Float32Array, data: UniformData): void {
|
| 417 |
+
buffer[0] = data.iTime
|
| 418 |
+
buffer[2] = data.iResolution[0]
|
| 419 |
+
buffer[3] = data.iResolution[1]
|
| 420 |
+
buffer[4] = data.lightPos[0]
|
| 421 |
+
buffer[5] = data.lightPos[1]
|
| 422 |
+
buffer[6] = data.lightDir[0]
|
| 423 |
+
buffer[7] = data.lightDir[1]
|
| 424 |
+
buffer[8] = data.color[0]
|
| 425 |
+
buffer[9] = data.color[1]
|
| 426 |
+
buffer[10] = data.color[2]
|
| 427 |
+
buffer[11] = data.speed
|
| 428 |
+
buffer[12] = data.lightSpread
|
| 429 |
+
buffer[13] = data.lightLength
|
| 430 |
+
buffer[14] = data.sourceWidth
|
| 431 |
+
buffer[15] = data.pulsating
|
| 432 |
+
buffer[16] = data.pulsatingMin
|
| 433 |
+
buffer[17] = data.pulsatingMax
|
| 434 |
+
buffer[18] = data.fadeDistance
|
| 435 |
+
buffer[19] = data.saturation
|
| 436 |
+
buffer[20] = data.noiseAmount
|
| 437 |
+
buffer[21] = data.distortion
|
| 438 |
+
buffer[22] = data.particlesEnabled
|
| 439 |
+
buffer[23] = data.particleAmount
|
| 440 |
+
buffer[24] = data.particleSizeMin
|
| 441 |
+
buffer[25] = data.particleSizeMax
|
| 442 |
+
buffer[26] = data.particleSpeed
|
| 443 |
+
buffer[27] = data.particleOpacity
|
| 444 |
+
buffer[28] = data.particleDrift
|
| 445 |
+
}
|
| 446 |
+
|
| 447 |
+
export default function Spotlight(props: SpotlightProps) {
|
| 448 |
+
let containerRef: HTMLDivElement | undefined
|
| 449 |
+
let canvasRef: HTMLCanvasElement | null = null
|
| 450 |
+
let deviceRef: GPUDevice | null = null
|
| 451 |
+
let contextRef: GPUCanvasContext | null = null
|
| 452 |
+
let pipelineRef: GPURenderPipeline | null = null
|
| 453 |
+
let uniformBufferRef: GPUBuffer | null = null
|
| 454 |
+
let bindGroupRef: GPUBindGroup | null = null
|
| 455 |
+
let animationIdRef: number | null = null
|
| 456 |
+
let cleanupFunctionRef: (() => void) | null = null
|
| 457 |
+
let uniformDataRef: UniformData | null = null
|
| 458 |
+
let uniformArrayRef: Float32Array | null = null
|
| 459 |
+
let configRef: SpotlightConfig = props.config()
|
| 460 |
+
let frameCount = 0
|
| 461 |
+
|
| 462 |
+
const [isVisible, setIsVisible] = createSignal(false)
|
| 463 |
+
|
| 464 |
+
createEffect(() => {
|
| 465 |
+
configRef = props.config()
|
| 466 |
+
})
|
| 467 |
+
|
| 468 |
+
onMount(() => {
|
| 469 |
+
if (!containerRef) return
|
| 470 |
+
|
| 471 |
+
const observer = new IntersectionObserver(
|
| 472 |
+
(entries) => {
|
| 473 |
+
const entry = entries[0]
|
| 474 |
+
setIsVisible(entry.isIntersecting)
|
| 475 |
+
},
|
| 476 |
+
{ threshold: 0.1 },
|
| 477 |
+
)
|
| 478 |
+
|
| 479 |
+
observer.observe(containerRef)
|
| 480 |
+
|
| 481 |
+
onCleanup(() => {
|
| 482 |
+
observer.disconnect()
|
| 483 |
+
})
|
| 484 |
+
})
|
| 485 |
+
|
| 486 |
+
createEffect(() => {
|
| 487 |
+
const visible = isVisible()
|
| 488 |
+
const config = props.config()
|
| 489 |
+
if (!visible || !containerRef) {
|
| 490 |
+
return
|
| 491 |
+
}
|
| 492 |
+
|
| 493 |
+
if (cleanupFunctionRef) {
|
| 494 |
+
cleanupFunctionRef()
|
| 495 |
+
cleanupFunctionRef = null
|
| 496 |
+
}
|
| 497 |
+
|
| 498 |
+
const initializeWebGPU = async () => {
|
| 499 |
+
if (!containerRef) {
|
| 500 |
+
return
|
| 501 |
+
}
|
| 502 |
+
|
| 503 |
+
await new Promise((resolve) => setTimeout(resolve, 10))
|
| 504 |
+
|
| 505 |
+
if (!containerRef) {
|
| 506 |
+
return
|
| 507 |
+
}
|
| 508 |
+
|
| 509 |
+
if (!navigator.gpu) {
|
| 510 |
+
console.warn("WebGPU is not supported in this browser")
|
| 511 |
+
return
|
| 512 |
+
}
|
| 513 |
+
|
| 514 |
+
const adapter = await navigator.gpu.requestAdapter({
|
| 515 |
+
powerPreference: "high-performance",
|
| 516 |
+
})
|
| 517 |
+
if (!adapter) {
|
| 518 |
+
console.warn("Failed to get WebGPU adapter")
|
| 519 |
+
return
|
| 520 |
+
}
|
| 521 |
+
|
| 522 |
+
const device = await adapter.requestDevice()
|
| 523 |
+
deviceRef = device
|
| 524 |
+
|
| 525 |
+
const canvas = document.createElement("canvas")
|
| 526 |
+
canvas.style.width = "100%"
|
| 527 |
+
canvas.style.height = "100%"
|
| 528 |
+
canvasRef = canvas
|
| 529 |
+
|
| 530 |
+
while (containerRef.firstChild) {
|
| 531 |
+
containerRef.removeChild(containerRef.firstChild)
|
| 532 |
+
}
|
| 533 |
+
containerRef.appendChild(canvas)
|
| 534 |
+
|
| 535 |
+
const context = canvas.getContext("webgpu")
|
| 536 |
+
if (!context) {
|
| 537 |
+
console.warn("Failed to get WebGPU context")
|
| 538 |
+
return
|
| 539 |
+
}
|
| 540 |
+
contextRef = context
|
| 541 |
+
|
| 542 |
+
const presentationFormat = navigator.gpu.getPreferredCanvasFormat()
|
| 543 |
+
context.configure({
|
| 544 |
+
device,
|
| 545 |
+
format: presentationFormat,
|
| 546 |
+
alphaMode: "premultiplied",
|
| 547 |
+
})
|
| 548 |
+
|
| 549 |
+
const shaderModule = device.createShaderModule({
|
| 550 |
+
code: WGSL_SHADER,
|
| 551 |
+
})
|
| 552 |
+
|
| 553 |
+
const uniformBuffer = device.createBuffer({
|
| 554 |
+
size: UNIFORM_BUFFER_SIZE,
|
| 555 |
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
| 556 |
+
})
|
| 557 |
+
uniformBufferRef = uniformBuffer
|
| 558 |
+
|
| 559 |
+
const bindGroupLayout = device.createBindGroupLayout({
|
| 560 |
+
entries: [
|
| 561 |
+
{
|
| 562 |
+
binding: 0,
|
| 563 |
+
visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
|
| 564 |
+
buffer: { type: "uniform" },
|
| 565 |
+
},
|
| 566 |
+
],
|
| 567 |
+
})
|
| 568 |
+
|
| 569 |
+
const bindGroup = device.createBindGroup({
|
| 570 |
+
layout: bindGroupLayout,
|
| 571 |
+
entries: [
|
| 572 |
+
{
|
| 573 |
+
binding: 0,
|
| 574 |
+
resource: { buffer: uniformBuffer },
|
| 575 |
+
},
|
| 576 |
+
],
|
| 577 |
+
})
|
| 578 |
+
bindGroupRef = bindGroup
|
| 579 |
+
|
| 580 |
+
const pipelineLayout = device.createPipelineLayout({
|
| 581 |
+
bindGroupLayouts: [bindGroupLayout],
|
| 582 |
+
})
|
| 583 |
+
|
| 584 |
+
const pipeline = device.createRenderPipeline({
|
| 585 |
+
layout: pipelineLayout,
|
| 586 |
+
vertex: {
|
| 587 |
+
module: shaderModule,
|
| 588 |
+
entryPoint: "vertexMain",
|
| 589 |
+
},
|
| 590 |
+
fragment: {
|
| 591 |
+
module: shaderModule,
|
| 592 |
+
entryPoint: "fragmentMain",
|
| 593 |
+
targets: [
|
| 594 |
+
{
|
| 595 |
+
format: presentationFormat,
|
| 596 |
+
blend: {
|
| 597 |
+
color: {
|
| 598 |
+
srcFactor: "src-alpha",
|
| 599 |
+
dstFactor: "one-minus-src-alpha",
|
| 600 |
+
operation: "add",
|
| 601 |
+
},
|
| 602 |
+
alpha: {
|
| 603 |
+
srcFactor: "one",
|
| 604 |
+
dstFactor: "one-minus-src-alpha",
|
| 605 |
+
operation: "add",
|
| 606 |
+
},
|
| 607 |
+
},
|
| 608 |
+
},
|
| 609 |
+
],
|
| 610 |
+
},
|
| 611 |
+
primitive: {
|
| 612 |
+
topology: "triangle-list",
|
| 613 |
+
},
|
| 614 |
+
})
|
| 615 |
+
pipelineRef = pipeline
|
| 616 |
+
|
| 617 |
+
const { clientWidth: wCSS, clientHeight: hCSS } = containerRef
|
| 618 |
+
const dpr = Math.min(window.devicePixelRatio, 2)
|
| 619 |
+
const w = wCSS * dpr
|
| 620 |
+
const h = hCSS * dpr
|
| 621 |
+
const { anchor, dir } = getAnchorAndDir(config.placement, w, h)
|
| 622 |
+
|
| 623 |
+
uniformDataRef = {
|
| 624 |
+
iTime: 0,
|
| 625 |
+
iResolution: [w, h],
|
| 626 |
+
lightPos: anchor,
|
| 627 |
+
lightDir: dir,
|
| 628 |
+
color: hexToRgb(config.color),
|
| 629 |
+
speed: config.speed,
|
| 630 |
+
lightSpread: config.spread,
|
| 631 |
+
lightLength: config.length,
|
| 632 |
+
sourceWidth: config.width,
|
| 633 |
+
pulsating: config.pulsating !== false ? 1.0 : 0.0,
|
| 634 |
+
pulsatingMin: config.pulsating !== false ? config.pulsating[0] : 1.0,
|
| 635 |
+
pulsatingMax: config.pulsating !== false ? config.pulsating[1] : 1.0,
|
| 636 |
+
fadeDistance: config.distance,
|
| 637 |
+
saturation: config.saturation,
|
| 638 |
+
noiseAmount: config.noiseAmount,
|
| 639 |
+
distortion: config.distortion,
|
| 640 |
+
particlesEnabled: config.particles.enabled ? 1.0 : 0.0,
|
| 641 |
+
particleAmount: config.particles.amount,
|
| 642 |
+
particleSizeMin: config.particles.size[0],
|
| 643 |
+
particleSizeMax: config.particles.size[1],
|
| 644 |
+
particleSpeed: config.particles.speed,
|
| 645 |
+
particleOpacity: config.particles.opacity,
|
| 646 |
+
particleDrift: config.particles.drift,
|
| 647 |
+
}
|
| 648 |
+
|
| 649 |
+
const updatePlacement = () => {
|
| 650 |
+
if (!containerRef || !canvasRef || !uniformDataRef) {
|
| 651 |
+
return
|
| 652 |
+
}
|
| 653 |
+
|
| 654 |
+
const dpr = Math.min(window.devicePixelRatio, 2)
|
| 655 |
+
const { clientWidth: wCSS, clientHeight: hCSS } = containerRef
|
| 656 |
+
const w = Math.floor(wCSS * dpr)
|
| 657 |
+
const h = Math.floor(hCSS * dpr)
|
| 658 |
+
|
| 659 |
+
canvasRef.width = w
|
| 660 |
+
canvasRef.height = h
|
| 661 |
+
|
| 662 |
+
uniformDataRef.iResolution = [w, h]
|
| 663 |
+
|
| 664 |
+
const { anchor, dir } = getAnchorAndDir(configRef.placement, w, h)
|
| 665 |
+
uniformDataRef.lightPos = anchor
|
| 666 |
+
uniformDataRef.lightDir = dir
|
| 667 |
+
}
|
| 668 |
+
|
| 669 |
+
const loop = (t: number) => {
|
| 670 |
+
if (!deviceRef || !contextRef || !pipelineRef || !uniformBufferRef || !bindGroupRef || !uniformDataRef) {
|
| 671 |
+
return
|
| 672 |
+
}
|
| 673 |
+
|
| 674 |
+
const timeSeconds = t * 0.001
|
| 675 |
+
uniformDataRef.iTime = timeSeconds
|
| 676 |
+
frameCount++
|
| 677 |
+
|
| 678 |
+
if (props.onAnimationFrame && frameCount % 2 === 0) {
|
| 679 |
+
const pulsatingMin = configRef.pulsating !== false ? configRef.pulsating[0] : 1.0
|
| 680 |
+
const pulsatingMax = configRef.pulsating !== false ? configRef.pulsating[1] : 1.0
|
| 681 |
+
const pulseCenter = (pulsatingMin + pulsatingMax) * 0.5
|
| 682 |
+
const pulseAmplitude = (pulsatingMax - pulsatingMin) * 0.5
|
| 683 |
+
const pulseValue =
|
| 684 |
+
configRef.pulsating !== false
|
| 685 |
+
? pulseCenter + pulseAmplitude * Math.sin(timeSeconds * configRef.speed * 3.0)
|
| 686 |
+
: 1.0
|
| 687 |
+
|
| 688 |
+
const baseIntensity1 = 0.45 + 0.15 * Math.sin(timeSeconds * configRef.speed * 1.5)
|
| 689 |
+
const baseIntensity2 = 0.3 + 0.2 * Math.cos(timeSeconds * configRef.speed * 1.1)
|
| 690 |
+
const intensity = Math.max((baseIntensity1 + baseIntensity2) * pulseValue, 0.55)
|
| 691 |
+
|
| 692 |
+
props.onAnimationFrame({
|
| 693 |
+
time: timeSeconds,
|
| 694 |
+
intensity,
|
| 695 |
+
pulseValue: Math.max(pulseValue, 0.9),
|
| 696 |
+
})
|
| 697 |
+
}
|
| 698 |
+
|
| 699 |
+
try {
|
| 700 |
+
if (!uniformArrayRef) {
|
| 701 |
+
uniformArrayRef = new Float32Array(36)
|
| 702 |
+
}
|
| 703 |
+
updateUniformBuffer(uniformArrayRef, uniformDataRef)
|
| 704 |
+
deviceRef.queue.writeBuffer(uniformBufferRef, 0, uniformArrayRef.buffer)
|
| 705 |
+
|
| 706 |
+
const commandEncoder = deviceRef.createCommandEncoder()
|
| 707 |
+
|
| 708 |
+
const textureView = contextRef.getCurrentTexture().createView()
|
| 709 |
+
|
| 710 |
+
const renderPass = commandEncoder.beginRenderPass({
|
| 711 |
+
colorAttachments: [
|
| 712 |
+
{
|
| 713 |
+
view: textureView,
|
| 714 |
+
clearValue: { r: 0, g: 0, b: 0, a: 0 },
|
| 715 |
+
loadOp: "clear",
|
| 716 |
+
storeOp: "store",
|
| 717 |
+
},
|
| 718 |
+
],
|
| 719 |
+
})
|
| 720 |
+
|
| 721 |
+
renderPass.setPipeline(pipelineRef)
|
| 722 |
+
renderPass.setBindGroup(0, bindGroupRef)
|
| 723 |
+
renderPass.draw(3)
|
| 724 |
+
renderPass.end()
|
| 725 |
+
|
| 726 |
+
deviceRef.queue.submit([commandEncoder.finish()])
|
| 727 |
+
|
| 728 |
+
animationIdRef = requestAnimationFrame(loop)
|
| 729 |
+
} catch (error) {
|
| 730 |
+
console.warn("WebGPU rendering error:", error)
|
| 731 |
+
return
|
| 732 |
+
}
|
| 733 |
+
}
|
| 734 |
+
|
| 735 |
+
window.addEventListener("resize", updatePlacement)
|
| 736 |
+
updatePlacement()
|
| 737 |
+
animationIdRef = requestAnimationFrame(loop)
|
| 738 |
+
|
| 739 |
+
cleanupFunctionRef = () => {
|
| 740 |
+
if (animationIdRef) {
|
| 741 |
+
cancelAnimationFrame(animationIdRef)
|
| 742 |
+
animationIdRef = null
|
| 743 |
+
}
|
| 744 |
+
|
| 745 |
+
window.removeEventListener("resize", updatePlacement)
|
| 746 |
+
|
| 747 |
+
if (uniformBufferRef) {
|
| 748 |
+
uniformBufferRef.destroy()
|
| 749 |
+
uniformBufferRef = null
|
| 750 |
+
}
|
| 751 |
+
|
| 752 |
+
if (deviceRef) {
|
| 753 |
+
deviceRef.destroy()
|
| 754 |
+
deviceRef = null
|
| 755 |
+
}
|
| 756 |
+
|
| 757 |
+
if (canvasRef && canvasRef.parentNode) {
|
| 758 |
+
canvasRef.parentNode.removeChild(canvasRef)
|
| 759 |
+
}
|
| 760 |
+
|
| 761 |
+
canvasRef = null
|
| 762 |
+
contextRef = null
|
| 763 |
+
pipelineRef = null
|
| 764 |
+
bindGroupRef = null
|
| 765 |
+
uniformDataRef = null
|
| 766 |
+
}
|
| 767 |
+
}
|
| 768 |
+
|
| 769 |
+
void initializeWebGPU()
|
| 770 |
+
|
| 771 |
+
onCleanup(() => {
|
| 772 |
+
if (cleanupFunctionRef) {
|
| 773 |
+
cleanupFunctionRef()
|
| 774 |
+
cleanupFunctionRef = null
|
| 775 |
+
}
|
| 776 |
+
})
|
| 777 |
+
})
|
| 778 |
+
|
| 779 |
+
createEffect(() => {
|
| 780 |
+
if (!uniformDataRef || !containerRef) {
|
| 781 |
+
return
|
| 782 |
+
}
|
| 783 |
+
|
| 784 |
+
const config = props.config()
|
| 785 |
+
|
| 786 |
+
uniformDataRef.color = hexToRgb(config.color)
|
| 787 |
+
uniformDataRef.speed = config.speed
|
| 788 |
+
uniformDataRef.lightSpread = config.spread
|
| 789 |
+
uniformDataRef.lightLength = config.length
|
| 790 |
+
uniformDataRef.sourceWidth = config.width
|
| 791 |
+
uniformDataRef.pulsating = config.pulsating !== false ? 1.0 : 0.0
|
| 792 |
+
uniformDataRef.pulsatingMin = config.pulsating !== false ? config.pulsating[0] : 1.0
|
| 793 |
+
uniformDataRef.pulsatingMax = config.pulsating !== false ? config.pulsating[1] : 1.0
|
| 794 |
+
uniformDataRef.fadeDistance = config.distance
|
| 795 |
+
uniformDataRef.saturation = config.saturation
|
| 796 |
+
uniformDataRef.noiseAmount = config.noiseAmount
|
| 797 |
+
uniformDataRef.distortion = config.distortion
|
| 798 |
+
uniformDataRef.particlesEnabled = config.particles.enabled ? 1.0 : 0.0
|
| 799 |
+
uniformDataRef.particleAmount = config.particles.amount
|
| 800 |
+
uniformDataRef.particleSizeMin = config.particles.size[0]
|
| 801 |
+
uniformDataRef.particleSizeMax = config.particles.size[1]
|
| 802 |
+
uniformDataRef.particleSpeed = config.particles.speed
|
| 803 |
+
uniformDataRef.particleOpacity = config.particles.opacity
|
| 804 |
+
uniformDataRef.particleDrift = config.particles.drift
|
| 805 |
+
|
| 806 |
+
const dpr = Math.min(window.devicePixelRatio, 2)
|
| 807 |
+
const { clientWidth: wCSS, clientHeight: hCSS } = containerRef
|
| 808 |
+
const { anchor, dir } = getAnchorAndDir(config.placement, wCSS * dpr, hCSS * dpr)
|
| 809 |
+
uniformDataRef.lightPos = anchor
|
| 810 |
+
uniformDataRef.lightDir = dir
|
| 811 |
+
})
|
| 812 |
+
|
| 813 |
+
return (
|
| 814 |
+
<div
|
| 815 |
+
ref={containerRef}
|
| 816 |
+
class={`spotlight-container ${props.class ?? ""}`.trim()}
|
| 817 |
+
style={{ opacity: props.config().opacity }}
|
| 818 |
+
/>
|
| 819 |
+
)
|
| 820 |
+
}
|