@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,200 @@
1
+ import { Billing } from "@jonsoc/console-core/billing.js"
2
+ import { createAsync, query, useParams } from "@solidjs/router"
3
+ import { createMemo, For, Show, createEffect, createSignal } from "solid-js"
4
+ import { formatDateUTC, formatDateForTable } from "../common"
5
+ import { withActor } from "~/context/auth.withActor"
6
+ import { IconChevronLeft, IconChevronRight, IconBreakdown } from "~/component/icon"
7
+ import styles from "./usage-section.module.css"
8
+ import { createStore } from "solid-js/store"
9
+
10
+ const PAGE_SIZE = 50
11
+
12
+ async function getUsageInfo(workspaceID: string, page: number) {
13
+ "use server"
14
+ return withActor(async () => {
15
+ return await Billing.usages(page, PAGE_SIZE)
16
+ }, workspaceID)
17
+ }
18
+
19
+ const queryUsageInfo = query(getUsageInfo, "usage.list")
20
+
21
+ export function UsageSection() {
22
+ const params = useParams()
23
+ const usage = createAsync(() => queryUsageInfo(params.id!, 0))
24
+ const [store, setStore] = createStore({ page: 0, usage: [] as Awaited<ReturnType<typeof getUsageInfo>> })
25
+ const [openBreakdownId, setOpenBreakdownId] = createSignal<string | null>(null)
26
+
27
+ createEffect(() => {
28
+ setStore({ usage: usage() })
29
+ }, [usage])
30
+
31
+ createEffect(() => {
32
+ if (!openBreakdownId()) return
33
+
34
+ const handleClickOutside = (e: MouseEvent) => {
35
+ const target = e.target as HTMLElement
36
+ if (!target.closest('[data-slot="tokens-with-breakdown"]')) {
37
+ setOpenBreakdownId(null)
38
+ }
39
+ }
40
+
41
+ document.addEventListener("click", handleClickOutside)
42
+ return () => document.removeEventListener("click", handleClickOutside)
43
+ })
44
+
45
+ const hasResults = createMemo(() => store.usage && store.usage.length > 0)
46
+ const canGoPrev = createMemo(() => store.page > 0)
47
+ const canGoNext = createMemo(() => store.usage && store.usage.length === PAGE_SIZE)
48
+
49
+ const calculateTotalInputTokens = (u: Awaited<ReturnType<typeof getUsageInfo>>[0]) => {
50
+ return u.inputTokens + (u.cacheReadTokens ?? 0) + (u.cacheWrite5mTokens ?? 0) + (u.cacheWrite1hTokens ?? 0)
51
+ }
52
+
53
+ const calculateTotalOutputTokens = (u: Awaited<ReturnType<typeof getUsageInfo>>[0]) => {
54
+ return u.outputTokens + (u.reasoningTokens ?? 0)
55
+ }
56
+
57
+ const goPrev = async () => {
58
+ const usage = await getUsageInfo(params.id!, store.page - 1)
59
+ setStore({
60
+ page: store.page - 1,
61
+ usage,
62
+ })
63
+ }
64
+ const goNext = async () => {
65
+ const usage = await getUsageInfo(params.id!, store.page + 1)
66
+ setStore({
67
+ page: store.page + 1,
68
+ usage,
69
+ })
70
+ }
71
+
72
+ return (
73
+ <section class={styles.root}>
74
+ <div data-slot="section-title">
75
+ <h2>Usage History</h2>
76
+ <p>Recent API usage and costs.</p>
77
+ </div>
78
+ <div data-slot="usage-table">
79
+ <Show
80
+ when={hasResults()}
81
+ fallback={
82
+ <div data-component="empty-state">
83
+ <p>Make your first API call to get started.</p>
84
+ </div>
85
+ }
86
+ >
87
+ <table data-slot="usage-table-element">
88
+ <thead>
89
+ <tr>
90
+ <th>Date</th>
91
+ <th>Model</th>
92
+ <th>Input</th>
93
+ <th>Output</th>
94
+ <th>Cost</th>
95
+ </tr>
96
+ </thead>
97
+ <tbody>
98
+ <For each={store.usage}>
99
+ {(usage, index) => {
100
+ const date = createMemo(() => new Date(usage.timeCreated))
101
+ const totalInputTokens = createMemo(() => calculateTotalInputTokens(usage))
102
+ const totalOutputTokens = createMemo(() => calculateTotalOutputTokens(usage))
103
+ const inputBreakdownId = `input-breakdown-${index()}`
104
+ const outputBreakdownId = `output-breakdown-${index()}`
105
+ const isInputOpen = createMemo(() => openBreakdownId() === inputBreakdownId)
106
+ const isOutputOpen = createMemo(() => openBreakdownId() === outputBreakdownId)
107
+ const isClaude = usage.model.toLowerCase().includes("claude")
108
+ return (
109
+ <tr>
110
+ <td data-slot="usage-date" title={formatDateUTC(date())}>
111
+ {formatDateForTable(date())}
112
+ </td>
113
+ <td data-slot="usage-model">{usage.model}</td>
114
+ <td data-slot="usage-tokens">
115
+ <div data-slot="tokens-with-breakdown" onClick={(e) => e.stopPropagation()}>
116
+ <button
117
+ data-slot="breakdown-button"
118
+ onClick={(e) => {
119
+ e.stopPropagation()
120
+ setOpenBreakdownId(isInputOpen() ? null : inputBreakdownId)
121
+ }}
122
+ >
123
+ <IconBreakdown />
124
+ </button>
125
+ <span onClick={() => setOpenBreakdownId(null)}>{totalInputTokens()}</span>
126
+ <Show when={isInputOpen()}>
127
+ <div data-slot="breakdown-popup" onClick={(e) => e.stopPropagation()}>
128
+ <div data-slot="breakdown-row">
129
+ <span data-slot="breakdown-label">Input</span>
130
+ <span data-slot="breakdown-value">{usage.inputTokens}</span>
131
+ </div>
132
+ <div data-slot="breakdown-row">
133
+ <span data-slot="breakdown-label">Cache Read</span>
134
+ <span data-slot="breakdown-value">{usage.cacheReadTokens ?? 0}</span>
135
+ </div>
136
+ <Show when={isClaude}>
137
+ <div data-slot="breakdown-row">
138
+ <span data-slot="breakdown-label">Cache Write</span>
139
+ <span data-slot="breakdown-value">{usage.cacheWrite5mTokens ?? 0}</span>
140
+ </div>
141
+ </Show>
142
+ </div>
143
+ </Show>
144
+ </div>
145
+ </td>
146
+ <td data-slot="usage-tokens">
147
+ <div data-slot="tokens-with-breakdown" onClick={(e) => e.stopPropagation()}>
148
+ <button
149
+ data-slot="breakdown-button"
150
+ onClick={(e) => {
151
+ e.stopPropagation()
152
+ setOpenBreakdownId(isOutputOpen() ? null : outputBreakdownId)
153
+ }}
154
+ >
155
+ <IconBreakdown />
156
+ </button>
157
+ <span onClick={() => setOpenBreakdownId(null)}>{totalOutputTokens()}</span>
158
+ <Show when={isOutputOpen()}>
159
+ <div data-slot="breakdown-popup" onClick={(e) => e.stopPropagation()}>
160
+ <div data-slot="breakdown-row">
161
+ <span data-slot="breakdown-label">Output</span>
162
+ <span data-slot="breakdown-value">{usage.outputTokens}</span>
163
+ </div>
164
+ <div data-slot="breakdown-row">
165
+ <span data-slot="breakdown-label">Reasoning</span>
166
+ <span data-slot="breakdown-value">{usage.reasoningTokens ?? 0}</span>
167
+ </div>
168
+ </div>
169
+ </Show>
170
+ </div>
171
+ </td>
172
+ <td data-slot="usage-cost">
173
+ <Show
174
+ when={usage.enrichment?.plan === "sub"}
175
+ fallback={<>${((usage.cost ?? 0) / 100000000).toFixed(4)}</>}
176
+ >
177
+ subscription (${((usage.cost ?? 0) / 100000000).toFixed(4)})
178
+ </Show>
179
+ </td>
180
+ </tr>
181
+ )
182
+ }}
183
+ </For>
184
+ </tbody>
185
+ </table>
186
+ <Show when={canGoPrev() || canGoNext()}>
187
+ <div data-slot="pagination">
188
+ <button disabled={!canGoPrev()} onClick={goPrev}>
189
+ <IconChevronLeft />
190
+ </button>
191
+ <button disabled={!canGoNext()} onClick={goNext}>
192
+ <IconChevronRight />
193
+ </button>
194
+ </div>
195
+ </Show>
196
+ </Show>
197
+ </div>
198
+ </section>
199
+ )
200
+ }
@@ -0,0 +1,308 @@
1
+ [data-page="workspace"] {
2
+ line-height: 1;
3
+ }
4
+
5
+ /* Workspace Layout */
6
+ [data-component="workspace-container"] {
7
+ display: flex;
8
+ height: calc(100vh - 73px);
9
+ }
10
+
11
+ [data-component="workspace-nav"] {
12
+ width: 240px;
13
+ flex-shrink: 0;
14
+ padding: var(--space-6) var(--space-4);
15
+ display: flex;
16
+ justify-content: flex-end;
17
+ }
18
+
19
+ /* Desktop Navigation */
20
+ [data-component="nav-desktop"] {
21
+ display: block;
22
+
23
+ @media (max-width: 48rem) {
24
+ display: none;
25
+ }
26
+
27
+ [data-component="workspace-nav-items"] {
28
+ display: flex;
29
+ flex-direction: column;
30
+ gap: var(--space-2);
31
+
32
+ [data-nav-button] {
33
+ padding: var(--space-3) var(--space-4);
34
+ border-radius: var(--border-radius-sm);
35
+ color: var(--color-text-muted);
36
+ text-decoration: none;
37
+ font-size: var(--font-size-sm);
38
+ font-weight: 500;
39
+ transition: all 0.15s ease;
40
+
41
+ &:hover {
42
+ color: var(--color-text);
43
+ }
44
+
45
+ &.active {
46
+ color: var(--color-text);
47
+ font-weight: 700;
48
+ position: relative;
49
+
50
+ &::before {
51
+ content: "";
52
+ position: absolute;
53
+ left: calc(-1 * var(--space-0-5));
54
+ top: 0;
55
+ bottom: 0;
56
+ width: 2px;
57
+ background-color: var(--color-text);
58
+ border-radius: 0 2px 2px 0;
59
+ }
60
+ }
61
+ }
62
+ }
63
+ }
64
+
65
+ /* Mobile Navigation */
66
+ [data-component="nav-mobile"] {
67
+ display: none;
68
+
69
+ @media (max-width: 48rem) {
70
+ display: flex;
71
+ align-items: stretch;
72
+ justify-content: flex-start;
73
+ width: 100%;
74
+ overflow-x: auto;
75
+ overflow-y: hidden;
76
+ scrollbar-width: none;
77
+ -ms-overflow-style: none;
78
+
79
+ &::-webkit-scrollbar {
80
+ display: none;
81
+ }
82
+ }
83
+
84
+ [data-component="workspace-nav-items"] {
85
+ display: flex;
86
+ flex-direction: row;
87
+ align-items: center;
88
+ gap: var(--space-1);
89
+ min-width: max-content;
90
+ height: 100%;
91
+
92
+ [data-nav-button] {
93
+ padding: var(--space-2) var(--space-3);
94
+ padding-bottom: calc(var(--space-2) + 4px);
95
+ border-radius: var(--border-radius-sm);
96
+ color: var(--color-text-muted);
97
+ text-decoration: none;
98
+ font-size: var(--font-size-sm);
99
+ font-weight: 500;
100
+ transition: all 0.15s ease;
101
+ white-space: nowrap;
102
+ flex-shrink: 0;
103
+ position: relative;
104
+
105
+ &:hover {
106
+ color: var(--color-text);
107
+ }
108
+
109
+ &.active {
110
+ color: var(--color-text);
111
+ font-weight: 700;
112
+
113
+ &::after {
114
+ content: "";
115
+ position: absolute;
116
+ bottom: 0;
117
+ left: 0;
118
+ right: 0;
119
+ height: 2px;
120
+ background-color: var(--color-text);
121
+ border-radius: 2px 2px 0 0;
122
+ }
123
+ }
124
+ }
125
+ }
126
+ }
127
+
128
+ [data-component="workspace-content"] {
129
+ flex: 1;
130
+ padding: var(--space-6) var(--space-8);
131
+ overflow-y: auto;
132
+
133
+ @media (max-width: 48rem) {
134
+ padding: var(--space-6) var(--space-4);
135
+ }
136
+ }
137
+
138
+ [data-page="workspace-[id]"] {
139
+ max-width: 64rem;
140
+ padding: var(--space-2) var(--space-4);
141
+ margin: 0;
142
+ width: 100%;
143
+ display: flex;
144
+ flex-direction: column;
145
+ gap: var(--space-10);
146
+
147
+ @media (max-width: 30rem) {
148
+ padding-top: var(--space-4);
149
+ padding-bottom: var(--space-4);
150
+
151
+ gap: var(--space-8);
152
+ }
153
+
154
+ [data-slot="sections"] {
155
+ display: flex;
156
+ flex-direction: column;
157
+ gap: var(--space-16);
158
+
159
+ @media (max-width: 30rem) {
160
+ gap: var(--space-8);
161
+ }
162
+
163
+ section {
164
+ display: flex;
165
+ flex-direction: column;
166
+ gap: var(--space-8);
167
+
168
+ @media (max-width: 30rem) {
169
+ gap: var(--space-6);
170
+ }
171
+
172
+ [data-slot="section-title"] {
173
+ display: flex;
174
+ flex-direction: column;
175
+ gap: var(--space-1);
176
+
177
+ h2 {
178
+ font-size: var(--font-size-md);
179
+ font-weight: 600;
180
+ line-height: 1.2;
181
+ letter-spacing: -0.03125rem;
182
+ margin: 0;
183
+ color: var(--color-text);
184
+
185
+ @media (max-width: 30rem) {
186
+ font-size: var(--font-size-md);
187
+ }
188
+ }
189
+
190
+ p {
191
+ line-height: 1.5;
192
+ font-size: var(--font-size-md);
193
+ color: var(--color-text-muted);
194
+
195
+ a {
196
+ color: var(--color-text-muted);
197
+ }
198
+
199
+ @media (max-width: 30rem) {
200
+ font-size: var(--font-size-sm);
201
+ }
202
+ }
203
+ }
204
+
205
+ [data-slot="section-content"] {
206
+ display: flex;
207
+ flex-direction: column;
208
+ gap: var(--space-3);
209
+ margin-top: var(--space-8);
210
+ }
211
+ }
212
+
213
+ section:not(:last-child) {
214
+ border-bottom: 1px solid var(--color-border);
215
+ padding-bottom: var(--space-16);
216
+
217
+ @media (max-width: 30rem) {
218
+ padding-bottom: var(--space-8);
219
+ }
220
+ }
221
+ }
222
+
223
+ /* Title section */
224
+ [data-component="header-section"] {
225
+ display: flex;
226
+ flex-direction: column;
227
+ gap: var(--space-2);
228
+ padding-bottom: var(--space-8);
229
+ border-bottom: 1px solid var(--color-border);
230
+
231
+ @media (max-width: 30rem) {
232
+ padding-bottom: var(--space-6);
233
+ }
234
+
235
+ h1 {
236
+ font-size: var(--font-size-2xl);
237
+ font-weight: 500;
238
+ line-height: 1.2;
239
+ letter-spacing: -0.03125rem;
240
+ margin: 0;
241
+ text-transform: uppercase;
242
+
243
+ @media (max-width: 30rem) {
244
+ font-size: var(--font-size-xl);
245
+ }
246
+ }
247
+
248
+ p {
249
+ line-height: 1.5;
250
+ font-size: var(--font-size-md);
251
+ color: var(--color-text);
252
+ display: flex;
253
+ align-items: center;
254
+ justify-content: space-between;
255
+ gap: var(--space-4);
256
+
257
+ @media (max-width: 48rem) {
258
+ flex-direction: column;
259
+ align-items: flex-start;
260
+ gap: var(--space-3);
261
+ }
262
+
263
+ a {
264
+ color: var(--color-text-muted);
265
+ }
266
+
267
+ [data-slot="billing-info"] {
268
+ flex-shrink: 0;
269
+ margin-left: auto;
270
+ }
271
+
272
+ [data-slot="balance"] {
273
+ font-size: var(--font-size-sm);
274
+ color: var(--color-text-muted);
275
+
276
+ b {
277
+ font-weight: 600;
278
+ color: var(--color-text);
279
+ }
280
+ }
281
+ }
282
+ }
283
+ }
284
+
285
+ @media (max-width: 48rem) {
286
+ [data-component="workspace-container"] {
287
+ flex-direction: column;
288
+ }
289
+
290
+ [data-component="workspace-nav"] {
291
+ width: 100%;
292
+ flex-direction: row;
293
+ border-right: none;
294
+ border-bottom: 1px solid var(--color-border);
295
+ padding: var(--space-4);
296
+ justify-content: flex-start;
297
+ overflow-x: auto;
298
+ overflow-y: hidden;
299
+ flex-shrink: 0;
300
+ min-height: fit-content;
301
+ scrollbar-width: none;
302
+ -ms-overflow-style: none;
303
+
304
+ &::-webkit-scrollbar {
305
+ display: none;
306
+ }
307
+ }
308
+ }
@@ -0,0 +1,62 @@
1
+ import { Show } from "solid-js"
2
+ import { createAsync, RouteSectionProps, useParams, A } from "@solidjs/router"
3
+ import { querySessionInfo } from "./common"
4
+ import "./[id].css"
5
+
6
+ export default function WorkspaceLayout(props: RouteSectionProps) {
7
+ const params = useParams()
8
+ const userInfo = createAsync(() => querySessionInfo(params.id!))
9
+
10
+ return (
11
+ <main data-page="workspace">
12
+ <div data-component="workspace-container">
13
+ <nav data-component="workspace-nav">
14
+ <nav data-component="nav-desktop">
15
+ <div data-component="workspace-nav-items">
16
+ <A href={`/workspace/${params.id}`} end activeClass="active" data-nav-button>
17
+ Zen
18
+ </A>
19
+ <A href={`/workspace/${params.id}/keys`} activeClass="active" data-nav-button>
20
+ API Keys
21
+ </A>
22
+ <A href={`/workspace/${params.id}/members`} activeClass="active" data-nav-button>
23
+ Members
24
+ </A>
25
+ <Show when={userInfo()?.isAdmin}>
26
+ <A href={`/workspace/${params.id}/billing`} activeClass="active" data-nav-button>
27
+ Billing
28
+ </A>
29
+ <A href={`/workspace/${params.id}/settings`} activeClass="active" data-nav-button>
30
+ Settings
31
+ </A>
32
+ </Show>
33
+ </div>
34
+ </nav>
35
+
36
+ <nav data-component="nav-mobile">
37
+ <div data-component="workspace-nav-items">
38
+ <A href={`/workspace/${params.id}`} end activeClass="active" data-nav-button>
39
+ Zen
40
+ </A>
41
+ <A href={`/workspace/${params.id}/keys`} activeClass="active" data-nav-button>
42
+ API Keys
43
+ </A>
44
+ <A href={`/workspace/${params.id}/members`} activeClass="active" data-nav-button>
45
+ Members
46
+ </A>
47
+ <Show when={userInfo()?.isAdmin}>
48
+ <A href={`/workspace/${params.id}/billing`} activeClass="active" data-nav-button>
49
+ Billing
50
+ </A>
51
+ <A href={`/workspace/${params.id}/settings`} activeClass="active" data-nav-button>
52
+ Settings
53
+ </A>
54
+ </Show>
55
+ </div>
56
+ </nav>
57
+ </nav>
58
+ <div data-component="workspace-content">{props.children}</div>
59
+ </div>
60
+ </main>
61
+ )
62
+ }
@@ -0,0 +1,120 @@
1
+ import { Resource } from "@jonsoc/console-resource"
2
+ import { Actor } from "@jonsoc/console-core/actor.js"
3
+ import { action, json, query } from "@solidjs/router"
4
+ import { withActor } from "~/context/auth.withActor"
5
+ import { Billing } from "@jonsoc/console-core/billing.js"
6
+ import { and, Database, desc, eq, isNull } from "@jonsoc/console-core/drizzle/index.js"
7
+ import { WorkspaceTable } from "@jonsoc/console-core/schema/workspace.sql.js"
8
+ import { UserTable } from "@jonsoc/console-core/schema/user.sql.js"
9
+
10
+ export function formatDateForTable(date: Date) {
11
+ const options: Intl.DateTimeFormatOptions = {
12
+ day: "numeric",
13
+ month: "short",
14
+ hour: "numeric",
15
+ minute: "2-digit",
16
+ hour12: true,
17
+ }
18
+ return date.toLocaleDateString(undefined, options).replace(",", ",")
19
+ }
20
+
21
+ export function formatDateUTC(date: Date) {
22
+ const options: Intl.DateTimeFormatOptions = {
23
+ weekday: "short",
24
+ year: "numeric",
25
+ month: "short",
26
+ day: "numeric",
27
+ hour: "numeric",
28
+ minute: "2-digit",
29
+ second: "2-digit",
30
+ timeZoneName: "short",
31
+ timeZone: "UTC",
32
+ }
33
+ return date.toLocaleDateString("en-US", options)
34
+ }
35
+
36
+ export function formatBalance(amount: number) {
37
+ const balance = ((amount ?? 0) / 100000000).toFixed(2)
38
+ return balance === "-0.00" ? "0.00" : balance
39
+ }
40
+
41
+ export async function getLastSeenWorkspaceID() {
42
+ "use server"
43
+ return withActor(async () => {
44
+ const actor = Actor.assert("account")
45
+ return Database.use(async (tx) =>
46
+ tx
47
+ .select({ id: WorkspaceTable.id })
48
+ .from(UserTable)
49
+ .innerJoin(WorkspaceTable, eq(UserTable.workspaceID, WorkspaceTable.id))
50
+ .where(
51
+ and(
52
+ eq(UserTable.accountID, actor.properties.accountID),
53
+ isNull(UserTable.timeDeleted),
54
+ isNull(WorkspaceTable.timeDeleted),
55
+ ),
56
+ )
57
+ .orderBy(desc(UserTable.timeSeen))
58
+ .limit(1)
59
+ .then((x) => x[0]?.id),
60
+ )
61
+ })
62
+ }
63
+
64
+ export const querySessionInfo = query(async (workspaceID: string) => {
65
+ "use server"
66
+ return withActor(() => {
67
+ return {
68
+ isAdmin: Actor.userRole() === "admin",
69
+ isBeta: Resource.App.stage === "production" ? workspaceID === "wrk_01K46JDFR0E75SG2Q8K172KF3Y" : true,
70
+ }
71
+ }, workspaceID)
72
+ }, "session.get")
73
+
74
+ export const createCheckoutUrl = action(
75
+ async (workspaceID: string, amount: number, successUrl: string, cancelUrl: string) => {
76
+ "use server"
77
+ return json(
78
+ await withActor(
79
+ () =>
80
+ Billing.generateCheckoutUrl({ amount, successUrl, cancelUrl })
81
+ .then((data) => ({ error: undefined, data }))
82
+ .catch((e) => ({
83
+ error: e.message as string,
84
+ data: undefined,
85
+ })),
86
+ workspaceID,
87
+ ),
88
+ )
89
+ },
90
+ "checkoutUrl",
91
+ )
92
+
93
+ export const queryBillingInfo = query(async (workspaceID: string) => {
94
+ "use server"
95
+ return withActor(async () => {
96
+ const billing = await Billing.get()
97
+ return {
98
+ customerID: billing.customerID,
99
+ paymentMethodID: billing.paymentMethodID,
100
+ paymentMethodType: billing.paymentMethodType,
101
+ paymentMethodLast4: billing.paymentMethodLast4,
102
+ balance: billing.balance,
103
+ reload: billing.reload,
104
+ reloadAmount: billing.reloadAmount ?? Billing.RELOAD_AMOUNT,
105
+ reloadAmountMin: Billing.RELOAD_AMOUNT_MIN,
106
+ reloadTrigger: billing.reloadTrigger ?? Billing.RELOAD_TRIGGER,
107
+ reloadTriggerMin: Billing.RELOAD_TRIGGER_MIN,
108
+ monthlyLimit: billing.monthlyLimit,
109
+ monthlyUsage: billing.monthlyUsage,
110
+ timeMonthlyUsageUpdated: billing.timeMonthlyUsageUpdated,
111
+ reloadError: billing.reloadError,
112
+ timeReloadError: billing.timeReloadError,
113
+ subscription: billing.subscription,
114
+ subscriptionID: billing.subscriptionID,
115
+ subscriptionPlan: billing.subscriptionPlan,
116
+ timeSubscriptionBooked: billing.timeSubscriptionBooked,
117
+ timeSubscriptionSelected: billing.timeSubscriptionSelected,
118
+ }
119
+ }, workspaceID)
120
+ }, "billing.get")