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

+
+ console.log(x)} placeholder="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