From 4ae7e1b19c3915e3e9b1a39195d54c4721836b03 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Thu, 11 Dec 2025 10:19:24 -0600 Subject: wip(desktop): progress --- packages/ui/src/components/toast.css | 175 +++++++++++++++++++++++++++++++++++ 1 file changed, 175 insertions(+) create mode 100644 packages/ui/src/components/toast.css (limited to 'packages/ui/src/components/toast.css') diff --git a/packages/ui/src/components/toast.css b/packages/ui/src/components/toast.css new file mode 100644 index 000000000..2c55a4b06 --- /dev/null +++ b/packages/ui/src/components/toast.css @@ -0,0 +1,175 @@ +[data-component="toast-region"] { + position: fixed; + bottom: 32px; + right: 32px; + z-index: 1000; + display: flex; + flex-direction: column; + gap: 8px; + max-width: 400px; + width: 100%; + pointer-events: none; + + [data-slot="toast-list"] { + display: flex; + flex-direction: column; + gap: 8px; + list-style: none; + margin: 0; + padding: 0; + } +} + +[data-component="toast"] { + display: flex; + align-items: flex-start; + gap: 20px; + padding: 16px 20px; + pointer-events: auto; + transition: all 150ms ease-out; + + border-radius: var(--radius-lg); + border: 1px solid var(--border-weak-base); + background: var(--surface-float-base); + color: rgba(253, 252, 252, 0.94); + box-shadow: var(--shadow-md); + + [data-slot="toast-inner"] { + display: flex; + align-items: flex-start; + gap: 10px; + } + + &[data-opened] { + animation: toastPopIn 150ms ease-out; + } + + &[data-closed] { + animation: toastPopOut 100ms ease-in forwards; + } + + &[data-swipe="move"] { + transform: translateX(var(--kb-toast-swipe-move-x)); + } + + &[data-swipe="cancel"] { + transform: translateX(0); + transition: transform 200ms ease-out; + } + + &[data-swipe="end"] { + animation: toastSwipeOut 100ms ease-out forwards; + } + + &[data-variant="success"] { + border-color: var(--color-semantic-positive); + } + + &[data-variant="error"] { + border-color: var(--color-semantic-danger); + } + + &[data-variant="loading"] { + border-color: var(--color-semantic-info); + } + + [data-slot="toast-icon"] { + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; + + [data-component="icon"] { + color: rgba(253, 252, 252, 0.94); + } + } + + [data-slot="toast-content"] { + flex: 1; + display: flex; + flex-direction: column; + gap: 2px; + min-width: 0; + } + + [data-slot="toast-title"] { + color: rgba(253, 252, 252, 0.94); + + /* text-14-medium */ + font-family: var(--font-family-sans); + font-size: 14px; + font-style: normal; + font-weight: var(--font-weight-medium); + line-height: var(--line-height-large); /* 142.857% */ + letter-spacing: var(--letter-spacing-normal); + + margin: 0; + } + + [data-slot="toast-description"] { + color: rgba(253, 249, 249, 0.7); + + /* text-14-regular */ + font-family: var(--font-family-sans); + font-size: var(--font-size-base); + font-style: normal; + font-weight: var(--font-weight-regular); + line-height: var(--line-height-x-large); /* 171.429% */ + letter-spacing: var(--letter-spacing-normal); + + margin: 0; + } + + [data-slot="toast-close-button"] { + flex-shrink: 0; + } + + [data-slot="toast-progress-track"] { + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: 3px; + background-color: var(--surface-base); + border-radius: 0 0 var(--radius-lg) var(--radius-lg); + overflow: hidden; + } + + [data-slot="toast-progress-fill"] { + height: 100%; + width: var(--kb-toast-progress-fill-width); + background-color: var(--color-primary); + transition: width 250ms linear; + } +} + +@keyframes toastPopIn { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes toastPopOut { + from { + opacity: 1; + transform: translateY(0); + } + to { + opacity: 0; + transform: translateY(20px); + } +} + +@keyframes toastSwipeOut { + from { + transform: translateX(var(--kb-toast-swipe-end-x)); + } + to { + transform: translateX(100%); + } +} -- cgit v1.2.3 From e845eedbc325b05a19679bc439a57cc0fbf23aa3 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Thu, 11 Dec 2025 11:28:34 -0600 Subject: wip(desktop): progress --- packages/desktop/src/pages/layout.tsx | 352 ++++++++++++++++++++-------------- packages/ui/src/components/list.css | 6 +- packages/ui/src/components/toast.css | 28 +++ packages/ui/src/components/toast.tsx | 42 ++-- 4 files changed, 262 insertions(+), 166 deletions(-) (limited to 'packages/ui/src/components/toast.css') diff --git a/packages/desktop/src/pages/layout.tsx b/packages/desktop/src/pages/layout.tsx index e9f10e3a2..4a3fa766b 100644 --- a/packages/desktop/src/pages/layout.tsx +++ b/packages/desktop/src/pages/layout.tsx @@ -1,4 +1,4 @@ -import { createEffect, createMemo, For, Match, ParentProps, Show, Switch, type JSX } from "solid-js" +import { createEffect, createMemo, For, Match, onMount, ParentProps, Show, Switch, type JSX } from "solid-js" import { DateTime } from "luxon" import { A, useNavigate, useParams } from "@solidjs/router" import { useLayout } from "@/context/layout" @@ -17,9 +17,9 @@ import { DiffChanges } from "@opencode-ai/ui/diff-changes" import { getFilename } from "@opencode-ai/util/path" import { Select } from "@opencode-ai/ui/select" import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu" -import { Session, Project, ProviderAuthMethod } from "@opencode-ai/sdk/v2/client" +import { Session, Project, ProviderAuthMethod, ProviderAuthAuthorization } from "@opencode-ai/sdk/v2/client" import { usePlatform } from "@/context/platform" -import { createStore } from "solid-js/store" +import { createStore, produce } from "solid-js/store" import { DragDropProvider, DragDropSensors, @@ -40,6 +40,7 @@ import { List, ListRef } from "@opencode-ai/ui/list" import { Input } from "@opencode-ai/ui/input" import { showToast, Toast } from "@opencode-ai/ui/toast" import { useGlobalSDK } from "@/context/global-sdk" +import { Spinner } from "@opencode-ai/ui/spinner" export default function Layout(props: ParentProps) { const [store, setStore] = createStore({ @@ -618,9 +619,6 @@ export default function Layout(props: ParentProps) { {iife(() => { - const [store, setStore] = createStore({ - method: undefined as undefined | ProviderAuthMethod, - }) const providerID = createMemo(() => layout.connect.provider()!) const provider = createMemo(() => globalSync.data.provider.all.find((x) => x.id === providerID())!) const methods = createMemo( @@ -632,12 +630,61 @@ export default function Layout(props: ParentProps) { }, ], ) - if (methods().length === 1) { - setStore("method", methods()[0]) + const [store, setStore] = createStore({ + method: undefined as undefined | ProviderAuthMethod, + authorization: undefined as undefined | ProviderAuthAuthorization, + state: "pending" as undefined | "pending" | "complete" | "error", + error: undefined as string | undefined, + }) + + async function selectMethod(index: number) { + const method = methods()[index] + setStore( + produce((draft) => { + draft.method = method + draft.authorization = undefined + draft.state = undefined + draft.error = undefined + }), + ) + + if (method.type === "oauth") { + setStore("state", "pending") + const start = Date.now() + await globalSDK.client.provider.oauth + .authorize({ + providerID: providerID(), + method: index, + }) + .then((x) => { + const elapsed = Date.now() - start + const delay = 1000 - elapsed + + if (delay > 0) { + setTimeout(() => { + setStore("state", "complete") + setStore("authorization", x.data!) + }, delay) + return + } + setStore("state", "complete") + setStore("authorization", x.data!) + }) + .catch((e) => { + setStore("state", "error") + setStore("error", String(e)) + }) + } } + onMount(() => { + if (methods().length === 1) { + selectMethod(0) + } + }) + let listRef: ListRef | undefined - const handleKey = (e: KeyboardEvent) => { + function handleKey(e: KeyboardEvent) { if (e.key === "Escape") return listRef?.onKeyDown(e) } @@ -661,7 +708,16 @@ export default function Layout(props: ParentProps) { icon="arrow-left" variant="ghost" onClick={() => { - if (store.method && methods.length > 1) { + if (methods().length === 1) { + layout.dialog.open("provider") + return + } + if (store.authorization) { + setStore("authorization", undefined) + setStore("method", undefined) + return + } + if (store.method) { setStore("method", undefined) return } @@ -677,154 +733,152 @@ export default function Layout(props: ParentProps) {
Connect {provider().name}
- - -
- Select login method for {provider().name}. -
-
- - (listRef = ref)} - items={methods} - key={(m) => m?.label} - onSelect={(method) => { - if (!method) return - setStore("method", method) - - if (method.type === "oauth") { - // const result = await sdk.client.provider.oauth.authorize({ - // providerID: provider.id, - // method: index, - // }) - // if (result.data?.method === "code") { - // dialog.replace(() => ( - // - // )) - // } - // if (result.data?.method === "auto") { - // dialog.replace(() => ( - // - // )) - // } - } - if (method.type === "api") { - // return dialog.replace(() => ) - } - }} - > - {(i) => ( -
- {/* TODO: add checkmark thing */} - {i.label} -
- )} -
-
-
- - {iife(() => { - const [formStore, setFormStore] = createStore({ - value: "", - error: undefined as string | undefined, - }) +
+ + +
Select login method for {provider().name}.
+
+ + (listRef = ref)} + items={methods} + key={(m) => m?.label} + onSelect={async (method, index) => { + if (!method) return + selectMethod(index) + }} + > + {(i) => ( +
+
+ + {/* TODO: add checkmark thing */} + {i.label} +
+ )} + +
+ + +
+
+ + Authorization in progress... +
+
+
+ +
+
+ + Authorization failed: {store.error} +
+
+
+ + {iife(() => { + const [formStore, setFormStore] = createStore({ + value: "", + error: undefined as string | undefined, + }) - async function handleSubmit(e: SubmitEvent) { - e.preventDefault() + async function handleSubmit(e: SubmitEvent) { + e.preventDefault() - const form = e.currentTarget as HTMLFormElement - const formData = new FormData(form) - const apiKey = formData.get("apiKey") as string + const form = e.currentTarget as HTMLFormElement + const formData = new FormData(form) + const apiKey = formData.get("apiKey") as string - if (!apiKey?.trim()) { - setFormStore("error", "API key is required") - return - } + if (!apiKey?.trim()) { + setFormStore("error", "API key is required") + return + } - setFormStore("error", undefined) - await globalSDK.client.auth.set({ - providerID: providerID(), - auth: { - type: "api", - key: apiKey, - }, - }) - await globalSDK.client.global.dispose() - setTimeout(() => { - showToast({ - variant: "success", - icon: "circle-check", - title: `${provider().name} connected`, - description: `${provider().name} models are now available to use.`, + setFormStore("error", undefined) + await globalSDK.client.auth.set({ + providerID: providerID(), + auth: { + type: "api", + key: apiKey, + }, }) - layout.connect.complete() - }, 500) - } + await globalSDK.client.global.dispose() + setTimeout(() => { + showToast({ + variant: "success", + icon: "circle-check", + title: `${provider().name} connected`, + description: `${provider().name} models are now available to use.`, + }) + layout.connect.complete() + }, 500) + } - return ( -
- - -
-
- OpenCode Zen gives you access to a curated set of reliable optimized models for - coding agents. + return ( +
+ + +
+
+ OpenCode Zen gives you access to a curated set of reliable optimized models for + coding agents. +
+
+ With a single API key you’ll get access to models such as Claude, GPT, Gemini, + GLM and more. +
+
+ Visit{" "} + {" "} + to collect your API key. +
+
+
- With a single API key you’ll get access to models such as Claude, GPT, Gemini, GLM - and more. + Enter your {provider().name} API key to connect your account and use{" "} + {provider().name} models in OpenCode.
-
- Visit{" "} - {" "} - to collect your API key. -
-
- - -
- Enter your {provider().name} API key to connect your account and use{" "} - {provider().name} models in OpenCode. -
-
- -
- - -
-
- ) - })} - - + + +
+ + +
+
+ ) + })} +
+ + + Code {store.authorization?.url} + Auto {store.authorization?.url} + + +
+
diff --git a/packages/ui/src/components/list.css b/packages/ui/src/components/list.css index 38dcb773b..783b0ef4a 100644 --- a/packages/ui/src/components/list.css +++ b/packages/ui/src/components/list.css @@ -98,17 +98,13 @@ display: block; } [data-slot="list-item-extra-icon"] { + display: block !important; color: var(--icon-strong-base) !important; } } &:active { background: var(--surface-raised-base-active); } - &:hover { - [data-slot="list-item-extra-icon"] { - color: var(--icon-strong-base) !important; - } - } } } } diff --git a/packages/ui/src/components/toast.css b/packages/ui/src/components/toast.css index 2c55a4b06..3389f477a 100644 --- a/packages/ui/src/components/toast.css +++ b/packages/ui/src/components/toast.css @@ -120,6 +120,34 @@ margin: 0; } + [data-slot="toast-actions"] { + display: flex; + gap: 16px; + margin-top: 8px; + } + + [data-slot="toast-action"] { + background: none; + border: none; + padding: 0; + cursor: pointer; + + color: rgba(253, 252, 252, 0.94); + font-family: var(--font-family-sans); + font-size: var(--font-size-base); + font-weight: var(--font-weight-medium); + line-height: var(--line-height-large); + letter-spacing: var(--letter-spacing-normal); + + &:hover { + text-decoration: underline; + } + + &:last-child { + color: rgba(253, 249, 249, 0.5); + } + } + [data-slot="toast-close-button"] { flex-shrink: 0; } diff --git a/packages/ui/src/components/toast.tsx b/packages/ui/src/components/toast.tsx index b6c9f8b08..5869f8a6b 100644 --- a/packages/ui/src/components/toast.tsx +++ b/packages/ui/src/components/toast.tsx @@ -57,6 +57,10 @@ function ToastDescription(props: ToastDescriptionProps & ComponentProps<"div">) return } +function ToastActions(props: ComponentProps<"div">) { + return
+} + function ToastCloseButton(props: ToastCloseButtonProps & ComponentProps<"button">) { return } @@ -75,6 +79,7 @@ export const Toast = Object.assign(ToastRoot, { Content: ToastContent, Title: ToastTitle, Description: ToastDescription, + Actions: ToastActions, CloseButton: ToastCloseButton, ProgressTrack: ToastProgressTrack, ProgressFill: ToastProgressFill, @@ -84,31 +89,44 @@ export { toaster } export type ToastVariant = "default" | "success" | "error" | "loading" +export interface ToastAction { + label: string + onClick: () => void +} + export interface ToastOptions { title?: string description?: string icon?: IconProps["name"] variant?: ToastVariant duration?: number + actions?: ToastAction[] } export function showToast(options: ToastOptions | string) { const opts = typeof options === "string" ? { description: options } : options return toaster.show((props) => ( -
- - + + + + + + {opts.title} + + + {opts.description} - - - {opts.title} - - - {opts.description} - - -
+ + + {opts.actions!.map((action) => ( + + ))} + + +
)) -- cgit v1.2.3 From bfdb2365810ef4ba8f7084d2d714fd2fe1449fce Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Thu, 11 Dec 2025 15:16:04 -0600 Subject: fix: toast colors --- packages/ui/src/components/toast.css | 32 ++++++++++++++++---------------- packages/ui/src/components/tooltip.css | 1 + 2 files changed, 17 insertions(+), 16 deletions(-) (limited to 'packages/ui/src/components/toast.css') diff --git a/packages/ui/src/components/toast.css b/packages/ui/src/components/toast.css index 3389f477a..fbc84f13c 100644 --- a/packages/ui/src/components/toast.css +++ b/packages/ui/src/components/toast.css @@ -31,7 +31,7 @@ border-radius: var(--radius-lg); border: 1px solid var(--border-weak-base); background: var(--surface-float-base); - color: rgba(253, 252, 252, 0.94); + color: var(--text-inverted-base); box-shadow: var(--shadow-md); [data-slot="toast-inner"] { @@ -61,17 +61,17 @@ animation: toastSwipeOut 100ms ease-out forwards; } - &[data-variant="success"] { - border-color: var(--color-semantic-positive); - } - - &[data-variant="error"] { - border-color: var(--color-semantic-danger); - } - - &[data-variant="loading"] { - border-color: var(--color-semantic-info); - } + /* &[data-variant="success"] { */ + /* border-color: var(--color-semantic-positive); */ + /* } */ + /**/ + /* &[data-variant="error"] { */ + /* border-color: var(--color-semantic-danger); */ + /* } */ + /**/ + /* &[data-variant="loading"] { */ + /* border-color: var(--color-semantic-info); */ + /* } */ [data-slot="toast-icon"] { flex-shrink: 0; @@ -93,7 +93,7 @@ } [data-slot="toast-title"] { - color: rgba(253, 252, 252, 0.94); + color: var(--text-inverted-strong); /* text-14-medium */ font-family: var(--font-family-sans); @@ -107,7 +107,7 @@ } [data-slot="toast-description"] { - color: rgba(253, 249, 249, 0.7); + color: var(--text-inverted-base); /* text-14-regular */ font-family: var(--font-family-sans); @@ -132,7 +132,7 @@ padding: 0; cursor: pointer; - color: rgba(253, 252, 252, 0.94); + color: var(--text-inverted-strong); font-family: var(--font-family-sans); font-size: var(--font-size-base); font-weight: var(--font-weight-medium); @@ -144,7 +144,7 @@ } &:last-child { - color: rgba(253, 249, 249, 0.5); + color: var(--text-inverted-weak); } } diff --git a/packages/ui/src/components/tooltip.css b/packages/ui/src/components/tooltip.css index 72ee269b2..637986249 100644 --- a/packages/ui/src/components/tooltip.css +++ b/packages/ui/src/components/tooltip.css @@ -7,6 +7,7 @@ max-width: 320px; border-radius: var(--radius-md); background-color: var(--surface-float-base); + color: var(--text-inverted-base); color: rgba(253, 252, 252, 0.94); padding: 2px 8px; border: 0.5px solid rgba(253, 252, 252, 0.2); -- cgit v1.2.3