From 37e6c8342f1f63a95918a71ac78130b9d10d1103 Mon Sep 17 00:00:00 2001
From: Adam <2363879+adamdotdevin@users.noreply.github.com>
Date: Tue, 14 Oct 2025 07:14:26 -0500
Subject: wip: css and ui packages
---
packages/ui/.gitignore | 28 ++++++
packages/ui/index.html | 14 +++
packages/ui/package.json | 23 +++++
packages/ui/src/app.tsx | 68 +++++++++++++++
packages/ui/src/components/button.tsx | 25 ++++++
packages/ui/src/components/icon.tsx | 155 ++++++++++++++++++++++++++++++++++
packages/ui/src/components/index.ts | 3 +
packages/ui/src/components/select.tsx | 92 ++++++++++++++++++++
packages/ui/src/components/style.css | 2 +
packages/ui/src/index.css | 39 +++++++++
packages/ui/src/index.tsx | 15 ++++
packages/ui/tsconfig.json | 20 +++++
packages/ui/vite.config.ts | 10 +++
13 files changed, 494 insertions(+)
create mode 100644 packages/ui/.gitignore
create mode 100644 packages/ui/index.html
create mode 100644 packages/ui/package.json
create mode 100644 packages/ui/src/app.tsx
create mode 100644 packages/ui/src/components/button.tsx
create mode 100644 packages/ui/src/components/icon.tsx
create mode 100644 packages/ui/src/components/index.ts
create mode 100644 packages/ui/src/components/select.tsx
create mode 100644 packages/ui/src/components/style.css
create mode 100644 packages/ui/src/index.css
create mode 100644 packages/ui/src/index.tsx
create mode 100644 packages/ui/tsconfig.json
create mode 100644 packages/ui/vite.config.ts
(limited to 'packages/ui')
diff --git a/packages/ui/.gitignore b/packages/ui/.gitignore
new file mode 100644
index 000000000..751513ce1
--- /dev/null
+++ b/packages/ui/.gitignore
@@ -0,0 +1,28 @@
+dist
+.wrangler
+.output
+.vercel
+.netlify
+.vinxi
+app.config.timestamp_*.js
+
+# Environment
+.env
+.env*.local
+
+# dependencies
+/node_modules
+
+# IDEs and editors
+/.idea
+.project
+.classpath
+*.launch
+.settings/
+
+# Temp
+gitignore
+
+# System Files
+.DS_Store
+Thumbs.db
diff --git a/packages/ui/index.html b/packages/ui/index.html
new file mode 100644
index 000000000..b7f74e79d
--- /dev/null
+++ b/packages/ui/index.html
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+ OpenCode UI
+
+
+
+
+
+
+
diff --git a/packages/ui/package.json b/packages/ui/package.json
new file mode 100644
index 000000000..0e5d4c379
--- /dev/null
+++ b/packages/ui/package.json
@@ -0,0 +1,23 @@
+{
+ "name": "@opencode-ai/ui",
+ "version": "0.15.0",
+ "type": "module",
+ "exports": {
+ ".": "./src/components/index.ts",
+ "./*": "./src/components/*"
+ },
+ "scripts": {
+ "dev": "vite"
+ },
+ "devDependencies": {
+ "typescript": "catalog:",
+ "vite": "catalog:",
+ "vite-plugin-solid": "catalog:"
+ },
+ "dependencies": {
+ "remeda": "catalog:",
+ "solid-js": "catalog:",
+ "@kobalte/core": "catalog:",
+ "@opencode-ai/css": "workspace:*"
+ }
+}
diff --git a/packages/ui/src/app.tsx b/packages/ui/src/app.tsx
new file mode 100644
index 000000000..be2ec357e
--- /dev/null
+++ b/packages/ui/src/app.tsx
@@ -0,0 +1,68 @@
+import type { Component } from "solid-js"
+import { Button } from "./components/button"
+import { Select } from "./components"
+import "@opencode-ai/css"
+import "./index.css"
+
+const App: Component = () => {
+ return (
+
+
+
Buttons
+
+
+
+
+
+
+
+
+ Select
+
+
+
+
+
Buttons
+
+
+
+
+
+
+
+
+ Select
+
+
+
+
+ )
+}
+
+export default App
diff --git a/packages/ui/src/components/button.tsx b/packages/ui/src/components/button.tsx
new file mode 100644
index 000000000..db1da2fb9
--- /dev/null
+++ b/packages/ui/src/components/button.tsx
@@ -0,0 +1,25 @@
+import { Button as Kobalte } from "@kobalte/core/button"
+import { type ComponentProps, splitProps } from "solid-js"
+
+export interface ButtonProps {
+ size?: "normal" | "large"
+ variant?: "primary" | "secondary" | "ghost"
+}
+
+export function Button(props: ComponentProps<"button"> & ButtonProps) {
+ const [split, rest] = splitProps(props, ["variant", "size", "class", "classList"])
+ return (
+
+ {props.children}
+
+ )
+}
diff --git a/packages/ui/src/components/icon.tsx b/packages/ui/src/components/icon.tsx
new file mode 100644
index 000000000..05dda6ea6
--- /dev/null
+++ b/packages/ui/src/components/icon.tsx
@@ -0,0 +1,155 @@
+import { splitProps, type ComponentProps } from "solid-js"
+
+// prettier-ignore
+const icons = {
+ close: '',
+ menu: ' ',
+ "chevron-right": '',
+ "chevron-left": '',
+ "chevron-down": '',
+ "chevron-up": '',
+ "chevron-down-square": '',
+ "chevron-up-square": '',
+ "chevron-right-square": '',
+ "chevron-left-square": '',
+ settings: '',
+ globe: '',
+ github: '',
+ hammer: '',
+ "avatar-square": '',
+ slash: '',
+ robot: '',
+ cloud: '',
+ "file-text": '',
+ file: '',
+ "file-checkmark": '',
+ "file-code": '',
+ "file-important": '',
+ "file-minus": '',
+ "file-plus": '',
+ files: '',
+ "file-zip": '',
+ jpg: '',
+ pdf: '',
+ png: '',
+ gif: '',
+ archive: '',
+ sun: '',
+ moon: '',
+ monitor: '',
+ command: '',
+ link: '',
+ share: '',
+ branch: '',
+ logout: '',
+ login: '',
+ keys: '',
+ key: '',
+ info: '',
+ warning: '',
+ checkmark: '',
+ "checkmark-square": '',
+ plus: '',
+ minus: '',
+ undo: '',
+ merge: '',
+ redo: '',
+ refresh: '',
+ rotate: '',
+ "arrow-left": '',
+ "arrow-down": '',
+ "arrow-right": '',
+ "arrow-up": '',
+ enter: '',
+ trash: '',
+ package: '',
+ box: '',
+ lock: '',
+ unlocked: '',
+ activity: '',
+ asterisk: '',
+ bell: '',
+ "bell-off": '',
+ bolt: '',
+ bookmark: '',
+ brain: '',
+ browser: '',
+ "browser-cursor": '',
+ bug: '',
+ "carat-down": '',
+ "carat-left": '',
+ "carat-right": '',
+ "carat-up": '',
+ cards: '',
+ chart: '',
+ "check-circle": '',
+ checklist: '',
+ "checklist-cards": '',
+ lab: '',
+ circle: '',
+ "circle-dotted": '',
+ clipboard: '',
+ clock: '',
+ "close-circle": '',
+ terminal: '',
+ code: '',
+ components: '',
+ copy: '',
+ cpu: '',
+ dashboard: '',
+ transfer: '',
+ devices: '',
+ diamond: '',
+ dice: '',
+ discord: '',
+ dots: '',
+ expand: '',
+ droplet: '',
+ "chevron-double-down": '',
+ "chevron-double-left": '',
+ "chevron-double-right": '',
+ "chevron-double-up": '',
+ "speech-bubble": '',
+ message: '',
+ annotation: '',
+ square: '',
+ "pull-request": '',
+ pencil: '',
+ sparkles: '',
+ photo: '',
+ columns: '',
+ "open-pane": '',
+ "close-pane": '',
+ "file-search": '',
+ "folder-search": '',
+ search: '',
+ "web-search": '',
+ loading: '',
+ mic: '',
+} as const
+
+export interface IconProps extends ComponentProps<"svg"> {
+ name: keyof typeof icons
+ size?: number
+}
+
+export function Icon(props: IconProps) {
+ const [local, others] = splitProps(props, ["name", "size", "class", "classList"])
+ const size = local.size ?? 24
+ return (
+
+ )
+}
diff --git a/packages/ui/src/components/index.ts b/packages/ui/src/components/index.ts
new file mode 100644
index 000000000..80d80299b
--- /dev/null
+++ b/packages/ui/src/components/index.ts
@@ -0,0 +1,3 @@
+export * from "./button"
+export * from "./icon"
+export * from "./select"
diff --git a/packages/ui/src/components/select.tsx b/packages/ui/src/components/select.tsx
new file mode 100644
index 000000000..c4d5443c1
--- /dev/null
+++ b/packages/ui/src/components/select.tsx
@@ -0,0 +1,92 @@
+import { Select as Kobalte } from "@kobalte/core/select"
+import { createMemo, type ComponentProps } from "solid-js"
+import { Icon, Button, type ButtonProps } from "@opencode-ai/ui"
+import { pipe, groupBy, entries, map } from "remeda"
+
+export interface SelectProps {
+ placeholder?: string
+ options: T[]
+ current?: T
+ value?: (x: T) => string
+ label?: (x: T) => string
+ groupBy?: (x: T) => string
+ onSelect?: (value: T | undefined) => void
+ class?: ComponentProps<"div">["class"]
+ classList?: ComponentProps<"div">["classList"]
+}
+
+export function Select(props: SelectProps & ButtonProps) {
+ const grouped = createMemo(() => {
+ const result = pipe(
+ props.options,
+ groupBy((x) => (props.groupBy ? props.groupBy(x) : "")),
+ // mapValues((x) => x.sort((a, b) => a.title.localeCompare(b.title))),
+ entries(),
+ map(([k, v]) => ({ category: k, options: v })),
+ )
+ return result
+ })
+
+ return (
+
+ data-component="select"
+ value={props.current}
+ options={grouped()}
+ optionValue={(x) => (props.value ? props.value(x) : (x as string))}
+ optionTextValue={(x) => (props.label ? props.label(x) : (x as string))}
+ optionGroupChildren="options"
+ placeholder={props.placeholder}
+ sectionComponent={(props) => (
+ {props.section.rawValue.category}
+ )}
+ itemComponent={(itemProps) => (
+
+
+ {props.label ? props.label(itemProps.item.rawValue) : (itemProps.item.rawValue as string)}
+
+
+
+
+
+ )}
+ onChange={(v) => {
+ props.onSelect?.(v ?? undefined)
+ }}
+ >
+
+ data-slot="value">
+ {(state) => {
+ const selected = state.selectedOption() ?? props.current
+ if (!selected) return props.placeholder || ""
+ if (props.label) return props.label(selected)
+ return selected as string
+ }}
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/packages/ui/src/components/style.css b/packages/ui/src/components/style.css
new file mode 100644
index 000000000..52539dc8f
--- /dev/null
+++ b/packages/ui/src/components/style.css
@@ -0,0 +1,2 @@
+/* re-exporting for convenience */
+@import "@opencode-ai/css";
diff --git a/packages/ui/src/index.css b/packages/ui/src/index.css
new file mode 100644
index 000000000..be7adaaed
--- /dev/null
+++ b/packages/ui/src/index.css
@@ -0,0 +1,39 @@
+:root {
+ body {
+ margin: 0;
+ background-color: var(--background-background);
+ color: var(--text-default-text);
+ }
+ main {
+ display: flex;
+ flex-direction: row;
+ height: 100vh;
+ overflow-x: hidden;
+ }
+ main > div {
+ flex: 1;
+ padding: 3rem;
+ min-width: 0;
+ overflow-y: auto;
+ overflow-x: hidden;
+ display: flex;
+ flex-direction: column;
+ gap: 2rem;
+ }
+ h3 {
+ font-size: 1.25rem;
+ font-weight: 600;
+ margin: 0 0 1rem 0;
+ }
+ section {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.75rem;
+ align-items: flex-start;
+ }
+}
+
+.dark {
+ background-color: var(--background-background);
+ color: var(--text-default-text);
+}
diff --git a/packages/ui/src/index.tsx b/packages/ui/src/index.tsx
new file mode 100644
index 000000000..216765711
--- /dev/null
+++ b/packages/ui/src/index.tsx
@@ -0,0 +1,15 @@
+/* @refresh reload */
+import { render } from "solid-js/web"
+import "solid-devtools"
+
+import App from "./app"
+
+const root = document.getElementById("root")
+
+if (import.meta.env.DEV && !(root instanceof HTMLElement)) {
+ throw new Error(
+ "Root element not found. Did you forget to add it to your index.html? Or maybe the id attribute got misspelled?",
+ )
+}
+
+render(() => , root!)
diff --git a/packages/ui/tsconfig.json b/packages/ui/tsconfig.json
new file mode 100644
index 000000000..8b4ebee2b
--- /dev/null
+++ b/packages/ui/tsconfig.json
@@ -0,0 +1,20 @@
+{
+ "compilerOptions": {
+ // General
+ "jsx": "preserve",
+ "jsxImportSource": "solid-js",
+ "target": "ESNext",
+
+ // Modules
+ "allowSyntheticDefaultImports": true,
+ "esModuleInterop": true,
+ "isolatedModules": true,
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "noEmit": true,
+
+ // Type Checking & Safety
+ "strict": true,
+ "types": ["vite/client"]
+ }
+}
diff --git a/packages/ui/vite.config.ts b/packages/ui/vite.config.ts
new file mode 100644
index 000000000..181a93887
--- /dev/null
+++ b/packages/ui/vite.config.ts
@@ -0,0 +1,10 @@
+import { defineConfig } from "vite"
+import solidPlugin from "vite-plugin-solid"
+
+export default defineConfig({
+ plugins: [solidPlugin()],
+ server: { port: 3001 },
+ build: {
+ target: "esnext",
+ },
+})
--
cgit v1.2.3