granola-toolkit 0.20.0 → 0.21.1
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 +2 -1
- package/dist/cli.js +550 -384
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -208,7 +208,8 @@ The initial browser client includes:
|
|
|
208
208
|
- a searchable meeting list
|
|
209
209
|
- sort and updated-date filters
|
|
210
210
|
- quick open by meeting id or title
|
|
211
|
-
- a meeting
|
|
211
|
+
- a focused meeting workspace with notes, transcript, metadata, and raw tabs
|
|
212
|
+
- keyboard-first workspace switching with `1`-`4`, `[` and `]`
|
|
212
213
|
- app-state status from the shared core
|
|
213
214
|
- note and transcript export actions backed by the same local API
|
|
214
215
|
- stronger empty and error states for list/detail failures
|
package/dist/cli.js
CHANGED
|
@@ -2132,382 +2132,8 @@ function resolveNoteFormat(value) {
|
|
|
2132
2132
|
}
|
|
2133
2133
|
}
|
|
2134
2134
|
//#endregion
|
|
2135
|
-
//#region src/
|
|
2136
|
-
|
|
2137
|
-
return `<!doctype html>
|
|
2138
|
-
<html lang="en">
|
|
2139
|
-
<head>
|
|
2140
|
-
<meta charset="utf-8" />
|
|
2141
|
-
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
2142
|
-
<title>Granola Toolkit</title>
|
|
2143
|
-
<style>
|
|
2144
|
-
:root {
|
|
2145
|
-
--bg: #f2ede2;
|
|
2146
|
-
--panel: rgba(255, 252, 247, 0.86);
|
|
2147
|
-
--panel-strong: #fffaf2;
|
|
2148
|
-
--line: rgba(36, 39, 44, 0.12);
|
|
2149
|
-
--ink: #1d242c;
|
|
2150
|
-
--muted: #5d6b77;
|
|
2151
|
-
--accent: #0d6a6d;
|
|
2152
|
-
--accent-soft: rgba(13, 106, 109, 0.12);
|
|
2153
|
-
--warm: #a34f2f;
|
|
2154
|
-
--ok: #246b4f;
|
|
2155
|
-
--error: #9d2c2c;
|
|
2156
|
-
--shadow: 0 24px 80px rgba(40, 32, 16, 0.12);
|
|
2157
|
-
--radius: 24px;
|
|
2158
|
-
--mono: "SF Mono", "IBM Plex Mono", "Cascadia Code", monospace;
|
|
2159
|
-
--serif: "Iowan Old Style", "Palatino Linotype", "Book Antiqua", Georgia, serif;
|
|
2160
|
-
--sans: "Avenir Next", "Segoe UI", sans-serif;
|
|
2161
|
-
}
|
|
2162
|
-
|
|
2163
|
-
* { box-sizing: border-box; }
|
|
2164
|
-
|
|
2165
|
-
body {
|
|
2166
|
-
margin: 0;
|
|
2167
|
-
min-height: 100vh;
|
|
2168
|
-
font-family: var(--sans);
|
|
2169
|
-
color: var(--ink);
|
|
2170
|
-
background:
|
|
2171
|
-
radial-gradient(circle at top left, rgba(163, 79, 47, 0.18), transparent 32%),
|
|
2172
|
-
radial-gradient(circle at right 12%, rgba(13, 106, 109, 0.16), transparent 28%),
|
|
2173
|
-
linear-gradient(180deg, #f8f2e8 0%, var(--bg) 100%);
|
|
2174
|
-
}
|
|
2175
|
-
|
|
2176
|
-
.shell {
|
|
2177
|
-
display: grid;
|
|
2178
|
-
grid-template-columns: 320px minmax(0, 1fr);
|
|
2179
|
-
gap: 18px;
|
|
2180
|
-
min-height: 100vh;
|
|
2181
|
-
padding: 24px;
|
|
2182
|
-
}
|
|
2183
|
-
|
|
2184
|
-
.pane {
|
|
2185
|
-
background: var(--panel);
|
|
2186
|
-
backdrop-filter: blur(18px);
|
|
2187
|
-
border: 1px solid var(--line);
|
|
2188
|
-
border-radius: var(--radius);
|
|
2189
|
-
box-shadow: var(--shadow);
|
|
2190
|
-
}
|
|
2191
|
-
|
|
2192
|
-
.sidebar {
|
|
2193
|
-
display: grid;
|
|
2194
|
-
grid-template-rows: auto auto 1fr;
|
|
2195
|
-
overflow: hidden;
|
|
2196
|
-
}
|
|
2197
|
-
|
|
2198
|
-
.hero, .toolbar, .detail-head {
|
|
2199
|
-
padding: 22px 24px;
|
|
2200
|
-
border-bottom: 1px solid var(--line);
|
|
2201
|
-
}
|
|
2202
|
-
|
|
2203
|
-
.hero h1 {
|
|
2204
|
-
margin: 0;
|
|
2205
|
-
font-family: var(--serif);
|
|
2206
|
-
font-size: clamp(2rem, 3vw, 2.8rem);
|
|
2207
|
-
font-weight: 600;
|
|
2208
|
-
letter-spacing: -0.04em;
|
|
2209
|
-
}
|
|
2210
|
-
|
|
2211
|
-
.hero p, .toolbar p {
|
|
2212
|
-
margin: 8px 0 0;
|
|
2213
|
-
color: var(--muted);
|
|
2214
|
-
line-height: 1.5;
|
|
2215
|
-
}
|
|
2216
|
-
|
|
2217
|
-
.search,
|
|
2218
|
-
.select,
|
|
2219
|
-
.field-input {
|
|
2220
|
-
width: 100%;
|
|
2221
|
-
margin-top: 16px;
|
|
2222
|
-
padding: 12px 14px;
|
|
2223
|
-
border: 1px solid var(--line);
|
|
2224
|
-
border-radius: 999px;
|
|
2225
|
-
background: rgba(255, 255, 255, 0.7);
|
|
2226
|
-
color: var(--ink);
|
|
2227
|
-
font: inherit;
|
|
2228
|
-
}
|
|
2229
|
-
|
|
2230
|
-
.field-row {
|
|
2231
|
-
display: grid;
|
|
2232
|
-
gap: 10px;
|
|
2233
|
-
margin-top: 12px;
|
|
2234
|
-
}
|
|
2235
|
-
|
|
2236
|
-
.field-row--inline {
|
|
2237
|
-
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
2238
|
-
}
|
|
2239
|
-
|
|
2240
|
-
.field-label {
|
|
2241
|
-
display: block;
|
|
2242
|
-
margin-bottom: 6px;
|
|
2243
|
-
color: var(--muted);
|
|
2244
|
-
font-size: 0.78rem;
|
|
2245
|
-
font-weight: 700;
|
|
2246
|
-
letter-spacing: 0.08em;
|
|
2247
|
-
text-transform: uppercase;
|
|
2248
|
-
}
|
|
2249
|
-
|
|
2250
|
-
.meeting-list {
|
|
2251
|
-
padding: 14px;
|
|
2252
|
-
overflow: auto;
|
|
2253
|
-
}
|
|
2254
|
-
|
|
2255
|
-
.meeting-row {
|
|
2256
|
-
width: 100%;
|
|
2257
|
-
display: grid;
|
|
2258
|
-
gap: 4px;
|
|
2259
|
-
text-align: left;
|
|
2260
|
-
margin: 0 0 10px;
|
|
2261
|
-
padding: 14px 16px;
|
|
2262
|
-
border: 1px solid transparent;
|
|
2263
|
-
border-radius: 18px;
|
|
2264
|
-
background: rgba(255, 255, 255, 0.72);
|
|
2265
|
-
color: inherit;
|
|
2266
|
-
cursor: pointer;
|
|
2267
|
-
transition: transform 140ms ease, border-color 140ms ease, background 140ms ease;
|
|
2268
|
-
}
|
|
2269
|
-
|
|
2270
|
-
.meeting-row:hover,
|
|
2271
|
-
.meeting-row[data-selected="true"] {
|
|
2272
|
-
transform: translateY(-1px);
|
|
2273
|
-
border-color: rgba(13, 106, 109, 0.25);
|
|
2274
|
-
background: var(--panel-strong);
|
|
2275
|
-
}
|
|
2276
|
-
|
|
2277
|
-
.meeting-row__title {
|
|
2278
|
-
font-weight: 600;
|
|
2279
|
-
}
|
|
2280
|
-
|
|
2281
|
-
.meeting-row__meta {
|
|
2282
|
-
color: var(--muted);
|
|
2283
|
-
font-size: 0.92rem;
|
|
2284
|
-
}
|
|
2285
|
-
|
|
2286
|
-
.meeting-empty {
|
|
2287
|
-
padding: 18px;
|
|
2288
|
-
color: var(--muted);
|
|
2289
|
-
}
|
|
2290
|
-
|
|
2291
|
-
.meeting-empty--error {
|
|
2292
|
-
color: var(--error);
|
|
2293
|
-
}
|
|
2294
|
-
|
|
2295
|
-
.detail {
|
|
2296
|
-
display: grid;
|
|
2297
|
-
grid-template-rows: auto auto 1fr;
|
|
2298
|
-
min-width: 0;
|
|
2299
|
-
}
|
|
2300
|
-
|
|
2301
|
-
.detail-head {
|
|
2302
|
-
display: flex;
|
|
2303
|
-
align-items: center;
|
|
2304
|
-
justify-content: space-between;
|
|
2305
|
-
gap: 18px;
|
|
2306
|
-
}
|
|
2307
|
-
|
|
2308
|
-
.detail-head h2 {
|
|
2309
|
-
margin: 0;
|
|
2310
|
-
font-family: var(--serif);
|
|
2311
|
-
font-size: clamp(1.8rem, 2.4vw, 2.4rem);
|
|
2312
|
-
font-weight: 600;
|
|
2313
|
-
}
|
|
2314
|
-
|
|
2315
|
-
.state-badge {
|
|
2316
|
-
padding: 10px 14px;
|
|
2317
|
-
border-radius: 999px;
|
|
2318
|
-
background: var(--accent-soft);
|
|
2319
|
-
color: var(--accent);
|
|
2320
|
-
font-size: 0.92rem;
|
|
2321
|
-
font-weight: 700;
|
|
2322
|
-
}
|
|
2323
|
-
|
|
2324
|
-
.state-badge[data-tone="busy"] { color: var(--warm); background: rgba(163, 79, 47, 0.12); }
|
|
2325
|
-
.state-badge[data-tone="error"] { color: var(--error); background: rgba(157, 44, 44, 0.12); }
|
|
2326
|
-
.state-badge[data-tone="ok"] { color: var(--ok); background: rgba(36, 107, 79, 0.12); }
|
|
2327
|
-
|
|
2328
|
-
.toolbar {
|
|
2329
|
-
display: flex;
|
|
2330
|
-
flex-wrap: wrap;
|
|
2331
|
-
align-items: center;
|
|
2332
|
-
justify-content: space-between;
|
|
2333
|
-
gap: 14px;
|
|
2334
|
-
}
|
|
2335
|
-
|
|
2336
|
-
.toolbar-actions {
|
|
2337
|
-
display: flex;
|
|
2338
|
-
flex-wrap: wrap;
|
|
2339
|
-
gap: 10px;
|
|
2340
|
-
}
|
|
2341
|
-
|
|
2342
|
-
.toolbar-form {
|
|
2343
|
-
display: grid;
|
|
2344
|
-
grid-template-columns: minmax(0, 1fr) auto;
|
|
2345
|
-
gap: 10px;
|
|
2346
|
-
width: min(440px, 100%);
|
|
2347
|
-
}
|
|
2348
|
-
|
|
2349
|
-
.button {
|
|
2350
|
-
border: 0;
|
|
2351
|
-
border-radius: 999px;
|
|
2352
|
-
padding: 12px 16px;
|
|
2353
|
-
font: inherit;
|
|
2354
|
-
font-weight: 700;
|
|
2355
|
-
cursor: pointer;
|
|
2356
|
-
}
|
|
2357
|
-
|
|
2358
|
-
.button--primary {
|
|
2359
|
-
background: var(--ink);
|
|
2360
|
-
color: white;
|
|
2361
|
-
}
|
|
2362
|
-
|
|
2363
|
-
.button--secondary {
|
|
2364
|
-
background: rgba(255, 255, 255, 0.72);
|
|
2365
|
-
color: var(--ink);
|
|
2366
|
-
border: 1px solid var(--line);
|
|
2367
|
-
}
|
|
2368
|
-
|
|
2369
|
-
.status-grid {
|
|
2370
|
-
display: grid;
|
|
2371
|
-
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
|
2372
|
-
gap: 14px;
|
|
2373
|
-
}
|
|
2374
|
-
|
|
2375
|
-
.status-label {
|
|
2376
|
-
display: block;
|
|
2377
|
-
margin-bottom: 6px;
|
|
2378
|
-
color: var(--muted);
|
|
2379
|
-
font-size: 0.78rem;
|
|
2380
|
-
letter-spacing: 0.08em;
|
|
2381
|
-
text-transform: uppercase;
|
|
2382
|
-
}
|
|
2383
|
-
|
|
2384
|
-
.detail-meta {
|
|
2385
|
-
display: flex;
|
|
2386
|
-
flex-wrap: wrap;
|
|
2387
|
-
gap: 10px;
|
|
2388
|
-
padding: 0 24px 18px;
|
|
2389
|
-
}
|
|
2390
|
-
|
|
2391
|
-
.detail-chip {
|
|
2392
|
-
padding: 10px 12px;
|
|
2393
|
-
border-radius: 999px;
|
|
2394
|
-
background: rgba(255, 255, 255, 0.72);
|
|
2395
|
-
border: 1px solid var(--line);
|
|
2396
|
-
color: var(--muted);
|
|
2397
|
-
font-size: 0.88rem;
|
|
2398
|
-
}
|
|
2399
|
-
|
|
2400
|
-
.detail-body {
|
|
2401
|
-
padding: 0 24px 24px;
|
|
2402
|
-
overflow: auto;
|
|
2403
|
-
}
|
|
2404
|
-
|
|
2405
|
-
.detail-section {
|
|
2406
|
-
margin-bottom: 20px;
|
|
2407
|
-
padding: 20px;
|
|
2408
|
-
background: rgba(255, 255, 255, 0.72);
|
|
2409
|
-
border: 1px solid var(--line);
|
|
2410
|
-
border-radius: 20px;
|
|
2411
|
-
}
|
|
2412
|
-
|
|
2413
|
-
.detail-section h2 {
|
|
2414
|
-
margin: 0 0 14px;
|
|
2415
|
-
font-size: 1rem;
|
|
2416
|
-
letter-spacing: 0.08em;
|
|
2417
|
-
text-transform: uppercase;
|
|
2418
|
-
}
|
|
2419
|
-
|
|
2420
|
-
.detail-pre {
|
|
2421
|
-
margin: 0;
|
|
2422
|
-
white-space: pre-wrap;
|
|
2423
|
-
word-break: break-word;
|
|
2424
|
-
font-family: var(--mono);
|
|
2425
|
-
line-height: 1.55;
|
|
2426
|
-
}
|
|
2427
|
-
|
|
2428
|
-
.empty {
|
|
2429
|
-
margin: 24px;
|
|
2430
|
-
padding: 24px;
|
|
2431
|
-
border-radius: 20px;
|
|
2432
|
-
background: rgba(255, 255, 255, 0.72);
|
|
2433
|
-
border: 1px dashed rgba(36, 39, 44, 0.2);
|
|
2434
|
-
color: var(--muted);
|
|
2435
|
-
}
|
|
2436
|
-
|
|
2437
|
-
@media (max-width: 900px) {
|
|
2438
|
-
.shell {
|
|
2439
|
-
grid-template-columns: 1fr;
|
|
2440
|
-
}
|
|
2441
|
-
|
|
2442
|
-
.field-row--inline,
|
|
2443
|
-
.toolbar-form {
|
|
2444
|
-
grid-template-columns: 1fr;
|
|
2445
|
-
}
|
|
2446
|
-
}
|
|
2447
|
-
</style>
|
|
2448
|
-
</head>
|
|
2449
|
-
<body>
|
|
2450
|
-
<div class="shell">
|
|
2451
|
-
<aside class="pane sidebar">
|
|
2452
|
-
<section class="hero">
|
|
2453
|
-
<h1>Granola Toolkit</h1>
|
|
2454
|
-
<p>Browser workspace for meetings, notes, transcripts, and export flows on top of one local server instance.</p>
|
|
2455
|
-
<input class="search" data-search placeholder="Search meetings, ids, or tags" />
|
|
2456
|
-
<div class="field-row field-row--inline">
|
|
2457
|
-
<label>
|
|
2458
|
-
<span class="field-label">Sort</span>
|
|
2459
|
-
<select class="select" data-sort>
|
|
2460
|
-
<option value="updated-desc">Newest first</option>
|
|
2461
|
-
<option value="updated-asc">Oldest first</option>
|
|
2462
|
-
<option value="title-asc">Title A-Z</option>
|
|
2463
|
-
<option value="title-desc">Title Z-A</option>
|
|
2464
|
-
</select>
|
|
2465
|
-
</label>
|
|
2466
|
-
<label>
|
|
2467
|
-
<span class="field-label">Updated From</span>
|
|
2468
|
-
<input class="field-input" data-updated-from type="date" />
|
|
2469
|
-
</label>
|
|
2470
|
-
</div>
|
|
2471
|
-
<label class="field-row">
|
|
2472
|
-
<span class="field-label">Updated To</span>
|
|
2473
|
-
<input class="field-input" data-updated-to type="date" />
|
|
2474
|
-
</label>
|
|
2475
|
-
</section>
|
|
2476
|
-
<section class="toolbar">
|
|
2477
|
-
<div>
|
|
2478
|
-
<p>Meetings are loaded from the shared server state so this view can later coexist with the terminal UI.</p>
|
|
2479
|
-
</div>
|
|
2480
|
-
<div class="toolbar-form">
|
|
2481
|
-
<input class="field-input" data-quick-open placeholder="Quick open by id or title" />
|
|
2482
|
-
<button class="button button--secondary" data-quick-open-button>Open</button>
|
|
2483
|
-
</div>
|
|
2484
|
-
</section>
|
|
2485
|
-
<section class="meeting-list" data-meeting-list></section>
|
|
2486
|
-
</aside>
|
|
2487
|
-
<main class="pane detail">
|
|
2488
|
-
<section class="detail-head">
|
|
2489
|
-
<div>
|
|
2490
|
-
<h2>Meeting Workspace</h2>
|
|
2491
|
-
<div data-app-state></div>
|
|
2492
|
-
</div>
|
|
2493
|
-
<div class="state-badge" data-state-badge data-tone="idle">Connecting…</div>
|
|
2494
|
-
</section>
|
|
2495
|
-
<section class="toolbar">
|
|
2496
|
-
<div class="toolbar-actions">
|
|
2497
|
-
<button class="button button--primary" data-refresh>Refresh</button>
|
|
2498
|
-
<button class="button button--secondary" data-export-notes>Export Notes</button>
|
|
2499
|
-
<button class="button button--secondary" data-export-transcripts>Export Transcripts</button>
|
|
2500
|
-
</div>
|
|
2501
|
-
<p>Initial beta web client. It speaks to the same local API that future TUI and attach flows will use.</p>
|
|
2502
|
-
</section>
|
|
2503
|
-
<div class="detail-meta" data-detail-meta></div>
|
|
2504
|
-
<div class="detail-body" data-detail-body>
|
|
2505
|
-
<div class="empty" data-empty>Select a meeting to inspect its notes and transcript.</div>
|
|
2506
|
-
</div>
|
|
2507
|
-
</main>
|
|
2508
|
-
</div>
|
|
2509
|
-
<script type="module">
|
|
2510
|
-
${String.raw`
|
|
2135
|
+
//#region src/web/client-script.ts
|
|
2136
|
+
const granolaWebClientScript = String.raw`
|
|
2511
2137
|
const state = {
|
|
2512
2138
|
appState: null,
|
|
2513
2139
|
detailError: "",
|
|
@@ -2516,10 +2142,12 @@ const state = {
|
|
|
2516
2142
|
quickOpen: "",
|
|
2517
2143
|
search: "",
|
|
2518
2144
|
selectedMeeting: null,
|
|
2145
|
+
selectedMeetingBundle: null,
|
|
2519
2146
|
selectedMeetingId: null,
|
|
2520
2147
|
sort: "updated-desc",
|
|
2521
2148
|
updatedFrom: "",
|
|
2522
2149
|
updatedTo: "",
|
|
2150
|
+
workspaceTab: "notes",
|
|
2523
2151
|
};
|
|
2524
2152
|
|
|
2525
2153
|
const els = {
|
|
@@ -2538,6 +2166,7 @@ const els = {
|
|
|
2538
2166
|
transcriptButton: document.querySelector("[data-export-transcripts]"),
|
|
2539
2167
|
updatedFrom: document.querySelector("[data-updated-from]"),
|
|
2540
2168
|
updatedTo: document.querySelector("[data-updated-to]"),
|
|
2169
|
+
workspaceTabs: document.querySelectorAll("[data-workspace-tab]"),
|
|
2541
2170
|
};
|
|
2542
2171
|
|
|
2543
2172
|
function escapeHtml(value) {
|
|
@@ -2579,6 +2208,12 @@ function currentFilterSummary() {
|
|
|
2579
2208
|
return parts.join(", ");
|
|
2580
2209
|
}
|
|
2581
2210
|
|
|
2211
|
+
function renderWorkspaceTabs() {
|
|
2212
|
+
for (const button of els.workspaceTabs) {
|
|
2213
|
+
button.dataset.selected = button.dataset.workspaceTab === state.workspaceTab ? "true" : "false";
|
|
2214
|
+
}
|
|
2215
|
+
}
|
|
2216
|
+
|
|
2582
2217
|
function renderAppState() {
|
|
2583
2218
|
if (!state.appState) {
|
|
2584
2219
|
els.appState.innerHTML = "<p>Waiting for server state…</p>";
|
|
@@ -2615,6 +2250,7 @@ function renderMeetingList() {
|
|
|
2615
2250
|
if (state.meetings.length === 0) {
|
|
2616
2251
|
state.selectedMeetingId = null;
|
|
2617
2252
|
state.selectedMeeting = null;
|
|
2253
|
+
state.selectedMeetingBundle = null;
|
|
2618
2254
|
const filterSummary = currentFilterSummary();
|
|
2619
2255
|
const message = filterSummary
|
|
2620
2256
|
? "No meetings match " + filterSummary + "."
|
|
@@ -2645,6 +2281,8 @@ function renderMeetingList() {
|
|
|
2645
2281
|
}
|
|
2646
2282
|
|
|
2647
2283
|
function renderMeetingDetail() {
|
|
2284
|
+
renderWorkspaceTabs();
|
|
2285
|
+
|
|
2648
2286
|
if (state.detailError) {
|
|
2649
2287
|
els.empty.hidden = false;
|
|
2650
2288
|
els.empty.textContent = state.detailError;
|
|
@@ -2669,15 +2307,46 @@ function renderMeetingDetail() {
|
|
|
2669
2307
|
'<div class="detail-chip">Transcript: ' + escapeHtml(String(record.meeting.transcriptSegmentCount)) + " segments</div>",
|
|
2670
2308
|
].join("");
|
|
2671
2309
|
|
|
2310
|
+
const bundle = state.selectedMeetingBundle;
|
|
2311
|
+
const metadataLines = [
|
|
2312
|
+
"Title: " + (record.meeting.title || record.meeting.id),
|
|
2313
|
+
"Created: " + record.meeting.createdAt,
|
|
2314
|
+
"Updated: " + record.meeting.updatedAt,
|
|
2315
|
+
"Tags: " + (record.meeting.tags.length ? record.meeting.tags.join(", ") : "none"),
|
|
2316
|
+
"Transcript loaded: " + (record.meeting.transcriptLoaded ? "yes" : "no"),
|
|
2317
|
+
].join("\n");
|
|
2318
|
+
|
|
2319
|
+
let mainTitle = "Notes";
|
|
2320
|
+
let mainBody = record.noteMarkdown || "";
|
|
2321
|
+
|
|
2322
|
+
switch (state.workspaceTab) {
|
|
2323
|
+
case "transcript":
|
|
2324
|
+
mainTitle = "Transcript";
|
|
2325
|
+
mainBody = record.transcriptText || "(Transcript unavailable)";
|
|
2326
|
+
break;
|
|
2327
|
+
case "metadata":
|
|
2328
|
+
mainTitle = "Metadata";
|
|
2329
|
+
mainBody = metadataLines;
|
|
2330
|
+
break;
|
|
2331
|
+
case "raw":
|
|
2332
|
+
mainTitle = "Raw Bundle";
|
|
2333
|
+
mainBody = JSON.stringify(bundle || record, null, 2);
|
|
2334
|
+
break;
|
|
2335
|
+
default:
|
|
2336
|
+
break;
|
|
2337
|
+
}
|
|
2338
|
+
|
|
2672
2339
|
els.detailBody.innerHTML = [
|
|
2673
|
-
'<
|
|
2674
|
-
"
|
|
2675
|
-
|
|
2676
|
-
"</
|
|
2677
|
-
|
|
2678
|
-
"
|
|
2679
|
-
|
|
2340
|
+
'<div class="workspace-grid">',
|
|
2341
|
+
'<aside class="detail-section workspace-sidebar">',
|
|
2342
|
+
"<h2>Meeting Metadata</h2>",
|
|
2343
|
+
'<pre class="detail-pre">' + escapeHtml(metadataLines) + "</pre>",
|
|
2344
|
+
"</aside>",
|
|
2345
|
+
'<section class="detail-section workspace-main">',
|
|
2346
|
+
"<h2>" + escapeHtml(mainTitle) + "</h2>",
|
|
2347
|
+
'<pre class="detail-pre">' + escapeHtml(mainBody) + "</pre>",
|
|
2680
2348
|
"</section>",
|
|
2349
|
+
"</div>",
|
|
2681
2350
|
].join("");
|
|
2682
2351
|
}
|
|
2683
2352
|
|
|
@@ -2733,6 +2402,7 @@ async function loadMeetings(options = {}) {
|
|
|
2733
2402
|
} catch (error) {
|
|
2734
2403
|
state.listError = error instanceof Error ? error.message : String(error);
|
|
2735
2404
|
state.selectedMeeting = null;
|
|
2405
|
+
state.selectedMeetingBundle = null;
|
|
2736
2406
|
state.detailError = state.listError;
|
|
2737
2407
|
renderMeetingList();
|
|
2738
2408
|
renderMeetingDetail();
|
|
@@ -2746,10 +2416,12 @@ async function loadMeeting(id) {
|
|
|
2746
2416
|
try {
|
|
2747
2417
|
state.detailError = "";
|
|
2748
2418
|
const payload = await fetchJson("/meetings/" + encodeURIComponent(id));
|
|
2419
|
+
state.selectedMeetingBundle = payload;
|
|
2749
2420
|
state.selectedMeeting = payload.meeting || null;
|
|
2750
2421
|
renderMeetingDetail();
|
|
2751
2422
|
} catch (error) {
|
|
2752
2423
|
state.selectedMeeting = null;
|
|
2424
|
+
state.selectedMeetingBundle = null;
|
|
2753
2425
|
state.detailError = error instanceof Error ? error.message : String(error);
|
|
2754
2426
|
renderMeetingDetail();
|
|
2755
2427
|
}
|
|
@@ -2891,6 +2563,59 @@ els.quickOpenButton.addEventListener("click", () => {
|
|
|
2891
2563
|
void quickOpenMeeting();
|
|
2892
2564
|
});
|
|
2893
2565
|
|
|
2566
|
+
els.workspaceTabs.forEach((button) => {
|
|
2567
|
+
button.addEventListener("click", () => {
|
|
2568
|
+
state.workspaceTab = button.dataset.workspaceTab || "notes";
|
|
2569
|
+
renderMeetingDetail();
|
|
2570
|
+
});
|
|
2571
|
+
});
|
|
2572
|
+
|
|
2573
|
+
document.addEventListener("keydown", (event) => {
|
|
2574
|
+
if (
|
|
2575
|
+
event.target instanceof HTMLInputElement ||
|
|
2576
|
+
event.target instanceof HTMLSelectElement ||
|
|
2577
|
+
event.target instanceof HTMLTextAreaElement
|
|
2578
|
+
) {
|
|
2579
|
+
return;
|
|
2580
|
+
}
|
|
2581
|
+
|
|
2582
|
+
const tabs = ["notes", "transcript", "metadata", "raw"];
|
|
2583
|
+
if (event.key === "1") {
|
|
2584
|
+
state.workspaceTab = "notes";
|
|
2585
|
+
renderMeetingDetail();
|
|
2586
|
+
return;
|
|
2587
|
+
}
|
|
2588
|
+
|
|
2589
|
+
if (event.key === "2") {
|
|
2590
|
+
state.workspaceTab = "transcript";
|
|
2591
|
+
renderMeetingDetail();
|
|
2592
|
+
return;
|
|
2593
|
+
}
|
|
2594
|
+
|
|
2595
|
+
if (event.key === "3") {
|
|
2596
|
+
state.workspaceTab = "metadata";
|
|
2597
|
+
renderMeetingDetail();
|
|
2598
|
+
return;
|
|
2599
|
+
}
|
|
2600
|
+
|
|
2601
|
+
if (event.key === "4") {
|
|
2602
|
+
state.workspaceTab = "raw";
|
|
2603
|
+
renderMeetingDetail();
|
|
2604
|
+
return;
|
|
2605
|
+
}
|
|
2606
|
+
|
|
2607
|
+
const currentIndex = tabs.indexOf(state.workspaceTab);
|
|
2608
|
+
if (event.key === "]") {
|
|
2609
|
+
state.workspaceTab = tabs[(currentIndex + 1) % tabs.length];
|
|
2610
|
+
renderMeetingDetail();
|
|
2611
|
+
}
|
|
2612
|
+
|
|
2613
|
+
if (event.key === "[") {
|
|
2614
|
+
state.workspaceTab = tabs[(currentIndex + tabs.length - 1) % tabs.length];
|
|
2615
|
+
renderMeetingDetail();
|
|
2616
|
+
}
|
|
2617
|
+
});
|
|
2618
|
+
|
|
2894
2619
|
const events = new EventSource("/events");
|
|
2895
2620
|
events.addEventListener("state.updated", (event) => {
|
|
2896
2621
|
const payload = JSON.parse(event.data);
|
|
@@ -2908,7 +2633,448 @@ void refreshAll().catch((error) => {
|
|
|
2908
2633
|
els.empty.hidden = false;
|
|
2909
2634
|
els.empty.textContent = error.message;
|
|
2910
2635
|
});
|
|
2911
|
-
|
|
2636
|
+
`;
|
|
2637
|
+
//#endregion
|
|
2638
|
+
//#region src/web/markup.ts
|
|
2639
|
+
const granolaWebMarkup = String.raw`
|
|
2640
|
+
<div class="shell">
|
|
2641
|
+
<aside class="pane sidebar">
|
|
2642
|
+
<section class="hero">
|
|
2643
|
+
<h1>Granola Toolkit</h1>
|
|
2644
|
+
<p>Browser workspace for meetings, notes, transcripts, and export flows on top of one local server instance.</p>
|
|
2645
|
+
<input class="search" data-search placeholder="Search meetings, ids, or tags" />
|
|
2646
|
+
<div class="field-row field-row--inline">
|
|
2647
|
+
<label>
|
|
2648
|
+
<span class="field-label">Sort</span>
|
|
2649
|
+
<select class="select" data-sort>
|
|
2650
|
+
<option value="updated-desc">Newest first</option>
|
|
2651
|
+
<option value="updated-asc">Oldest first</option>
|
|
2652
|
+
<option value="title-asc">Title A-Z</option>
|
|
2653
|
+
<option value="title-desc">Title Z-A</option>
|
|
2654
|
+
</select>
|
|
2655
|
+
</label>
|
|
2656
|
+
<label>
|
|
2657
|
+
<span class="field-label">Updated From</span>
|
|
2658
|
+
<input class="field-input" data-updated-from type="date" />
|
|
2659
|
+
</label>
|
|
2660
|
+
</div>
|
|
2661
|
+
<label class="field-row">
|
|
2662
|
+
<span class="field-label">Updated To</span>
|
|
2663
|
+
<input class="field-input" data-updated-to type="date" />
|
|
2664
|
+
</label>
|
|
2665
|
+
</section>
|
|
2666
|
+
<section class="toolbar">
|
|
2667
|
+
<div>
|
|
2668
|
+
<p>Meetings are loaded from the shared server state so this view can later coexist with the terminal UI.</p>
|
|
2669
|
+
</div>
|
|
2670
|
+
<div class="toolbar-form">
|
|
2671
|
+
<input class="field-input" data-quick-open placeholder="Quick open by id or title" />
|
|
2672
|
+
<button class="button button--secondary" data-quick-open-button>Open</button>
|
|
2673
|
+
</div>
|
|
2674
|
+
</section>
|
|
2675
|
+
<section class="meeting-list" data-meeting-list></section>
|
|
2676
|
+
</aside>
|
|
2677
|
+
<main class="pane detail">
|
|
2678
|
+
<section class="detail-head">
|
|
2679
|
+
<div>
|
|
2680
|
+
<h2>Meeting Workspace</h2>
|
|
2681
|
+
<div data-app-state></div>
|
|
2682
|
+
</div>
|
|
2683
|
+
<div class="state-badge" data-state-badge data-tone="idle">Connecting…</div>
|
|
2684
|
+
</section>
|
|
2685
|
+
<section class="toolbar">
|
|
2686
|
+
<div class="toolbar-actions">
|
|
2687
|
+
<button class="button button--primary" data-refresh>Refresh</button>
|
|
2688
|
+
<button class="button button--secondary" data-export-notes>Export Notes</button>
|
|
2689
|
+
<button class="button button--secondary" data-export-transcripts>Export Transcripts</button>
|
|
2690
|
+
</div>
|
|
2691
|
+
<p>Initial beta web client. It speaks to the same local API that future TUI and attach flows will use.</p>
|
|
2692
|
+
</section>
|
|
2693
|
+
<nav class="workspace-tabs">
|
|
2694
|
+
<button class="workspace-tab" data-workspace-tab="notes">Notes</button>
|
|
2695
|
+
<button class="workspace-tab" data-workspace-tab="transcript">Transcript</button>
|
|
2696
|
+
<button class="workspace-tab" data-workspace-tab="metadata">Metadata</button>
|
|
2697
|
+
<button class="workspace-tab" data-workspace-tab="raw">Raw</button>
|
|
2698
|
+
<span class="workspace-hint">1-4 switch tabs, [ and ] cycle</span>
|
|
2699
|
+
</nav>
|
|
2700
|
+
<div class="detail-meta" data-detail-meta></div>
|
|
2701
|
+
<div class="detail-body" data-detail-body>
|
|
2702
|
+
<div class="empty" data-empty>Select a meeting to inspect its notes and transcript.</div>
|
|
2703
|
+
</div>
|
|
2704
|
+
</main>
|
|
2705
|
+
</div>
|
|
2706
|
+
`;
|
|
2707
|
+
//#endregion
|
|
2708
|
+
//#region src/web/styles.ts
|
|
2709
|
+
const granolaWebStyles = String.raw`
|
|
2710
|
+
:root {
|
|
2711
|
+
--bg: #f2ede2;
|
|
2712
|
+
--panel: rgba(255, 252, 247, 0.86);
|
|
2713
|
+
--panel-strong: #fffaf2;
|
|
2714
|
+
--line: rgba(36, 39, 44, 0.12);
|
|
2715
|
+
--ink: #1d242c;
|
|
2716
|
+
--muted: #5d6b77;
|
|
2717
|
+
--accent: #0d6a6d;
|
|
2718
|
+
--accent-soft: rgba(13, 106, 109, 0.12);
|
|
2719
|
+
--warm: #a34f2f;
|
|
2720
|
+
--ok: #246b4f;
|
|
2721
|
+
--error: #9d2c2c;
|
|
2722
|
+
--shadow: 0 24px 80px rgba(40, 32, 16, 0.12);
|
|
2723
|
+
--radius: 24px;
|
|
2724
|
+
--mono: "SF Mono", "IBM Plex Mono", "Cascadia Code", monospace;
|
|
2725
|
+
--serif: "Iowan Old Style", "Palatino Linotype", "Book Antiqua", Georgia, serif;
|
|
2726
|
+
--sans: "Avenir Next", "Segoe UI", sans-serif;
|
|
2727
|
+
}
|
|
2728
|
+
|
|
2729
|
+
* { box-sizing: border-box; }
|
|
2730
|
+
|
|
2731
|
+
body {
|
|
2732
|
+
margin: 0;
|
|
2733
|
+
min-height: 100vh;
|
|
2734
|
+
font-family: var(--sans);
|
|
2735
|
+
color: var(--ink);
|
|
2736
|
+
background:
|
|
2737
|
+
radial-gradient(circle at top left, rgba(163, 79, 47, 0.18), transparent 32%),
|
|
2738
|
+
radial-gradient(circle at right 12%, rgba(13, 106, 109, 0.16), transparent 28%),
|
|
2739
|
+
linear-gradient(180deg, #f8f2e8 0%, var(--bg) 100%);
|
|
2740
|
+
}
|
|
2741
|
+
|
|
2742
|
+
.shell {
|
|
2743
|
+
display: grid;
|
|
2744
|
+
grid-template-columns: 320px minmax(0, 1fr);
|
|
2745
|
+
gap: 18px;
|
|
2746
|
+
min-height: 100vh;
|
|
2747
|
+
padding: 24px;
|
|
2748
|
+
}
|
|
2749
|
+
|
|
2750
|
+
.pane {
|
|
2751
|
+
background: var(--panel);
|
|
2752
|
+
backdrop-filter: blur(18px);
|
|
2753
|
+
border: 1px solid var(--line);
|
|
2754
|
+
border-radius: var(--radius);
|
|
2755
|
+
box-shadow: var(--shadow);
|
|
2756
|
+
}
|
|
2757
|
+
|
|
2758
|
+
.sidebar {
|
|
2759
|
+
display: grid;
|
|
2760
|
+
grid-template-rows: auto auto 1fr;
|
|
2761
|
+
overflow: hidden;
|
|
2762
|
+
}
|
|
2763
|
+
|
|
2764
|
+
.hero, .toolbar, .detail-head {
|
|
2765
|
+
padding: 22px 24px;
|
|
2766
|
+
border-bottom: 1px solid var(--line);
|
|
2767
|
+
}
|
|
2768
|
+
|
|
2769
|
+
.hero h1 {
|
|
2770
|
+
margin: 0;
|
|
2771
|
+
font-family: var(--serif);
|
|
2772
|
+
font-size: clamp(2rem, 3vw, 2.8rem);
|
|
2773
|
+
font-weight: 600;
|
|
2774
|
+
letter-spacing: -0.04em;
|
|
2775
|
+
}
|
|
2776
|
+
|
|
2777
|
+
.hero p, .toolbar p {
|
|
2778
|
+
margin: 8px 0 0;
|
|
2779
|
+
color: var(--muted);
|
|
2780
|
+
line-height: 1.5;
|
|
2781
|
+
}
|
|
2782
|
+
|
|
2783
|
+
.search,
|
|
2784
|
+
.select,
|
|
2785
|
+
.field-input {
|
|
2786
|
+
width: 100%;
|
|
2787
|
+
margin-top: 16px;
|
|
2788
|
+
padding: 12px 14px;
|
|
2789
|
+
border: 1px solid var(--line);
|
|
2790
|
+
border-radius: 999px;
|
|
2791
|
+
background: rgba(255, 255, 255, 0.7);
|
|
2792
|
+
color: var(--ink);
|
|
2793
|
+
font: inherit;
|
|
2794
|
+
}
|
|
2795
|
+
|
|
2796
|
+
.field-row {
|
|
2797
|
+
display: grid;
|
|
2798
|
+
gap: 10px;
|
|
2799
|
+
margin-top: 12px;
|
|
2800
|
+
}
|
|
2801
|
+
|
|
2802
|
+
.field-row--inline {
|
|
2803
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
2804
|
+
}
|
|
2805
|
+
|
|
2806
|
+
.field-label {
|
|
2807
|
+
display: block;
|
|
2808
|
+
margin-bottom: 6px;
|
|
2809
|
+
color: var(--muted);
|
|
2810
|
+
font-size: 0.78rem;
|
|
2811
|
+
font-weight: 700;
|
|
2812
|
+
letter-spacing: 0.08em;
|
|
2813
|
+
text-transform: uppercase;
|
|
2814
|
+
}
|
|
2815
|
+
|
|
2816
|
+
.meeting-list {
|
|
2817
|
+
padding: 14px;
|
|
2818
|
+
overflow: auto;
|
|
2819
|
+
}
|
|
2820
|
+
|
|
2821
|
+
.meeting-row {
|
|
2822
|
+
width: 100%;
|
|
2823
|
+
display: grid;
|
|
2824
|
+
gap: 4px;
|
|
2825
|
+
text-align: left;
|
|
2826
|
+
margin: 0 0 10px;
|
|
2827
|
+
padding: 14px 16px;
|
|
2828
|
+
border: 1px solid transparent;
|
|
2829
|
+
border-radius: 18px;
|
|
2830
|
+
background: rgba(255, 255, 255, 0.72);
|
|
2831
|
+
color: inherit;
|
|
2832
|
+
cursor: pointer;
|
|
2833
|
+
transition: transform 140ms ease, border-color 140ms ease, background 140ms ease;
|
|
2834
|
+
}
|
|
2835
|
+
|
|
2836
|
+
.meeting-row:hover,
|
|
2837
|
+
.meeting-row[data-selected="true"] {
|
|
2838
|
+
transform: translateY(-1px);
|
|
2839
|
+
border-color: rgba(13, 106, 109, 0.25);
|
|
2840
|
+
background: var(--panel-strong);
|
|
2841
|
+
}
|
|
2842
|
+
|
|
2843
|
+
.meeting-row__title {
|
|
2844
|
+
font-weight: 600;
|
|
2845
|
+
}
|
|
2846
|
+
|
|
2847
|
+
.meeting-row__meta {
|
|
2848
|
+
color: var(--muted);
|
|
2849
|
+
font-size: 0.92rem;
|
|
2850
|
+
}
|
|
2851
|
+
|
|
2852
|
+
.meeting-empty {
|
|
2853
|
+
padding: 18px;
|
|
2854
|
+
color: var(--muted);
|
|
2855
|
+
}
|
|
2856
|
+
|
|
2857
|
+
.meeting-empty--error {
|
|
2858
|
+
color: var(--error);
|
|
2859
|
+
}
|
|
2860
|
+
|
|
2861
|
+
.detail {
|
|
2862
|
+
display: grid;
|
|
2863
|
+
grid-template-rows: auto auto 1fr;
|
|
2864
|
+
min-width: 0;
|
|
2865
|
+
}
|
|
2866
|
+
|
|
2867
|
+
.detail-head {
|
|
2868
|
+
display: flex;
|
|
2869
|
+
align-items: center;
|
|
2870
|
+
justify-content: space-between;
|
|
2871
|
+
gap: 18px;
|
|
2872
|
+
}
|
|
2873
|
+
|
|
2874
|
+
.detail-head h2 {
|
|
2875
|
+
margin: 0;
|
|
2876
|
+
font-family: var(--serif);
|
|
2877
|
+
font-size: clamp(1.8rem, 2.4vw, 2.4rem);
|
|
2878
|
+
font-weight: 600;
|
|
2879
|
+
}
|
|
2880
|
+
|
|
2881
|
+
.state-badge {
|
|
2882
|
+
padding: 10px 14px;
|
|
2883
|
+
border-radius: 999px;
|
|
2884
|
+
background: var(--accent-soft);
|
|
2885
|
+
color: var(--accent);
|
|
2886
|
+
font-size: 0.92rem;
|
|
2887
|
+
font-weight: 700;
|
|
2888
|
+
}
|
|
2889
|
+
|
|
2890
|
+
.state-badge[data-tone="busy"] { color: var(--warm); background: rgba(163, 79, 47, 0.12); }
|
|
2891
|
+
.state-badge[data-tone="error"] { color: var(--error); background: rgba(157, 44, 44, 0.12); }
|
|
2892
|
+
.state-badge[data-tone="ok"] { color: var(--ok); background: rgba(36, 107, 79, 0.12); }
|
|
2893
|
+
|
|
2894
|
+
.toolbar {
|
|
2895
|
+
display: flex;
|
|
2896
|
+
flex-wrap: wrap;
|
|
2897
|
+
align-items: center;
|
|
2898
|
+
justify-content: space-between;
|
|
2899
|
+
gap: 14px;
|
|
2900
|
+
}
|
|
2901
|
+
|
|
2902
|
+
.toolbar-actions {
|
|
2903
|
+
display: flex;
|
|
2904
|
+
flex-wrap: wrap;
|
|
2905
|
+
gap: 10px;
|
|
2906
|
+
}
|
|
2907
|
+
|
|
2908
|
+
.toolbar-form {
|
|
2909
|
+
display: grid;
|
|
2910
|
+
grid-template-columns: minmax(0, 1fr) auto;
|
|
2911
|
+
gap: 10px;
|
|
2912
|
+
width: min(440px, 100%);
|
|
2913
|
+
}
|
|
2914
|
+
|
|
2915
|
+
.workspace-tabs {
|
|
2916
|
+
display: flex;
|
|
2917
|
+
flex-wrap: wrap;
|
|
2918
|
+
align-items: center;
|
|
2919
|
+
gap: 10px;
|
|
2920
|
+
padding: 0 24px 18px;
|
|
2921
|
+
}
|
|
2922
|
+
|
|
2923
|
+
.workspace-tab {
|
|
2924
|
+
border: 1px solid var(--line);
|
|
2925
|
+
border-radius: 999px;
|
|
2926
|
+
padding: 10px 14px;
|
|
2927
|
+
background: rgba(255, 255, 255, 0.72);
|
|
2928
|
+
color: var(--muted);
|
|
2929
|
+
cursor: pointer;
|
|
2930
|
+
font: inherit;
|
|
2931
|
+
font-weight: 700;
|
|
2932
|
+
}
|
|
2933
|
+
|
|
2934
|
+
.workspace-tab[data-selected="true"] {
|
|
2935
|
+
background: var(--ink);
|
|
2936
|
+
color: white;
|
|
2937
|
+
border-color: var(--ink);
|
|
2938
|
+
}
|
|
2939
|
+
|
|
2940
|
+
.workspace-hint {
|
|
2941
|
+
color: var(--muted);
|
|
2942
|
+
font-size: 0.86rem;
|
|
2943
|
+
margin-left: auto;
|
|
2944
|
+
}
|
|
2945
|
+
|
|
2946
|
+
.button {
|
|
2947
|
+
border: 0;
|
|
2948
|
+
border-radius: 999px;
|
|
2949
|
+
padding: 12px 16px;
|
|
2950
|
+
font: inherit;
|
|
2951
|
+
font-weight: 700;
|
|
2952
|
+
cursor: pointer;
|
|
2953
|
+
}
|
|
2954
|
+
|
|
2955
|
+
.button--primary {
|
|
2956
|
+
background: var(--ink);
|
|
2957
|
+
color: white;
|
|
2958
|
+
}
|
|
2959
|
+
|
|
2960
|
+
.button--secondary {
|
|
2961
|
+
background: rgba(255, 255, 255, 0.72);
|
|
2962
|
+
color: var(--ink);
|
|
2963
|
+
border: 1px solid var(--line);
|
|
2964
|
+
}
|
|
2965
|
+
|
|
2966
|
+
.status-grid {
|
|
2967
|
+
display: grid;
|
|
2968
|
+
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
|
2969
|
+
gap: 14px;
|
|
2970
|
+
}
|
|
2971
|
+
|
|
2972
|
+
.status-label {
|
|
2973
|
+
display: block;
|
|
2974
|
+
margin-bottom: 6px;
|
|
2975
|
+
color: var(--muted);
|
|
2976
|
+
font-size: 0.78rem;
|
|
2977
|
+
letter-spacing: 0.08em;
|
|
2978
|
+
text-transform: uppercase;
|
|
2979
|
+
}
|
|
2980
|
+
|
|
2981
|
+
.detail-meta {
|
|
2982
|
+
display: flex;
|
|
2983
|
+
flex-wrap: wrap;
|
|
2984
|
+
gap: 10px;
|
|
2985
|
+
padding: 0 24px 18px;
|
|
2986
|
+
}
|
|
2987
|
+
|
|
2988
|
+
.detail-chip {
|
|
2989
|
+
padding: 10px 12px;
|
|
2990
|
+
border-radius: 999px;
|
|
2991
|
+
background: rgba(255, 255, 255, 0.72);
|
|
2992
|
+
border: 1px solid var(--line);
|
|
2993
|
+
color: var(--muted);
|
|
2994
|
+
font-size: 0.88rem;
|
|
2995
|
+
}
|
|
2996
|
+
|
|
2997
|
+
.detail-body {
|
|
2998
|
+
padding: 0 24px 24px;
|
|
2999
|
+
overflow: auto;
|
|
3000
|
+
}
|
|
3001
|
+
|
|
3002
|
+
.workspace-grid {
|
|
3003
|
+
display: grid;
|
|
3004
|
+
grid-template-columns: minmax(240px, 320px) minmax(0, 1fr);
|
|
3005
|
+
gap: 18px;
|
|
3006
|
+
}
|
|
3007
|
+
|
|
3008
|
+
.workspace-sidebar,
|
|
3009
|
+
.workspace-main {
|
|
3010
|
+
margin-bottom: 0;
|
|
3011
|
+
}
|
|
3012
|
+
|
|
3013
|
+
.detail-section {
|
|
3014
|
+
margin-bottom: 20px;
|
|
3015
|
+
padding: 20px;
|
|
3016
|
+
background: rgba(255, 255, 255, 0.72);
|
|
3017
|
+
border: 1px solid var(--line);
|
|
3018
|
+
border-radius: 20px;
|
|
3019
|
+
}
|
|
3020
|
+
|
|
3021
|
+
.detail-section h2 {
|
|
3022
|
+
margin: 0 0 14px;
|
|
3023
|
+
font-size: 1rem;
|
|
3024
|
+
letter-spacing: 0.08em;
|
|
3025
|
+
text-transform: uppercase;
|
|
3026
|
+
}
|
|
3027
|
+
|
|
3028
|
+
.detail-pre {
|
|
3029
|
+
margin: 0;
|
|
3030
|
+
white-space: pre-wrap;
|
|
3031
|
+
word-break: break-word;
|
|
3032
|
+
font-family: var(--mono);
|
|
3033
|
+
line-height: 1.55;
|
|
3034
|
+
}
|
|
3035
|
+
|
|
3036
|
+
.empty {
|
|
3037
|
+
margin: 24px;
|
|
3038
|
+
padding: 24px;
|
|
3039
|
+
border-radius: 20px;
|
|
3040
|
+
background: rgba(255, 255, 255, 0.72);
|
|
3041
|
+
border: 1px dashed rgba(36, 39, 44, 0.2);
|
|
3042
|
+
color: var(--muted);
|
|
3043
|
+
}
|
|
3044
|
+
|
|
3045
|
+
@media (max-width: 900px) {
|
|
3046
|
+
.shell {
|
|
3047
|
+
grid-template-columns: 1fr;
|
|
3048
|
+
}
|
|
3049
|
+
|
|
3050
|
+
.field-row--inline,
|
|
3051
|
+
.toolbar-form,
|
|
3052
|
+
.workspace-grid {
|
|
3053
|
+
grid-template-columns: 1fr;
|
|
3054
|
+
}
|
|
3055
|
+
|
|
3056
|
+
.workspace-hint {
|
|
3057
|
+
margin-left: 0;
|
|
3058
|
+
}
|
|
3059
|
+
}
|
|
3060
|
+
`;
|
|
3061
|
+
//#endregion
|
|
3062
|
+
//#region src/server/web.ts
|
|
3063
|
+
function renderGranolaWebPage() {
|
|
3064
|
+
return `<!doctype html>
|
|
3065
|
+
<html lang="en">
|
|
3066
|
+
<head>
|
|
3067
|
+
<meta charset="utf-8" />
|
|
3068
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
3069
|
+
<title>Granola Toolkit</title>
|
|
3070
|
+
<style>
|
|
3071
|
+
${granolaWebStyles}
|
|
3072
|
+
</style>
|
|
3073
|
+
</head>
|
|
3074
|
+
<body>
|
|
3075
|
+
${granolaWebMarkup}
|
|
3076
|
+
<script type="module">
|
|
3077
|
+
${granolaWebClientScript}
|
|
2912
3078
|
<\/script>
|
|
2913
3079
|
</body>
|
|
2914
3080
|
</html>`;
|