SaylorTwift HF Staff commited on
Commit
5f24abe
·
verified ·
1 Parent(s): 251a574

Add files using upload-large-folder tool

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. packages/console/app/public/apple-touch-icon-v3.png +0 -0
  2. packages/console/app/public/apple-touch-icon.png +0 -0
  3. packages/console/app/public/favicon-96x96-v3.png +0 -0
  4. packages/console/app/public/favicon-96x96.png +0 -0
  5. packages/console/app/public/favicon-v3.ico +0 -0
  6. packages/console/app/public/favicon-v3.svg +7 -0
  7. packages/console/app/public/favicon.ico +0 -0
  8. packages/console/app/public/favicon.svg +7 -0
  9. packages/console/app/public/robots.txt +9 -0
  10. packages/console/app/public/site.webmanifest +24 -0
  11. packages/console/app/public/social-share-black.png +0 -0
  12. packages/console/app/public/social-share-zen.png +0 -0
  13. packages/console/app/public/social-share.png +0 -0
  14. packages/console/app/public/theme.json +182 -0
  15. packages/console/app/public/web-app-manifest-192x192.png +0 -0
  16. packages/console/app/public/web-app-manifest-512x512.png +0 -0
  17. packages/console/app/script/generate-sitemap.ts +108 -0
  18. packages/console/app/src/app.css +1 -0
  19. packages/console/app/src/app.tsx +44 -0
  20. packages/console/app/src/asset/go-ornate-dark.svg +6 -0
  21. packages/console/app/src/asset/go-ornate-light.svg +6 -0
  22. packages/console/app/src/asset/logo-ornate-dark.svg +18 -0
  23. packages/console/app/src/asset/logo-ornate-light.svg +18 -0
  24. packages/console/app/src/asset/logo.svg +18 -0
  25. packages/console/app/src/asset/zen-ornate-dark.svg +8 -0
  26. packages/console/app/src/asset/zen-ornate-light.svg +8 -0
  27. packages/console/app/src/component/desktop-promo.css +103 -0
  28. packages/console/app/src/component/desktop-promo.tsx +60 -0
  29. packages/console/app/src/component/dropdown.css +80 -0
  30. packages/console/app/src/component/dropdown.tsx +79 -0
  31. packages/console/app/src/component/email-signup.tsx +47 -0
  32. packages/console/app/src/component/faq.tsx +33 -0
  33. packages/console/app/src/component/footer.tsx +48 -0
  34. packages/console/app/src/component/go-models.ts +53 -0
  35. packages/console/app/src/component/go-referral.css +289 -0
  36. packages/console/app/src/component/go-referral.tsx +306 -0
  37. packages/console/app/src/component/header-context-menu.css +63 -0
  38. packages/console/app/src/component/header.tsx +288 -0
  39. packages/console/app/src/component/icon.tsx +286 -0
  40. packages/console/app/src/component/language-picker.css +135 -0
  41. packages/console/app/src/component/language-picker.tsx +40 -0
  42. packages/console/app/src/component/legal.tsx +28 -0
  43. packages/console/app/src/component/limits-graph.css +300 -0
  44. packages/console/app/src/component/limits-graph.tsx +166 -0
  45. packages/console/app/src/component/locale-links.tsx +36 -0
  46. packages/console/app/src/component/modal.css +134 -0
  47. packages/console/app/src/component/modal.tsx +46 -0
  48. packages/console/app/src/component/rolling-number.tsx +58 -0
  49. packages/console/app/src/component/spotlight.css +15 -0
  50. 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">-&gt;</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
+ }