@jonsoc/console-app 1.1.34

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (217) hide show
  1. package/.opencode/agent/css.md +149 -0
  2. package/README.md +32 -0
  3. package/package.json +49 -0
  4. package/public/apple-touch-icon-v3.png +1 -0
  5. package/public/apple-touch-icon.png +1 -0
  6. package/public/email +1 -0
  7. package/public/favicon-96x96-v3.png +1 -0
  8. package/public/favicon-96x96.png +1 -0
  9. package/public/favicon-v3.ico +1 -0
  10. package/public/favicon-v3.svg +1 -0
  11. package/public/favicon.ico +1 -0
  12. package/public/favicon.svg +1 -0
  13. package/public/opencode-brand-assets.zip +0 -0
  14. package/public/robots.txt +6 -0
  15. package/public/site.webmanifest +1 -0
  16. package/public/social-share-black.png +1 -0
  17. package/public/social-share-zen.png +1 -0
  18. package/public/social-share.png +1 -0
  19. package/public/theme.json +182 -0
  20. package/public/web-app-manifest-192x192.png +1 -0
  21. package/public/web-app-manifest-512x512.png +1 -0
  22. package/script/generate-sitemap.ts +103 -0
  23. package/src/app.css +1 -0
  24. package/src/app.tsx +27 -0
  25. package/src/asset/black/hero.png +0 -0
  26. package/src/asset/brand/opencode-brand-assets.zip +0 -0
  27. package/src/asset/brand/opencode-logo-dark.png +0 -0
  28. package/src/asset/brand/opencode-logo-dark.svg +16 -0
  29. package/src/asset/brand/opencode-logo-light.png +0 -0
  30. package/src/asset/brand/opencode-logo-light.svg +16 -0
  31. package/src/asset/brand/opencode-wordmark-dark.png +0 -0
  32. package/src/asset/brand/opencode-wordmark-dark.svg +30 -0
  33. package/src/asset/brand/opencode-wordmark-light.png +0 -0
  34. package/src/asset/brand/opencode-wordmark-light.svg +30 -0
  35. package/src/asset/brand/opencode-wordmark-simple-dark.png +0 -0
  36. package/src/asset/brand/opencode-wordmark-simple-dark.svg +22 -0
  37. package/src/asset/brand/opencode-wordmark-simple-light.png +0 -0
  38. package/src/asset/brand/opencode-wordmark-simple-light.svg +22 -0
  39. package/src/asset/brand/preview-opencode-dark.png +0 -0
  40. package/src/asset/brand/preview-opencode-logo-dark.png +0 -0
  41. package/src/asset/brand/preview-opencode-logo-light.png +0 -0
  42. package/src/asset/brand/preview-opencode-wordmark-dark.png +0 -0
  43. package/src/asset/brand/preview-opencode-wordmark-light.png +0 -0
  44. package/src/asset/brand/preview-opencode-wordmark-simple-dark.png +0 -0
  45. package/src/asset/brand/preview-opencode-wordmark-simple-light.png +0 -0
  46. package/src/asset/lander/avatar-adam.png +0 -0
  47. package/src/asset/lander/avatar-david.png +0 -0
  48. package/src/asset/lander/avatar-dax.png +0 -0
  49. package/src/asset/lander/avatar-frank.png +0 -0
  50. package/src/asset/lander/avatar-jay.png +0 -0
  51. package/src/asset/lander/brand-assets-dark.svg +10 -0
  52. package/src/asset/lander/brand-assets-light.svg +10 -0
  53. package/src/asset/lander/brand.png +0 -0
  54. package/src/asset/lander/check.svg +3 -0
  55. package/src/asset/lander/copy.svg +3 -0
  56. package/src/asset/lander/desktop-app-icon.png +0 -0
  57. package/src/asset/lander/dock.png +0 -0
  58. package/src/asset/lander/logo-dark.svg +11 -0
  59. package/src/asset/lander/logo-light.svg +11 -0
  60. package/src/asset/lander/opencode-comparison-min.mp4 +0 -0
  61. package/src/asset/lander/opencode-comparison-poster.png +0 -0
  62. package/src/asset/lander/opencode-desktop-icon.png +0 -0
  63. package/src/asset/lander/opencode-logo-dark.svg +11 -0
  64. package/src/asset/lander/opencode-logo-light.svg +11 -0
  65. package/src/asset/lander/opencode-min.mp4 +0 -0
  66. package/src/asset/lander/opencode-poster.png +0 -0
  67. package/src/asset/lander/opencode-wordmark-dark.svg +25 -0
  68. package/src/asset/lander/opencode-wordmark-light.svg +25 -0
  69. package/src/asset/lander/screenshot-github.png +0 -0
  70. package/src/asset/lander/screenshot-splash.png +0 -0
  71. package/src/asset/lander/screenshot-vscode.png +0 -0
  72. package/src/asset/lander/screenshot.png +0 -0
  73. package/src/asset/lander/wordmark-dark.svg +3 -0
  74. package/src/asset/lander/wordmark-light.svg +3 -0
  75. package/src/asset/logo-ornate-dark.svg +18 -0
  76. package/src/asset/logo-ornate-light.svg +18 -0
  77. package/src/asset/logo.svg +18 -0
  78. package/src/asset/zen-ornate-dark.svg +8 -0
  79. package/src/asset/zen-ornate-light.svg +8 -0
  80. package/src/component/dropdown.css +80 -0
  81. package/src/component/dropdown.tsx +79 -0
  82. package/src/component/email-signup.tsx +48 -0
  83. package/src/component/faq.tsx +33 -0
  84. package/src/component/footer.tsx +38 -0
  85. package/src/component/header-context-menu.css +63 -0
  86. package/src/component/header.tsx +279 -0
  87. package/src/component/icon.tsx +257 -0
  88. package/src/component/legal.tsx +20 -0
  89. package/src/component/modal.css +66 -0
  90. package/src/component/modal.tsx +24 -0
  91. package/src/component/spotlight.css +15 -0
  92. package/src/component/spotlight.tsx +820 -0
  93. package/src/config.ts +29 -0
  94. package/src/context/auth.session.ts +0 -0
  95. package/src/context/auth.ts +116 -0
  96. package/src/context/auth.withActor.ts +7 -0
  97. package/src/entry-client.tsx +4 -0
  98. package/src/entry-server.tsx +30 -0
  99. package/src/global.d.ts +5 -0
  100. package/src/lib/github.ts +38 -0
  101. package/src/middleware.ts +5 -0
  102. package/src/routes/[...404].css +130 -0
  103. package/src/routes/[...404].tsx +38 -0
  104. package/src/routes/api/enterprise.ts +47 -0
  105. package/src/routes/auth/[...callback].ts +41 -0
  106. package/src/routes/auth/authorize.ts +10 -0
  107. package/src/routes/auth/index.ts +12 -0
  108. package/src/routes/auth/logout.ts +17 -0
  109. package/src/routes/auth/status.ts +7 -0
  110. package/src/routes/bench/[id].tsx +365 -0
  111. package/src/routes/bench/index.tsx +86 -0
  112. package/src/routes/bench/submission.ts +29 -0
  113. package/src/routes/black/common.tsx +62 -0
  114. package/src/routes/black/index.tsx +108 -0
  115. package/src/routes/black/subscribe/[plan].tsx +449 -0
  116. package/src/routes/black/workspace.css +214 -0
  117. package/src/routes/black/workspace.tsx +229 -0
  118. package/src/routes/black.css +828 -0
  119. package/src/routes/black.tsx +285 -0
  120. package/src/routes/brand/index.css +555 -0
  121. package/src/routes/brand/index.tsx +252 -0
  122. package/src/routes/changelog/index.css +477 -0
  123. package/src/routes/changelog/index.tsx +147 -0
  124. package/src/routes/debug/index.ts +13 -0
  125. package/src/routes/desktop-feedback.ts +5 -0
  126. package/src/routes/discord.ts +5 -0
  127. package/src/routes/docs/[...path].ts +20 -0
  128. package/src/routes/docs/index.ts +20 -0
  129. package/src/routes/download/[platform].ts +38 -0
  130. package/src/routes/download/index.css +750 -0
  131. package/src/routes/download/index.tsx +482 -0
  132. package/src/routes/download/types.ts +4 -0
  133. package/src/routes/enterprise/index.css +578 -0
  134. package/src/routes/enterprise/index.tsx +251 -0
  135. package/src/routes/index.css +1251 -0
  136. package/src/routes/index.tsx +840 -0
  137. package/src/routes/legal/privacy-policy/index.css +343 -0
  138. package/src/routes/legal/privacy-policy/index.tsx +1512 -0
  139. package/src/routes/legal/terms-of-service/index.css +254 -0
  140. package/src/routes/legal/terms-of-service/index.tsx +512 -0
  141. package/src/routes/openapi.json.ts +7 -0
  142. package/src/routes/s/[id].ts +20 -0
  143. package/src/routes/stripe/webhook.ts +532 -0
  144. package/src/routes/t/[...path].tsx +20 -0
  145. package/src/routes/temp.tsx +172 -0
  146. package/src/routes/user-menu.css +18 -0
  147. package/src/routes/user-menu.tsx +32 -0
  148. package/src/routes/workspace/[id]/billing/billing-section.module.css +185 -0
  149. package/src/routes/workspace/[id]/billing/billing-section.tsx +240 -0
  150. package/src/routes/workspace/[id]/billing/black-section.module.css +142 -0
  151. package/src/routes/workspace/[id]/billing/black-section.tsx +269 -0
  152. package/src/routes/workspace/[id]/billing/black-waitlist-section.module.css +23 -0
  153. package/src/routes/workspace/[id]/billing/index.tsx +32 -0
  154. package/src/routes/workspace/[id]/billing/monthly-limit-section.module.css +96 -0
  155. package/src/routes/workspace/[id]/billing/monthly-limit-section.tsx +133 -0
  156. package/src/routes/workspace/[id]/billing/payment-section.module.css +93 -0
  157. package/src/routes/workspace/[id]/billing/payment-section.tsx +122 -0
  158. package/src/routes/workspace/[id]/billing/reload-section.module.css +261 -0
  159. package/src/routes/workspace/[id]/billing/reload-section.tsx +213 -0
  160. package/src/routes/workspace/[id]/graph-section.module.css +145 -0
  161. package/src/routes/workspace/[id]/graph-section.tsx +475 -0
  162. package/src/routes/workspace/[id]/index.tsx +81 -0
  163. package/src/routes/workspace/[id]/keys/index.tsx +11 -0
  164. package/src/routes/workspace/[id]/keys/key-section.module.css +197 -0
  165. package/src/routes/workspace/[id]/keys/key-section.tsx +176 -0
  166. package/src/routes/workspace/[id]/members/index.tsx +11 -0
  167. package/src/routes/workspace/[id]/members/member-section.module.css +249 -0
  168. package/src/routes/workspace/[id]/members/member-section.tsx +343 -0
  169. package/src/routes/workspace/[id]/members/role-dropdown.css +72 -0
  170. package/src/routes/workspace/[id]/members/role-dropdown.tsx +43 -0
  171. package/src/routes/workspace/[id]/model-section.module.css +173 -0
  172. package/src/routes/workspace/[id]/model-section.tsx +174 -0
  173. package/src/routes/workspace/[id]/new-user-section.module.css +143 -0
  174. package/src/routes/workspace/[id]/new-user-section.tsx +104 -0
  175. package/src/routes/workspace/[id]/provider-section.module.css +138 -0
  176. package/src/routes/workspace/[id]/provider-section.tsx +188 -0
  177. package/src/routes/workspace/[id]/settings/index.tsx +11 -0
  178. package/src/routes/workspace/[id]/settings/settings-section.module.css +94 -0
  179. package/src/routes/workspace/[id]/settings/settings-section.tsx +122 -0
  180. package/src/routes/workspace/[id]/usage-section.module.css +185 -0
  181. package/src/routes/workspace/[id]/usage-section.tsx +200 -0
  182. package/src/routes/workspace/[id].css +308 -0
  183. package/src/routes/workspace/[id].tsx +62 -0
  184. package/src/routes/workspace/common.tsx +120 -0
  185. package/src/routes/workspace-picker.css +74 -0
  186. package/src/routes/workspace-picker.tsx +122 -0
  187. package/src/routes/workspace.css +107 -0
  188. package/src/routes/workspace.tsx +38 -0
  189. package/src/routes/zen/index.css +866 -0
  190. package/src/routes/zen/index.tsx +343 -0
  191. package/src/routes/zen/util/dataDumper.ts +44 -0
  192. package/src/routes/zen/util/error.ts +13 -0
  193. package/src/routes/zen/util/handler.ts +784 -0
  194. package/src/routes/zen/util/logger.ts +12 -0
  195. package/src/routes/zen/util/provider/anthropic.ts +752 -0
  196. package/src/routes/zen/util/provider/google.ts +75 -0
  197. package/src/routes/zen/util/provider/openai-compatible.ts +546 -0
  198. package/src/routes/zen/util/provider/openai.ts +630 -0
  199. package/src/routes/zen/util/provider/provider.ts +210 -0
  200. package/src/routes/zen/util/rateLimiter.ts +41 -0
  201. package/src/routes/zen/util/stickyProviderTracker.ts +16 -0
  202. package/src/routes/zen/util/trialLimiter.ts +49 -0
  203. package/src/routes/zen/v1/chat/completions.ts +11 -0
  204. package/src/routes/zen/v1/messages.ts +11 -0
  205. package/src/routes/zen/v1/models/[model].ts +13 -0
  206. package/src/routes/zen/v1/models.ts +60 -0
  207. package/src/routes/zen/v1/responses.ts +11 -0
  208. package/src/style/base.css +21 -0
  209. package/src/style/component/button.css +102 -0
  210. package/src/style/index.css +8 -0
  211. package/src/style/reset.css +76 -0
  212. package/src/style/token/color.css +91 -0
  213. package/src/style/token/font.css +21 -0
  214. package/src/style/token/space.css +46 -0
  215. package/sst-env.d.ts +9 -0
  216. package/tsconfig.json +21 -0
  217. package/vite.config.ts +25 -0
