Skip to content

Commit 7251045

Browse files
thdxradamdotdevinactions-user
authored
feat: add desktop/web app package (anomalyco#2606)
Co-authored-by: adamdotdevin <2363879+adamdottv@users.noreply.github.com> Co-authored-by: Adam <2363879+adamdotdevin@users.noreply.github.com> Co-authored-by: GitHub Action <action@github.com>
1 parent 4954edf commit 7251045

1,133 files changed

Lines changed: 22708 additions & 551 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

bun.lock

Lines changed: 499 additions & 548 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/app/AGENTS.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Agent Guidelines for @opencode/app
2+
3+
## Build/Test Commands
4+
5+
- **Development**: `bun run dev` (starts Vite dev server on port 3000)
6+
- **Build**: `bun run build` (production build)
7+
- **Preview**: `bun run serve` (preview production build)
8+
- **Validation**: Use `bun run typecheck` only - do not build or run project for validation
9+
- **Testing**: Do not create or run automated tests
10+
11+
## Code Style
12+
13+
- **Framework**: SolidJS with TypeScript
14+
- **Imports**: Use `@/` alias for src/ directory (e.g., `import Button from "@/ui/button"`)
15+
- **Formatting**: Prettier configured with semicolons disabled, 120 character line width
16+
- **Components**: Use function declarations, splitProps for component props
17+
- **Types**: Define interfaces for component props, avoid `any` type
18+
- **CSS**: TailwindCSS with custom CSS variables theme system
19+
- **Naming**: PascalCase for components, camelCase for variables/functions, snake_case for file names
20+
- **File Structure**: UI primitives in `/ui/`, higher-level components in `/components/`, pages in `/pages/`, providers in `/providers/`
21+
22+
## Key Dependencies
23+
24+
- SolidJS, @solidjs/router, @kobalte/core (UI primitives)
25+
- TailwindCSS 4.x with @tailwindcss/vite
26+
- Custom theme system with CSS variables
27+
28+
No special rules files found.

packages/app/README.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
## Usage
2+
3+
Those templates dependencies are maintained via [pnpm](https://pnpm.io) via `pnpm up -Lri`.
4+
5+
This is the reason you see a `pnpm-lock.yaml`. That being said, any package manager will work. This file can be safely be removed once you clone a template.
6+
7+
```bash
8+
$ npm install # or pnpm install or yarn install
9+
```
10+
11+
### Learn more on the [Solid Website](https://solidjs.com) and come chat with us on our [Discord](https://discord.com/invite/solidjs)
12+
13+
## Available Scripts
14+
15+
In the project directory, you can run:
16+
17+
### `npm run dev` or `npm start`
18+
19+
Runs the app in the development mode.<br>
20+
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
21+
22+
The page will reload if you make edits.<br>
23+
24+
### `npm run build`
25+
26+
Builds the app for production to the `dist` folder.<br>
27+
It correctly bundles Solid in production mode and optimizes the build for the best performance.
28+
29+
The build is minified and the filenames include the hashes.<br>
30+
Your app is ready to be deployed!
31+
32+
## Deployment
33+
34+
You can deploy the `dist` folder to any static host provider (netlify, surge, now, etc.)

packages/app/index.html

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<!doctype html>
2+
<html lang="en" class="h-full bg-background">
3+
<head>
4+
<meta charset="utf-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1" />
6+
<meta name="theme-color" content="#000000" />
7+
<link rel="shortcut icon" type="image/ico" href="/src/assets/favicon.ico" />
8+
<link rel="stylesheet" href="/src/assets/theme.css" />
9+
<title>opencode</title>
10+
</head>
11+
<body class="h-full overscroll-none select-none">
12+
<script>
13+
;(function () {
14+
const savedTheme = localStorage.getItem("theme") || "opencode"
15+
const savedDarkMode = localStorage.getItem("darkMode") !== "false"
16+
document.documentElement.setAttribute("data-theme", savedTheme)
17+
document.documentElement.setAttribute("data-dark", savedDarkMode.toString())
18+
})()
19+
</script>
20+
<noscript>You need to enable JavaScript to run this app.</noscript>
21+
<div id="root"></div>
22+
<script src="/src/index.tsx" type="module"></script>
23+
</body>
24+
</html>

packages/app/package.json

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
{
2+
"name": "@opencode/app",
3+
"version": "0.3.126",
4+
"description": "",
5+
"type": "module",
6+
"scripts": {
7+
"start": "vite",
8+
"dev": "vite",
9+
"build": "vite build",
10+
"serve": "vite preview",
11+
"typecheck": "tsc --noEmit"
12+
},
13+
"license": "MIT",
14+
"devDependencies": {
15+
"@tailwindcss/vite": "4.1.11",
16+
"@types/luxon": "3.7.1",
17+
"@types/node": "catalog:",
18+
"typescript": "catalog:",
19+
"vite": "^6.0.0",
20+
"vite-plugin-icons-spritesheet": "3.0.1",
21+
"vite-plugin-solid": "^2.11.6"
22+
},
23+
"dependencies": {
24+
"@kobalte/core": "0.13.11",
25+
"@opencode-ai/sdk": "workspace:*",
26+
"@shikijs/transformers": "3.9.2",
27+
"@solid-primitives/resize-observer": "2.1.3",
28+
"@solid-primitives/scroll": "2.1.3",
29+
"@solidjs/router": "0.15.3",
30+
"@thisbeyond/solid-dnd": "0.7.5",
31+
"diff": "8.0.2",
32+
"luxon": "3.7.1",
33+
"marked": "16.2.0",
34+
"marked-shiki": "1.2.1",
35+
"remeda": "catalog:",
36+
"shiki": "3.9.2",
37+
"solid-js": "catalog:",
38+
"solid-list": "0.3.0",
39+
"tailwindcss": "4.1.11",
40+
"virtua": "0.42.3"
41+
},
42+
"prettier": {
43+
"semi": false,
44+
"printWidth": 120
45+
}
46+
}
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
import type { Plugin } from "vite"
2+
import { readdir, readFile, writeFile } from "fs/promises"
3+
import { join, resolve } from "path"
4+
5+
interface ThemeDefinition {
6+
$schema?: string
7+
defs?: Record<string, string>
8+
theme: Record<string, any>
9+
}
10+
11+
interface ResolvedThemeColor {
12+
dark: string
13+
light: string
14+
}
15+
16+
class ColorResolver {
17+
private colors: Map<string, any> = new Map()
18+
private visited: Set<string> = new Set()
19+
20+
constructor(defs: Record<string, string> = {}, theme: Record<string, any> = {}) {
21+
Object.entries(defs).forEach(([key, value]) => {
22+
this.colors.set(key, value)
23+
})
24+
Object.entries(theme).forEach(([key, value]) => {
25+
this.colors.set(key, value)
26+
})
27+
}
28+
29+
resolveColor(key: string, value: any): ResolvedThemeColor {
30+
if (this.visited.has(key)) {
31+
throw new Error(`Circular reference detected for color ${key}`)
32+
}
33+
34+
this.visited.add(key)
35+
36+
try {
37+
if (typeof value === "string") {
38+
if (value.startsWith("#") || value === "none") {
39+
return { dark: value, light: value }
40+
}
41+
const resolved = this.resolveReference(value)
42+
return { dark: resolved, light: resolved }
43+
}
44+
if (typeof value === "object" && value !== null) {
45+
const dark = this.resolveColorValue(value.dark || value.light || "#000000")
46+
const light = this.resolveColorValue(value.light || value.dark || "#ffffff")
47+
return { dark, light }
48+
}
49+
return { dark: "#000000", light: "#ffffff" }
50+
} finally {
51+
this.visited.delete(key)
52+
}
53+
}
54+
55+
private resolveColorValue(value: any): string {
56+
if (typeof value === "string") {
57+
if (value.startsWith("#") || value === "none") {
58+
return value
59+
}
60+
return this.resolveReference(value)
61+
}
62+
return value
63+
}
64+
65+
private resolveReference(ref: string): string {
66+
const colorValue = this.colors.get(ref)
67+
if (colorValue === undefined) {
68+
throw new Error(`Color reference '${ref}' not found`)
69+
}
70+
if (typeof colorValue === "string") {
71+
if (colorValue.startsWith("#") || colorValue === "none") {
72+
return colorValue
73+
}
74+
return this.resolveReference(colorValue)
75+
}
76+
return colorValue
77+
}
78+
}
79+
80+
function kebabCase(str: string): string {
81+
return str.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase()
82+
}
83+
84+
function parseTheme(themeData: ThemeDefinition): Record<string, ResolvedThemeColor> {
85+
const resolver = new ColorResolver(themeData.defs, themeData.theme)
86+
const colors: Record<string, ResolvedThemeColor> = {}
87+
Object.entries(themeData.theme).forEach(([key, value]) => {
88+
colors[key] = resolver.resolveColor(key, value)
89+
})
90+
return colors
91+
}
92+
93+
async function loadThemes(): Promise<Record<string, Record<string, ResolvedThemeColor>>> {
94+
const themesDir = resolve(__dirname, "../../tui/internal/theme/themes")
95+
const files = await readdir(themesDir)
96+
const themes: Record<string, Record<string, ResolvedThemeColor>> = {}
97+
98+
for (const file of files) {
99+
if (!file.endsWith(".json")) continue
100+
101+
const themeName = file.replace(".json", "")
102+
const themeData: ThemeDefinition = JSON.parse(await readFile(join(themesDir, file), "utf-8"))
103+
104+
themes[themeName] = parseTheme(themeData)
105+
}
106+
107+
return themes
108+
}
109+
110+
function generateCSS(themes: Record<string, Record<string, ResolvedThemeColor>>): string {
111+
let css = `/* Auto-generated theme CSS - Do not edit manually */\n:root {\n`
112+
113+
const defaultTheme = themes["opencode"] || Object.values(themes)[0]
114+
if (defaultTheme) {
115+
Object.entries(defaultTheme).forEach(([key, color]) => {
116+
const cssVar = `--theme-${kebabCase(key)}`
117+
css += ` ${cssVar}: ${color.light};\n`
118+
})
119+
}
120+
css += `}\n\n`
121+
122+
Object.entries(themes).forEach(([themeName, colors]) => {
123+
css += `[data-theme="${themeName}"][data-dark="false"] {\n`
124+
Object.entries(colors).forEach(([key, color]) => {
125+
const cssVar = `--theme-${kebabCase(key)}`
126+
css += ` ${cssVar}: ${color.light};\n`
127+
})
128+
css += `}\n\n`
129+
130+
css += `[data-theme="${themeName}"][data-dark="true"] {\n`
131+
Object.entries(colors).forEach(([key, color]) => {
132+
const cssVar = `--theme-${kebabCase(key)}`
133+
css += ` ${cssVar}: ${color.dark};\n`
134+
})
135+
css += `}\n\n`
136+
})
137+
138+
return css
139+
}
140+
141+
export function generateThemeCSS(): Plugin {
142+
return {
143+
name: "generate-theme-css",
144+
async buildStart() {
145+
try {
146+
console.log("Generating theme CSS...")
147+
const themes = await loadThemes()
148+
const css = generateCSS(themes)
149+
150+
const outputPath = resolve(__dirname, "../src/assets/theme.css")
151+
await writeFile(outputPath, css)
152+
153+
console.log(`✅ Generated theme CSS with ${Object.keys(themes).length} themes`)
154+
console.log(` Output: ${outputPath}`)
155+
} catch (error) {
156+
throw new Error(`Theme CSS generation failed: ${error}`)
157+
}
158+
},
159+
}
160+
}
14.7 KB
Binary file not shown.
Lines changed: 1 addition & 0 deletions
Loading
Lines changed: 1 addition & 0 deletions
Loading
Lines changed: 1 addition & 0 deletions
Loading

0 commit comments

Comments
 (0)