Bladeren bron

ignore: cloud progress

Jay V 5 maanden geleden
bovenliggende
commit
a05e677412

+ 2 - 1
cloud/app/src/routes/index.tsx

@@ -8,7 +8,8 @@ import IMG_VSCODE from "../asset/lander/screenshot-vscode.png"
 import IMG_GITHUB from "../asset/lander/screenshot-github.png"
 import { IconCopy, IconCheck } from "../component/icon"
 import { createAsync, query, redirect } from "@solidjs/router"
-import { getActor, withActor } from "~/context/auth"
+import { getActor } from "~/context/auth"
+import { withActor } from "~/context/auth.withActor"
 import { Account } from "@opencode/cloud-core/account.js"
 
 function CopyStatus() {

+ 160 - 112
cloud/app/src/routes/workspace/[id].tsx

@@ -49,7 +49,7 @@ const createPortalUrl = action(async (returnUrl: string) => {
   return withActor(() => Billing.generatePortalUrl({ returnUrl }))
 }, "portalUrl")
 
-export default function() {
+export default function () {
   const actor = createAsync(() => getActor())
   onMount(() => {
     console.log("MOUNTED", actor())
@@ -154,126 +154,174 @@ export default function() {
   }
 
   return (
-    <div>
-      <h1>Actor</h1>
-      <div>{JSON.stringify(actor())}</div>
-      <h1>API Keys</h1>
-      <Show
-        when={!showCreateForm()}
-        fallback={
-          <div data-slot="create-form">
-            <input
-              data-component="input"
-              type="text"
-              placeholder="Enter key name"
-              value={keyName()}
-              onInput={(e) => setKeyName(e.currentTarget.value)}
-              onKeyPress={(e) => e.key === "Enter" && handleCreateKey()}
-            />
-            <div data-slot="form-actions">
-              <button
-                color="primary"
-                disabled={createKeySubmission.pending || !keyName().trim()}
-                onClick={handleCreateKey}
-              >
-                {createKeySubmission.pending ? "Creating..." : "Create"}
-              </button>
-              <button
-                color="ghost"
-                onClick={() => {
-                  setShowCreateForm(false)
-                  setKeyName("")
-                }}
-              >
-                Cancel
-              </button>
-            </div>
-          </div>
-        }
-      >
-        <button
-          color="primary"
-          onClick={() => {
-            console.log("clicked")
-            setShowCreateForm(true)
-          }}
-        >
-          Create API Key
-        </button>
-      </Show>
-      <div data-slot="key-list">
-        <For
-          each={keys()}
+    <div data-slot="root">
+      {/* Actor Section */}
+      <section data-slot="actor-section">
+        <div data-slot="section-title">
+          <h1>Actor</h1>
+          <p>Current authenticated user information and session details.</p>
+        </div>
+        <div>{JSON.stringify(actor())}</div>
+      </section>
+
+      {/* API Keys Section */}
+      <section data-slot="keys-section">
+        <div data-slot="section-title">
+          <h1>API Keys</h1>
+          <p>Manage your API keys for accessing opencode services.</p>
+        </div>
+        <Show
+          when={!showCreateForm()}
           fallback={
-            <div data-slot="empty-state">
-              <p>Create an API key to access opencode gateway</p>
+            <div data-slot="create-form">
+              <input
+                data-component="input"
+                type="text"
+                placeholder="Enter key name"
+                value={keyName()}
+                onInput={(e) => setKeyName(e.currentTarget.value)}
+                onKeyPress={(e) => e.key === "Enter" && handleCreateKey()}
+              />
+              <div data-slot="form-actions">
+                <button
+                  color="primary"
+                  disabled={createKeySubmission.pending || !keyName().trim()}
+                  onClick={handleCreateKey}
+                >
+                  {createKeySubmission.pending ? "Creating..." : "Create"}
+                </button>
+                <button
+                  color="ghost"
+                  onClick={() => {
+                    setShowCreateForm(false)
+                    setKeyName("")
+                  }}
+                >
+                  Cancel
+                </button>
+              </div>
             </div>
           }
         >
-          {(key) => (
-            <div data-slot="key-item">
-              <div data-slot="key-info">
-                <div data-slot="key-name">{key.name}</div>
-                <div data-slot="key-value">{formatKey(key.key)}</div>
-                <div data-slot="key-meta">
-                  Created: {formatDate(key.timeCreated)}
-                  {key.timeUsed && ` • Last used: ${formatDate(key.timeUsed)}`}
-                </div>
+          <button
+            color="primary"
+            onClick={() => {
+              console.log("clicked")
+              setShowCreateForm(true)
+            }}
+          >
+            Create API Key
+          </button>
+        </Show>
+        <div data-slot="key-list">
+          <For
+            each={keys()}
+            fallback={
+              <div data-slot="empty-state">
+                <p>Create an API key to access opencode gateway</p>
               </div>
-              <div data-slot="key-actions">
-                <button color="ghost" onClick={() => copyToClipboard(key.key)} title="Copy API key">
-                  Copy
-                </button>
-                <button color="ghost" onClick={() => handleDeleteKey(key.id)} title="Delete API key">
-                  Delete
-                </button>
+            }
+          >
+            {(key) => (
+              <div data-slot="key-item">
+                <div data-slot="key-info">
+                  <div data-slot="key-name">{key.name}</div>
+                  <div data-slot="key-value">{formatKey(key.key)}</div>
+                  <div data-slot="key-meta">
+                    Created: {formatDate(key.timeCreated)}
+                    {key.timeUsed && ` • Last used: ${formatDate(key.timeUsed)}`}
+                  </div>
+                </div>
+                <div data-slot="key-actions">
+                  <button color="ghost" onClick={() => copyToClipboard(key.key)} title="Copy API key">
+                    Copy
+                  </button>
+                  <button color="ghost" onClick={() => handleDeleteKey(key.id)} title="Delete API key">
+                    Delete
+                  </button>
+                </div>
               </div>
-            </div>
-          )}
-        </For>
-      </div>
+            )}
+          </For>
+        </div>
+      </section>
 
-      <h1>Balance</h1>
-      <p>Manage your billing and add credits to your account.</p>
-      <p>
-        {(() => {
-          const balanceStr = ((billingInfo()?.billing?.balance ?? 0) / 100000000).toFixed(2)
-          return `$${balanceStr === "-0.00" ? "0.00" : balanceStr}`
-        })()}
-      </p>
-      <button color="primary" disabled={isLoading()} onClick={handleBuyCredits}>
-        {isLoading() ? "Loading..." : "Buy Credits"}
-      </button>
+      {/* Balance Section */}
+      <section data-slot="balance-section">
+        <div data-slot="section-title">
+          <h1>Balance</h1>
+          <p>Manage your billing and add credits to your account.</p>
+        </div>
+        <div data-slot="balance">
+          <p>
+            {(() => {
+              const balanceStr = ((billingInfo()?.billing?.balance ?? 0) / 100000000).toFixed(2)
+              return `$${balanceStr === "-0.00" ? "0.00" : balanceStr}`
+            })()}
+          </p>
+          <button color="primary" disabled={isLoading()} onClick={handleBuyCredits}>
+            {isLoading() ? "Loading..." : "Buy Credits"}
+          </button>
+        </div>
+      </section>
 
-      <h1>Payments History</h1>
-      <p>Your recent payment transactions.</p>
-      <For each={billingInfo()?.payments} fallback={<p>No payments found.</p>}>
-        {(payment) => (
-          <div data-slot="payment-item">
-            <span data-slot="payment-id">{payment.id}</span>
-            {"  |  "}
-            <span data-slot="payment-amount">${((payment.amount ?? 0) / 100000000).toFixed(2)}</span>
-            {"  |  "}
-            <span data-slot="payment-date">{new Date(payment.timeCreated).toLocaleDateString()}</span>
-          </div>
-        )}
-      </For>
+      {/* Payments Section */}
+      <section data-slot="payments-section">
+        <div data-slot="section-title">
+          <h1>Payments History</h1>
+          <p>Your recent payment transactions.</p>
+        </div>
+        <div data-slot="payments-list">
+          <For
+            each={billingInfo()?.payments}
+            fallback={
+              <div data-slot="empty-state">
+                <p>No payment history yet. Your payments will appear here after your first purchase.</p>
+              </div>
+            }
+          >
+            {(payment) => (
+              <div data-slot="payment-item">
+                <span data-slot="payment-id">{payment.id}</span>
+                {"  |  "}
+                <span data-slot="payment-amount">${((payment.amount ?? 0) / 100000000).toFixed(2)}</span>
+                {"  |  "}
+                <span data-slot="payment-date">{new Date(payment.timeCreated).toLocaleDateString()}</span>
+              </div>
+            )}
+          </For>
+        </div>
+      </section>
 
-      <h1>Usage History</h1>
-      <p>Your recent API usage and costs.</p>
-      <For each={billingInfo()?.usage} fallback={<p>No usage found.</p>}>
-        {(usage) => (
-          <div data-slot="usage-item">
-            <span data-slot="usage-model">{usage.model}</span>
-            {"  |  "}
-            <span data-slot="usage-tokens">{usage.inputTokens + usage.outputTokens} tokens</span>
-            {"  |  "}
-            <span data-slot="usage-cost">${((usage.cost ?? 0) / 100000000).toFixed(4)}</span>
-            {"  |  "}
-            <span data-slot="usage-date">{new Date(usage.timeCreated).toLocaleDateString()}</span>
-          </div>
-        )}
-      </For>
+      {/* Usage Section */}
+      <section data-slot="usage-section">
+        <div data-slot="section-title">
+          <h1>Usage History</h1>
+          <p>Your recent API usage and costs.</p>
+        </div>
+        <div data-slot="usage-list">
+          <For
+            each={billingInfo()?.usage}
+            fallback={
+              <div data-slot="empty-state">
+                <p>No API usage yet. Your usage history will appear here after your first API calls.</p>
+              </div>
+            }
+          >
+            {(usage) => (
+              <div data-slot="usage-item">
+                <span data-slot="usage-model">{usage.model}</span>
+                {"  |  "}
+                <span data-slot="usage-tokens">{usage.inputTokens + usage.outputTokens} tokens</span>
+                {"  |  "}
+                <span data-slot="usage-cost">${((usage.cost ?? 0) / 100000000).toFixed(4)}</span>
+                {"  |  "}
+                <span data-slot="usage-date">{new Date(usage.timeCreated).toLocaleDateString()}</span>
+              </div>
+            )}
+          </For>
+        </div>
+      </section>
     </div>
   )
 }

+ 219 - 202
cloud/app/src/routes/workspace/index.css

@@ -1,251 +1,268 @@
-[data-page="workspace"] {
-  /* Main content container */
-  & > div {
-    display: flex;
-    flex-direction: column;
-    gap: var(--space-6);
-  }
+/* Root container */
+[data-slot="root"] {
+  max-width: 64rem;
+  margin: 0 auto;
+  width: 100%;
+  display: flex;
+  flex-direction: column;
+  gap: var(--space-6);
+}
 
-  /* Adjust header spacing */
-  [data-component="workspace-header"] + div {
-    margin-top: var(--space-2);
-  }
+/* Adjust header spacing */
+[data-component="workspace-header"] + div {
+  margin-top: var(--space-2);
+}
 
-  /* Section headers */
-  h1 {
-    font-size: var(--font-size-3xl);
-    font-weight: 500;
-    line-height: 1.2;
-    letter-spacing: -0.05em;
-    margin: 0;
-    color: var(--color-text);
+/* Section titles */
+[data-slot="section-title"] {
+  display: flex;
+  flex-direction: column;
+  gap: var(--space-0-5);
+}
 
-    @media (max-width: 30rem) {
-      font-size: var(--font-size-2xl);
-      line-height: 1.25;
-    }
+[data-slot="section-title"] h1 {
+  font-size: var(--font-size-lg);
+  font-weight: 500;
+  line-height: 1.2;
+  letter-spacing: -0.03125rem;
+  margin: 0;
+  color: var(--color-text-secondary);
+  text-transform: uppercase;
+
+  @media (max-width: 30rem) {
+    font-size: var(--font-size-lg);
+    line-height: 1.25;
   }
+}
 
-  /* Section descriptions */
-  p {
-    margin: 0;
-    color: var(--color-text-secondary);
-    font-size: var(--font-size-md);
-    line-height: 1.5;
-  }
+[data-slot="section-title"] p {
+  font-size: var(--font-size-sm);
+  color: var(--color-text-muted);
+}
 
-  /* API Keys Section */
-  [data-slot="create-form"] {
-    display: flex;
-    flex-direction: column;
-    gap: var(--space-3);
-    padding: var(--space-4);
-    background-color: var(--color-bg-surface);
-    border: 1px solid var(--color-border);
-    border-radius: var(--space-2);
-    max-width: 32rem;
+/* Section descriptions */
+p {
+  margin: 0;
+  color: var(--color-text-secondary);
+  font-size: var(--font-size-md);
+  line-height: 1.5;
+}
 
-    input {
-      padding: var(--space-2) var(--space-3);
-      border: 1px solid var(--color-border);
-      border-radius: var(--space-2);
-      background-color: var(--color-bg);
-      color: var(--color-text);
-      font-size: var(--font-size-sm);
-      font-family: var(--font-mono);
+/* Section containers */
+section {
+  display: flex;
+  flex-direction: column;
+  gap: var(--space-6);
+}
 
-      &:focus {
-        outline: none;
-        border-color: var(--color-accent);
-      }
+/* API Keys Section */
+[data-slot="create-form"] {
+  display: flex;
+  flex-direction: column;
+  gap: var(--space-3);
+  padding: var(--space-4);
+  background-color: var(--color-bg-surface);
+  border: 1px solid var(--color-border);
+  border-radius: var(--border-radius-sm);
+  max-width: 32rem;
+
+  input {
+    padding: var(--space-2) var(--space-3);
+    border: 1px solid var(--color-border);
+    border-radius: var(--border-radius-sm);
+    background-color: var(--color-bg);
+    color: var(--color-text);
+    font-size: var(--font-size-sm);
+    font-family: var(--font-mono);
 
-      &::placeholder {
-        color: var(--color-text-disabled);
-      }
+    &:focus {
+      outline: none;
+      border-color: var(--color-accent);
     }
 
-    [data-slot="form-actions"] {
-      display: flex;
-      gap: var(--space-2);
-      justify-content: flex-end;
+    &::placeholder {
+      color: var(--color-text-disabled);
     }
   }
 
-  [data-slot="key-list"] {
+  [data-slot="form-actions"] {
     display: flex;
-    flex-direction: column;
     gap: var(--space-2);
+    justify-content: flex-end;
   }
+}
 
-  [data-slot="key-item"] {
-    display: flex;
-    justify-content: space-between;
-    align-items: flex-start;
-    padding: var(--space-4);
-    background-color: var(--color-bg-surface);
-    border: 1px solid var(--color-border);
-    border-radius: var(--space-2);
-    gap: var(--space-4);
-
-    @media (max-width: 30rem) {
-      flex-direction: column;
-      gap: var(--space-3);
-    }
-  }
+[data-slot="key-list"],
+[data-slot="payments-list"],
+[data-slot="usage-list"] {
+  display: flex;
+  flex-direction: column;
+  gap: var(--space-2);
+}
 
-  [data-slot="key-info"] {
-    display: flex;
+[data-slot="key-item"] {
+  display: flex;
+  justify-content: space-between;
+  align-items: flex-start;
+  padding: var(--space-4);
+  background-color: var(--color-bg-surface);
+  border: 1px solid var(--color-border);
+  border-radius: var(--border-radius-sm);
+  gap: var(--space-4);
+
+  @media (max-width: 30rem) {
     flex-direction: column;
-    gap: var(--space-1);
-    flex: 1;
+    gap: var(--space-3);
   }
+}
 
-  [data-slot="key-name"] {
-    font-size: var(--font-size-md);
-    font-weight: 500;
-    color: var(--color-text);
-  }
+[data-slot="key-info"] {
+  display: flex;
+  flex-direction: column;
+  gap: var(--space-1);
+  flex: 1;
+}
 
-  [data-slot="key-value"] {
-    font-size: var(--font-size-xs);
-    font-family: var(--font-mono);
-    color: var(--color-text-secondary);
-    background-color: var(--color-bg);
-    padding: var(--space-1) var(--space-2);
-    border-radius: var(--space-1);
-    border: 1px solid var(--color-border-muted);
-  }
+[data-slot="key-name"] {
+  font-size: var(--font-size-md);
+  font-weight: 500;
+  color: var(--color-text);
+}
 
-  [data-slot="key-meta"] {
-    font-size: var(--font-size-xs);
-    color: var(--color-text-disabled);
-  }
+[data-slot="key-value"] {
+  font-size: var(--font-size-xs);
+  font-family: var(--font-mono);
+  color: var(--color-text-secondary);
+  background-color: var(--color-bg);
+  padding: var(--space-1) var(--space-2);
+  border-radius: var(--border-radius-sm);
+  border: 1px solid var(--color-border-muted);
+}
 
-  [data-slot="key-actions"] {
-    display: flex;
-    gap: var(--space-2);
-  }
+[data-slot="key-meta"] {
+  font-size: var(--font-size-xs);
+  color: var(--color-text-disabled);
+}
+
+[data-slot="key-actions"] {
+  display: flex;
+  gap: var(--space-2);
+}
+
+[data-slot="empty-state"] {
+  padding: var(--space-8);
+  text-align: center;
+  border: 1px dashed var(--color-border);
+  border-radius: var(--border-radius-sm);
 
-  [data-slot="empty-state"] {
-    padding: var(--space-8);
-    text-align: center;
+  p {
+    margin: 0;
+    font-size: var(--font-size-sm);
     color: var(--color-text-muted);
-    background-color: var(--color-bg-surface);
-    border: 1px solid var(--color-border);
-    border-radius: var(--space-2);
+  }
+}
 
-    p {
-      margin: 0;
-      font-size: var(--font-size-sm);
-    }
+/* Balance Section */
+[data-slot="balance"] {
+  display: flex;
+  flex-direction: column;
+  gap: var(--space-3);
+  padding: var(--space-4);
+  background-color: var(--color-bg-surface);
+  border: 1px solid var(--color-border);
+  border-radius: var(--border-radius-sm);
+  max-width: 32rem;
+
+  p {
+    font-size: var(--font-size-2xl);
+    font-weight: 500;
+    color: var(--color-text);
+    margin: 0;
   }
+}
 
-  /* Balance Section */
-  [data-slot="balance"] {
-    display: flex;
+/* Payment and Usage Items */
+[data-slot="payment-item"],
+[data-slot="usage-item"] {
+  display: flex;
+  align-items: center;
+  gap: var(--space-4);
+  padding: var(--space-3);
+  background-color: var(--color-bg-surface);
+  border: 1px solid var(--color-border);
+  border-radius: var(--border-radius-sm);
+  font-size: var(--font-size-sm);
+  font-family: var(--font-mono);
+
+  @media (max-width: 30rem) {
     flex-direction: column;
-    gap: var(--space-3);
-    padding: var(--space-4);
-    background-color: var(--color-bg-surface);
-    border: 1px solid var(--color-border);
-    border-radius: var(--space-2);
-    max-width: 32rem;
-
-    p {
-      font-size: var(--font-size-2xl);
-      font-weight: 500;
-      color: var(--color-text);
-      margin: 0;
-    }
+    align-items: flex-start;
+    gap: var(--space-2);
   }
+}
 
-  /* Payment and Usage Items */
-  [data-slot="payment-item"],
-  [data-slot="usage-item"] {
-    display: flex;
-    align-items: center;
-    gap: var(--space-4);
-    padding: var(--space-3);
-    background-color: var(--color-bg-surface);
-    border: 1px solid var(--color-border);
-    border-radius: var(--space-2);
-    font-size: var(--font-size-sm);
-    font-family: var(--font-mono);
+[data-slot="payment-id"],
+[data-slot="payment-amount"],
+[data-slot="payment-date"],
+[data-slot="usage-model"],
+[data-slot="usage-tokens"],
+[data-slot="usage-cost"],
+[data-slot="usage-date"] {
+  color: var(--color-text-muted);
+}
 
-    @media (max-width: 30rem) {
-      flex-direction: column;
-      align-items: flex-start;
-      gap: var(--space-2);
-    }
+/* Buttons */
+button {
+  padding: var(--space-2) var(--space-4);
+  border: 1px solid var(--color-border);
+  border-radius: var(--border-radius-sm);
+  background-color: var(--color-bg);
+  color: var(--color-text);
+  font-size: var(--font-size-sm);
+  font-family: var(--font-sans);
+  cursor: pointer;
+  transition: all 0.15s ease;
+
+  &:hover {
+    background-color: var(--color-surface-hover);
+    border-color: var(--color-accent);
   }
 
-  [data-slot="payment-id"],
-  [data-slot="payment-amount"],
-  [data-slot="payment-date"],
-  [data-slot="usage-model"],
-  [data-slot="usage-tokens"],
-  [data-slot="usage-cost"],
-  [data-slot="usage-date"] {
-    color: var(--color-text-muted);
+  &:active {
+    transform: translateY(1px);
   }
 
-  /* Buttons */
-  button {
-    padding: var(--space-2) var(--space-4);
-    border: 1px solid var(--color-border);
-    border-radius: var(--space-2);
-    background-color: var(--color-bg);
-    color: var(--color-text);
-    font-size: var(--font-size-sm);
-    font-family: var(--font-sans);
-    cursor: pointer;
-    transition: all 0.15s ease;
+  &:disabled {
+    opacity: 0.5;
+    cursor: not-allowed;
 
     &:hover {
-      background-color: var(--color-surface-hover);
-      border-color: var(--color-accent);
-    }
-
-    &:active {
-      transform: translateY(1px);
-    }
-
-    &:disabled {
-      opacity: 0.5;
-      cursor: not-allowed;
-
-      &:hover {
-        background-color: var(--color-bg);
-        border-color: var(--color-border);
-        transform: none;
-      }
+      background-color: var(--color-bg);
+      border-color: var(--color-border);
+      transform: none;
     }
+  }
 
-    &[color="primary"] {
-      background-color: var(--color-primary);
-      border-color: var(--color-primary);
-      color: var(--color-primary-text);
+  &[color="primary"] {
+    background-color: var(--color-primary);
+    border-color: var(--color-primary);
+    color: var(--color-primary-text);
 
-      &:hover {
-        background-color: var(--color-primary-hover);
-        border-color: var(--color-primary-hover);
-      }
+    &:hover {
+      background-color: var(--color-primary-hover);
+      border-color: var(--color-primary-hover);
     }
+  }
 
-    &[color="ghost"] {
-      background-color: transparent;
-      border-color: transparent;
-      color: var(--color-text-muted);
+  &[color="ghost"] {
+    background-color: transparent;
+    border-color: transparent;
+    color: var(--color-text-muted);
 
-      &:hover {
-        background-color: var(--color-surface-hover);
-        border-color: var(--color-border);
-        color: var(--color-text);
-      }
+    &:hover {
+      background-color: var(--color-surface-hover);
+      border-color: var(--color-border);
+      color: var(--color-text);
     }
   }
-
-  @media (prefers-color-scheme: dark) {
-    /* Dark mode specific adjustments if needed */
-  }
 }

+ 1 - 1
cloud/app/src/routes/workspace/workspace.css

@@ -18,7 +18,7 @@
     display: flex;
     justify-content: space-between;
     align-items: center;
-    padding: var(--space-4) var(--space-3);
+    padding: var(--space-4) var(--space-4);
     margin: calc(-1 * var(--space-6));
     margin-bottom: var(--space-6);
     border-bottom: 1px solid var(--color-border);

+ 4 - 0
cloud/app/src/style/token/space.css

@@ -39,4 +39,8 @@ body {
   --space-72: 18rem;
   --space-80: 20rem;
   --space-96: 24rem;
+
+  --border-radius-sm: 0.1875rem;
+  --border-radius-md: 0.3125rem;
+  --border-radius-lg: 0.5rem;
 }