@@ -0,0 +1,188 @@
1
+ import { json, query, action, useParams, createAsync, useSubmission } from "@solidjs/router"
2
+ import { createEffect, For, Show } from "solid-js"
3
+ import { Provider } from "@jonsoc/console-core/provider.js"
4
+ import { withActor } from "~/context/auth.withActor"
5
+ import { createStore } from "solid-js/store"
6
+ import styles from "./provider-section.module.css"
7
+
8
+ const PROVIDERS = [
9
+ { name: "OpenAI", key: "openai", prefix: "sk-" },
10
+ { name: "Anthropic", key: "anthropic", prefix: "sk-ant-" },
11
+ { name: "Google Gemini", key: "google", prefix: "AI" },
12
+ ] as const
13
+
14
+ type Provider = (typeof PROVIDERS)[number]
15
+
16
+ function maskCredentials(credentials: string) {
17
+ return `${credentials.slice(0, 8)}...${credentials.slice(-8)}`
18
+ }
19
+
20
+ const removeProvider = action(async (form: FormData) => {
21
+ "use server"
22
+ const provider = form.get("provider")?.toString()
23
+ if (!provider) return { error: "Provider is required" }
24
+ const workspaceID = form.get("workspaceID")?.toString()
25
+ if (!workspaceID) return { error: "Workspace ID is required" }
26
+ return json(await withActor(() => Provider.remove({ provider }), workspaceID), {
27
+ revalidate: listProviders.key,
28
+ })
29
+ }, "provider.remove")
30
+
31
+ const saveProvider = action(async (form: FormData) => {
32
+ "use server"
33
+ const provider = form.get("provider")?.toString()
34
+ const credentials = form.get("credentials")?.toString()
35
+ if (!provider) return { error: "Provider is required" }
36
+ if (!credentials) return { error: "API key is required" }
37
+ const workspaceID = form.get("workspaceID")?.toString()
38
+ if (!workspaceID) return { error: "Workspace ID is required" }
39
+ return json(
40
+ await withActor(
41
+ () =>
42
+ Provider.create({ provider, credentials })
43
+ .then(() => ({ error: undefined }))
44
+ .catch((e) => ({ error: e.message as string })),
45
+ workspaceID,
46
+ ),
47
+ { revalidate: listProviders.key },
48
+ )
49
+ }, "provider.save")
50
+
51
+ const listProviders = query(async (workspaceID: string) => {
52
+ "use server"
53
+ return withActor(() => Provider.list(), workspaceID)
54
+ }, "provider.list")
55
+
56
+ function ProviderRow(props: { provider: Provider }) {
57
+ const params = useParams()
58
+ const providers = createAsync(() => listProviders(params.id!))
59
+ const saveSubmission = useSubmission(saveProvider, ([fd]) => fd.get("provider")?.toString() === props.provider.key)
60
+ const removeSubmission = useSubmission(
61
+ removeProvider,
62
+ ([fd]) => fd.get("provider")?.toString() === props.provider.key,
63
+ )
64
+ const [store, setStore] = createStore({ editing: false })
65
+
66
+ let input: HTMLInputElement
67
+
68
+ const providerData = () => providers()?.find((p) => p.provider === props.provider.key)
69
+
70
+ createEffect(() => {
71
+ if (!saveSubmission.pending && saveSubmission.result && !saveSubmission.result.error) {
72
+ hide()
73
+ }
74
+ })
75
+
76
+ function show() {
77
+ while (true) {
78
+ saveSubmission.clear()
79
+ if (!saveSubmission.result) break
80
+ }
81
+ setStore("editing", true)
82
+ setTimeout(() => input?.focus(), 0)
83
+ }
84
+
85
+ function hide() {
86
+ setStore("editing", false)
87
+ }
88
+
89
+ return (
90
+ <tr data-slot="provider-row">
91
+ <td data-slot="provider-name">{props.provider.name}</td>
92
+ <td data-slot="provider-key">
93
+ <Show
94
+ when={store.editing}
95
+ fallback={<span>{providerData() ? maskCredentials(providerData()!.credentials) : "-"}</span>}
96
+ >
97
+ <form id={`provider-form-${props.provider.key}`} action={saveProvider} method="post" data-slot="edit-form">
98
+ <div data-slot="input-wrapper">
99
+ <input
100
+ ref={(r) => (input = r)}
101
+ name="credentials"
102
+ type="text"
103
+ placeholder={`Enter ${props.provider.name} API key (${props.provider.prefix}...)`}
104
+ autocomplete="off"
105
+ data-form-type="other"
106
+ data-lpignore="true"
107
+ />
108
+ <Show when={saveSubmission.result && saveSubmission.result.error}>
109
+ {(err) => <div data-slot="form-error">{err()}</div>}
110
+ </Show>
111
+ </div>
112
+ <input type="hidden" name="provider" value={props.provider.key} />
113
+ <input type="hidden" name="workspaceID" value={params.id} />
114
+ </form>
115
+ </Show>
116
+ </td>
117
+ <td data-slot="provider-action">
118
+ <Show
119
+ when={store.editing}
120
+ fallback={
121
+ <Show
122
+ when={!!providerData()}
123
+ fallback={
124
+ <button data-color="ghost" onClick={() => show()}>
125
+ Configure
126
+ </button>
127
+ }
128
+ >
129
+ <div data-slot="configured-actions">
130
+ <button data-color="ghost" onClick={() => show()}>
131
+ Edit
132
+ </button>
133
+ <form action={removeProvider} method="post" data-slot="delete-form">
134
+ <input type="hidden" name="provider" value={props.provider.key} />
135
+ <input type="hidden" name="workspaceID" value={params.id} />
136
+ <button data-color="ghost" type="submit" disabled={removeSubmission.pending}>
137
+ Delete
138
+ </button>
139
+ </form>
140
+ </div>
141
+ </Show>
142
+ }
143
+ >
144
+ <div data-slot="form-actions">
145
+ <button
146
+ type="submit"
147
+ data-color="ghost"
148
+ disabled={saveSubmission.pending}
149
+ form={`provider-form-${props.provider.key}`}
150
+ >
151
+ {saveSubmission.pending ? "Saving..." : "Save"}
152
+ </button>
153
+ <Show when={!saveSubmission.pending}>
154
+ <button type="reset" data-color="ghost" onClick={() => hide()}>
155
+ Cancel
156
+ </button>
157
+ </Show>
158
+ </div>
159
+ </Show>
160
+ </td>
161
+ </tr>
162
+ )
163
+ }
164
+
165
+ export function ProviderSection() {
166
+ return (
167
+ <section class={styles.root}>
168
+ <div data-slot="section-title">
169
+ <h2>Bring Your Own Key</h2>
170
+ <p>Configure your own API keys from AI providers.</p>
171
+ </div>
172
+ <div data-slot="providers-table">
173
+ <table data-slot="providers-table-element">
174
+ <thead>
175
+ <tr>
176
+ <th>Provider</th>
177
+ <th>API Key</th>
178
+ <th></th>
179
+ </tr>
180
+ </thead>
181
+ <tbody>
182
+ <For each={PROVIDERS}>{(provider) => <ProviderRow provider={provider} />}</For>
183
+ </tbody>
184
+ </table>
185
+ </div>
186
+ </section>
187
+ )
188
+ }
@@ -0,0 +1,11 @@
1
+ import { SettingsSection } from "./settings-section"
2
+
3
+ export default function () {
4
+ return (
5
+ <div data-page="workspace-[id]">
6
+ <div data-slot="sections">
7
+ <SettingsSection />
8
+ </div>
9
+ </div>
10
+ )
11
+ }
@@ -0,0 +1,94 @@
1
+ .root {
2
+ max-width: 40rem;
3
+
4
+ [data-slot="setting"] {
5
+ display: flex;
6
+ flex-direction: column;
7
+ gap: var(--space-3);
8
+
9
+ p {
10
+ line-height: 1.2;
11
+ margin: 0;
12
+ color: var(--color-text-muted);
13
+ }
14
+
15
+ [data-slot="value-with-action"] {
16
+ display: flex;
17
+ align-items: center;
18
+ justify-content: space-between;
19
+ gap: var(--space-3);
20
+
21
+ @media (max-width: 30rem) {
22
+ flex-direction: column;
23
+ align-items: flex-start;
24
+ gap: var(--space-2);
25
+ }
26
+ }
27
+
28
+ [data-slot="current-value"] {
29
+ color: var(--color-text);
30
+ line-height: 1.4;
31
+ margin: 0;
32
+ }
33
+
34
+ > button {
35
+ align-self: flex-start;
36
+ }
37
+ }
38
+
39
+ [data-slot="create-form"] {
40
+ display: flex;
41
+ flex-direction: column;
42
+ gap: var(--space-2);
43
+
44
+ [data-slot="input-container"] {
45
+ display: flex;
46
+ flex-direction: row;
47
+ align-items: center;
48
+ gap: var(--space-2);
49
+
50
+ @media (max-width: 30rem) {
51
+ flex-direction: column;
52
+ align-items: stretch;
53
+ }
54
+
55
+ button {
56
+ white-space: nowrap;
57
+ flex-shrink: 0;
58
+ }
59
+ }
60
+
61
+ input {
62
+ flex: 1;
63
+ padding: var(--space-2) var(--space-3);
64
+ border: 1px solid var(--color-border);
65
+ border-radius: var(--border-radius-sm);
66
+ background-color: var(--color-bg);
67
+ color: var(--color-text);
68
+ font-size: var(--font-size-sm);
69
+ line-height: 1.5;
70
+ min-width: 0;
71
+
72
+ &:focus {
73
+ outline: none;
74
+ border-color: var(--color-accent);
75
+ box-shadow: 0 0 0 3px var(--color-accent-alpha);
76
+ }
77
+
78
+ &::placeholder {
79
+ color: var(--color-text-disabled);
80
+ }
81
+ }
82
+
83
+ > button[type="reset"] {
84
+ align-self: flex-start;
85
+ }
86
+
87
+ [data-slot="form-error"] {
88
+ color: var(--color-danger);
89
+ font-size: var(--font-size-sm);
90
+ line-height: 1.4;
91
+ margin-top: calc(var(--space-1) * -1);
92
+ }
93
+ }
94
+ }
@@ -0,0 +1,122 @@
1
+ import { json, action, useParams, useSubmission, createAsync, query } from "@solidjs/router"
2
+ import { createEffect, Show } from "solid-js"
3
+ import { createStore } from "solid-js/store"
4
+ import { withActor } from "~/context/auth.withActor"
5
+ import { Workspace } from "@jonsoc/console-core/workspace.js"
6
+ import styles from "./settings-section.module.css"
7
+ import { Database, eq } from "@jonsoc/console-core/drizzle/index.js"
8
+ import { WorkspaceTable } from "@jonsoc/console-core/schema/workspace.sql.js"
9
+
10
+ const getWorkspaceInfo = query(async (workspaceID: string) => {
11
+ "use server"
12
+ return withActor(
13
+ () =>
14
+ Database.use((tx) =>
15
+ tx
16
+ .select({
17
+ id: WorkspaceTable.id,
18
+ name: WorkspaceTable.name,
19
+ slug: WorkspaceTable.slug,
20
+ })
21
+ .from(WorkspaceTable)
22
+ .where(eq(WorkspaceTable.id, workspaceID))
23
+ .then((rows) => rows[0] || null),
24
+ ),
25
+ workspaceID,
26
+ )
27
+ }, "workspace.get")
28
+
29
+ const updateWorkspace = action(async (form: FormData) => {
30
+ "use server"
31
+ const name = form.get("name")?.toString().trim()
32
+ if (!name) return { error: "Workspace name is required." }
33
+ if (name.length > 255) return { error: "Name must be 255 characters or less." }
34
+ const workspaceID = form.get("workspaceID")?.toString()
35
+ if (!workspaceID) return { error: "Workspace ID is required." }
36
+ return json(
37
+ await withActor(
38
+ () =>
39
+ Workspace.update({ name })
40
+ .then(() => ({ error: undefined }))
41
+ .catch((e) => ({ error: e.message as string })),
42
+ workspaceID,
43
+ ),
44
+ )
45
+ }, "workspace.update")
46
+
47
+ export function SettingsSection() {
48
+ const params = useParams()
49
+ const workspaceInfo = createAsync(() => getWorkspaceInfo(params.id!))
50
+ const submission = useSubmission(updateWorkspace)
51
+ const [store, setStore] = createStore({ show: false })
52
+
53
+ let input: HTMLInputElement
54
+
55
+ createEffect(() => {
56
+ if (!submission.pending && submission.result && !submission.result.error) {
57
+ hide()
58
+ }
59
+ })
60
+
61
+ function show() {
62
+ while (true) {
63
+ submission.clear()
64
+ if (!submission.result) break
65
+ }
66
+ setStore("show", true)
67
+ input.focus()
68
+ }
69
+
70
+ function hide() {
71
+ setStore("show", false)
72
+ }
73
+
74
+ return (
75
+ <section class={styles.root}>
76
+ <div data-slot="section-title">
77
+ <h2>Settings</h2>
78
+ <p>Update your workspace name and preferences.</p>
79
+ </div>
80
+ <div data-slot="section-content">
81
+ <div data-slot="setting">
82
+ <p>Workspace name</p>
83
+ <Show
84
+ when={!store.show}
85
+ fallback={
86
+ <form action={updateWorkspace} method="post" data-slot="create-form">
87
+ <div data-slot="input-container">
88
+ <input
89
+ required
90
+ ref={(r) => (input = r)}
91
+ data-component="input"
92
+ name="name"
93
+ type="text"
94
+ placeholder="Workspace name"
95
+ value={workspaceInfo()?.name ?? "Default"}
96
+ />
97
+ <input type="hidden" name="workspaceID" value={params.id} />
98
+ <button type="submit" data-color="primary" disabled={submission.pending}>
99
+ {submission.pending ? "Updating..." : "Save"}
100
+ </button>
101
+ <button type="reset" data-color="ghost" onClick={() => hide()}>
102
+ Cancel
103
+ </button>
104
+ </div>
105
+ <Show when={submission.result && submission.result.error}>
106
+ {(err) => <div data-slot="form-error">{err()}</div>}
107
+ </Show>
108
+ </form>
109
+ }
110
+ >
111
+ <div data-slot="value-with-action">
112
+ <p data-slot="current-value">{workspaceInfo()?.name}</p>
113
+ <button data-color="primary" onClick={() => show()}>
114
+ Edit
115
+ </button>
116
+ </div>
117
+ </Show>
118
+ </div>
119
+ </div>
120
+ </section>
121
+ )
122
+ }
@@ -0,0 +1,185 @@
1
+ .root {
2
+ /* Empty state */
3
+ [data-component="empty-state"] {
4
+ padding: var(--space-20) var(--space-6);
5
+ text-align: center;
6
+ border: 1px dashed var(--color-border);
7
+ border-radius: var(--border-radius-sm);
8
+
9
+ p {
10
+ font-size: var(--font-size-sm);
11
+ color: var(--color-text-muted);
12
+ }
13
+ }
14
+
15
+ /* Table container */
16
+ [data-slot="usage-table"] {
17
+ overflow-x: auto;
18
+ }
19
+
20
+ /* Table element */
21
+ [data-slot="usage-table-element"] {
22
+ width: 100%;
23
+ border-collapse: collapse;
24
+ font-size: var(--font-size-sm);
25
+
26
+ thead {
27
+ border-bottom: 1px solid var(--color-border);
28
+ }
29
+
30
+ th {
31
+ padding: var(--space-3) var(--space-4);
32
+ text-align: left;
33
+ font-weight: normal;
34
+ color: var(--color-text-muted);
35
+ text-transform: uppercase;
36
+ }
37
+
38
+ td {
39
+ padding: var(--space-3) var(--space-4);
40
+ border-bottom: 1px solid var(--color-border-muted);
41
+ color: var(--color-text-muted);
42
+ font-family: var(--font-mono);
43
+
44
+ &[data-slot="usage-date"] {
45
+ color: var(--color-text-muted);
46
+ }
47
+
48
+ &[data-slot="usage-model"] {
49
+ font-family: var(--font-sans);
50
+ color: var(--color-text-secondary);
51
+ max-width: 200px;
52
+ word-break: break-word;
53
+ }
54
+
55
+ &[data-slot="usage-cost"] {
56
+ color: var(--color-text-muted);
57
+ }
58
+
59
+ [data-slot="tokens-with-breakdown"] {
60
+ position: relative;
61
+ display: flex;
62
+ align-items: center;
63
+ gap: var(--space-2);
64
+ }
65
+
66
+ [data-slot="breakdown-button"] {
67
+ display: inline-flex;
68
+ align-items: center;
69
+ justify-content: center;
70
+ padding: 0;
71
+ background: transparent;
72
+ border: none;
73
+ color: var(--color-text-muted);
74
+ cursor: pointer;
75
+ transition: color 0.15s ease;
76
+
77
+ &:hover {
78
+ color: var(--color-text);
79
+ }
80
+
81
+ svg {
82
+ width: 16px;
83
+ height: 16px;
84
+ }
85
+ }
86
+
87
+ [data-slot="breakdown-popup"] {
88
+ position: absolute;
89
+ left: 0;
90
+ top: 100%;
91
+ margin-top: var(--space-2);
92
+ background: var(--color-bg);
93
+ border: 1px solid var(--color-border);
94
+ border-radius: var(--border-radius-sm);
95
+ padding: var(--space-2);
96
+ z-index: 10;
97
+ min-width: 180px;
98
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
99
+ font-size: var(--font-size-xs);
100
+
101
+ @media (prefers-color-scheme: dark) {
102
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
103
+ }
104
+ }
105
+ }
106
+
107
+ tbody tr:last-child td {
108
+ border-bottom: none;
109
+ }
110
+ }
111
+
112
+ /* Pagination */
113
+ [data-slot="pagination"] {
114
+ display: flex;
115
+ justify-content: flex-end;
116
+ gap: var(--space-2);
117
+ padding: var(--space-4) 0;
118
+ border-top: 1px solid var(--color-border-muted);
119
+ margin-top: var(--space-2);
120
+
121
+ button {
122
+ padding: var(--space-2) var(--space-4);
123
+ background: var(--color-bg-secondary);
124
+ border: 1px solid var(--color-border);
125
+ border-radius: var(--border-radius-sm);
126
+ color: var(--color-text);
127
+ font-size: var(--font-size-sm);
128
+ cursor: pointer;
129
+ transition: all 0.15s ease;
130
+
131
+ svg {
132
+ width: 16px;
133
+ height: 16px;
134
+ stroke-width: 2;
135
+ }
136
+
137
+ &:hover:not(:disabled) {
138
+ background: var(--color-bg-tertiary);
139
+ border-color: var(--color-border-hover);
140
+ }
141
+
142
+ &:disabled {
143
+ opacity: 0.5;
144
+ cursor: not-allowed;
145
+ }
146
+ }
147
+ }
148
+
149
+ /* Mobile responsive */
150
+ @media (max-width: 40rem) {
151
+ [data-slot="usage-table-element"] {
152
+ th,
153
+ td {
154
+ padding: var(--space-2) var(--space-3);
155
+ font-size: var(--font-size-xs);
156
+ }
157
+
158
+ /* Hide Model column on mobile */
159
+ th:nth-child(2),
160
+ td:nth-child(2) {
161
+ display: none;
162
+ }
163
+ }
164
+ }
165
+
166
+ /* Breakdown popup content */
167
+ [data-slot="breakdown-row"] {
168
+ display: flex;
169
+ justify-content: space-between;
170
+ align-items: center;
171
+ gap: var(--space-4);
172
+ padding: var(--space-1) 0;
173
+ }
174
+
175
+ [data-slot="breakdown-label"] {
176
+ color: var(--color-text-muted);
177
+ font-size: var(--font-size-xs);
178
+ }
179
+
180
+ [data-slot="breakdown-value"] {
181
+ color: var(--color-text);
182
+ font-weight: 500;
183
+ font-size: var(--font-size-xs);
184
+ }
185
+ }