@deskwork/studio 0.9.5
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/dist/build-client-assets.d.ts +51 -0
- package/dist/build-client-assets.d.ts.map +1 -0
- package/dist/build-client-assets.js +341 -0
- package/dist/build-client-assets.js.map +1 -0
- package/dist/components/scrapbook-item.d.ts +108 -0
- package/dist/components/scrapbook-item.d.ts.map +1 -0
- package/dist/components/scrapbook-item.js +205 -0
- package/dist/components/scrapbook-item.js.map +1 -0
- package/dist/lib/editorial-skills-catalogue.d.ts +33 -0
- package/dist/lib/editorial-skills-catalogue.d.ts.map +1 -0
- package/dist/lib/editorial-skills-catalogue.js +211 -0
- package/dist/lib/editorial-skills-catalogue.js.map +1 -0
- package/dist/lib/override-render.d.ts +41 -0
- package/dist/lib/override-render.d.ts.map +1 -0
- package/dist/lib/override-render.js +80 -0
- package/dist/lib/override-render.js.map +1 -0
- package/dist/listen.d.ts +78 -0
- package/dist/listen.d.ts.map +1 -0
- package/dist/listen.js +155 -0
- package/dist/listen.js.map +1 -0
- package/dist/pages/chrome.d.ts +26 -0
- package/dist/pages/chrome.d.ts.map +1 -0
- package/dist/pages/chrome.js +50 -0
- package/dist/pages/chrome.js.map +1 -0
- package/dist/pages/content-detail.d.ts +14 -0
- package/dist/pages/content-detail.d.ts.map +1 -0
- package/dist/pages/content-detail.js +279 -0
- package/dist/pages/content-detail.js.map +1 -0
- package/dist/pages/content.d.ts +39 -0
- package/dist/pages/content.d.ts.map +1 -0
- package/dist/pages/content.js +414 -0
- package/dist/pages/content.js.map +1 -0
- package/dist/pages/dashboard.d.ts +32 -0
- package/dist/pages/dashboard.d.ts.map +1 -0
- package/dist/pages/dashboard.js +803 -0
- package/dist/pages/dashboard.js.map +1 -0
- package/dist/pages/help.d.ts +24 -0
- package/dist/pages/help.d.ts.map +1 -0
- package/dist/pages/help.js +433 -0
- package/dist/pages/help.js.map +1 -0
- package/dist/pages/html.d.ts +35 -0
- package/dist/pages/html.d.ts.map +1 -0
- package/dist/pages/html.js +73 -0
- package/dist/pages/html.js.map +1 -0
- package/dist/pages/index.d.ts +21 -0
- package/dist/pages/index.d.ts.map +1 -0
- package/dist/pages/index.js +174 -0
- package/dist/pages/index.js.map +1 -0
- package/dist/pages/layout.d.ts +33 -0
- package/dist/pages/layout.d.ts.map +1 -0
- package/dist/pages/layout.js +50 -0
- package/dist/pages/layout.js.map +1 -0
- package/dist/pages/review-scrapbook-drawer.d.ts +20 -0
- package/dist/pages/review-scrapbook-drawer.d.ts.map +1 -0
- package/dist/pages/review-scrapbook-drawer.js +98 -0
- package/dist/pages/review-scrapbook-drawer.js.map +1 -0
- package/dist/pages/review.d.ts +68 -0
- package/dist/pages/review.d.ts.map +1 -0
- package/dist/pages/review.js +434 -0
- package/dist/pages/review.js.map +1 -0
- package/dist/pages/scrapbook.d.ts +21 -0
- package/dist/pages/scrapbook.d.ts.map +1 -0
- package/dist/pages/scrapbook.js +250 -0
- package/dist/pages/scrapbook.js.map +1 -0
- package/dist/pages/shortform.d.ts +17 -0
- package/dist/pages/shortform.d.ts.map +1 -0
- package/dist/pages/shortform.js +142 -0
- package/dist/pages/shortform.js.map +1 -0
- package/dist/request-context.d.ts +52 -0
- package/dist/request-context.d.ts.map +1 -0
- package/dist/request-context.js +84 -0
- package/dist/request-context.js.map +1 -0
- package/dist/routes/api.d.ts +36 -0
- package/dist/routes/api.d.ts.map +1 -0
- package/dist/routes/api.js +175 -0
- package/dist/routes/api.js.map +1 -0
- package/dist/routes/scrapbook-file.d.ts +19 -0
- package/dist/routes/scrapbook-file.d.ts.map +1 -0
- package/dist/routes/scrapbook-file.js +77 -0
- package/dist/routes/scrapbook-file.js.map +1 -0
- package/dist/routes/scrapbook-mutations.d.ts +33 -0
- package/dist/routes/scrapbook-mutations.d.ts.map +1 -0
- package/dist/routes/scrapbook-mutations.js +310 -0
- package/dist/routes/scrapbook-mutations.js.map +1 -0
- package/dist/server.d.ts +52 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +581 -0
- package/dist/server.js.map +1 -0
- package/dist/tailscale.d.ts +63 -0
- package/dist/tailscale.d.ts.map +1 -0
- package/dist/tailscale.js +118 -0
- package/dist/tailscale.js.map +1 -0
- package/package.json +60 -0
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scrapbook viewer — `/dev/scrapbook/:site/<path>` (path may include `/`).
|
|
3
|
+
*
|
|
4
|
+
* Reads the scrapbook directory at the given path and lists every
|
|
5
|
+
* file with type chips + relative timestamps, plus secret items
|
|
6
|
+
* (inside `scrapbook/secret/`) in a quiet second section. Empty
|
|
7
|
+
* scrapbooks render an empty state with quick-add affordances.
|
|
8
|
+
*
|
|
9
|
+
* The `path` argument is the hierarchical address of the scrapbook —
|
|
10
|
+
* any slash-separated kebab-case identifier under the site's
|
|
11
|
+
* contentDir. It does not need to correspond to a calendar entry;
|
|
12
|
+
* organizational nodes (e.g. `the-outbound/characters` with no
|
|
13
|
+
* own README) can host their own scrapbooks too.
|
|
14
|
+
*
|
|
15
|
+
* Port of `pages/dev/scrapbook/[site]/[slug].astro`. Layout swap
|
|
16
|
+
* (Astro `<Layout>` → studio shell) and CSS link added; structurally
|
|
17
|
+
* similar otherwise.
|
|
18
|
+
*/
|
|
19
|
+
import { formatRelativeTime, formatSize, listScrapbook, } from '@deskwork/core/scrapbook';
|
|
20
|
+
import { html, unsafe } from "./html.js";
|
|
21
|
+
import { layout } from "./layout.js";
|
|
22
|
+
import { renderEditorialFolio } from "./chrome.js";
|
|
23
|
+
function renderItemRow(item, index, opts = {}) {
|
|
24
|
+
const { secret = false, withTools = true } = opts;
|
|
25
|
+
const editBtn = withTools && item.kind === 'md'
|
|
26
|
+
? unsafe(html `<button type="button" class="scrapbook-tool" data-action="edit">edit</button>`)
|
|
27
|
+
: '';
|
|
28
|
+
const seq = String(index + 1).padStart(2, '0');
|
|
29
|
+
const kindLabel = item.kind === 'other' ? '·' : item.kind.toUpperCase();
|
|
30
|
+
const idPrefix = secret ? 'secret-' : '';
|
|
31
|
+
const dataSecret = secret ? ' data-secret="true"' : '';
|
|
32
|
+
// The "mark secret/public" toggle is the cross-section rename
|
|
33
|
+
// affordance. The button label flips with the source section.
|
|
34
|
+
const sectionToggleLabel = secret ? 'mark public' : 'mark secret';
|
|
35
|
+
const toolbar = withTools
|
|
36
|
+
? unsafe(html `<div class="scrapbook-toolbar" data-toolbar>
|
|
37
|
+
${editBtn}
|
|
38
|
+
<button type="button" class="scrapbook-tool" data-action="rename">rename</button>
|
|
39
|
+
<button type="button" class="scrapbook-tool" data-action="toggle-secret">${sectionToggleLabel}</button>
|
|
40
|
+
<button type="button" class="scrapbook-tool scrapbook-tool--delete" data-action="delete">delete</button>
|
|
41
|
+
</div>`)
|
|
42
|
+
: '';
|
|
43
|
+
return unsafe(html `
|
|
44
|
+
<li class="scrapbook-item${secret ? ' scrapbook-item--secret' : ''}" data-state="closed" data-open="false"
|
|
45
|
+
data-filename="${item.name}" data-kind="${item.kind}"
|
|
46
|
+
data-size="${item.size}" data-mtime="${item.mtime}"${unsafe(dataSecret)}
|
|
47
|
+
id="${idPrefix}item-${encodeURIComponent(item.name)}">
|
|
48
|
+
<button type="button" class="scrapbook-item-header" aria-expanded="false">
|
|
49
|
+
<span class="scrapbook-seq" aria-hidden="true">§ ${seq}</span>
|
|
50
|
+
<span class="scrapbook-kind scrapbook-kind--${item.kind}" aria-hidden="true">${kindLabel}</span>
|
|
51
|
+
<span class="scrapbook-filename" data-filename-cell>${item.name}</span>
|
|
52
|
+
<time class="scrapbook-mtime" datetime="${item.mtime}">${formatRelativeTime(item.mtime)}</time>
|
|
53
|
+
<span class="scrapbook-disclosure" aria-hidden="true">▸</span>
|
|
54
|
+
</button>
|
|
55
|
+
${toolbar}
|
|
56
|
+
<div class="scrapbook-perforation" aria-hidden="true"></div>
|
|
57
|
+
<div class="scrapbook-item-body" data-body>
|
|
58
|
+
<div data-body-content></div>
|
|
59
|
+
</div>
|
|
60
|
+
</li>`);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Build a hierarchical breadcrumb from the path. Each segment links to
|
|
64
|
+
* the scrapbook view for its prefix. The root segment (site) just
|
|
65
|
+
* goes back to the editorial dashboard.
|
|
66
|
+
*/
|
|
67
|
+
function renderBreadcrumb(site, path) {
|
|
68
|
+
const segments = path.split('/');
|
|
69
|
+
const links = [];
|
|
70
|
+
for (let i = 0; i < segments.length; i++) {
|
|
71
|
+
const prefix = segments.slice(0, i + 1).join('/');
|
|
72
|
+
const isLast = i === segments.length - 1;
|
|
73
|
+
if (isLast) {
|
|
74
|
+
links.push(html `<span class="scrapbook-breadcrumb-current">${segments[i]}</span>`);
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
links.push(html `<a class="scrapbook-breadcrumb-link" href="/dev/scrapbook/${site}/${prefix}">${segments[i]}</a>`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
const sep = '<span class="scrapbook-breadcrumb-sep" aria-hidden="true">›</span>';
|
|
81
|
+
const joined = links.join(`\n${sep}\n`);
|
|
82
|
+
return unsafe(html `
|
|
83
|
+
<nav class="scrapbook-breadcrumb" aria-label="scrapbook hierarchy">
|
|
84
|
+
<a class="scrapbook-breadcrumb-link" href="/dev/editorial-studio">${site}</a>
|
|
85
|
+
<span class="scrapbook-breadcrumb-sep" aria-hidden="true">›</span>
|
|
86
|
+
${unsafe(joined)}
|
|
87
|
+
</nav>`);
|
|
88
|
+
}
|
|
89
|
+
function renderIndexSidebar(items, site, path) {
|
|
90
|
+
const totalBytes = items.reduce((acc, item) => acc + item.size, 0);
|
|
91
|
+
const lastModified = items.length > 0
|
|
92
|
+
? items.reduce((a, b) => (a.mtime > b.mtime ? a : b)).mtime
|
|
93
|
+
: null;
|
|
94
|
+
return unsafe(html `
|
|
95
|
+
<aside class="scrapbook-index">
|
|
96
|
+
<p class="scrapbook-index-kicker">
|
|
97
|
+
<span aria-hidden="true">§</span> The folder
|
|
98
|
+
</p>
|
|
99
|
+
<p class="scrapbook-index-meta">${path}</p>
|
|
100
|
+
<p class="scrapbook-index-meta">${site}</p>
|
|
101
|
+
<hr />
|
|
102
|
+
<ol class="scrapbook-index-list" data-scrapbook-index>
|
|
103
|
+
${items.map((item, i) => unsafe(html `<li data-index-for="${item.name}">
|
|
104
|
+
<span class="scrapbook-index-num">No. ${String(i + 1).padStart(2, '0')}</span>
|
|
105
|
+
<a href="#item-${encodeURIComponent(item.name)}">${item.name}</a>
|
|
106
|
+
</li>`))}
|
|
107
|
+
</ol>
|
|
108
|
+
<hr />
|
|
109
|
+
<p class="scrapbook-index-totals">${items.length} ${items.length === 1 ? 'item' : 'items'} · ${formatSize(totalBytes)}</p>
|
|
110
|
+
${lastModified
|
|
111
|
+
? unsafe(html `<p class="scrapbook-index-subtotal">last modified ${formatRelativeTime(lastModified)}</p>`)
|
|
112
|
+
: ''}
|
|
113
|
+
<hr />
|
|
114
|
+
<div class="scrapbook-index-actions">
|
|
115
|
+
<button type="button" class="scrapbook-index-btn" data-action="new-note">+ new note</button>
|
|
116
|
+
<button type="button" class="scrapbook-index-btn" data-action="upload">+ upload file</button>
|
|
117
|
+
</div>
|
|
118
|
+
<hr />
|
|
119
|
+
<p class="scrapbook-index-path">${site}/${path}/scrapbook/</p>
|
|
120
|
+
</aside>`);
|
|
121
|
+
}
|
|
122
|
+
function renderEmpty() {
|
|
123
|
+
return unsafe(html `
|
|
124
|
+
<section class="scrapbook-empty">
|
|
125
|
+
<p>
|
|
126
|
+
This scrapbook is empty. Write the first note, or drop a file
|
|
127
|
+
anywhere on this page.
|
|
128
|
+
</p>
|
|
129
|
+
<div class="scrapbook-empty-actions">
|
|
130
|
+
<button type="button" class="scrapbook-index-btn" data-action="new-note">+ new note</button>
|
|
131
|
+
<button type="button" class="scrapbook-index-btn" data-action="upload">+ upload file</button>
|
|
132
|
+
</div>
|
|
133
|
+
</section>`);
|
|
134
|
+
}
|
|
135
|
+
function renderReadingPanel(items) {
|
|
136
|
+
return unsafe(html `
|
|
137
|
+
<section class="scrapbook-reading">
|
|
138
|
+
<form class="scrapbook-composer" data-scrapbook-composer hidden>
|
|
139
|
+
<div class="scrapbook-composer-header">
|
|
140
|
+
<span class="scrapbook-composer-seq" aria-hidden="true">✎</span>
|
|
141
|
+
<span class="scrapbook-composer-kind">NEW</span>
|
|
142
|
+
<input type="text" class="scrapbook-composer-filename" data-composer-filename
|
|
143
|
+
placeholder="note-name.md" aria-label="new note filename" />
|
|
144
|
+
<div class="scrapbook-editor-footer scrapbook-composer-actions">
|
|
145
|
+
<label class="scrapbook-secret-toggle" title="save under scrapbook/secret/ — never published">
|
|
146
|
+
<input type="checkbox" data-composer-secret />
|
|
147
|
+
<span>secret</span>
|
|
148
|
+
</label>
|
|
149
|
+
<button type="button" class="scrapbook-tool" data-action="composer-cancel">cancel</button>
|
|
150
|
+
<button type="submit" class="scrapbook-tool scrapbook-tool--primary" data-action="composer-save">save →</button>
|
|
151
|
+
</div>
|
|
152
|
+
</div>
|
|
153
|
+
<div class="scrapbook-composer-body">
|
|
154
|
+
<textarea data-composer-body
|
|
155
|
+
placeholder="Write the note in markdown. Cmd/Ctrl+S saves."
|
|
156
|
+
aria-label="new note body"></textarea>
|
|
157
|
+
</div>
|
|
158
|
+
</form>
|
|
159
|
+
<ol class="scrapbook-items" data-scrapbook-items>
|
|
160
|
+
${items.map((item, i) => renderItemRow(item, i))}
|
|
161
|
+
</ol>
|
|
162
|
+
<div class="scrapbook-drop" data-scrapbook-drop role="button" tabindex="0"
|
|
163
|
+
aria-label="upload a file to the scrapbook">
|
|
164
|
+
<span class="scrapbook-drop-label">── drop a file here, or pick one ──</span>
|
|
165
|
+
<input type="file" data-scrapbook-file-input
|
|
166
|
+
accept="image/*,application/json,text/plain,text/markdown,.md,.json,.txt" />
|
|
167
|
+
<label class="scrapbook-secret-toggle scrapbook-secret-toggle--upload"
|
|
168
|
+
title="save the upload under scrapbook/secret/ — never published">
|
|
169
|
+
<input type="checkbox" data-upload-secret />
|
|
170
|
+
<span>upload as secret</span>
|
|
171
|
+
</label>
|
|
172
|
+
</div>
|
|
173
|
+
</section>`);
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Quiet second section listing items inside `scrapbook/secret/`. Read-
|
|
177
|
+
* only in v1 — operators populate the directory by hand or via the
|
|
178
|
+
* core API; the studio surface just shows what's there. The "private"
|
|
179
|
+
* badge gives unmistakable visual differentiation from the public
|
|
180
|
+
* items above.
|
|
181
|
+
*/
|
|
182
|
+
function renderSecretSection(items) {
|
|
183
|
+
return unsafe(html `
|
|
184
|
+
<section class="scrapbook-secret" data-scrapbook-secret>
|
|
185
|
+
<header class="scrapbook-secret-header">
|
|
186
|
+
<span class="scrapbook-secret-mark" aria-hidden="true">⚿</span>
|
|
187
|
+
<h2 class="scrapbook-secret-title">Secret</h2>
|
|
188
|
+
<span class="scrapbook-secret-badge" aria-label="private — never published">
|
|
189
|
+
private
|
|
190
|
+
</span>
|
|
191
|
+
<span class="scrapbook-secret-count">
|
|
192
|
+
${items.length} ${items.length === 1 ? 'item' : 'items'}
|
|
193
|
+
</span>
|
|
194
|
+
</header>
|
|
195
|
+
<p class="scrapbook-secret-help">
|
|
196
|
+
Items inside <code>scrapbook/secret/</code>. Excluded from the
|
|
197
|
+
public site by the host's content-collection patterns.
|
|
198
|
+
</p>
|
|
199
|
+
<ol class="scrapbook-items scrapbook-items--secret">
|
|
200
|
+
${items.map((item, i) => renderItemRow(item, i, { secret: true, withTools: true }))}
|
|
201
|
+
</ol>
|
|
202
|
+
</section>`);
|
|
203
|
+
}
|
|
204
|
+
export function renderScrapbookPage(ctx, site, path) {
|
|
205
|
+
// Validate site against the project's configured site list. Without
|
|
206
|
+
// this check, an unknown site key reaches the path resolver and
|
|
207
|
+
// produces either an opaque error or a path traversal vector.
|
|
208
|
+
if (!(site in ctx.config.sites)) {
|
|
209
|
+
throw new Error(`unknown site: ${site}`);
|
|
210
|
+
}
|
|
211
|
+
const summary = listScrapbook(ctx.projectRoot, ctx.config, site, path);
|
|
212
|
+
const items = summary.items;
|
|
213
|
+
const secretItems = summary.secretItems;
|
|
214
|
+
const publicBlock = items.length === 0
|
|
215
|
+
? renderEmpty().__raw
|
|
216
|
+
: renderReadingPanel(items).__raw + renderIndexSidebar(items, site, path).__raw;
|
|
217
|
+
const secretBlock = secretItems.length > 0 ? renderSecretSection(secretItems).__raw : '';
|
|
218
|
+
const body = html `
|
|
219
|
+
${renderEditorialFolio('content', `scrapbook · ${site}/${path}`)}
|
|
220
|
+
<main class="scrapbook-page" data-site="${site}" data-slug="${path}" data-scrapbook-root>
|
|
221
|
+
<header class="er-pagehead er-pagehead--compact scrapbook-header">
|
|
222
|
+
${renderBreadcrumb(site, path)}
|
|
223
|
+
<p class="er-pagehead__kicker scrapbook-kicker">
|
|
224
|
+
<span class="scrapbook-kicker-mark" aria-hidden="true">§</span>
|
|
225
|
+
Scrapbook
|
|
226
|
+
</p>
|
|
227
|
+
<h1 class="er-pagehead__title scrapbook-title">${path}</h1>
|
|
228
|
+
<a class="scrapbook-back" href="/dev/editorial-studio">← back to the desk</a>
|
|
229
|
+
</header>
|
|
230
|
+
<div class="scrapbook-status" data-scrapbook-status hidden></div>
|
|
231
|
+
${unsafe(publicBlock)}
|
|
232
|
+
${unsafe(secretBlock)}
|
|
233
|
+
<div class="scrapbook-drop-overlay" data-scrapbook-overlay aria-hidden="true">
|
|
234
|
+
<span class="scrapbook-drop-overlay-text">drop to add to the scrapbook ◇</span>
|
|
235
|
+
</div>
|
|
236
|
+
</main>`;
|
|
237
|
+
return layout({
|
|
238
|
+
title: `scrapbook · ${path} — dev`,
|
|
239
|
+
cssHrefs: [
|
|
240
|
+
'/static/css/editorial-review.css',
|
|
241
|
+
'/static/css/editorial-nav.css',
|
|
242
|
+
'/static/css/scrapbook.css',
|
|
243
|
+
'/static/css/blog-figure.css',
|
|
244
|
+
],
|
|
245
|
+
bodyAttrs: 'data-review-ui="studio"',
|
|
246
|
+
bodyHtml: body,
|
|
247
|
+
scriptModules: ['/static/dist/scrapbook-client.js'],
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
//# sourceMappingURL=scrapbook.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scrapbook.js","sourceRoot":"","sources":["../../src/pages/scrapbook.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shortform desk index — `/dev/editorial-review-shortform`.
|
|
3
|
+
*
|
|
4
|
+
* Phase 21c: this page used to render compose textareas + dead Save /
|
|
5
|
+
* Approve / Iterate / Reject buttons that had no handlers. The new
|
|
6
|
+
* design unifies shortform + longform behind one review surface, so
|
|
7
|
+
* the desk becomes a pure navigation index — every open shortform
|
|
8
|
+
* workflow is a link into `/dev/editorial-review/<workflow.id>` where
|
|
9
|
+
* the operator gets the full editor (save / iterate / approve /
|
|
10
|
+
* reject) without a parallel composer to maintain.
|
|
11
|
+
*
|
|
12
|
+
* The folio strip + page chrome stay; the per-card textarea +
|
|
13
|
+
* inline action buttons go away.
|
|
14
|
+
*/
|
|
15
|
+
import type { StudioContext } from '../routes/api.ts';
|
|
16
|
+
export declare function renderShortformPage(ctx: StudioContext): string;
|
|
17
|
+
//# sourceMappingURL=shortform.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shortform.d.ts","sourceRoot":"","sources":["../../src/pages/shortform.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAIH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAoGtD,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,aAAa,GAAG,MAAM,CA4C9D"}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shortform desk index — `/dev/editorial-review-shortform`.
|
|
3
|
+
*
|
|
4
|
+
* Phase 21c: this page used to render compose textareas + dead Save /
|
|
5
|
+
* Approve / Iterate / Reject buttons that had no handlers. The new
|
|
6
|
+
* design unifies shortform + longform behind one review surface, so
|
|
7
|
+
* the desk becomes a pure navigation index — every open shortform
|
|
8
|
+
* workflow is a link into `/dev/editorial-review/<workflow.id>` where
|
|
9
|
+
* the operator gets the full editor (save / iterate / approve /
|
|
10
|
+
* reject) without a parallel composer to maintain.
|
|
11
|
+
*
|
|
12
|
+
* The folio strip + page chrome stay; the per-card textarea +
|
|
13
|
+
* inline action buttons go away.
|
|
14
|
+
*/
|
|
15
|
+
import { listOpen } from '@deskwork/core/review/pipeline';
|
|
16
|
+
import { html, unsafe } from "./html.js";
|
|
17
|
+
import { layout } from "./layout.js";
|
|
18
|
+
import { renderEditorialFolio } from "./chrome.js";
|
|
19
|
+
const PLATFORM_ORDER = ['reddit', 'linkedin', 'youtube', 'instagram'];
|
|
20
|
+
function siteLabel(site) {
|
|
21
|
+
return site.slice(0, 2).toUpperCase();
|
|
22
|
+
}
|
|
23
|
+
function loadOpenShortform(ctx) {
|
|
24
|
+
const open = [];
|
|
25
|
+
for (const w of listOpen(ctx.projectRoot, ctx.config)) {
|
|
26
|
+
if (w.contentKind === 'shortform')
|
|
27
|
+
open.push(w);
|
|
28
|
+
}
|
|
29
|
+
open.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
|
30
|
+
return open;
|
|
31
|
+
}
|
|
32
|
+
function groupByPlatform(workflows) {
|
|
33
|
+
const byPlatform = new Map();
|
|
34
|
+
for (const w of workflows) {
|
|
35
|
+
const key = w.platform ?? 'other';
|
|
36
|
+
const list = byPlatform.get(key) ?? [];
|
|
37
|
+
list.push(w);
|
|
38
|
+
byPlatform.set(key, list);
|
|
39
|
+
}
|
|
40
|
+
const ordered = [
|
|
41
|
+
...PLATFORM_ORDER.filter((p) => byPlatform.has(p)),
|
|
42
|
+
...[...byPlatform.keys()].filter((p) => !PLATFORM_ORDER.includes(p)),
|
|
43
|
+
];
|
|
44
|
+
return { byPlatform, ordered };
|
|
45
|
+
}
|
|
46
|
+
function fmtRelTime(iso, now) {
|
|
47
|
+
const t = new Date(iso).getTime();
|
|
48
|
+
const s = Math.max(0, Math.floor((now.getTime() - t) / 1000));
|
|
49
|
+
if (s < 60)
|
|
50
|
+
return `${s}s ago`;
|
|
51
|
+
const m = Math.floor(s / 60);
|
|
52
|
+
if (m < 60)
|
|
53
|
+
return `${m}m ago`;
|
|
54
|
+
const h = Math.floor(m / 60);
|
|
55
|
+
if (h < 48)
|
|
56
|
+
return `${h}h ago`;
|
|
57
|
+
return `${Math.floor(h / 24)}d ago`;
|
|
58
|
+
}
|
|
59
|
+
function renderRow(w, now) {
|
|
60
|
+
const channelMarkup = w.channel
|
|
61
|
+
? unsafe(html `<span class="channel">${w.channel}</span>`)
|
|
62
|
+
: unsafe('');
|
|
63
|
+
const reviewUrl = `/dev/editorial-review/${w.id}`;
|
|
64
|
+
return unsafe(html `
|
|
65
|
+
<a class="er-row er-shortform-row"
|
|
66
|
+
href="${reviewUrl}"
|
|
67
|
+
data-workflow-id="${w.id}"
|
|
68
|
+
data-platform="${w.platform ?? 'other'}"
|
|
69
|
+
data-state="${w.state}"
|
|
70
|
+
data-site="${w.site}">
|
|
71
|
+
<span class="er-row-num">→</span>
|
|
72
|
+
<span class="er-row-site er-row-site--${w.site}" title="${w.site}">${siteLabel(w.site)}</span>
|
|
73
|
+
<span class="er-row-slug">${w.slug}</span>
|
|
74
|
+
${channelMarkup}
|
|
75
|
+
<span class="er-stamp er-stamp-${w.state}">${w.state.replace('-', ' ')}</span>
|
|
76
|
+
<span class="er-row-ts">v${w.currentVersion} · ${fmtRelTime(w.updatedAt, now)}</span>
|
|
77
|
+
<span class="er-row-hint">Open in review →</span>
|
|
78
|
+
</a>`);
|
|
79
|
+
}
|
|
80
|
+
function renderPlatformSection(platform, workflows, now) {
|
|
81
|
+
const rows = workflows.map((w) => renderRow(w, now).__raw).join('');
|
|
82
|
+
return unsafe(html `
|
|
83
|
+
<section class="er-platform-section">
|
|
84
|
+
<div class="er-platform-header">
|
|
85
|
+
<h2>${platform}</h2>
|
|
86
|
+
<span class="er-platform-count">№ ${String(workflows.length).padStart(2, '0')}</span>
|
|
87
|
+
</div>
|
|
88
|
+
${unsafe(rows)}
|
|
89
|
+
</section>`);
|
|
90
|
+
}
|
|
91
|
+
function renderEmptyState() {
|
|
92
|
+
const platformList = PLATFORM_ORDER.join(', ');
|
|
93
|
+
return unsafe(html `
|
|
94
|
+
<div class="er-empty" style="margin-top: var(--er-space-5);">
|
|
95
|
+
No short-form galleys on the desk.<br />
|
|
96
|
+
Supported platforms: <em>${platformList}</em>.<br />
|
|
97
|
+
Start a new shortform draft from the dashboard's
|
|
98
|
+
<a href="/dev/editorial-studio">coverage matrix</a>.
|
|
99
|
+
</div>`);
|
|
100
|
+
}
|
|
101
|
+
export function renderShortformPage(ctx) {
|
|
102
|
+
const workflows = loadOpenShortform(ctx);
|
|
103
|
+
const { byPlatform, ordered } = groupByPlatform(workflows);
|
|
104
|
+
const now = ctx.now ? ctx.now() : new Date();
|
|
105
|
+
const cardsBlock = workflows.length === 0
|
|
106
|
+
? renderEmptyState().__raw
|
|
107
|
+
: ordered
|
|
108
|
+
.map((p) => renderPlatformSection(p, byPlatform.get(p) ?? [], now).__raw)
|
|
109
|
+
.join('');
|
|
110
|
+
const body = html `
|
|
111
|
+
${renderEditorialFolio('reviews', 'shortform desk')}
|
|
112
|
+
<header class="er-pagehead er-pagehead--centered">
|
|
113
|
+
<p class="er-pagehead__kicker">All sites · short form</p>
|
|
114
|
+
<h1 class="er-pagehead__title">The <em>compositor</em>'s desk</h1>
|
|
115
|
+
<p class="er-pagehead__deck">Open shortform galleys — click any row to open the unified review surface.</p>
|
|
116
|
+
<p class="er-pagehead__meta">
|
|
117
|
+
<span>${workflows.length} in flight</span>
|
|
118
|
+
<span class="sep">·</span>
|
|
119
|
+
<span>${ordered.length} ${ordered.length === 1 ? 'platform' : 'platforms'}</span>
|
|
120
|
+
</p>
|
|
121
|
+
</header>
|
|
122
|
+
<main class="er-container" style="padding-top: var(--er-space-4); padding-bottom: var(--er-space-6);">
|
|
123
|
+
${unsafe(cardsBlock)}
|
|
124
|
+
<p style="margin-top: var(--er-space-5); font-family: var(--er-font-display); font-style: italic; color: var(--er-faded);">
|
|
125
|
+
<a href="/dev/editorial-studio">← back to the studio</a>
|
|
126
|
+
</p>
|
|
127
|
+
</main>
|
|
128
|
+
<div class="er-toast" id="toast" hidden></div>`;
|
|
129
|
+
return layout({
|
|
130
|
+
title: 'Short form — all sites — dev',
|
|
131
|
+
cssHrefs: [
|
|
132
|
+
'/static/css/editorial-review.css',
|
|
133
|
+
'/static/css/editorial-nav.css',
|
|
134
|
+
'/static/css/editorial-studio.css',
|
|
135
|
+
],
|
|
136
|
+
bodyAttrs: 'data-review-ui="shortform"',
|
|
137
|
+
bodyHtml: body,
|
|
138
|
+
embeddedJson: [],
|
|
139
|
+
scriptModules: ['/static/dist/editorial-studio-client.js'],
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
//# sourceMappingURL=shortform.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shortform.js","sourceRoot":"","sources":["../../src/pages/shortform.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-request content-index memoization for the studio.
|
|
3
|
+
*
|
|
4
|
+
* The content index (uuid → fs path) is the cornerstone of Phase 19c's
|
|
5
|
+
* id-driven file lookups. Building it walks every markdown file under
|
|
6
|
+
* each site's contentDir, so we want one build per HTTP request even
|
|
7
|
+
* when multiple page renderers need the index. We also want a fresh
|
|
8
|
+
* build on every NEW request — keeps the index always-fresh against fs
|
|
9
|
+
* changes between requests (no stale-cache invariant to maintain).
|
|
10
|
+
*
|
|
11
|
+
* The memo lives on the Hono context. A request-scoped `Map<site, ContentIndex>`
|
|
12
|
+
* lazily populates the first time a renderer asks for an index for a
|
|
13
|
+
* given site. The middleware initializes the empty map; renderers call
|
|
14
|
+
* `getRequestContentIndex(c, ctx, site)` to retrieve-or-build.
|
|
15
|
+
*
|
|
16
|
+
* Test injection: callers can override the build function via
|
|
17
|
+
* `setIndexBuilder` — the test harness uses this to count invocations
|
|
18
|
+
* and verify a single request only builds the index once.
|
|
19
|
+
*/
|
|
20
|
+
import type { Context, MiddlewareHandler } from 'hono';
|
|
21
|
+
import type { ContentIndex } from '@deskwork/core/content-index';
|
|
22
|
+
import type { StudioContext } from './routes/api.ts';
|
|
23
|
+
/**
|
|
24
|
+
* Pluggable index builder. Production points at `buildContentIndex`;
|
|
25
|
+
* tests override to count invocations.
|
|
26
|
+
*/
|
|
27
|
+
export type IndexBuilder = (projectRoot: string, config: StudioContext['config'], site: string) => ContentIndex;
|
|
28
|
+
/**
|
|
29
|
+
* Override the index builder. Tests use this to inject a spy. Pass the
|
|
30
|
+
* default `buildContentIndex` to reset.
|
|
31
|
+
*/
|
|
32
|
+
export declare function setIndexBuilder(builder: IndexBuilder): void;
|
|
33
|
+
/**
|
|
34
|
+
* Reset the index builder to the production default. Tests call this
|
|
35
|
+
* in `afterEach` to avoid bleed.
|
|
36
|
+
*/
|
|
37
|
+
export declare function resetIndexBuilder(): void;
|
|
38
|
+
/**
|
|
39
|
+
* Hono middleware: attach an empty per-request index cache to the
|
|
40
|
+
* context. Mounted before page routes in `createApp`.
|
|
41
|
+
*/
|
|
42
|
+
export declare function contentIndexMiddleware(): MiddlewareHandler;
|
|
43
|
+
/**
|
|
44
|
+
* Retrieve-or-build the content index for `site` within the current
|
|
45
|
+
* request. Builds on first call; reuses on every subsequent call in
|
|
46
|
+
* the same request lifetime. Falls through to a one-shot build when
|
|
47
|
+
* the middleware isn't installed (defensive — keeps callers working
|
|
48
|
+
* even outside the HTTP path, e.g. unit-test renderers that pass a
|
|
49
|
+
* synthetic Context).
|
|
50
|
+
*/
|
|
51
|
+
export declare function getRequestContentIndex(c: Context, studioCtx: StudioContext, site: string): ContentIndex;
|
|
52
|
+
//# sourceMappingURL=request-context.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"request-context.d.ts","sourceRoot":"","sources":["../src/request-context.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,iBAAiB,EAAE,MAAM,MAAM,CAAC;AACvD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,8BAA8B,CAAC;AAEjE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAuBrD;;;GAGG;AACH,MAAM,MAAM,YAAY,GAAG,CACzB,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,aAAa,CAAC,QAAQ,CAAC,EAC/B,IAAI,EAAE,MAAM,KACT,YAAY,CAAC;AAIlB;;;GAGG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,YAAY,GAAG,IAAI,CAE3D;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,IAAI,IAAI,CAExC;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,IAAI,iBAAiB,CAM1D;AAOD;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CACpC,CAAC,EAAE,OAAO,EACV,SAAS,EAAE,aAAa,EACxB,IAAI,EAAE,MAAM,GACX,YAAY,CAWd"}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-request content-index memoization for the studio.
|
|
3
|
+
*
|
|
4
|
+
* The content index (uuid → fs path) is the cornerstone of Phase 19c's
|
|
5
|
+
* id-driven file lookups. Building it walks every markdown file under
|
|
6
|
+
* each site's contentDir, so we want one build per HTTP request even
|
|
7
|
+
* when multiple page renderers need the index. We also want a fresh
|
|
8
|
+
* build on every NEW request — keeps the index always-fresh against fs
|
|
9
|
+
* changes between requests (no stale-cache invariant to maintain).
|
|
10
|
+
*
|
|
11
|
+
* The memo lives on the Hono context. A request-scoped `Map<site, ContentIndex>`
|
|
12
|
+
* lazily populates the first time a renderer asks for an index for a
|
|
13
|
+
* given site. The middleware initializes the empty map; renderers call
|
|
14
|
+
* `getRequestContentIndex(c, ctx, site)` to retrieve-or-build.
|
|
15
|
+
*
|
|
16
|
+
* Test injection: callers can override the build function via
|
|
17
|
+
* `setIndexBuilder` — the test harness uses this to count invocations
|
|
18
|
+
* and verify a single request only builds the index once.
|
|
19
|
+
*/
|
|
20
|
+
import { buildContentIndex } from '@deskwork/core/content-index';
|
|
21
|
+
const CONTEXT_KEY = 'deskwork:contentIndices';
|
|
22
|
+
/**
|
|
23
|
+
* Type guard: narrow an unknown value to our IndexCache shape. The
|
|
24
|
+
* stored entries are guaranteed by the middleware (only it writes to
|
|
25
|
+
* this slot), so an `instanceof Map` check is sufficient at the
|
|
26
|
+
* boundary. The value param of the map carries through unchanged
|
|
27
|
+
* because the middleware always stores `Map<string, ContentIndex>`
|
|
28
|
+
* and `c.get` returns the same reference.
|
|
29
|
+
*/
|
|
30
|
+
function isIndexCache(value) {
|
|
31
|
+
return value instanceof Map;
|
|
32
|
+
}
|
|
33
|
+
let activeBuilder = buildContentIndex;
|
|
34
|
+
/**
|
|
35
|
+
* Override the index builder. Tests use this to inject a spy. Pass the
|
|
36
|
+
* default `buildContentIndex` to reset.
|
|
37
|
+
*/
|
|
38
|
+
export function setIndexBuilder(builder) {
|
|
39
|
+
activeBuilder = builder;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Reset the index builder to the production default. Tests call this
|
|
43
|
+
* in `afterEach` to avoid bleed.
|
|
44
|
+
*/
|
|
45
|
+
export function resetIndexBuilder() {
|
|
46
|
+
activeBuilder = buildContentIndex;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Hono middleware: attach an empty per-request index cache to the
|
|
50
|
+
* context. Mounted before page routes in `createApp`.
|
|
51
|
+
*/
|
|
52
|
+
export function contentIndexMiddleware() {
|
|
53
|
+
return async (c, next) => {
|
|
54
|
+
const cache = new Map();
|
|
55
|
+
c.set(CONTEXT_KEY, cache);
|
|
56
|
+
await next();
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function getCache(c) {
|
|
60
|
+
const raw = c.get(CONTEXT_KEY);
|
|
61
|
+
return isIndexCache(raw) ? raw : null;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Retrieve-or-build the content index for `site` within the current
|
|
65
|
+
* request. Builds on first call; reuses on every subsequent call in
|
|
66
|
+
* the same request lifetime. Falls through to a one-shot build when
|
|
67
|
+
* the middleware isn't installed (defensive — keeps callers working
|
|
68
|
+
* even outside the HTTP path, e.g. unit-test renderers that pass a
|
|
69
|
+
* synthetic Context).
|
|
70
|
+
*/
|
|
71
|
+
export function getRequestContentIndex(c, studioCtx, site) {
|
|
72
|
+
const cache = getCache(c);
|
|
73
|
+
if (cache !== null) {
|
|
74
|
+
const cached = cache.get(site);
|
|
75
|
+
if (cached !== undefined)
|
|
76
|
+
return cached;
|
|
77
|
+
const fresh = activeBuilder(studioCtx.projectRoot, studioCtx.config, site);
|
|
78
|
+
cache.set(site, fresh);
|
|
79
|
+
return fresh;
|
|
80
|
+
}
|
|
81
|
+
// No middleware → no memoization. Builds per call.
|
|
82
|
+
return activeBuilder(studioCtx.projectRoot, studioCtx.config, site);
|
|
83
|
+
}
|
|
84
|
+
//# sourceMappingURL=request-context.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"request-context.js","sourceRoot":"","sources":["../src/request-context.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAIH,OAAO,EAAE,iBAAiB,EAAE,MAAM,8BAA8B,CAAC;AAGjE,MAAM,WAAW,GAAG,yBAAyB,CAAC;AAS9C;;;;;;;GAOG;AACH,SAAS,YAAY,CAAC,KAAc;IAClC,OAAO,KAAK,YAAY,GAAG,CAAC;AAC9B,CAAC;AAYD,IAAI,aAAa,GAAiB,iBAAiB,CAAC;AAEpD;;;GAGG;AACH,MAAM,UAAU,eAAe,CAAC,OAAqB;IACnD,aAAa,GAAG,OAAO,CAAC;AAC1B,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,iBAAiB;IAC/B,aAAa,GAAG,iBAAiB,CAAC;AACpC,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,sBAAsB;IACpC,OAAO,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,EAAE;QACvB,MAAM,KAAK,GAAe,IAAI,GAAG,EAAE,CAAC;QACpC,CAAC,CAAC,GAAG,CAAC,WAAW,EAAE,KAAK,CAAC,CAAC;QAC1B,MAAM,IAAI,EAAE,CAAC;IACf,CAAC,CAAC;AACJ,CAAC;AAED,SAAS,QAAQ,CAAC,CAAU;IAC1B,MAAM,GAAG,GAAY,CAAC,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;IACxC,OAAO,YAAY,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC;AACxC,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,sBAAsB,CACpC,CAAU,EACV,SAAwB,EACxB,IAAY;IAEZ,MAAM,KAAK,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC;IAC1B,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;QACnB,MAAM,MAAM,GAAG,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QAC/B,IAAI,MAAM,KAAK,SAAS;YAAE,OAAO,MAAM,CAAC;QACxC,MAAM,KAAK,GAAG,aAAa,CAAC,SAAS,CAAC,WAAW,EAAE,SAAS,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;QAC3E,KAAK,CAAC,GAAG,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;QACvB,OAAO,KAAK,CAAC;IACf,CAAC;IACD,mDAAmD;IACnD,OAAO,aAAa,CAAC,SAAS,CAAC,WAAW,EAAE,SAAS,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;AACtE,CAAC"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API route handlers for the studio. Each route is a thin wrapper over a
|
|
3
|
+
* `lib/review/handlers.ts` function — it parses the HTTP request, calls
|
|
4
|
+
* the handler with `(projectRoot, config, body)`, and serializes the
|
|
5
|
+
* `{status, body}` result as JSON.
|
|
6
|
+
*
|
|
7
|
+
* Mirrors the audiocontrol Astro routes byte-for-byte so the existing
|
|
8
|
+
* client code (editorial-review-client.ts, editorial-studio-client.ts)
|
|
9
|
+
* keeps working without changes.
|
|
10
|
+
*/
|
|
11
|
+
import { Hono } from 'hono';
|
|
12
|
+
import type { DeskworkConfig } from '@deskwork/core/config';
|
|
13
|
+
import type { OverrideResolver } from '@deskwork/core/overrides';
|
|
14
|
+
export interface StudioContext {
|
|
15
|
+
projectRoot: string;
|
|
16
|
+
config: DeskworkConfig;
|
|
17
|
+
/**
|
|
18
|
+
* Clock injection point for tests. The dashboard / help pages render
|
|
19
|
+
* a press-check date in the masthead; passing `now` lets tests stub a
|
|
20
|
+
* deterministic value rather than asserting on `new Date()` output.
|
|
21
|
+
* Defaults to "live" (a fresh `Date()` per render) in `createApp`.
|
|
22
|
+
*/
|
|
23
|
+
now?: () => Date;
|
|
24
|
+
/**
|
|
25
|
+
* Phase 23f: per-project override resolver. Looks up
|
|
26
|
+
* `<projectRoot>/.deskwork/{templates,prompts,doctor}/<name>.ts`.
|
|
27
|
+
* Optional on the type so existing call sites that pre-date the
|
|
28
|
+
* customization layer (and tests that only care about default
|
|
29
|
+
* rendering) can omit it; `createApp` derives one from `projectRoot`
|
|
30
|
+
* when not supplied. Production server boot constructs the resolver
|
|
31
|
+
* once and threads it through.
|
|
32
|
+
*/
|
|
33
|
+
resolver?: OverrideResolver;
|
|
34
|
+
}
|
|
35
|
+
export declare function createApiRouter(ctx: StudioContext): Hono;
|
|
36
|
+
//# sourceMappingURL=api.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../../src/routes/api.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAW5B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAC5D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAiCjE,MAAM,WAAW,aAAa;IAC5B,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,cAAc,CAAC;IACvB;;;;;OAKG;IACH,GAAG,CAAC,EAAE,MAAM,IAAI,CAAC;IACjB;;;;;;;;OAQG;IACH,QAAQ,CAAC,EAAE,gBAAgB,CAAC;CAC7B;AAED,wBAAgB,eAAe,CAAC,GAAG,EAAE,aAAa,GAAG,IAAI,CAkIxD"}
|