@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,343 @@
1
+ import { json, query, action, useParams, createAsync, useSubmission } from "@solidjs/router"
2
+ import { createEffect, For, Show } from "solid-js"
3
+ import { withActor } from "~/context/auth.withActor"
4
+ import { createStore } from "solid-js/store"
5
+ import styles from "./member-section.module.css"
6
+ import { UserRole } from "@jonsoc/console-core/schema/user.sql.js"
7
+ import { Actor } from "@jonsoc/console-core/actor.js"
8
+ import { User } from "@jonsoc/console-core/user.js"
9
+ import { RoleDropdown } from "./role-dropdown"
10
+
11
+ const listMembers = query(async (workspaceID: string) => {
12
+ "use server"
13
+ return withActor(async () => {
14
+ return {
15
+ members: await User.list(),
16
+ actorID: Actor.userID(),
17
+ actorRole: Actor.userRole(),
18
+ }
19
+ }, workspaceID)
20
+ }, "member.list")
21
+
22
+ const inviteMember = action(async (form: FormData) => {
23
+ "use server"
24
+ const email = form.get("email")?.toString().trim()
25
+ if (!email) return { error: "Email is required" }
26
+ const workspaceID = form.get("workspaceID")?.toString()
27
+ if (!workspaceID) return { error: "Workspace ID is required" }
28
+ const role = form.get("role")?.toString() as (typeof UserRole)[number]
29
+ if (!role) return { error: "Role is required" }
30
+ const limit = form.get("limit")?.toString()
31
+ const monthlyLimit = limit && limit.trim() !== "" ? parseInt(limit) : null
32
+ if (monthlyLimit !== null && monthlyLimit < 0) return { error: "Set a valid monthly limit" }
33
+ return json(
34
+ await withActor(
35
+ () =>
36
+ User.invite({ email, role, monthlyLimit })
37
+ .then((data) => ({ error: undefined, data }))
38
+ .catch((e) => ({ error: e.message as string })),
39
+ workspaceID,
40
+ ),
41
+ { revalidate: listMembers.key },
42
+ )
43
+ }, "member.create")
44
+
45
+ const removeMember = action(async (form: FormData) => {
46
+ "use server"
47
+ const id = form.get("id")?.toString()
48
+ if (!id) return { error: "ID is required" }
49
+ const workspaceID = form.get("workspaceID")?.toString()
50
+ if (!workspaceID) return { error: "Workspace ID is required" }
51
+ return json(
52
+ await withActor(
53
+ () =>
54
+ User.remove(id)
55
+ .then((data) => ({ error: undefined, data }))
56
+ .catch((e) => ({ error: e.message as string })),
57
+ workspaceID,
58
+ ),
59
+ { revalidate: listMembers.key },
60
+ )
61
+ }, "member.remove")
62
+
63
+ const updateMember = action(async (form: FormData) => {
64
+ "use server"
65
+
66
+ const id = form.get("id")?.toString()
67
+ if (!id) return { error: "ID is required" }
68
+ const workspaceID = form.get("workspaceID")?.toString()
69
+ if (!workspaceID) return { error: "Workspace ID is required" }
70
+ const role = form.get("role")?.toString() as (typeof UserRole)[number]
71
+ if (!role) return { error: "Role is required" }
72
+ const limit = form.get("limit")?.toString()
73
+ const monthlyLimit = limit && limit.trim() !== "" ? parseInt(limit) : null
74
+ if (monthlyLimit !== null && monthlyLimit < 0) return { error: "Set a valid monthly limit" }
75
+
76
+ return json(
77
+ await withActor(
78
+ () =>
79
+ User.update({ id, role, monthlyLimit })
80
+ .then((data) => ({ error: undefined, data }))
81
+ .catch((e) => ({ error: e.message as string })),
82
+ workspaceID,
83
+ ),
84
+ { revalidate: listMembers.key },
85
+ )
86
+ }, "member.update")
87
+
88
+ function MemberRow(props: { member: any; workspaceID: string; actorID: string; actorRole: string }) {
89
+ const submission = useSubmission(updateMember)
90
+ const isCurrentUser = () => props.actorID === props.member.id
91
+ const isAdmin = () => props.actorRole === "admin"
92
+ const [store, setStore] = createStore({
93
+ editing: false,
94
+ selectedRole: props.member.role as (typeof UserRole)[number],
95
+ limit: "",
96
+ })
97
+
98
+ createEffect(() => {
99
+ if (!submission.pending && submission.result && !submission.result.error) {
100
+ setStore("editing", false)
101
+ }
102
+ })
103
+
104
+ function show() {
105
+ while (true) {
106
+ submission.clear()
107
+ if (!submission.result) break
108
+ }
109
+ setStore("editing", true)
110
+ setStore("selectedRole", props.member.role)
111
+ setStore("limit", props.member.monthlyLimit?.toString() ?? "")
112
+ }
113
+
114
+ function hide() {
115
+ setStore("editing", false)
116
+ }
117
+
118
+ function getUsageDisplay() {
119
+ const currentUsage = (() => {
120
+ const dateLastUsed = props.member.timeMonthlyUsageUpdated
121
+ if (!dateLastUsed) return 0
122
+
123
+ const current = new Date().toLocaleDateString("en-US", {
124
+ year: "numeric",
125
+ month: "long",
126
+ timeZone: "UTC",
127
+ })
128
+ const lastUsed = dateLastUsed.toLocaleDateString("en-US", {
129
+ year: "numeric",
130
+ month: "long",
131
+ timeZone: "UTC",
132
+ })
133
+ return current === lastUsed ? (props.member.monthlyUsage ?? 0) : 0
134
+ })()
135
+
136
+ const limit = props.member.monthlyLimit ? `$${props.member.monthlyLimit}` : "no limit"
137
+ return `$${(currentUsage / 100000000).toFixed(2)} / ${limit}`
138
+ }
139
+
140
+ return (
141
+ <tr>
142
+ <td data-slot="member-email">{props.member.authEmail ?? props.member.email}</td>
143
+ <td data-slot="member-role">
144
+ <Show when={store.editing && !isCurrentUser()} fallback={<span>{props.member.role}</span>}>
145
+ <RoleDropdown
146
+ value={store.selectedRole}
147
+ options={roleOptions}
148
+ onChange={(value) => setStore("selectedRole", value as (typeof UserRole)[number])}
149
+ />
150
+ </Show>
151
+ </td>
152
+ <td data-slot="member-usage">
153
+ <Show when={store.editing} fallback={<span>{getUsageDisplay()}</span>}>
154
+ <input
155
+ data-component="input"
156
+ type="number"
157
+ value={store.limit}
158
+ onInput={(e) => setStore("limit", e.currentTarget.value)}
159
+ placeholder="No limit"
160
+ min="0"
161
+ />
162
+ </Show>
163
+ </td>
164
+ <td data-slot="member-joined">{props.member.timeSeen ? "" : "invited"}</td>
165
+ <Show when={isAdmin()}>
166
+ <td data-slot="member-actions">
167
+ <Show
168
+ when={store.editing}
169
+ fallback={
170
+ <>
171
+ <button data-color="ghost" onClick={() => show()}>
172
+ Edit
173
+ </button>
174
+ <Show when={!isCurrentUser()}>
175
+ <form action={removeMember} method="post">
176
+ <input type="hidden" name="id" value={props.member.id} />
177
+ <input type="hidden" name="workspaceID" value={props.workspaceID} />
178
+ <button data-color="ghost">Delete</button>
179
+ </form>
180
+ </Show>
181
+ </>
182
+ }
183
+ >
184
+ <form action={updateMember} method="post" data-slot="inline-edit-form">
185
+ <input type="hidden" name="id" value={props.member.id} />
186
+ <input type="hidden" name="workspaceID" value={props.workspaceID} />
187
+ <input type="hidden" name="role" value={store.selectedRole} />
188
+ <input type="hidden" name="limit" value={store.limit} />
189
+ <button type="submit" data-color="ghost" disabled={submission.pending}>
190
+ {submission.pending ? "Saving..." : "Save"}
191
+ </button>
192
+ <Show when={!submission.pending}>
193
+ <button type="button" data-color="ghost" onClick={() => hide()}>
194
+ Cancel
195
+ </button>
196
+ </Show>
197
+ </form>
198
+ </Show>
199
+ </td>
200
+ </Show>
201
+ </tr>
202
+ )
203
+ }
204
+
205
+ const roleOptions = [
206
+ { value: "admin", description: "Can manage models, members, and billing" },
207
+ { value: "member", description: "Can only generate API keys for themselves" },
208
+ ]
209
+
210
+ export function MemberSection() {
211
+ const params = useParams()
212
+ const data = createAsync(() => listMembers(params.id!))
213
+ const submission = useSubmission(inviteMember)
214
+ const [store, setStore] = createStore({
215
+ show: false,
216
+ selectedRole: "member" as (typeof UserRole)[number],
217
+ limit: "",
218
+ })
219
+
220
+ let input: HTMLInputElement
221
+
222
+ createEffect(() => {
223
+ if (!submission.pending && submission.result && !submission.result.error) {
224
+ setStore("show", false)
225
+ }
226
+ })
227
+
228
+ function show() {
229
+ while (true) {
230
+ submission.clear()
231
+ if (!submission.result) break
232
+ }
233
+ setStore("show", true)
234
+ setStore("selectedRole", "member")
235
+ setStore("limit", "")
236
+ setTimeout(() => input?.focus(), 0)
237
+ }
238
+
239
+ function hide() {
240
+ setStore("show", false)
241
+ }
242
+
243
+ return (
244
+ <section class={styles.root}>
245
+ <div data-slot="section-title">
246
+ <h2>Members</h2>
247
+ <div data-slot="title-row">
248
+ <p>Manage workspace members and their permissions.</p>
249
+ <Show when={data()?.actorRole === "admin"}>
250
+ <button data-color="primary" onClick={() => show()}>
251
+ Invite Member
252
+ </button>
253
+ </Show>
254
+ </div>
255
+ </div>
256
+ <div data-slot="beta-notice">
257
+ Workspaces are free for teams during the beta.{" "}
258
+ <a href="/docs/zen/#for-teams" target="_blank" rel="noopener noreferrer">
259
+ Learn more
260
+ </a>
261
+ .
262
+ </div>
263
+ <Show when={store.show}>
264
+ <form action={inviteMember} method="post" data-slot="create-form">
265
+ <div data-slot="input-row">
266
+ <div data-slot="input-field">
267
+ <p>Invitee</p>
268
+ <input
269
+ ref={(r) => (input = r)}
270
+ data-component="input"
271
+ name="email"
272
+ type="text"
273
+ placeholder="Enter email"
274
+ />
275
+ </div>
276
+ <div data-slot="input-field">
277
+ <p>Role</p>
278
+ <RoleDropdown
279
+ value={store.selectedRole}
280
+ options={roleOptions}
281
+ onChange={(value) => setStore("selectedRole", value as (typeof UserRole)[number])}
282
+ />
283
+ </div>
284
+ <div data-slot="input-field">
285
+ <p>Monthly spending limit</p>
286
+ <input
287
+ data-component="input"
288
+ name="limit"
289
+ type="number"
290
+ placeholder="No limit"
291
+ value={store.limit}
292
+ onInput={(e) => setStore("limit", e.currentTarget.value)}
293
+ min="0"
294
+ />
295
+ </div>
296
+ </div>
297
+ <Show when={submission.result && submission.result.error}>
298
+ {(err) => <div data-slot="form-error">{err()}</div>}
299
+ </Show>
300
+ <input type="hidden" name="role" value={store.selectedRole} />
301
+ <input type="hidden" name="workspaceID" value={params.id} />
302
+ <div data-slot="form-actions">
303
+ <button type="reset" data-color="ghost" onClick={() => hide()}>
304
+ Cancel
305
+ </button>
306
+ <button type="submit" data-color="primary" disabled={submission.pending}>
307
+ {submission.pending ? "Inviting..." : "Invite"}
308
+ </button>
309
+ </div>
310
+ </form>
311
+ </Show>
312
+ <div data-slot="members-table">
313
+ <table data-slot="members-table-element">
314
+ <thead>
315
+ <tr>
316
+ <th>Email</th>
317
+ <th>Role</th>
318
+ <th>Month limit</th>
319
+ <th></th>
320
+ <Show when={data()?.actorRole === "admin"}>
321
+ <th></th>
322
+ </Show>
323
+ </tr>
324
+ </thead>
325
+ <tbody>
326
+ <Show when={data() && data()!.members.length > 0}>
327
+ <For each={data()!.members}>
328
+ {(member) => (
329
+ <MemberRow
330
+ member={member}
331
+ workspaceID={params.id!}
332
+ actorID={data()!.actorID}
333
+ actorRole={data()!.actorRole}
334
+ />
335
+ )}
336
+ </For>
337
+ </Show>
338
+ </tbody>
339
+ </table>
340
+ </div>
341
+ </section>
342
+ )
343
+ }
@@ -0,0 +1,72 @@
1
+ .role-dropdown {
2
+ [data-slot="trigger"] {
3
+ border: 1px solid var(--color-border);
4
+ background-color: var(--color-bg);
5
+ width: 100%;
6
+ text-transform: capitalize;
7
+ padding: var(--space-2) var(--space-3);
8
+ border-radius: var(--border-radius-sm);
9
+ color: var(--color-text);
10
+ font-size: var(--font-size-sm);
11
+ line-height: 1.5;
12
+ min-width: 0;
13
+
14
+ &:hover {
15
+ border-color: var(--color-accent);
16
+ background-color: var(--color-bg);
17
+ }
18
+
19
+ &:focus {
20
+ outline: none;
21
+ border-color: var(--color-accent);
22
+ box-shadow: 0 0 0 3px var(--color-accent-alpha);
23
+ }
24
+ }
25
+
26
+ [data-slot="chevron"] {
27
+ opacity: 0.6;
28
+ }
29
+
30
+ [data-slot="dropdown"] {
31
+ padding: var(--space-1);
32
+ min-width: 280px;
33
+ width: max-content;
34
+ }
35
+
36
+ [data-slot="role-item"] {
37
+ display: block;
38
+ width: 100%;
39
+ padding: var(--space-2) var(--space-3);
40
+ border: none;
41
+ background-color: transparent;
42
+ color: var(--color-text);
43
+ font-size: var(--font-size-sm);
44
+ text-align: left;
45
+ cursor: pointer;
46
+ border-radius: var(--border-radius-sm);
47
+ transition: background-color 0.15s ease;
48
+
49
+ &:hover {
50
+ background-color: var(--color-bg-surface);
51
+ }
52
+
53
+ &[data-selected="true"] {
54
+ background-color: var(--color-accent-alpha);
55
+ }
56
+
57
+ div {
58
+ strong {
59
+ display: block;
60
+ color: var(--color-text);
61
+ margin-bottom: var(--space-1);
62
+ text-transform: capitalize;
63
+ }
64
+
65
+ p {
66
+ font-size: var(--font-size-xs);
67
+ color: var(--color-text-muted);
68
+ margin: 0;
69
+ }
70
+ }
71
+ }
72
+ }
@@ -0,0 +1,43 @@
1
+ import { createSignal } from "solid-js"
2
+ import { Dropdown } from "~/component/dropdown"
3
+ import "./role-dropdown.css"
4
+
5
+ interface RoleOption {
6
+ value: string
7
+ description: string
8
+ }
9
+
10
+ interface RoleDropdownProps {
11
+ value: string
12
+ options: RoleOption[]
13
+ onChange: (value: string) => void
14
+ }
15
+
16
+ export function RoleDropdown(props: RoleDropdownProps) {
17
+ const [open, setOpen] = createSignal(false)
18
+
19
+ const handleSelect = (value: string) => {
20
+ props.onChange(value)
21
+ setOpen(false)
22
+ }
23
+
24
+ return (
25
+ <Dropdown trigger={props.value} open={open()} onOpenChange={setOpen} class="role-dropdown">
26
+ <>
27
+ {props.options.map((option) => (
28
+ <button
29
+ data-slot="role-item"
30
+ data-selected={props.value === option.value}
31
+ type="button"
32
+ onClick={() => handleSelect(option.value)}
33
+ >
34
+ <div>
35
+ <strong>{option.value}</strong>
36
+ <p>{option.description}</p>
37
+ </div>
38
+ </button>
39
+ ))}
40
+ </>
41
+ </Dropdown>
42
+ )
43
+ }
@@ -0,0 +1,173 @@
1
+ [data-slot="models-list"] {
2
+ display: flex;
3
+ flex-direction: column;
4
+ }
5
+
6
+ [data-slot="models-table"] {
7
+ overflow-x: auto;
8
+ }
9
+
10
+ [data-slot="models-table-element"] {
11
+ width: 100%;
12
+ border-collapse: collapse;
13
+ font-size: var(--font-size-sm);
14
+
15
+ thead {
16
+ border-bottom: 1px solid var(--color-border);
17
+ }
18
+
19
+ th {
20
+ padding: var(--space-3) var(--space-4);
21
+ text-align: left;
22
+ font-weight: normal;
23
+ color: var(--color-text-muted);
24
+ text-transform: uppercase;
25
+ }
26
+
27
+ td {
28
+ padding: var(--space-3) var(--space-4);
29
+ border-bottom: 1px solid var(--color-border-muted);
30
+ color: var(--color-text-muted);
31
+ font-family: var(--font-mono);
32
+
33
+ &[data-slot="model-name"] {
34
+ color: var(--color-text);
35
+ font-family: var(--font-mono);
36
+ font-weight: 500;
37
+
38
+ div {
39
+ display: flex;
40
+ align-items: center;
41
+ gap: 8px;
42
+ }
43
+ }
44
+
45
+ &[data-slot="training-data"] {
46
+ text-align: center;
47
+ color: var(--color-text);
48
+ }
49
+
50
+ &[data-slot="model-toggle"] {
51
+ text-align: left;
52
+ font-family: var(--font-sans);
53
+ }
54
+
55
+ [data-slot="model-toggle-label"] {
56
+ /* Toggle container */
57
+ position: relative;
58
+ display: inline-block;
59
+ width: 2.5rem;
60
+ height: 1.5rem;
61
+ cursor: pointer;
62
+
63
+ /* Hidden checkbox input */
64
+ input {
65
+ opacity: 0;
66
+ width: 0;
67
+ height: 0;
68
+ }
69
+
70
+ /* Toggle track (background) */
71
+ span {
72
+ position: absolute;
73
+ inset: 0;
74
+ background-color: #ccc;
75
+ border: 1px solid #bbb;
76
+ border-radius: 1.5rem;
77
+ transition: all 0.3s ease;
78
+ cursor: pointer;
79
+
80
+ /* Toggle handle (slider) */
81
+ &::before {
82
+ content: "";
83
+ position: absolute;
84
+ top: 50%;
85
+ left: 0.125rem;
86
+ width: 1.25rem;
87
+ height: 1.25rem;
88
+ background-color: white;
89
+ border: 1px solid #ddd;
90
+ border-radius: 50%;
91
+ transform: translateY(-50%);
92
+ transition: all 0.3s ease;
93
+ }
94
+ }
95
+
96
+ /* Checked state - track */
97
+ input:checked + span {
98
+ background-color: #21ad0e;
99
+ border-color: #148605;
100
+
101
+ /* Checked state - handle */
102
+ &::before {
103
+ transform: translateX(1rem) translateY(-50%);
104
+ }
105
+ }
106
+
107
+ /* Hover states */
108
+ &:hover span {
109
+ box-shadow: 0 0 0 2px rgba(34, 197, 94, 0.2);
110
+ }
111
+
112
+ input:checked:hover + span {
113
+ box-shadow: 0 0 0 2px rgba(34, 197, 94, 0.3);
114
+ }
115
+
116
+ /* Disabled state */
117
+ &:has(input:disabled) {
118
+ cursor: not-allowed;
119
+ }
120
+
121
+ input:disabled + span {
122
+ opacity: 0.5;
123
+ cursor: not-allowed;
124
+ }
125
+
126
+ input:disabled:checked + span {
127
+ opacity: 0.5;
128
+ }
129
+
130
+ input:disabled ~ span:hover {
131
+ box-shadow: none;
132
+ }
133
+ }
134
+ }
135
+
136
+ tbody tr {
137
+ &:last-child td {
138
+ border-bottom: none;
139
+ }
140
+
141
+ &[data-disabled="true"] {
142
+ td[data-slot="model-name"] {
143
+ color: var(--color-text-muted);
144
+ }
145
+ }
146
+ }
147
+ }
148
+
149
+ @media (max-width: 40rem) {
150
+ [data-slot="models-table-element"] {
151
+ th,
152
+ td {
153
+ padding: var(--space-2) var(--space-3);
154
+ font-size: var(--font-size-xs);
155
+ }
156
+
157
+ th {
158
+ &:nth-child(2)
159
+
160
+ /* Training Data */ {
161
+ display: none;
162
+ }
163
+ }
164
+
165
+ td {
166
+ &:nth-child(2)
167
+
168
+ /* Training Data */ {
169
+ display: none;
170
+ }
171
+ }
172
+ }
173
+ }