@hachej/boring-core 0.1.40 → 0.1.42
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.
- package/README.md +28 -279
- package/dist/app/front/index.js +1 -1
- package/dist/app/server/index.js +1 -1
- package/dist/{chunk-2JDK4XUZ.js → chunk-GZVKZD4P.js} +2 -2
- package/dist/{chunk-5R3U6QKD.js → chunk-MLTJKZL4.js} +49 -17
- package/dist/front/index.js +1 -1
- package/dist/server/index.js +1 -1
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -7,314 +7,63 @@
|
|
|
7
7
|
|
|
8
8
|
</div>
|
|
9
9
|
|
|
10
|
-
The foundation package for boring-ui apps: Postgres/Drizzle database
|
|
10
|
+
The foundation package for boring-ui v2 apps: Postgres/Drizzle database, better-auth
|
|
11
|
+
(email/password, verification, password reset, magic links, optional Google), TOML+env
|
|
12
|
+
config loader, Fastify HTTP app factory, and a React frontend shell with auth/workspace
|
|
13
|
+
gating. Every child app imports core first; domain logic, agent runtime, and workspace UI
|
|
14
|
+
come from the sibling `@hachej/boring-*` packages.
|
|
11
15
|
|
|
12
16
|
```bash
|
|
13
17
|
pnpm add @hachej/boring-core
|
|
14
18
|
```
|
|
15
19
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
## TL;DR
|
|
19
|
-
|
|
20
|
-
**The Problem**: Building a multi-user agent-powered app means re-implementing auth, sessions, workspaces, invites, email flows, and an app shell every single time. These are the same across every deployment.
|
|
21
|
-
|
|
22
|
-
**The Solution**: `@hachej/boring-core` provides a complete app skeleton — Postgres DB, better-auth with email verification + password reset + magic links, workspace membership with roles, email transport (Resend/SMTP/console), and a `<BoringApp>` React shell with auth pages. You bring the domain logic.
|
|
23
|
-
|
|
24
|
-
### Why Use @hachej/boring-core?
|
|
25
|
-
|
|
26
|
-
| Feature | What It Does |
|
|
27
|
-
|---------|--------------|
|
|
28
|
-
| **Full auth suite** | Email/password + email verification + password reset + magic links (better-auth) |
|
|
29
|
-
| **Workspace management** | Create, update, delete workspaces; member roles (owner/editor/viewer); invites |
|
|
30
|
-
| **Fastify app factory** | Pre-wired with helmet, CORS, rate limiting, secret redaction, graceful shutdown |
|
|
31
|
-
| **Drizzle + Postgres** | Ready-to-run schema for users, workspaces, members, invites, settings |
|
|
32
|
-
| **Email transport** | Resend (default), SMTP, or console — pluggable via URL scheme |
|
|
33
|
-
| **<BoringApp> shell** | Client-rendered React shell with auth gate, theme toggle, workspace switcher |
|
|
34
|
-
| **Config loader** | TOML + env vars merged, Zod-validated, redacted for frontend |
|
|
20
|
+
## Usage essentials
|
|
35
21
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
## Quick Example
|
|
22
|
+
Most apps use the composed `app/*` surfaces, which fuse core + workspace + agent:
|
|
39
23
|
|
|
40
24
|
```ts
|
|
41
|
-
//
|
|
42
|
-
import {
|
|
43
|
-
|
|
44
|
-
const config = await loadConfig()
|
|
45
|
-
const app = await createCoreApp(config) // Fastify + DB + auth + routes
|
|
25
|
+
// server entry
|
|
26
|
+
import { createCoreWorkspaceAgentServer } from "@hachej/boring-core/app/server"
|
|
46
27
|
|
|
28
|
+
const app = await createCoreWorkspaceAgentServer({ plugins })
|
|
47
29
|
await app.listen({ port: 3000 })
|
|
48
30
|
```
|
|
49
31
|
|
|
50
32
|
```tsx
|
|
51
|
-
//
|
|
52
|
-
<BoringApp>
|
|
53
|
-
<Route path="/workspace/:id" element={<WorkspaceRoute />} />
|
|
54
|
-
<Route path="/settings" element={<Settings />} />
|
|
55
|
-
</BoringApp>
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
```tsx
|
|
59
|
-
// In your components — typed auth + workspace access
|
|
60
|
-
const user = useUser()
|
|
61
|
-
const workspace = useCurrentWorkspace()
|
|
62
|
-
const role = useWorkspaceRole() // 'owner' | 'editor' | 'viewer'
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
---
|
|
66
|
-
|
|
67
|
-
## Design Philosophy
|
|
68
|
-
|
|
69
|
-
1. **Core owns persistence and identity** — DB tables, auth, sessions, workspaces, invites. Everything else injects stores via interfaces.
|
|
70
|
-
2. **One config source** — `boring.app.toml` + environment variables merged, Zod-validated at boot. No scattered config.
|
|
71
|
-
3. **Email flows are real, not stubs** — password reset, email verification, magic links, workspace invites — all shipped with React Email templates.
|
|
72
|
-
4. **Swap seams, not rewrites** — `AuthProvider`, `UserStore`, `WorkspaceStore` are interfaces. The default impl is Postgres; swap via `createCoreApp({ authProvider })`.
|
|
73
|
-
5. **Fail closed on auth** — config fetch failure throws a `ConfigFetchError` with retries. Users see "Cannot reach server" not a blank page.
|
|
74
|
-
|
|
75
|
-
---
|
|
76
|
-
|
|
77
|
-
## Installation
|
|
78
|
-
|
|
79
|
-
```bash
|
|
80
|
-
# pnpm
|
|
81
|
-
pnpm add @hachej/boring-core @hachej/boring-workspace
|
|
82
|
-
|
|
83
|
-
# npm
|
|
84
|
-
npm install @hachej/boring-core @hachej/boring-workspace
|
|
85
|
-
|
|
86
|
-
# from source
|
|
87
|
-
git clone https://github.com/hachej/boring-ui.git
|
|
88
|
-
cd boring-ui && pnpm install
|
|
89
|
-
pnpm --filter @hachej/boring-core build
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
### Dependencies
|
|
93
|
-
|
|
94
|
-
Postgres is required for production. For dev, set `CORE_STORES=local` and core runs in-memory (state resets on restart).
|
|
95
|
-
|
|
96
|
-
---
|
|
97
|
-
|
|
98
|
-
## Quick Start
|
|
99
|
-
|
|
100
|
-
### 1. Set Environment
|
|
101
|
-
|
|
102
|
-
```bash
|
|
103
|
-
# .env
|
|
104
|
-
DATABASE_URL=postgres://user:pass@localhost:5432/myapp
|
|
105
|
-
BETTER_AUTH_SECRET=<32-byte random hex>
|
|
106
|
-
BETTER_AUTH_URL=http://localhost:3000
|
|
107
|
-
WORKSPACE_SETTINGS_ENCRYPTION_KEY=<32-byte hex>
|
|
108
|
-
MAIL_FROM=noreply@myapp.dev
|
|
109
|
-
MAIL_TRANSPORT_URL=resend://re_xxxxxxxxxxxxxxxx
|
|
110
|
-
```
|
|
111
|
-
|
|
112
|
-
### 2. Create Config File
|
|
113
|
-
|
|
114
|
-
```toml
|
|
115
|
-
# boring.app.toml
|
|
116
|
-
[app]
|
|
117
|
-
id = "my-app"
|
|
118
|
-
|
|
119
|
-
[frontend.branding]
|
|
120
|
-
name = "My App"
|
|
121
|
-
logo = "/logo.svg"
|
|
122
|
-
|
|
123
|
-
[features]
|
|
124
|
-
invites_enabled = true
|
|
125
|
-
invite_ttl_days = 7
|
|
126
|
-
```
|
|
127
|
-
|
|
128
|
-
### 3. Run Migrations
|
|
129
|
-
|
|
130
|
-
```bash
|
|
131
|
-
pnpm drizzle-kit generate --config node_modules/@hachej/boring-core/drizzle.config.ts
|
|
132
|
-
pnpm drizzle-kit migrate --config node_modules/@hachej/boring-core/drizzle.config.ts
|
|
133
|
-
```
|
|
134
|
-
|
|
135
|
-
### 4. Server Entry
|
|
136
|
-
|
|
137
|
-
```ts
|
|
138
|
-
import { createCoreApp, loadConfig } from "@hachej/boring-core/server"
|
|
139
|
-
|
|
140
|
-
const config = await loadConfig()
|
|
141
|
-
const app = await createCoreApp(config)
|
|
142
|
-
|
|
143
|
-
// add child-app routes
|
|
144
|
-
app.get("/api/v1/my-thing", async () => ({ ok: true }))
|
|
145
|
-
|
|
146
|
-
await app.listen({ port: config.port })
|
|
147
|
-
```
|
|
148
|
-
|
|
149
|
-
### 5. Frontend Entry
|
|
150
|
-
|
|
151
|
-
```tsx
|
|
33
|
+
// frontend entry
|
|
152
34
|
import { createRoot } from "react-dom/client"
|
|
153
|
-
import {
|
|
154
|
-
import
|
|
155
|
-
import "@hachej/boring-core/theme.css"
|
|
35
|
+
import { CoreWorkspaceAgentFront } from "@hachej/boring-core/app/front"
|
|
36
|
+
import "@hachej/boring-core/app/front/styles.css"
|
|
156
37
|
|
|
157
38
|
createRoot(document.getElementById("root")!).render(
|
|
158
|
-
<
|
|
159
|
-
<Route path="/" element={<Dashboard />} />
|
|
160
|
-
</BoringApp>,
|
|
39
|
+
<CoreWorkspaceAgentFront apiBaseUrl="" chatEntryMode="chat-first" plugins={plugins} />,
|
|
161
40
|
)
|
|
162
41
|
```
|
|
163
42
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
## Package Surfaces
|
|
43
|
+
For a core-only app (no agent/workspace), use `createCoreApp(config)` from
|
|
44
|
+
`@hachej/boring-core/server` and `CoreFront` from `@hachej/boring-core/front`.
|
|
167
45
|
|
|
168
|
-
|
|
169
|
-
|--------|-------------|--------------|
|
|
170
|
-
| `@hachej/boring-core/server` | Node | `createCoreApp`, `loadConfig`, auth, stores, routes |
|
|
171
|
-
| `@hachej/boring-core/server/db` | Node | Drizzle schema, migrations, store interfaces |
|
|
172
|
-
| `@hachej/boring-core/front` | Browser | `<BoringApp>`, hooks, auth pages, components |
|
|
173
|
-
| `@hachej/boring-core/shared` | Any | `User`, `Workspace`, `HttpError`, `ErrorCode` types |
|
|
174
|
-
| `@hachej/boring-core/theme.css` | Browser | CSS theme tokens for the frontend shell |
|
|
175
|
-
| `@hachej/boring-core/app/front` | Browser | App composition helpers (`WorkspaceAgentFront`, etc.) |
|
|
176
|
-
| `@hachej/boring-core/app/server` | Node | App composition helpers (`createWorkspaceAgentApp`) |
|
|
46
|
+
### Required environment (production)
|
|
177
47
|
|
|
178
|
-
|
|
48
|
+
`DATABASE_URL`, `BETTER_AUTH_SECRET`, `BETTER_AUTH_URL`,
|
|
49
|
+
`WORKSPACE_SETTINGS_ENCRYPTION_KEY`, `MAIL_FROM`, `MAIL_TRANSPORT_URL`
|
|
50
|
+
(`resend://…`, `smtp://…`, or `console://`), `CORS_ORIGINS`. Config is also read from
|
|
51
|
+
`boring.app.toml`. For dev without Postgres, set `CORE_STORES=local` (in-memory, resets
|
|
52
|
+
on restart; not supported by `createCoreWorkspaceAgentServer`).
|
|
179
53
|
|
|
180
|
-
|
|
54
|
+
Migrations live in `drizzle/`; run them with `drizzle-kit migrate` against
|
|
55
|
+
`drizzle.config.ts`.
|
|
181
56
|
|
|
182
|
-
|
|
57
|
+
## Documentation
|
|
183
58
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
| `BETTER_AUTH_SECRET` | Yes | 32-byte hex — signs session cookies |
|
|
188
|
-
| `BETTER_AUTH_URL` | Yes | Public URL for OAuth callbacks |
|
|
189
|
-
| `WORKSPACE_SETTINGS_ENCRYPTION_KEY` | Yes (prod) | 32-byte hex — encrypts workspace settings |
|
|
190
|
-
| `MAIL_FROM` | Yes (prod) | Sender address for auth emails |
|
|
191
|
-
| `MAIL_TRANSPORT_URL` | Yes (prod) | `resend://key`, `smtp://host`, or `console://` |
|
|
192
|
-
| `CORE_STORES` | No | `postgres` (default) or `local` (in-memory dev) |
|
|
193
|
-
| `CORS_ORIGINS` | Yes (prod) | Comma-separated allowlist |
|
|
194
|
-
| `SEND_WELCOME_EMAIL` | No | Default `true` — suppress with `false` |
|
|
195
|
-
| `SESSION_TTL_SECONDS` | No | Default 2,592,000 (30 days) |
|
|
196
|
-
|
|
197
|
-
---
|
|
198
|
-
|
|
199
|
-
## Architecture
|
|
200
|
-
|
|
201
|
-
```
|
|
202
|
-
┌──────────────────────┐
|
|
203
|
-
│ Browser Client │
|
|
204
|
-
│ /auth/* + /me + │
|
|
205
|
-
│ /workspaces/* │
|
|
206
|
-
└──────────┬───────────┘
|
|
207
|
-
│ HTTP (typed, cookie auth)
|
|
208
|
-
┌──────────▼───────────┐
|
|
209
|
-
│ Fastify App │
|
|
210
|
-
│ ├── authHook (req.user)
|
|
211
|
-
│ ├── helmet + CORS │
|
|
212
|
-
│ ├── rate limits │
|
|
213
|
-
│ ├── secret redaction│
|
|
214
|
-
│ └── graceful shutdown
|
|
215
|
-
└──────────┬───────────┘
|
|
216
|
-
│
|
|
217
|
-
┌──────────▼───────────┐
|
|
218
|
-
│ better-auth │
|
|
219
|
-
│ (sessions, email, │
|
|
220
|
-
│ password reset) │
|
|
221
|
-
└──────────┬───────────┘
|
|
222
|
-
│
|
|
223
|
-
┌──────────▼───────────┐
|
|
224
|
-
│ Drizzle + Postgres │
|
|
225
|
-
│ users, sessions, │
|
|
226
|
-
│ workspaces, members, │
|
|
227
|
-
│ invites, settings │
|
|
228
|
-
└──────────────────────┘
|
|
229
|
-
```
|
|
230
|
-
|
|
231
|
-
### Error Handling Contract
|
|
232
|
-
|
|
233
|
-
All errors flow through a single `setErrorHandler`:
|
|
234
|
-
|
|
235
|
-
| Condition | Status | Code |
|
|
236
|
-
|-----------|--------|------|
|
|
237
|
-
| No/expired session | 401 | `unauthorized` |
|
|
238
|
-
| Insufficient role | 403 | `forbidden` / `not_member` |
|
|
239
|
-
| Zod validation fail | 400 | `validation_failed` |
|
|
240
|
-
| Rate limited | 429 | `rate_limited` + `Retry-After` |
|
|
241
|
-
| DB ping fails | 503 | `db_unavailable` |
|
|
242
|
-
| Everything else | 500 | `internal_error` |
|
|
243
|
-
|
|
244
|
-
Every response includes `{ error, code, message, requestId }`. Client-side `apiFetch` parses this into `HttpError` instances.
|
|
245
|
-
|
|
246
|
-
---
|
|
247
|
-
|
|
248
|
-
## How @hachej/boring-core Compares
|
|
249
|
-
|
|
250
|
-
| Feature | @hachej/boring-core | Supabase + custom | Firebase | Roll your own |
|
|
251
|
-
|---------|---------------------|-------------------|----------|---------------|
|
|
252
|
-
| Auth flows | ✅ email + reset + magic link | ✅ OAuth only | ✅ OAuth/phone | Weeks to build |
|
|
253
|
-
| Workspaces + invites | ✅ owner/editor/viewer roles | ❌ Custom tables | ❌ Custom rules | ~1 week |
|
|
254
|
-
| Email templates | ✅ 5 React Email templates | ❌ You write them | ❌ SendGrid setup | ~3 days |
|
|
255
|
-
| App shell | ✅ `<BoringApp>` + hooks | ❌ DIY | ❌ DIY | ~1 week |
|
|
256
|
-
| Rate limiting | ✅ pre-wired routes | ❌ Edge functions | ⚠️ Cloud rules | ~2 days |
|
|
257
|
-
| Config validation | ✅ TOML + env + Zod | ❌ dotenv only | ⚠️ Remote config | Custom |
|
|
258
|
-
|
|
259
|
-
**When to use @hachej/boring-core:**
|
|
260
|
-
- Building a multi-user app around an AI agent
|
|
261
|
-
- You need auth + workspaces + invites in days, not weeks
|
|
262
|
-
- You're deploying to Fly.io, Render, Railway, or any Postgres-capable host
|
|
263
|
-
|
|
264
|
-
**When it might not fit:**
|
|
265
|
-
- You need server-side rendering (client-rendered only)
|
|
266
|
-
- You want SQLite (Postgres-only with Drizzle)
|
|
267
|
-
- You need Google/Apple/Discord OAuth (planned for v1.x)
|
|
268
|
-
- You need billing/Stripe integration (future `@boring/cloud` package)
|
|
269
|
-
|
|
270
|
-
---
|
|
271
|
-
|
|
272
|
-
## Troubleshooting
|
|
273
|
-
|
|
274
|
-
| Error | Cause | Fix |
|
|
275
|
-
|-------|-------|-----|
|
|
276
|
-
| `ConfigValidationError` at boot | Missing required env var | Check `.env` has all required vars |
|
|
277
|
-
| `config_fetch_failed` in browser | API server not reachable | Verify `BETTER_AUTH_URL` matches |
|
|
278
|
-
| `mail_disabled` warning at boot | `MAIL_FROM` not set | Set `MAIL_TRANSPORT_URL=console://` for dev |
|
|
279
|
-
| `unauthorized` on `/api/v1/me` | No session cookie | Check `BETTER_AUTH_URL` and `CORS_ORIGINS` |
|
|
280
|
-
| `db_unavailable` on `/health` | Postgres can't connect | Verify `DATABASE_URL` and network access |
|
|
281
|
-
|
|
282
|
-
---
|
|
283
|
-
|
|
284
|
-
## Limitations
|
|
285
|
-
|
|
286
|
-
- **Postgres only** — No SQLite/libsql support in v1.
|
|
287
|
-
- **Client-rendered only** — `<BoringApp>` mounts client-side. No SSR.
|
|
288
|
-
- **GitHub OAuth deferred** — Planned for v1.x, bundled with agent's GitHub App install.
|
|
289
|
-
- **No billing** — Stripe integration planned for `@boring/cloud` package.
|
|
290
|
-
- **In-memory stores are dev-only** — `CORE_STORES=local` resets on restart. Not for production.
|
|
291
|
-
- **Partial swap seams** — `AuthProvider` is swappable, but the React auth surfaces (`useSession`, sign-in pages) are better-auth-shaped.
|
|
292
|
-
|
|
293
|
-
---
|
|
294
|
-
|
|
295
|
-
## FAQ
|
|
296
|
-
|
|
297
|
-
**Q: Can I use this without Postgres?**
|
|
298
|
-
A: In dev, yes — set `CORE_STORES=local`. State is in-memory and resets on restart. For production, Postgres is required.
|
|
299
|
-
|
|
300
|
-
**Q: How do I add Google/Discord OAuth?**
|
|
301
|
-
A: better-auth supports these out of the box. Add the provider config to `createAuth()` in the core source. Official v1.x support planned.
|
|
302
|
-
|
|
303
|
-
**Q: Can I swap better-auth for Clerk/Neon?**
|
|
304
|
-
A: The `AuthProvider` interface is designed as a swap seam. You'll need to re-implement the React auth surfaces (`SignInPage`, `useSession`, etc.) and preserve the `users.id` continuity invariant.
|
|
305
|
-
|
|
306
|
-
**Q: How do email templates work?**
|
|
307
|
-
A: Five React Email components (`VerifyEmail`, `ResetPassword`, `MagicLink`, `WorkspaceInvite`, `Welcome`) rendered via `@react-email/render`. CSS is inlined. Swap them by providing your own mail transport.
|
|
308
|
-
|
|
309
|
-
**Q: What's the difference between `@hachej/boring-core/server` and `@hachej/boring-core/server/db`?**
|
|
310
|
-
A: `server` includes the full Fastify app, routes, auth, and stores. `server/db` is the Drizzle schema + connection + store interfaces only — useful for migration tooling and type-only imports.
|
|
59
|
+
See [`docs/README.md`](./docs/README.md) for the architecture overview, public API
|
|
60
|
+
surface, key abstractions, and links to the gating, plugin, and deployment docs. The
|
|
61
|
+
reference app is [`apps/full-app`](../../apps/full-app/).
|
|
311
62
|
|
|
312
63
|
---
|
|
313
64
|
|
|
314
65
|
*About Contributions:* Please don't take this the wrong way, but I do not accept outside contributions for any of my projects. I simply don't have the mental bandwidth to review anything, and it's my name on the thing, so I'm responsible for any problems it causes; thus, the risk-reward is highly asymmetric from my perspective. I'd also have to worry about other "stakeholders," which seems unwise for tools I mostly make for myself for free. Feel free to submit issues, and even PRs if you want to illustrate a proposed fix, but know I won't merge them directly. Instead, I'll have Claude or Codex review submissions via `gh` and independently decide whether and how to address them. Bug reports in particular are welcome. Sorry if this offends, but I want to avoid wasted time and hurt feelings. I understand this isn't in sync with the prevailing open-source ethos that seeks community contributions, but it's the only way I can move at this velocity and keep my sanity.
|
|
315
66
|
|
|
316
|
-
---
|
|
317
|
-
|
|
318
67
|
## License
|
|
319
68
|
|
|
320
69
|
MIT
|
package/dist/app/front/index.js
CHANGED
package/dist/app/server/index.js
CHANGED
|
@@ -1625,7 +1625,7 @@ function createPostSignupHook(deps) {
|
|
|
1625
1625
|
}
|
|
1626
1626
|
}
|
|
1627
1627
|
if (!inviteAccepted) {
|
|
1628
|
-
await workspaceStore.create(user.id, "
|
|
1628
|
+
await workspaceStore.create(user.id, "Default workspace", config.appId, { isDefault: true });
|
|
1629
1629
|
}
|
|
1630
1630
|
if (!inviteAccepted && config.features.sendWelcomeEmail !== false && transport) {
|
|
1631
1631
|
const getStartedUrl = `${config.auth.url}/`;
|
|
@@ -1951,7 +1951,7 @@ var updateWorkspaceBody = z3.object({
|
|
|
1951
1951
|
}).strict();
|
|
1952
1952
|
|
|
1953
1953
|
// src/server/routes/workspaces.ts
|
|
1954
|
-
var DEFAULT_WORKSPACE_NAME = "
|
|
1954
|
+
var DEFAULT_WORKSPACE_NAME = "Default workspace";
|
|
1955
1955
|
var workspaceRoutesPlugin = async (app) => {
|
|
1956
1956
|
const store = app.workspaceStore;
|
|
1957
1957
|
const provisioner = app.provisioner;
|
|
@@ -2180,7 +2180,7 @@ import {
|
|
|
2180
2180
|
Label as Label7,
|
|
2181
2181
|
useToast
|
|
2182
2182
|
} from "@hachej/boring-ui-kit";
|
|
2183
|
-
import {
|
|
2183
|
+
import { ChevronsUpDown, LayoutGrid, Plus, Settings as Settings2 } from "lucide-react";
|
|
2184
2184
|
import { useNavigate as useNavigate3 } from "react-router-dom";
|
|
2185
2185
|
import { z as z6 } from "zod";
|
|
2186
2186
|
import { Fragment as Fragment4, jsx as jsx17, jsxs as jsxs9 } from "react/jsx-runtime";
|
|
@@ -2219,6 +2219,13 @@ function hrefForWorkspace(prefix, workspaceId, suffix = "") {
|
|
|
2219
2219
|
function workspaceInitial(name) {
|
|
2220
2220
|
return (name.trim()[0] ?? "W").toUpperCase();
|
|
2221
2221
|
}
|
|
2222
|
+
function OpenInNewTabIcon({ className }) {
|
|
2223
|
+
return /* @__PURE__ */ jsxs9("svg", { className, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [
|
|
2224
|
+
/* @__PURE__ */ jsx17("path", { d: "M15 3h6v6" }),
|
|
2225
|
+
/* @__PURE__ */ jsx17("path", { d: "M10 14 21 3" }),
|
|
2226
|
+
/* @__PURE__ */ jsx17("path", { d: "M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" })
|
|
2227
|
+
] });
|
|
2228
|
+
}
|
|
2222
2229
|
function WorkspaceSwitcher({
|
|
2223
2230
|
appTitle = "Boring",
|
|
2224
2231
|
workspacePathPrefix = "/workspace"
|
|
@@ -2293,6 +2300,9 @@ function WorkspaceSwitcher({
|
|
|
2293
2300
|
}
|
|
2294
2301
|
}
|
|
2295
2302
|
const switcherLabel = currentWorkspace?.name ?? "Select workspace";
|
|
2303
|
+
function openWorkspaceInNewTab(workspaceId) {
|
|
2304
|
+
window.open(hrefForWorkspace(workspacePathPrefix, workspaceId), "_blank", "noopener,noreferrer");
|
|
2305
|
+
}
|
|
2296
2306
|
return /* @__PURE__ */ jsxs9(Fragment4, { children: [
|
|
2297
2307
|
workspaces.length === 0 ? /* @__PURE__ */ jsxs9(
|
|
2298
2308
|
Button11,
|
|
@@ -2352,21 +2362,43 @@ function WorkspaceSwitcher({
|
|
|
2352
2362
|
] }) }),
|
|
2353
2363
|
/* @__PURE__ */ jsx17("div", { className: "max-h-72 overflow-y-auto pr-1", children: workspaces.map((workspace) => {
|
|
2354
2364
|
const isCurrent = currentWorkspace?.id === workspace.id;
|
|
2355
|
-
return /* @__PURE__ */ jsxs9(
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2365
|
+
return /* @__PURE__ */ jsxs9("div", { className: "group relative w-full", children: [
|
|
2366
|
+
/* @__PURE__ */ jsxs9(
|
|
2367
|
+
DropdownMenuItem2,
|
|
2368
|
+
{
|
|
2369
|
+
"aria-label": workspace.name,
|
|
2370
|
+
"data-current": isCurrent ? "true" : "false",
|
|
2371
|
+
onSelect: () => navigate(hrefForWorkspace(workspacePathPrefix, workspace.id)),
|
|
2372
|
+
style: { paddingRight: 72 },
|
|
2373
|
+
className: "gap-3 rounded-md py-2 text-[13px] focus:bg-foreground/[0.06] focus:text-foreground",
|
|
2374
|
+
children: [
|
|
2375
|
+
/* @__PURE__ */ jsx17("span", { className: "flex h-7 w-7 shrink-0 items-center justify-center rounded-md border border-border/60 bg-background text-xs font-semibold text-muted-foreground", children: workspaceInitial(workspace.name) }),
|
|
2376
|
+
/* @__PURE__ */ jsx17("span", { className: "min-w-0 flex-1 truncate text-sm", children: workspace.name })
|
|
2377
|
+
]
|
|
2378
|
+
}
|
|
2379
|
+
),
|
|
2380
|
+
/* @__PURE__ */ jsx17(
|
|
2381
|
+
"button",
|
|
2382
|
+
{
|
|
2383
|
+
type: "button",
|
|
2384
|
+
tabIndex: -1,
|
|
2385
|
+
"aria-label": `Open ${workspace.name} in new tab`,
|
|
2386
|
+
title: "Open in new tab",
|
|
2387
|
+
onPointerDown: (event) => {
|
|
2388
|
+
event.preventDefault();
|
|
2389
|
+
event.stopPropagation();
|
|
2390
|
+
},
|
|
2391
|
+
onClick: (event) => {
|
|
2392
|
+
event.preventDefault();
|
|
2393
|
+
event.stopPropagation();
|
|
2394
|
+
openWorkspaceInNewTab(workspace.id);
|
|
2395
|
+
},
|
|
2396
|
+
style: { right: 4, top: "50%", transform: "translateY(-50%)" },
|
|
2397
|
+
className: "absolute z-10 flex h-7 w-7 items-center justify-center rounded-md text-muted-foreground opacity-0 transition hover:bg-foreground/10 hover:text-foreground focus:opacity-100 focus:outline-none focus-visible:ring-1 focus-visible:ring-ring group-hover:opacity-100 group-focus-within:opacity-100",
|
|
2398
|
+
children: /* @__PURE__ */ jsx17(OpenInNewTabIcon, { className: "h-3.5 w-3.5" })
|
|
2399
|
+
}
|
|
2400
|
+
)
|
|
2401
|
+
] }, workspace.id);
|
|
2370
2402
|
}) }),
|
|
2371
2403
|
/* @__PURE__ */ jsx17(DropdownMenuSeparator2, { className: "-mx-2" }),
|
|
2372
2404
|
/* @__PURE__ */ jsxs9(
|
|
@@ -2431,7 +2463,7 @@ function WorkspaceSwitcher({
|
|
|
2431
2463
|
setName(event.target.value);
|
|
2432
2464
|
if (serverError) setServerError(null);
|
|
2433
2465
|
},
|
|
2434
|
-
placeholder: "
|
|
2466
|
+
placeholder: "Default workspace",
|
|
2435
2467
|
"aria-invalid": nameError ? "true" : "false",
|
|
2436
2468
|
autoFocus: true
|
|
2437
2469
|
}
|
package/dist/front/index.js
CHANGED
package/dist/server/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hachej/boring-core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.42",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"description": "Foundation package for boring-ui-v2 apps: DB, auth, config, HTTP app factory, and frontend app shell.",
|
|
@@ -79,9 +79,9 @@
|
|
|
79
79
|
"react-router-dom": "^7.14.2",
|
|
80
80
|
"smol-toml": "^1.6.1",
|
|
81
81
|
"zod": "^3.25.76",
|
|
82
|
-
"@hachej/boring-agent": "0.1.
|
|
83
|
-
"@hachej/boring-
|
|
84
|
-
"@hachej/boring-
|
|
82
|
+
"@hachej/boring-agent": "0.1.42",
|
|
83
|
+
"@hachej/boring-ui-kit": "0.1.42",
|
|
84
|
+
"@hachej/boring-workspace": "0.1.42"
|
|
85
85
|
},
|
|
86
86
|
"devDependencies": {
|
|
87
87
|
"@testing-library/jest-dom": "^6.9.1",
|