@skillre/dsh-plugin-sticky-wall 0.0.0-stage → 0.2.2

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/lib/client.js ADDED
@@ -0,0 +1,1694 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "@skillre/dsh-plugin-sticky-wall",
3
+ factory: (require) => {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+ "use strict";
7
+ var __defProp = Object.defineProperty;
8
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
9
+ var __getOwnPropNames = Object.getOwnPropertyNames;
10
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
11
+ var __export = (target, all) => {
12
+ for (var name in all)
13
+ __defProp(target, name, { get: all[name], enumerable: true });
14
+ };
15
+ var __copyProps = (to, from, except, desc) => {
16
+ if (from && typeof from === "object" || typeof from === "function") {
17
+ for (let key of __getOwnPropNames(from))
18
+ if (!__hasOwnProp.call(to, key) && key !== except)
19
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
20
+ }
21
+ return to;
22
+ };
23
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
24
+
25
+ // src/client/index.tsx
26
+ var index_exports = {};
27
+ __export(index_exports, {
28
+ apply: () => apply,
29
+ inject: () => inject
30
+ });
31
+ module.exports = __toCommonJS(index_exports);
32
+
33
+ // src/types.ts
34
+ var ENDPOINT = "skillre-sticky-wall/execute";
35
+ var PANEL_ID = "skillre-sticky-wall";
36
+ var LIMITS = Object.freeze({
37
+ /** One note's body, in characters. */
38
+ body: 4e3,
39
+ /** One note's title, in characters. */
40
+ title: 120,
41
+ /** A note's free-form tag list. */
42
+ tags: 8,
43
+ /** One tag, in characters. */
44
+ tag: 32,
45
+ /** Live (non-archived) notes a wall may hold. */
46
+ notes: 400,
47
+ /** Note ids accepted in one request. */
48
+ bulk: 200
49
+ });
50
+ var COLORS = Object.freeze([
51
+ "yellow",
52
+ "pink",
53
+ "green",
54
+ "blue",
55
+ "purple",
56
+ "orange"
57
+ ]);
58
+ var SORTS = Object.freeze(["pinned", "created", "updated"]);
59
+ var WIDTHS = Object.freeze(["sm", "md", "lg"]);
60
+ var WIDTH_PX = Object.freeze({
61
+ sm: 178,
62
+ md: 222,
63
+ lg: 276
64
+ });
65
+ var LAYOUT_LIMITS = Object.freeze({
66
+ /** Absolute value of x and y, in world pixels. */
67
+ coord: 2e4,
68
+ /** Absolute rotation, in degrees. */
69
+ rotate: 12
70
+ });
71
+
72
+ // src/client/api.ts
73
+ var ActionError = class extends Error {
74
+ constructor(code, message, details = {}) {
75
+ super(message);
76
+ this.code = code;
77
+ this.details = details;
78
+ this.name = "ActionError";
79
+ }
80
+ };
81
+ function unwrap(result) {
82
+ if (result === null || typeof result !== "object" || !("ok" in result)) throw new ActionError("invalid-response", "The Host returned an invalid sticky-wall response.");
83
+ if (!result.ok) throw new ActionError(result.error.code, result.error.message, result.error.details);
84
+ return result.value;
85
+ }
86
+ function createApi(connection, lifetime) {
87
+ const call = async (request, signal) => {
88
+ const signalToUse = signal === void 0 ? lifetime : AbortSignal.any([lifetime, signal]);
89
+ return unwrap(await connection.rpc.call("/api", ENDPOINT, request, signalToUse));
90
+ };
91
+ return {
92
+ async list(includeArchived, sort, signal) {
93
+ const value = await call({ action: "list", includeArchived, sort }, signal);
94
+ return Array.isArray(value?.notes) ? value.notes : [];
95
+ },
96
+ async create(input, signal) {
97
+ const value = await call({ action: "create", note: input }, signal);
98
+ const note = value?.changed?.[0];
99
+ if (!note) throw new ActionError("invalid-response", "The Host created no note.");
100
+ return note;
101
+ },
102
+ async update(id, patch, signal) {
103
+ const value = await call({ action: "update", id, patch }, signal);
104
+ const note = value?.changed?.[0];
105
+ if (!note) throw new ActionError("invalid-response", "The Host updated no note.");
106
+ return note;
107
+ },
108
+ async archive(ids, archived, signal) {
109
+ const value = await call({ action: "archive", ids, archived }, signal);
110
+ return Array.isArray(value?.changed) ? value.changed : [];
111
+ },
112
+ async remove(ids, signal) {
113
+ const value = await call({ action: "delete", ids }, signal);
114
+ return Array.isArray(value?.removed) ? value.removed : [];
115
+ },
116
+ async clearArchived(signal) {
117
+ const value = await call({ action: "clear-archived" }, signal);
118
+ return Array.isArray(value?.removed) ? value.removed : [];
119
+ },
120
+ async move(id, layout, signal) {
121
+ const value = await call({ action: "move", id, layout }, signal);
122
+ const note = value?.changed?.[0];
123
+ if (!note) throw new ActionError("invalid-response", "The Host moved no note.");
124
+ return note;
125
+ },
126
+ async arrange(signal) {
127
+ const value = await call({ action: "arrange" }, signal);
128
+ return Array.isArray(value?.changed) ? value.changed : [];
129
+ }
130
+ };
131
+ }
132
+
133
+ // src/client/text.ts
134
+ var NAMESPACE = "skillreStickyWall";
135
+ var zh = {
136
+ "nav": "\u4FBF\u7B7E\u5899",
137
+ "title": "\u4FBF\u7B7E\u5899",
138
+ "subtitle": "\u968F\u624B\u8BB0\u4E0B\u4E00\u95EA\u800C\u8FC7\u7684\u7075\u611F\uFF0C\u548C\u4E34\u65F6\u7684\u6D3B\u513F\uFF1B\u62D6\u4E00\u62D6\uFF0C\u628A\u5B83\u4EEC\u6446\u6210\u4F60\u81EA\u5DF1\u7684\u6837\u5B50\u3002",
139
+ "empty": "\u8FD8\u6CA1\u6709\u4FBF\u7B7E\u3002\u70B9\u300C\u65B0\u5EFA\u300D\u5199\u4E0B\u7B2C\u4E00\u6761\u3002",
140
+ "emptyArchived": "\u5F52\u6863\u533A\u662F\u7A7A\u7684\u3002",
141
+ "new": "\u65B0\u5EFA",
142
+ "save": "\u4FDD\u5B58",
143
+ "cancel": "\u53D6\u6D88",
144
+ "close": "\u5173\u95ED",
145
+ "edit": "\u7F16\u8F91",
146
+ "pin": "\u7F6E\u9876",
147
+ "unpin": "\u53D6\u6D88\u7F6E\u9876",
148
+ "archive": "\u5F52\u6863",
149
+ "restore": "\u6062\u590D",
150
+ "delete": "\u5220\u9664",
151
+ "clearArchived": "\u6E05\u7A7A\u5F52\u6863",
152
+ "confirmClear": "\u786E\u5B9A\u6C38\u4E45\u5220\u9664\u6240\u6709\u5F52\u6863\u4FBF\u7B7E\uFF1F",
153
+ "confirmDelete": "\u786E\u5B9A\u5220\u9664\u9009\u4E2D\u7684\u4FBF\u7B7E\uFF1F",
154
+ "showArchived": "\u663E\u793A\u5F52\u6863",
155
+ "hideArchived": "\u9690\u85CF\u5F52\u6863",
156
+ "sortPinned": "\u7F6E\u9876\u4F18\u5148",
157
+ "sortCreated": "\u6309\u521B\u5EFA",
158
+ "sortUpdated": "\u6309\u66F4\u65B0",
159
+ "all": "\u5168\u90E8",
160
+ "filters": "\u7B5B\u9009",
161
+ "search": "\u641C\u7D22\u4FBF\u7B7E\u2026",
162
+ "bodyPlaceholder": "\u5199\u70B9\u4EC0\u4E48\u2026",
163
+ "titlePlaceholder": "\u6807\u9898\uFF08\u53EF\u9009\uFF09",
164
+ "tagsPlaceholder": "\u6807\u7B7E\uFF0C\u7528\u7A7A\u683C\u5206\u9694",
165
+ "colorLabel": "\u989C\u8272",
166
+ "saveHint": "\u2318/Ctrl + Enter \u4FDD\u5B58 \xB7 Esc \u53D6\u6D88",
167
+ "count": "{count} \u5F20\u4FBF\u7B7E",
168
+ "countArchived": "{count} \u5F20\u5DF2\u5F52\u6863",
169
+ "updated": "\u66F4\u65B0\u4E8E {time}",
170
+ "created": "\u521B\u5EFA\u4E8E {time}",
171
+ "loading": "\u52A0\u8F7D\u4E2D\u2026",
172
+ "failed": "\u64CD\u4F5C\u5931\u8D25",
173
+ "noResults": "\u6CA1\u6709\u5339\u914D\u7684\u4FBF\u7B7E",
174
+ "retry": "\u91CD\u8BD5",
175
+ "tooLong": "\u5185\u5BB9\u592A\u957F\u4E86",
176
+ "emptyBody": "\u4FBF\u7B7E\u4E0D\u80FD\u4E3A\u7A7A",
177
+ "wallFull": "\u4FBF\u7B7E\u5899\u6EE1\u4E86\uFF0C\u8BF7\u5148\u5F52\u6863\u6216\u5220\u9664\u4E00\u4E9B\u4FBF\u7B7E",
178
+ "emptyTags": "\u8FD8\u6CA1\u6709\u6807\u7B7E",
179
+ "pinHint": "\u7F6E\u9876\u7684\u4FBF\u7B7E\u6392\u5728\u6700\u524D",
180
+ "more": "\u66F4\u591A",
181
+ "agentHint": "\u4E5F\u53EF\u4EE5\u8BA9\u6211\u5E2E\u4F60\u8BB0\uFF1A\u76F4\u63A5\u8BF4\u300C\u8BB0\u4E00\u4E0B\u2026\u2026\u300D",
182
+ "preview": "\u9884\u89C8",
183
+ "widthLabel": "\u5361\u7247\u5BBD\u5EA6",
184
+ "width_sm": "\u5C0F",
185
+ "width_md": "\u4E2D",
186
+ "width_lg": "\u5927",
187
+ "resize": "\u6362\u5C3A\u5BF8",
188
+ "arrange": "\u6574\u7406",
189
+ "viewLabel": "\u89C6\u56FE",
190
+ "view_canvas": "\u753B\u5E03",
191
+ "view_grid": "\u7F51\u683C",
192
+ "view_list": "\u5217\u8868",
193
+ "zoomLabel": "\u7F29\u653E",
194
+ "zoomIn": "\u653E\u5927",
195
+ "zoomOut": "\u7F29\u5C0F",
196
+ "zoomReset": "\u91CD\u7F6E\u89C6\u56FE",
197
+ "dragHint": "\u62D6\u52A8\u5361\u7247\u6446\u653E\u4F4D\u7F6E\uFF0C\u2318/Ctrl + \u6EDA\u8F6E\u7F29\u653E",
198
+ "markdownHint": "\u6B63\u6587\u652F\u6301 Markdown\uFF1A\u6807\u9898\u3001\u5217\u8868\u3001\u7C97\u4F53\u3001\u4EE3\u7801\u5757\u3001\u94FE\u63A5\u3001\u8868\u683C"
199
+ };
200
+ var en = {
201
+ "nav": "Notes",
202
+ "title": "Sticky wall",
203
+ "subtitle": "Jot down a passing idea or a piece of temporary work \u2014 then drag the cards into your own arrangement.",
204
+ "empty": 'No notes yet. Hit "New" to write the first one.',
205
+ "emptyArchived": "Nothing archived yet.",
206
+ "new": "New",
207
+ "save": "Save",
208
+ "cancel": "Cancel",
209
+ "close": "Close",
210
+ "edit": "Edit",
211
+ "pin": "Pin",
212
+ "unpin": "Unpin",
213
+ "archive": "Archive",
214
+ "restore": "Restore",
215
+ "delete": "Delete",
216
+ "clearArchived": "Clear archive",
217
+ "confirmClear": "Permanently delete every archived note?",
218
+ "confirmDelete": "Delete the selected notes?",
219
+ "showArchived": "Show archived",
220
+ "hideArchived": "Hide archived",
221
+ "sortPinned": "Pinned first",
222
+ "sortCreated": "By created",
223
+ "sortUpdated": "By updated",
224
+ "all": "All",
225
+ "filters": "Filter",
226
+ "search": "Search notes\u2026",
227
+ "bodyPlaceholder": "Write something\u2026",
228
+ "titlePlaceholder": "Title (optional)",
229
+ "tagsPlaceholder": "Tags, space separated",
230
+ "colorLabel": "Color",
231
+ "saveHint": "\u2318/Ctrl + Enter to save \xB7 Esc to cancel",
232
+ "count": "{count} notes",
233
+ "countArchived": "{count} archived",
234
+ "updated": "Updated {time}",
235
+ "created": "Created {time}",
236
+ "loading": "Loading\u2026",
237
+ "failed": "Something went wrong",
238
+ "noResults": "No matching notes",
239
+ "retry": "Retry",
240
+ "tooLong": "Too long",
241
+ "emptyBody": "A note cannot be empty",
242
+ "wallFull": "The wall is full \u2014 archive or delete a note first",
243
+ "emptyTags": "No tags yet",
244
+ "pinHint": "Pinned notes stay on top",
245
+ "more": "More",
246
+ "agentHint": 'You can also ask me to note it: say "note that \u2026"',
247
+ "preview": "Preview",
248
+ "widthLabel": "Card width",
249
+ "width_sm": "S",
250
+ "width_md": "M",
251
+ "width_lg": "L",
252
+ "resize": "Resize",
253
+ "arrange": "Tidy",
254
+ "viewLabel": "View",
255
+ "view_canvas": "Canvas",
256
+ "view_grid": "Grid",
257
+ "view_list": "List",
258
+ "zoomLabel": "Zoom",
259
+ "zoomIn": "Zoom in",
260
+ "zoomOut": "Zoom out",
261
+ "zoomReset": "Reset view",
262
+ "dragHint": "Drag a card to place it \xB7 \u2318/Ctrl + wheel to zoom",
263
+ "markdownHint": "The body renders Markdown: headings, lists, bold, code blocks, links, tables"
264
+ };
265
+
266
+ // src/client/view.tsx
267
+ var import_react = require("react");
268
+
269
+ // src/layout.ts
270
+ var PREVIEW_CELL_W = 300;
271
+ var PREVIEW_CELL_H = 232;
272
+ var PREVIEW_COLUMNS = 4;
273
+ var PREVIEW_TILT = 4;
274
+ function hash(text) {
275
+ let value = 2166136261;
276
+ for (let index = 0; index < text.length; index += 1) {
277
+ value ^= text.charCodeAt(index);
278
+ value = Math.imul(value, 16777619) >>> 0;
279
+ }
280
+ return value >>> 0;
281
+ }
282
+ function unit(text) {
283
+ return hash(text) / 4294967296;
284
+ }
285
+ function round(value) {
286
+ return Math.round(value * 10) / 10;
287
+ }
288
+ function clampLayout(layout) {
289
+ const coord = LAYOUT_LIMITS.coord;
290
+ const tilt = LAYOUT_LIMITS.rotate;
291
+ return {
292
+ x: round(Math.min(coord, Math.max(-coord, Number(layout.x) || 0))),
293
+ y: round(Math.min(coord, Math.max(-coord, Number(layout.y) || 0))),
294
+ rotate: round(Math.min(tilt, Math.max(-tilt, Number(layout.rotate) || 0))),
295
+ width: layout.width
296
+ };
297
+ }
298
+ function previewWidth(id) {
299
+ const roll = unit(`${id}:w`);
300
+ if (roll < 0.42) return "md";
301
+ if (roll < 0.74) return "sm";
302
+ return "lg";
303
+ }
304
+ var MAX_CARD_HEIGHT = Object.freeze({
305
+ sm: 236,
306
+ md: 288,
307
+ lg: 336
308
+ });
309
+ function layoutOf(note, index) {
310
+ return note.layout ?? previewLayout(note, index);
311
+ }
312
+ function previewLayout(note, index) {
313
+ const column = index % PREVIEW_COLUMNS;
314
+ const row = Math.floor(index / PREVIEW_COLUMNS);
315
+ const width = previewWidth(note.id);
316
+ return clampLayout({
317
+ x: column * PREVIEW_CELL_W + unit(`${note.id}:px`) * 42,
318
+ y: row * PREVIEW_CELL_H + unit(`${note.id}:py`) * 34,
319
+ rotate: (unit(`${note.id}:pr`) - 0.5) * 2 * PREVIEW_TILT,
320
+ width
321
+ });
322
+ }
323
+
324
+ // src/client/markdown.tsx
325
+ var import_jsx_runtime = require("react/jsx-runtime");
326
+ var FENCE = /^(```|~~~)\s*([A-Za-z0-9_+-]*)\s*$/;
327
+ var HEADING = /^(#{1,4})\s+(.*)$/;
328
+ var RULE = /^(?:[-*_]\s*){3,}$/;
329
+ var BULLET = /^[-*+]\s+(.*)$/;
330
+ var ORDERED = /^\d{1,3}[.)]\s+(.*)$/;
331
+ var QUOTE = /^>\s?(.*)$/;
332
+ var TABLE_DIVIDER = /^\|?\s*:?-{2,}:?\s*(?:\|\s*:?-{2,}:?\s*)*\|?\s*$/;
333
+ function splitRow(line) {
334
+ const trimmed = line.trim().replace(/^\|/, "").replace(/\|$/, "");
335
+ return trimmed.split("|").map((cell) => cell.trim());
336
+ }
337
+ function parseBlocks(source) {
338
+ const lines = source.replace(/\r\n?/g, "\n").split("\n");
339
+ const blocks = [];
340
+ let index = 0;
341
+ while (index < lines.length) {
342
+ const line = lines[index] ?? "";
343
+ if (line.trim() === "") {
344
+ index += 1;
345
+ continue;
346
+ }
347
+ const fence = line.trim().match(FENCE);
348
+ if (fence !== null) {
349
+ const marker = fence[1] ?? "```";
350
+ const language = fence[2] ?? "";
351
+ const body2 = [];
352
+ index += 1;
353
+ while (index < lines.length && (lines[index] ?? "").trim() !== marker) {
354
+ body2.push(lines[index] ?? "");
355
+ index += 1;
356
+ }
357
+ if (index < lines.length) index += 1;
358
+ blocks.push({ kind: "code", text: body2.join("\n"), language });
359
+ continue;
360
+ }
361
+ const heading = line.match(HEADING);
362
+ if (heading !== null) {
363
+ const level = Math.min(4, (heading[1] ?? "#").length);
364
+ blocks.push({ kind: "heading", level, text: (heading[2] ?? "").trim() });
365
+ index += 1;
366
+ continue;
367
+ }
368
+ if (RULE.test(line.trim())) {
369
+ blocks.push({ kind: "rule" });
370
+ index += 1;
371
+ continue;
372
+ }
373
+ if (QUOTE.test(line.trim())) {
374
+ const body2 = [];
375
+ while (index < lines.length) {
376
+ const current = lines[index] ?? "";
377
+ const match = current.match(QUOTE);
378
+ if (match === null) break;
379
+ body2.push(match[1] ?? "");
380
+ index += 1;
381
+ }
382
+ blocks.push({ kind: "quote", text: body2.join("\n").trim() });
383
+ continue;
384
+ }
385
+ if (line.includes("|") && TABLE_DIVIDER.test(lines[index + 1] ?? "")) {
386
+ const header = splitRow(line);
387
+ const rows = [];
388
+ index += 2;
389
+ while (index < lines.length && (lines[index] ?? "").includes("|") && (lines[index] ?? "").trim() !== "") {
390
+ rows.push(splitRow(lines[index] ?? ""));
391
+ index += 1;
392
+ }
393
+ blocks.push({ kind: "table", header, rows });
394
+ continue;
395
+ }
396
+ if (BULLET.test(line.trim()) || ORDERED.test(line.trim())) {
397
+ const ordered = ORDERED.test(line.trim());
398
+ const items = [];
399
+ while (index < lines.length) {
400
+ const current = lines[index] ?? "";
401
+ const match = current.trim().match(ordered ? ORDERED : BULLET);
402
+ if (match === null) break;
403
+ items.push((match[1] ?? "").trim());
404
+ index += 1;
405
+ }
406
+ blocks.push({ kind: "list", ordered, items });
407
+ continue;
408
+ }
409
+ const body = [line.trim()];
410
+ index += 1;
411
+ while (index < lines.length) {
412
+ const current = lines[index] ?? "";
413
+ if (current.trim() === "") break;
414
+ if (FENCE.test(current.trim()) || HEADING.test(current) || RULE.test(current.trim())) break;
415
+ if (BULLET.test(current.trim()) || ORDERED.test(current.trim()) || QUOTE.test(current.trim())) break;
416
+ body.push(current.trim());
417
+ index += 1;
418
+ }
419
+ blocks.push({ kind: "paragraph", text: body.join("\n") });
420
+ }
421
+ return blocks;
422
+ }
423
+ function safeHref(raw) {
424
+ const value = raw.trim();
425
+ if (value === "") return void 0;
426
+ if (/^(?:https?|mailto):/i.test(value)) return value;
427
+ if (/^www\./i.test(value)) return `https://${value}`;
428
+ return void 0;
429
+ }
430
+ function renderInline(text, keyPrefix = "i") {
431
+ const nodes = [];
432
+ let rest = text;
433
+ let counter = 0;
434
+ const pattern = /(`[^`]+`)|(\*\*[^*]+\*\*)|(\*[^*]+\*)|(_[^_]+_)|(~~[^~]+~~)|(\[[^\]]+\]\([^)\s]+\))|((?:https?:\/\/|www\.)[^\s<>()]+)/g;
435
+ let match;
436
+ while ((match = pattern.exec(rest)) !== null) {
437
+ if (match.index > 0) {
438
+ const head = rest.slice(0, match.index);
439
+ const parts = head.split("\n");
440
+ parts.forEach((part, position) => {
441
+ if (part !== "") nodes.push(part);
442
+ if (position < parts.length - 1) nodes.push(/* @__PURE__ */ (0, import_jsx_runtime.jsx)("br", {}, `${keyPrefix}-br-${counter++}`));
443
+ });
444
+ }
445
+ const token = match[0];
446
+ const key = `${keyPrefix}-${counter++}`;
447
+ if (token.startsWith("`")) {
448
+ nodes.push(/* @__PURE__ */ (0, import_jsx_runtime.jsx)("code", { className: "skw-md-code", children: token.slice(1, -1) }, key));
449
+ } else if (token.startsWith("**")) {
450
+ nodes.push(/* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { children: renderInline(token.slice(2, -2), `${key}s`) }, key));
451
+ } else if (token.startsWith("~~")) {
452
+ nodes.push(/* @__PURE__ */ (0, import_jsx_runtime.jsx)("s", { children: renderInline(token.slice(2, -2), `${key}s`) }, key));
453
+ } else if (token.startsWith("*")) {
454
+ nodes.push(/* @__PURE__ */ (0, import_jsx_runtime.jsx)("em", { children: renderInline(token.slice(1, -1), `${key}e`) }, key));
455
+ } else if (token.startsWith("_")) {
456
+ nodes.push(/* @__PURE__ */ (0, import_jsx_runtime.jsx)("em", { children: renderInline(token.slice(1, -1), `${key}e`) }, key));
457
+ } else if (token.startsWith("[")) {
458
+ const split = token.match(/\[([^\]]+)\]\(([^)\s]+)\)/);
459
+ const label = split?.[1] ?? token;
460
+ const href = safeHref(split?.[2] ?? "");
461
+ nodes.push(
462
+ href === void 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: label }, key) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("a", { className: "skw-md-link", href, target: "_blank", rel: "noreferrer noopener", children: renderInline(label, `${key}l`) }, key)
463
+ );
464
+ } else {
465
+ const href = safeHref(token);
466
+ nodes.push(
467
+ href === void 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: token }, key) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("a", { className: "skw-md-link", href, target: "_blank", rel: "noreferrer noopener", children: token }, key)
468
+ );
469
+ }
470
+ rest = rest.slice(match.index + token.length);
471
+ pattern.lastIndex = 0;
472
+ }
473
+ if (rest !== "") {
474
+ const parts = rest.split("\n");
475
+ parts.forEach((part, position) => {
476
+ if (part !== "") nodes.push(part);
477
+ if (position < parts.length - 1) nodes.push(/* @__PURE__ */ (0, import_jsx_runtime.jsx)("br", {}, `${keyPrefix}-br-${counter++}`));
478
+ });
479
+ }
480
+ return nodes;
481
+ }
482
+ function renderBlock(block, key) {
483
+ switch (block.kind) {
484
+ case "heading": {
485
+ const Tag = `h${Math.min(block.level + 2, 6)}`;
486
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Tag, { className: "skw-md-heading", children: renderInline(block.text, `${key}h`) }, key);
487
+ }
488
+ case "code":
489
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("pre", { className: "skw-md-pre", "data-language": block.language || void 0, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("code", { children: block.text }) }, key);
490
+ case "quote":
491
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("blockquote", { className: "skw-md-quote", children: renderInline(block.text, `${key}q`) }, key);
492
+ case "rule":
493
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("hr", { className: "skw-md-rule" }, key);
494
+ case "list":
495
+ return block.ordered ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ol", { className: "skw-md-list", children: block.items.map((item, position) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("li", { children: renderTask(item, `${key}-${position}`) }, `${key}-${position}`)) }, key) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ul", { className: "skw-md-list", children: block.items.map((item, position) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("li", { children: renderTask(item, `${key}-${position}`) }, `${key}-${position}`)) }, key);
496
+ case "table":
497
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("table", { className: "skw-md-table", children: [
498
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("thead", { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("tr", { children: block.header.map((cell, position) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("th", { children: renderInline(cell, `${key}h${position}`) }, `${key}-h-${position}`)) }) }),
499
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("tbody", { children: block.rows.map((row, rowIndex) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("tr", { children: row.map((cell, position) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("td", { children: renderInline(cell, `${key}r${rowIndex}${position}`) }, `${key}-r-${rowIndex}-${position}`)) }, `${key}-r-${rowIndex}`)) })
500
+ ] }, key);
501
+ case "paragraph":
502
+ default:
503
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "skw-md-p", children: renderInline(block.text, `${key}p`) }, key);
504
+ }
505
+ }
506
+ function renderTask(item, key) {
507
+ const match = item.match(/^\[( |x|X)\]\s+(.*)$/);
508
+ if (match === null) return renderInline(item, key);
509
+ const checked = (match[1] ?? " ") !== " ";
510
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "skw-md-task", "data-checked": checked, children: [
511
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "skw-md-box", "aria-hidden": "true", children: checked ? "\u2713" : "" }),
512
+ renderInline(match[2] ?? "", `${key}t`)
513
+ ] }, key);
514
+ }
515
+ function Markdown({ source, compact = false }) {
516
+ const blocks = parseBlocks(source);
517
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "skw-md", "data-compact": compact, children: blocks.map((block, index) => renderBlock(block, `b${index}`)) });
518
+ }
519
+
520
+ // src/client/styles.ts
521
+ var WALL_CSS = `
522
+ .skw-page {
523
+ position: relative; height: 100%; display: flex; flex-direction: column;
524
+ background-color: var(--dsw-alias-bg-base);
525
+ /* A barely-there dot grid reads as paper without competing with the notes. */
526
+ background-image: radial-gradient(circle at 1px 1px, var(--dsw-alias-border-l1) 1px, transparent 0);
527
+ background-size: 18px 18px;
528
+ color: var(--dsw-alias-label-primary); font-size: 13px; line-height: 20px;
529
+ overflow: hidden; -webkit-font-smoothing: antialiased;
530
+ }
531
+ .skw-page *, .skw-page *::before, .skw-page *::after { box-sizing: border-box; }
532
+ .skw-page button { font: inherit; }
533
+ .skw-head { flex: 0 0 auto; display: flex; align-items: flex-start; gap: 12px; padding: 18px 22px 14px; }
534
+ .skw-head-text { min-width: 0; flex: 1; }
535
+ .skw-title { margin: 0; font-size: 19px; line-height: 27px; font-weight: 650; letter-spacing: -.2px; display: flex; align-items: center; gap: 9px; }
536
+ /* The title mark echoes the sidebar glyph in the brand color, so the page and
537
+ the sidebar entry read as the same object. */
538
+ .skw-title-mark { display: inline-flex; align-items: center; justify-content: center; width: 26px; height: 26px; flex: none; border-radius: 8px; background: var(--dsw-alias-brand-primary); color: var(--dsw-alias-label-primary-foreground); }
539
+ .skw-subtitle { margin: 3px 0 0; font-size: 12px; line-height: 18px; color: var(--dsw-alias-label-secondary); }
540
+ .skw-actions { display: flex; align-items: center; gap: 6px; flex: none; }
541
+ .skw-btn { height: 28px; padding: 0 10px; display: inline-flex; align-items: center; gap: 6px; border: 1px solid var(--dsw-alias-border-l2); border-radius: 7px; background: var(--dsw-alias-bg-layer-1); color: var(--dsw-alias-label-secondary); cursor: pointer; white-space: nowrap; }
542
+ .skw-btn:hover { background: var(--dsw-alias-bg-layer-2); color: var(--dsw-alias-label-primary); }
543
+ .skw-btn:focus-visible { outline: 2px solid var(--dsw-alias-brand-primary); outline-offset: 1px; }
544
+ .skw-btn[disabled] { opacity: .5; cursor: default; }
545
+ .skw-btn-primary { background: var(--dsw-alias-brand-primary); border-color: transparent; color: var(--dsw-alias-label-primary-foreground); font-weight: 500; }
546
+ .skw-btn-primary:hover { background: var(--dsw-alias-brand-primary); color: var(--dsw-alias-label-primary-foreground); filter: brightness(1.06); }
547
+ .skw-btn-quiet { border-color: transparent; background: transparent; }
548
+ .skw-btn-danger { color: var(--dsw-alias-state-error-primary); border-color: transparent; background: transparent; }
549
+ .skw-btn-danger:hover { background: var(--dsw-alias-state-error-secondary); color: var(--dsw-alias-label-primary-foreground); }
550
+
551
+ .skw-toolbar { flex: 0 0 auto; display: flex; align-items: center; gap: 10px; padding: 0 22px 14px; flex-wrap: wrap; }
552
+ .skw-search-wrap { position: relative; flex: 1 1 240px; min-width: 160px; display: flex; align-items: center; }
553
+ .skw-search-icon { position: absolute; left: 10px; display: flex; color: var(--dsw-alias-label-tertiary); pointer-events: none; }
554
+ .skw-search { width: 100%; height: 32px; padding: 0 11px 0 31px; border: 1px solid var(--dsw-alias-border-l2); border-radius: 9px; background: var(--dsw-alias-bg-layer-1); color: var(--dsw-alias-label-primary); transition: border-color .12s ease, box-shadow .12s ease; }
555
+ .skw-search::placeholder { color: var(--dsw-alias-label-tertiary); }
556
+ .skw-search:focus-visible { outline: none; border-color: var(--dsw-alias-brand-primary); box-shadow: 0 0 0 3px color-mix(in srgb, var(--dsw-alias-brand-primary) 18%, transparent); }
557
+ /* Chips: one row of quiet pills, the active one filled. No boxed segments, so
558
+ filters, sorting and the archive toggle stop reading as three boxes. */
559
+ .skw-seg { display: inline-flex; gap: 4px; }
560
+ .skw-seg button { height: 30px; padding: 0 12px; border: 1px solid transparent; border-radius: 999px; background: transparent; color: var(--dsw-alias-label-secondary); cursor: pointer; font-size: 12.5px; transition: background .12s ease, color .12s ease; }
561
+ .skw-seg button:hover { background: var(--dsw-alias-bg-layer-2); color: var(--dsw-alias-label-primary); }
562
+ .skw-seg button[data-on="true"] { background: var(--dsw-alias-label-primary); color: var(--dsw-alias-bg-base); font-weight: 550; }
563
+ .skw-seg button:focus-visible { outline: 2px solid var(--dsw-alias-brand-primary); outline-offset: 1px; }
564
+ .skw-toolbar-sep { width: 1px; height: 18px; background: var(--dsw-alias-border-l2); flex: none; }
565
+ .skw-toggle { height: 30px; padding: 0 11px; display: inline-flex; align-items: center; gap: 6px; border: 1px dashed var(--dsw-alias-border-l2); border-radius: 999px; background: transparent; color: var(--dsw-alias-label-secondary); cursor: pointer; font-size: 12.5px; transition: background .12s ease, color .12s ease, border-color .12s ease; }
566
+ .skw-toggle:hover { color: var(--dsw-alias-label-primary); border-color: var(--dsw-alias-border-l4); }
567
+ .skw-toggle[data-on="true"] { border-style: solid; border-color: var(--dsw-alias-border-l3); background: var(--dsw-alias-bg-layer-2); color: var(--dsw-alias-label-primary); }
568
+ .skw-toggle:focus-visible { outline: 2px solid var(--dsw-alias-brand-primary); outline-offset: 1px; }
569
+ .skw-count { margin-left: auto; font-size: 12px; color: var(--dsw-alias-label-tertiary); font-variant-numeric: tabular-nums; white-space: nowrap; }
570
+
571
+ .skw-body { flex: 1 1 auto; overflow: auto; overscroll-behavior: contain; padding: 4px 22px 40px; }
572
+ .skw-body[data-view="canvas"] { padding: 0; overflow: hidden; }
573
+ /* Masonry-style columns, not a row grid: cards keep their natural height and
574
+ short notes no longer sit in a tall row with dead space underneath. */
575
+ .skw-grid { column-count: 5; column-gap: 14px; }
576
+ @media (max-width: 1500px) { .skw-grid { column-count: 4; } }
577
+ @media (max-width: 1180px) { .skw-grid { column-count: 3; } }
578
+ @media (max-width: 860px) { .skw-grid { column-count: 2; } }
579
+ @media (max-width: 560px) { .skw-grid { column-count: 1; } }
580
+
581
+ /* The free canvas.
582
+ A scroll container holding a world-sized surface; every card is absolutely
583
+ positioned in world coordinates. Scrolling pans, and \u2318/Ctrl + wheel scales
584
+ the world around its origin, so a wall keeps its spatial memory instead of
585
+ being re-flowed by the window width. */
586
+ .skw-canvas-scroll { width: 100%; height: 100%; overflow: auto; overscroll-behavior: contain; }
587
+ .skw-canvas {
588
+ position: relative; min-width: 100%; min-height: 100%;
589
+ /* The same dot grid as the page, so the canvas reads as the paper itself. */
590
+ background-image: radial-gradient(circle at 1px 1px, var(--dsw-alias-border-l1) 1px, transparent 0);
591
+ background-size: 22px 22px;
592
+ }
593
+ .skw-world { position: absolute; inset: 0; }
594
+ .skw-note-slot { position: absolute; inset: 0; pointer-events: none; }
595
+ /* Cards inside the canvas are absolutely placed by their stored layout, and
596
+ only the card itself takes pointer events, so empty paper stays scrollable. */
597
+ /* The canvas card.
598
+ Performance note: transform must NEVER be transitioned here. The drag writes
599
+ a new transform on every animation frame, and an active transition makes each
600
+ write an animation target \u2014 the browser then interpolates toward it, so the
601
+ card visibly lags the pointer by ~160ms and reads as sticky. Only the
602
+ shadow is animated. */
603
+ .skw-canvas .skw-note {
604
+ position: absolute; pointer-events: auto; margin: 0;
605
+ cursor: grab; touch-action: none;
606
+ /* One layer per card, promoted once. Toggling will-change on drag would
607
+ itself cost a style recalculation on the press. */
608
+ will-change: transform;
609
+ transition: box-shadow .18s ease;
610
+ /* The Host's tidy pass estimates card heights from text columns, so the
611
+ rendered box has to honour the same budget: without a cap a long note grows
612
+ past its estimate and the shelf packing underneath it overlaps. The value
613
+ mirrors MAX_CARD_HEIGHT in src/layout.ts. */
614
+ overflow: hidden;
615
+ }
616
+ .skw-canvas .skw-note[data-width="sm"] { max-height: 236px; }
617
+ .skw-canvas .skw-note[data-width="md"] { max-height: 288px; }
618
+ .skw-canvas .skw-note[data-width="lg"] { max-height: 336px; }
619
+ /* While dragging: no hover lift, no text selection, a lifted shadow, and no
620
+ pointer events on the card's own children so the drag cannot be interrupted
621
+ by a child capturing the pointer. */
622
+ .skw-canvas .skw-note[data-dragging="true"] {
623
+ cursor: grabbing; z-index: 20;
624
+ box-shadow: 0 4px 10px var(--dsw-alias-bg-mask-2), 0 26px 46px -14px var(--skw-note-glow);
625
+ user-select: none; -webkit-user-select: none;
626
+ }
627
+ .skw-canvas .skw-note[data-dragging="true"] * { pointer-events: none; }
628
+ /* The hover lift belongs to the grid view only. On the canvas the card already
629
+ has a stored position, and a translateY(-2px) here would fight the drag's
630
+ own transform (the two rules would overwrite each other). */
631
+ .skw-canvas .skw-note:hover { box-shadow: 0 2px 4px var(--dsw-alias-bg-mask-2), 0 14px 28px -10px var(--skw-note-glow); }
632
+
633
+ /* Dense one-line-per-note index. */
634
+ .skw-list { display: flex; flex-direction: column; gap: 2px; }
635
+ .skw-row {
636
+ display: flex; align-items: center; gap: 9px; width: 100%; min-height: 34px;
637
+ padding: 4px 10px; border: 0; border-radius: 8px; background: transparent;
638
+ color: var(--dsw-alias-label-primary); cursor: pointer; text-align: left;
639
+ }
640
+ .skw-row:hover { background: var(--dsw-alias-bg-layer-2); }
641
+ .skw-row:focus-visible { outline: 2px solid var(--dsw-alias-brand-primary); outline-offset: 1px; }
642
+ .skw-row[data-archived="true"] { opacity: .6; }
643
+ .skw-row .skw-spacer { flex: 1; }
644
+ .skw-row-dot { width: 7px; height: 7px; border-radius: 50%; flex: none; background: var(--skw-note-accent, var(--dsw-alias-brand-primary)); }
645
+ .skw-row-title { font-size: 13px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 52%; }
646
+ .skw-row-tags { font-size: 11px; color: var(--dsw-alias-label-tertiary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 34%; }
647
+ .skw-row-badge { color: var(--dsw-alias-brand-primary); font-size: 11px; }
648
+ .skw-row-time { font-size: 11px; color: var(--dsw-alias-label-tertiary); font-variant-numeric: tabular-nums; white-space: nowrap; }
649
+ .skw-row[data-color="yellow"] { --skw-note-accent: #e0b53a; }
650
+ .skw-row[data-color="pink"] { --skw-note-accent: #e78aa8; }
651
+ .skw-row[data-color="green"] { --skw-note-accent: #55b874; }
652
+ .skw-row[data-color="blue"] { --skw-note-accent: #5b9de0; }
653
+ .skw-row[data-color="purple"] { --skw-note-accent: #9b81e0; }
654
+ .skw-row[data-color="orange"] { --skw-note-accent: #e79a4e; }
655
+
656
+ /* Floating camera control, bottom-left: it belongs to the canvas, not to the
657
+ toolbar, so the toolbar stays about filtering and the view switch. */
658
+ .skw-cameras {
659
+ position: absolute; left: 22px; bottom: 18px; z-index: 12;
660
+ display: inline-flex; align-items: center; gap: 2px;
661
+ padding: 3px; border-radius: 999px; border: 1px solid var(--dsw-alias-border-l2);
662
+ background: var(--dsw-alias-bg-layer-1); box-shadow: 0 4px 14px var(--dsw-alias-bg-mask-2);
663
+ }
664
+ .skw-camera-btn {
665
+ width: 26px; height: 26px; display: inline-flex; align-items: center; justify-content: center;
666
+ border: 0; border-radius: 50%; background: transparent; color: var(--dsw-alias-label-secondary);
667
+ cursor: pointer; font-size: 13px; line-height: 1;
668
+ }
669
+ .skw-camera-btn:hover { background: var(--dsw-alias-bg-layer-2); color: var(--dsw-alias-label-primary); }
670
+ .skw-camera-btn:focus-visible { outline: 2px solid var(--dsw-alias-brand-primary); outline-offset: 1px; }
671
+ .skw-camera-value { min-width: 38px; text-align: center; font-size: 11px; color: var(--dsw-alias-label-tertiary); font-variant-numeric: tabular-nums; }
672
+ .skw-empty { margin: 64px auto 0; max-width: 380px; text-align: center; color: var(--dsw-alias-label-secondary); }
673
+ .skw-empty p { margin: 0 0 14px; font-size: 13px; }
674
+ .skw-empty-icon { font-size: 30px; line-height: 1; display: block; margin-bottom: 12px; opacity: .65; }
675
+
676
+ .skw-note {
677
+ position: relative; display: flex; flex-direction: column; gap: 4px;
678
+ /* Natural height, no min-height: an empty-ish note stays a small card. */
679
+ padding: 9px 12px 11px;
680
+ border-radius: 12px;
681
+ border: 1px solid var(--skw-note-edge);
682
+ background: var(--skw-note-bg);
683
+ color: var(--skw-note-ink);
684
+ /* A soft tinted drop shadow picks up the note's own color, which is what
685
+ makes the wall read as paper rather than as flat rectangles. */
686
+ box-shadow: 0 1px 2px var(--dsw-alias-bg-mask-2), 0 6px 16px -8px var(--skw-note-glow);
687
+ cursor: default;
688
+ transition: box-shadow .18s ease, transform .18s ease;
689
+ break-inside: avoid; -webkit-column-break-inside: avoid;
690
+ margin: 0 0 14px;
691
+ }
692
+ .skw-note:hover { box-shadow: 0 2px 4px var(--dsw-alias-bg-mask-2), 0 14px 28px -10px var(--skw-note-glow); }
693
+ /* The lift is a grid-view affordance; on the canvas the card's transform is
694
+ owned by its placement and the drag, so it must not be overridden here. */
695
+ .skw-grid .skw-note:hover { transform: translateY(-2px); }
696
+ .skw-note:focus-visible { outline: 2px solid var(--dsw-alias-brand-primary); outline-offset: 2px; }
697
+ .skw-note[data-archived="true"] { opacity: .68; }
698
+ /* Pinned notes get a colored left rail instead of just a heavier shadow, so
699
+ "pinned" is legible at a glance across the wall. */
700
+ .skw-note[data-pinned="true"]::before { content: ""; position: absolute; left: 0; top: 12px; bottom: 12px; width: 3px; border-radius: 0 3px 3px 0; background: var(--skw-note-accent); }
701
+ /* Two rows: the action bar owns the top-right line, the title owns a full-width
702
+ line below it. Keeping them separate means a long title wraps only on its own
703
+ width, and the action bar reserves its height so hover never reflows text. */
704
+ .skw-note-bar { display: flex; align-items: center; gap: 6px; min-height: 17px; margin: -3px -4px -5px 0; }
705
+ .skw-note-bar .skw-spacer { flex: 1; }
706
+ /* A small color dot marks the note's palette at a glance; the action buttons
707
+ sit beside it on the same top row. */
708
+ .skw-note-dot { width: 7px; height: 7px; border-radius: 50%; flex: none; background: var(--skw-note-accent); box-shadow: 0 0 0 3px color-mix(in srgb, var(--skw-note-accent) 16%, transparent); }
709
+ .skw-note-head { display: flex; align-items: flex-start; gap: 6px; }
710
+ .skw-note-title { font-size: 13px; line-height: 19px; font-weight: 650; letter-spacing: -.1px; overflow-wrap: anywhere; }
711
+ .skw-note-body { margin: 0; overflow-wrap: anywhere; font-size: 13px; line-height: 20px; flex: 1; color: var(--skw-note-ink); opacity: .92; }
712
+ /* Clamp the rendered Markdown block, not a plain string: display:-webkit-box
713
+ on the wrapper clips every child, so a long note stays a card and not a page. */
714
+ .skw-note-body.is-clamped { display: -webkit-box; -webkit-line-clamp: 12; -webkit-box-orient: vertical; overflow: hidden; }
715
+ .skw-note-tags { display: flex; flex-wrap: wrap; gap: 5px; margin-top: 1px; }
716
+ /* Tags read as real labels: a tinted pill with the note's accent, not a grey
717
+ block. */
718
+ .skw-tag { padding: 0 7px; height: 19px; line-height: 19px; border-radius: 999px; font-size: 11px; font-weight: 500; letter-spacing: .1px; background: color-mix(in srgb, var(--skw-note-accent) 22%, transparent); color: var(--skw-note-ink); }
719
+ .skw-note-foot { display: flex; align-items: center; gap: 5px; font-size: 11px; line-height: 16px; color: var(--skw-note-ink); opacity: .58; margin-top: 2px; }
720
+ .skw-note-foot .skw-spacer { flex: 1; }
721
+ .skw-note-badge { display: inline-flex; align-items: center; gap: 3px; padding: 0 6px; height: 17px; border-radius: 999px; background: color-mix(in srgb, var(--skw-note-accent) 26%, transparent); font-weight: 550; opacity: 1; }
722
+ /* In-flow, never absolute: the buttons can no longer sit on top of the text.
723
+ They fade in on hover/focus for calm cards, and stay visible where hover is
724
+ not a thing (touch/coarse pointers) so the actions remain reachable. */
725
+ .skw-note-tools { display: flex; flex: none; gap: 1px; opacity: 0; transition: opacity .12s ease; }
726
+ .skw-note:hover .skw-note-tools, .skw-note:focus-within .skw-note-tools { opacity: 1; }
727
+ @media (hover: none) { .skw-note-tools { opacity: 1; } }
728
+ /* Compact controls: 18px, so four of them take a small corner of the bar and
729
+ never squeeze the title row. */
730
+ .skw-icon-btn { width: 19px; height: 19px; display: inline-flex; align-items: center; justify-content: center; padding: 0; border: 0; border-radius: 5px; background: transparent; color: var(--skw-note-ink); opacity: .55; cursor: pointer; font-size: 11px; line-height: 1; transition: background .12s ease, opacity .12s ease; }
731
+ .skw-icon-btn:hover { opacity: 1; background: color-mix(in srgb, var(--skw-note-ink) 14%, transparent); }
732
+ .skw-icon-btn:focus-visible { outline: 2px solid var(--dsw-alias-brand-primary); outline-offset: 1px; opacity: 1; }
733
+ .skw-icon-btn[data-on="true"] { opacity: 1; color: var(--skw-note-accent-ink); }
734
+ .skw-pin { flex: none; font-size: 11px; line-height: 1; }
735
+
736
+ /* Rendered Markdown.
737
+ Sized in em so it follows the card's own font size at every width preset,
738
+ and kept quiet: headings are one step up, not a document outline. */
739
+ .skw-md { font-size: inherit; line-height: inherit; }
740
+ .skw-md[data-compact="true"] > *:first-child { margin-top: 0; }
741
+ .skw-md[data-compact="true"] > *:last-child { margin-bottom: 0; }
742
+ .skw-md .skw-md-p { margin: 0 0 .5em; }
743
+ .skw-md .skw-md-p:last-child { margin-bottom: 0; }
744
+ .skw-md .skw-md-heading { margin: .55em 0 .35em; font-weight: 650; line-height: 1.35; letter-spacing: -.1px; }
745
+ .skw-md .skw-md-heading:first-child { margin-top: 0; }
746
+ .skw-md h3.skw-md-heading { font-size: 1.12em; }
747
+ .skw-md h4.skw-md-heading { font-size: 1.04em; }
748
+ .skw-md h5.skw-md-heading, .skw-md h6.skw-md-heading { font-size: 1em; }
749
+ .skw-md .skw-md-list { margin: .35em 0 .55em; padding-left: 1.15em; }
750
+ .skw-md .skw-md-list li { margin: .12em 0; }
751
+ .skw-md .skw-md-list li::marker { color: var(--skw-note-accent-ink); }
752
+ .skw-md .skw-md-pre {
753
+ margin: .45em 0; padding: 7px 9px; border-radius: 7px; overflow: auto;
754
+ background: color-mix(in srgb, var(--skw-note-ink) 9%, transparent);
755
+ border: 1px solid color-mix(in srgb, var(--skw-note-ink) 14%, transparent);
756
+ font-size: .92em; line-height: 1.45; max-height: 180px;
757
+ }
758
+ .skw-md .skw-md-pre code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; white-space: pre; }
759
+ .skw-md .skw-md-code {
760
+ padding: .08em .34em; border-radius: 4px; font-size: .94em;
761
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
762
+ background: color-mix(in srgb, var(--skw-note-ink) 11%, transparent);
763
+ }
764
+ .skw-md .skw-md-quote {
765
+ margin: .4em 0; padding: .1em 0 .1em .7em;
766
+ border-left: 2px solid var(--skw-note-accent);
767
+ color: color-mix(in srgb, var(--skw-note-ink) 82%, transparent);
768
+ }
769
+ .skw-md .skw-md-rule { margin: .6em 0; border: 0; border-top: 1px solid color-mix(in srgb, var(--skw-note-ink) 22%, transparent); }
770
+ .skw-md .skw-md-link { color: var(--skw-note-accent-ink); text-decoration: underline; text-underline-offset: 2px; }
771
+ .skw-md .skw-md-link:hover { text-decoration-thickness: 2px; }
772
+ .skw-md .skw-md-table { margin: .45em 0; border-collapse: collapse; width: 100%; font-size: .94em; }
773
+ .skw-md .skw-md-table th, .skw-md .skw-md-table td {
774
+ padding: 3px 6px; border: 1px solid color-mix(in srgb, var(--skw-note-ink) 18%, transparent); text-align: left;
775
+ }
776
+ .skw-md .skw-md-table th { background: color-mix(in srgb, var(--skw-note-ink) 7%, transparent); font-weight: 600; }
777
+ .skw-md .skw-md-task { display: inline-flex; align-items: baseline; gap: 5px; }
778
+ .skw-md .skw-md-box {
779
+ display: inline-flex; align-items: center; justify-content: center;
780
+ width: 12px; height: 12px; flex: none; border-radius: 3px; font-size: 9px; line-height: 1;
781
+ border: 1px solid color-mix(in srgb, var(--skw-note-ink) 40%, transparent);
782
+ }
783
+ .skw-md .skw-md-task[data-checked="true"] .skw-md-box { background: var(--skw-note-accent); border-color: transparent; color: var(--skw-note-accent-ink); }
784
+ .skw-md .skw-md-task[data-checked="true"] { opacity: .78; }
785
+
786
+ /* Composer preview pane: same Markdown, on a neutral sheet instead of a note. */
787
+ .skw-preview-box {
788
+ min-height: 132px; max-height: 42vh; overflow: auto; padding: 9px 11px;
789
+ border: 1px solid var(--dsw-alias-border-l2); border-radius: 8px;
790
+ background: var(--dsw-alias-bg-layer-1); color: var(--dsw-alias-label-primary);
791
+ font-size: 13px; line-height: 20px;
792
+ }
793
+ .skw-preview-box .skw-md-link { color: var(--dsw-alias-brand-primary); }
794
+ .skw-preview-box .skw-md-code { background: var(--dsw-alias-bg-layer-2); }
795
+ .skw-preview-box .skw-md-pre { background: var(--dsw-alias-bg-layer-2); border-color: var(--dsw-alias-border-l1); }
796
+ .skw-preview-empty { margin: 0; color: var(--dsw-alias-label-tertiary); }
797
+ .skw-sheet-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
798
+ .skw-sheet-row .skw-spacer { flex: 1; }
799
+ .skw-sizes { display: inline-flex; gap: 2px; }
800
+ .skw-size {
801
+ height: 22px; min-width: 26px; padding: 0 8px; border: 1px solid var(--dsw-alias-border-l2);
802
+ border-radius: 999px; background: transparent; color: var(--dsw-alias-label-secondary);
803
+ cursor: pointer; font-size: 11.5px;
804
+ }
805
+ .skw-size:hover { color: var(--dsw-alias-label-primary); border-color: var(--dsw-alias-border-l3); }
806
+ .skw-size[data-on="true"] { background: var(--dsw-alias-label-primary); color: var(--dsw-alias-bg-base); border-color: transparent; }
807
+ .skw-size:focus-visible { outline: 2px solid var(--dsw-alias-brand-primary); outline-offset: 1px; }
808
+ /* The view switch is a small segment of its own, visually separated from the
809
+ filter chips because it changes the whole surface rather than a subset. */
810
+ .skw-view-seg { border-left: 1px solid var(--dsw-alias-border-l2); padding-left: 10px; margin-left: 2px; }
811
+
812
+ /* Pastel palettes.
813
+ Each note color defines five variables instead of one flat background, so the
814
+ card, its edge, its accent (dot/rail/tags) and its tinted shadow all derive
815
+ from the same hue. accent-ink is the readable text color for accents that
816
+ sit on the paper. Dark mode restates every group: a single shared hex would
817
+ either glow or go muddy on the dark shell. */
818
+ .skw-note[data-color="yellow"] {
819
+ --skw-note-bg: #fdf7dc; --skw-note-edge: #ecdc9e; --skw-note-ink: #4a3d12;
820
+ --skw-note-accent: #e0b53a; --skw-note-accent-ink: #8a6a0d; --skw-note-glow: #e8c86055;
821
+ }
822
+ .skw-note[data-color="pink"] {
823
+ --skw-note-bg: #fde9ef; --skw-note-edge: #f2c9d6; --skw-note-ink: #5a2437;
824
+ --skw-note-accent: #e78aa8; --skw-note-accent-ink: #a33d63; --skw-note-glow: #ef9ab855;
825
+ }
826
+ .skw-note[data-color="green"] {
827
+ --skw-note-bg: #e7f6ea; --skw-note-edge: #c3e5cd; --skw-note-ink: #1d3f27;
828
+ --skw-note-accent: #55b874; --skw-note-accent-ink: #2c7a47; --skw-note-glow: #6cc48c55;
829
+ }
830
+ .skw-note[data-color="blue"] {
831
+ --skw-note-bg: #e6f1fd; --skw-note-edge: #c2daf1; --skw-note-ink: #1b3550;
832
+ --skw-note-accent: #5b9de0; --skw-note-accent-ink: #2b6599; --skw-note-glow: #6ba5e055;
833
+ }
834
+ .skw-note[data-color="purple"] {
835
+ --skw-note-bg: #efe9fd; --skw-note-edge: #d6c9f2; --skw-note-ink: #36264f;
836
+ --skw-note-accent: #9b81e0; --skw-note-accent-ink: #5f3fa8; --skw-note-glow: #a68ee055;
837
+ }
838
+ .skw-note[data-color="orange"] {
839
+ --skw-note-bg: #fdeee0; --skw-note-edge: #f1d3b4; --skw-note-ink: #4f2d12;
840
+ --skw-note-accent: #e79a4e; --skw-note-accent-ink: #a45c16; --skw-note-glow: #edaa6a55;
841
+ }
842
+
843
+ body[data-ds-dark-theme] .skw-note[data-color="yellow"] {
844
+ --skw-note-bg: #3d3822; --skw-note-edge: #524a2c; --skw-note-ink: #f2e8c6;
845
+ --skw-note-accent: #e8c968; --skw-note-accent-ink: #f3dd9e; --skw-note-glow: #00000055;
846
+ }
847
+ body[data-ds-dark-theme] .skw-note[data-color="pink"] {
848
+ --skw-note-bg: #3f2630; --skw-note-edge: #55323f; --skw-note-ink: #f7dee6;
849
+ --skw-note-accent: #e78aa8; --skw-note-accent-ink: #f6bccd; --skw-note-glow: #00000055;
850
+ }
851
+ body[data-ds-dark-theme] .skw-note[data-color="green"] {
852
+ --skw-note-bg: #203a29; --skw-note-edge: #2d5038; --skw-note-ink: #d8f0de;
853
+ --skw-note-accent: #6cc48c; --skw-note-accent-ink: #a9e2bd; --skw-note-glow: #00000055;
854
+ }
855
+ body[data-ds-dark-theme] .skw-note[data-color="blue"] {
856
+ --skw-note-bg: #1f3346; --skw-note-edge: #2b475f; --skw-note-ink: #d6e8fa;
857
+ --skw-note-accent: #6ba5e0; --skw-note-accent-ink: #a8cdf0; --skw-note-glow: #00000055;
858
+ }
859
+ body[data-ds-dark-theme] .skw-note[data-color="purple"] {
860
+ --skw-note-bg: #2f2846; --skw-note-edge: #41365c; --skw-note-ink: #e3dbf7;
861
+ --skw-note-accent: #a68ee0; --skw-note-accent-ink: #c8b6f0; --skw-note-glow: #00000055;
862
+ }
863
+ body[data-ds-dark-theme] .skw-note[data-color="orange"] {
864
+ --skw-note-bg: #3f2e1c; --skw-note-edge: #55402a; --skw-note-ink: #f7e0c9;
865
+ --skw-note-accent: #e79a4e; --skw-note-accent-ink: #f4bd8c; --skw-note-glow: #00000055;
866
+ }
867
+
868
+ /* Composer sheet */
869
+ .skw-scrim { position: fixed; inset: 0; background: var(--dsw-alias-bg-mask-2); display: flex; align-items: center; justify-content: center; padding: 24px; z-index: 40; }
870
+ .skw-sheet { width: min(560px, 100%); max-height: 86vh; display: flex; flex-direction: column; border-radius: 12px; border: 1px solid var(--dsw-alias-border-l2); background: var(--dsw-alias-bg-base); box-shadow: 0 12px 40px var(--dsw-alias-bg-mask-3); overflow: hidden; }
871
+ .skw-sheet-head { display: flex; align-items: center; gap: 8px; padding: 12px 16px; border-bottom: 1px solid var(--dsw-alias-border-l1); font-weight: 600; }
872
+ .skw-sheet-body { padding: 12px 16px; display: flex; flex-direction: column; gap: 8px; overflow: auto; }
873
+ .skw-input, .skw-textarea { width: 100%; border: 1px solid var(--dsw-alias-border-l2); border-radius: 8px; background: var(--dsw-alias-bg-layer-1); color: var(--dsw-alias-label-primary); padding: 7px 10px; }
874
+ .skw-input:focus-visible, .skw-textarea:focus-visible { outline: 2px solid var(--dsw-alias-brand-primary); outline-offset: 1px; }
875
+ .skw-textarea { min-height: 132px; resize: vertical; line-height: 19px; }
876
+ .skw-swatches { display: flex; gap: 6px; align-items: center; }
877
+ .skw-swatch { width: 22px; height: 22px; border-radius: 50%; border: 2px solid transparent; cursor: pointer; padding: 0; }
878
+ .skw-swatch[data-on="true"] { border-color: var(--dsw-alias-label-primary); }
879
+ .skw-sheet-foot { display: flex; align-items: center; gap: 8px; padding: 12px 16px; border-top: 1px solid var(--dsw-alias-border-l1); }
880
+ .skw-sheet-foot .skw-spacer { flex: 1; }
881
+ .skw-hint { font-size: 11px; color: var(--dsw-alias-label-tertiary); }
882
+ .skw-error { margin: 0; padding: 6px 16px; font-size: 12px; color: var(--dsw-alias-state-error-primary); }
883
+
884
+ /* Sidebar glyph */
885
+ .skw-glyph { display: block; }
886
+ `;
887
+ var SWATCHES = [
888
+ { key: "yellow", css: "#f7d774" },
889
+ { key: "pink", css: "#f5b8c8" },
890
+ { key: "green", css: "#a8d8b0" },
891
+ { key: "blue", css: "#a8cdf0" },
892
+ { key: "purple", css: "#c3b3ee" },
893
+ { key: "orange", css: "#f3b98a" }
894
+ ];
895
+
896
+ // src/client/view.tsx
897
+ var import_jsx_runtime2 = require("react/jsx-runtime");
898
+ var ZOOM_MIN = 0.4;
899
+ var ZOOM_MAX = 2;
900
+ var DRAG_THRESHOLD = 3;
901
+ var WIDTH_CYCLE = ["sm", "md", "lg"];
902
+ function SidebarGlyph({ size = 18 }) {
903
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("svg", { className: "skw-glyph", width: size, height: size, viewBox: "0 0 24 24", fill: "none", "aria-hidden": "true", children: [
904
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("rect", { x: "4", y: "7.5", width: "11.5", height: "11.5", rx: "2", stroke: "currentColor", strokeWidth: "1.6" }),
905
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M8.5 5.2h8.2a2.4 2.4 0 0 1 2.4 2.4v8.2", stroke: "currentColor", strokeWidth: "1.6", strokeLinecap: "round" }),
906
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M6.9 11h5.7M6.9 14.4h4", stroke: "currentColor", strokeWidth: "1.4", strokeLinecap: "round", opacity: "0.75" })
907
+ ] });
908
+ }
909
+ function formatTime(iso, locale) {
910
+ const stamp = new Date(iso);
911
+ if (Number.isNaN(stamp.getTime())) return iso;
912
+ const now = Date.now();
913
+ const minutes = Math.round((now - stamp.getTime()) / 6e4);
914
+ const zh2 = locale.toLowerCase().startsWith("zh");
915
+ if (minutes < 1) return zh2 ? "\u521A\u521A" : "just now";
916
+ if (minutes < 60) return zh2 ? `${minutes} \u5206\u949F\u524D` : `${minutes}m ago`;
917
+ const hours = Math.round(minutes / 60);
918
+ if (hours < 24) return zh2 ? `${hours} \u5C0F\u65F6\u524D` : `${hours}h ago`;
919
+ const days = Math.round(hours / 24);
920
+ if (days < 7) return zh2 ? `${days} \u5929\u524D` : `${days}d ago`;
921
+ return stamp.toLocaleDateString(zh2 ? "zh-CN" : "en-US", { month: "short", day: "numeric" });
922
+ }
923
+ function parseTags(raw) {
924
+ return raw.split(/[,,\s]+/).map((tag) => tag.trim()).filter((tag) => tag !== "");
925
+ }
926
+ function Composer({
927
+ draft,
928
+ t,
929
+ busy,
930
+ error,
931
+ onChange,
932
+ onSave,
933
+ onCancel
934
+ }) {
935
+ const bodyRef = (0, import_react.useRef)(null);
936
+ const [preview, setPreview] = (0, import_react.useState)(false);
937
+ (0, import_react.useEffect)(() => {
938
+ bodyRef.current?.focus();
939
+ }, []);
940
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "skw-scrim", role: "presentation", onMouseDown: (event) => {
941
+ if (event.target === event.currentTarget && !busy) onCancel();
942
+ }, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
943
+ "div",
944
+ {
945
+ className: "skw-sheet",
946
+ role: "dialog",
947
+ "aria-modal": "true",
948
+ "aria-label": draft.id ? t("edit") : t("new"),
949
+ onKeyDown: (event) => {
950
+ if (event.repeat) return;
951
+ if (event.key === "Escape" && !busy) onCancel();
952
+ if (event.key === "Enter" && (event.metaKey || event.ctrlKey) && !busy) {
953
+ event.preventDefault();
954
+ onSave();
955
+ }
956
+ },
957
+ children: [
958
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "skw-sheet-head", children: [
959
+ draft.id ? t("edit") : t("new"),
960
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "skw-spacer" }),
961
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
962
+ "button",
963
+ {
964
+ className: "skw-btn skw-btn-quiet",
965
+ type: "button",
966
+ "data-on": preview,
967
+ "aria-pressed": preview,
968
+ onClick: () => setPreview((value) => !value),
969
+ children: preview ? t("edit") : t("preview")
970
+ }
971
+ )
972
+ ] }),
973
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "skw-sheet-body", children: [
974
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
975
+ "input",
976
+ {
977
+ className: "skw-input",
978
+ value: draft.title,
979
+ placeholder: t("titlePlaceholder"),
980
+ maxLength: LIMITS.title,
981
+ onChange: (event) => onChange({ title: event.target.value }),
982
+ "aria-label": t("titlePlaceholder")
983
+ }
984
+ ),
985
+ preview ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "skw-preview-box", children: draft.body.trim() === "" ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "skw-preview-empty", children: t("bodyPlaceholder") }) : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Markdown, { source: draft.body }) }) : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
986
+ "textarea",
987
+ {
988
+ ref: bodyRef,
989
+ className: "skw-textarea",
990
+ value: draft.body,
991
+ placeholder: t("bodyPlaceholder"),
992
+ maxLength: LIMITS.body,
993
+ onChange: (event) => onChange({ body: event.target.value }),
994
+ "aria-label": t("bodyPlaceholder")
995
+ }
996
+ ),
997
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
998
+ "input",
999
+ {
1000
+ className: "skw-input",
1001
+ value: draft.tags,
1002
+ placeholder: t("tagsPlaceholder"),
1003
+ onChange: (event) => onChange({ tags: event.target.value }),
1004
+ "aria-label": t("tagsPlaceholder")
1005
+ }
1006
+ ),
1007
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "skw-sheet-row", children: [
1008
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "skw-swatches", role: "group", "aria-label": t("colorLabel"), children: SWATCHES.map((swatch) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1009
+ "button",
1010
+ {
1011
+ type: "button",
1012
+ className: "skw-swatch",
1013
+ "data-on": draft.color === swatch.key,
1014
+ style: { background: swatch.css },
1015
+ "aria-label": swatch.key,
1016
+ "aria-pressed": draft.color === swatch.key,
1017
+ onClick: () => onChange({ color: swatch.key })
1018
+ },
1019
+ swatch.key
1020
+ )) }),
1021
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "skw-spacer" }),
1022
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "skw-sizes", role: "group", "aria-label": t("widthLabel"), children: WIDTH_CYCLE.map((width) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1023
+ "button",
1024
+ {
1025
+ type: "button",
1026
+ className: "skw-size",
1027
+ "data-on": draft.width === width,
1028
+ "aria-pressed": draft.width === width,
1029
+ onClick: () => onChange({ width }),
1030
+ children: t(`width_${width}`)
1031
+ },
1032
+ width
1033
+ )) })
1034
+ ] }),
1035
+ error !== "" && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "skw-error", role: "alert", children: error })
1036
+ ] }),
1037
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "skw-sheet-foot", children: [
1038
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "skw-hint", children: t("saveHint") }),
1039
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "skw-spacer" }),
1040
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { className: "skw-btn", type: "button", onClick: onCancel, disabled: busy, children: t("cancel") }),
1041
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { className: "skw-btn skw-btn-primary", type: "button", onClick: onSave, disabled: busy, children: t("save") })
1042
+ ] })
1043
+ ]
1044
+ }
1045
+ ) });
1046
+ }
1047
+ function ConfirmDialog({
1048
+ title,
1049
+ message,
1050
+ confirmLabel,
1051
+ cancelLabel,
1052
+ busy,
1053
+ onConfirm,
1054
+ onCancel
1055
+ }) {
1056
+ const cancelRef = (0, import_react.useRef)(null);
1057
+ (0, import_react.useEffect)(() => {
1058
+ const previous = typeof document === "undefined" ? null : document.activeElement;
1059
+ cancelRef.current?.focus();
1060
+ return () => {
1061
+ if (previous !== null && typeof previous.focus === "function") previous.focus();
1062
+ };
1063
+ }, []);
1064
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1065
+ "div",
1066
+ {
1067
+ className: "skw-scrim",
1068
+ role: "presentation",
1069
+ onMouseDown: (event) => {
1070
+ if (event.target === event.currentTarget && !busy) onCancel();
1071
+ },
1072
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
1073
+ "div",
1074
+ {
1075
+ className: "skw-sheet",
1076
+ role: "dialog",
1077
+ "aria-modal": "true",
1078
+ "aria-label": title,
1079
+ style: { width: "min(420px, 100%)" },
1080
+ onKeyDown: (event) => {
1081
+ if (event.repeat) return;
1082
+ if (event.key === "Escape" && !busy) {
1083
+ event.stopPropagation();
1084
+ onCancel();
1085
+ }
1086
+ },
1087
+ children: [
1088
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "skw-sheet-head", children: title }),
1089
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "skw-sheet-body", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { style: { margin: 0 }, children: message }) }),
1090
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "skw-sheet-foot", children: [
1091
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "skw-spacer" }),
1092
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { ref: cancelRef, className: "skw-btn", type: "button", onClick: onCancel, disabled: busy, children: cancelLabel }),
1093
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { className: "skw-btn skw-btn-danger", type: "button", onClick: onConfirm, disabled: busy, children: confirmLabel })
1094
+ ] })
1095
+ ]
1096
+ }
1097
+ )
1098
+ }
1099
+ );
1100
+ }
1101
+ function NoteCard({
1102
+ note,
1103
+ index,
1104
+ t,
1105
+ locale,
1106
+ onEdit,
1107
+ onPatch,
1108
+ onRemove,
1109
+ onResize,
1110
+ onDragStart
1111
+ }) {
1112
+ const layout = layoutOf(note, index);
1113
+ const style = {
1114
+ left: `${layout.x}px`,
1115
+ top: `${layout.y}px`,
1116
+ width: `${WIDTH_PX[layout.width]}px`,
1117
+ transform: `rotate(${layout.rotate}deg)`
1118
+ };
1119
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
1120
+ "article",
1121
+ {
1122
+ className: "skw-note",
1123
+ "data-color": note.color,
1124
+ "data-pinned": note.pinned,
1125
+ "data-archived": note.archived,
1126
+ "data-width": layout.width,
1127
+ style,
1128
+ "aria-label": note.title || note.body.slice(0, 40),
1129
+ tabIndex: 0,
1130
+ onPointerDown: (event) => onDragStart(note, event),
1131
+ onDoubleClick: () => onEdit(note),
1132
+ onKeyDown: (event) => {
1133
+ if (event.key === "Enter" && !event.metaKey && !event.ctrlKey) {
1134
+ event.preventDefault();
1135
+ onEdit(note);
1136
+ }
1137
+ },
1138
+ children: [
1139
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "skw-note-bar", children: [
1140
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "skw-note-dot", "aria-hidden": "true" }),
1141
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "skw-spacer" }),
1142
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "skw-note-tools", children: [
1143
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1144
+ "button",
1145
+ {
1146
+ className: "skw-icon-btn",
1147
+ type: "button",
1148
+ title: note.pinned ? t("unpin") : t("pin"),
1149
+ "data-on": note.pinned,
1150
+ "aria-label": note.pinned ? t("unpin") : t("pin"),
1151
+ onPointerDown: (event) => event.stopPropagation(),
1152
+ onClick: () => onPatch(note, { pinned: !note.pinned }),
1153
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "skw-pin", "aria-hidden": "true", children: note.pinned ? "\u2605" : "\u2606" })
1154
+ }
1155
+ ),
1156
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1157
+ "button",
1158
+ {
1159
+ className: "skw-icon-btn",
1160
+ type: "button",
1161
+ title: t("resize"),
1162
+ "aria-label": t("resize"),
1163
+ onPointerDown: (event) => event.stopPropagation(),
1164
+ onClick: () => onResize(note, WIDTH_CYCLE[(WIDTH_CYCLE.indexOf(layout.width) + 1) % WIDTH_CYCLE.length] ?? "md"),
1165
+ children: "\u2922"
1166
+ }
1167
+ ),
1168
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1169
+ "button",
1170
+ {
1171
+ className: "skw-icon-btn",
1172
+ type: "button",
1173
+ title: t("edit"),
1174
+ "aria-label": t("edit"),
1175
+ onPointerDown: (event) => event.stopPropagation(),
1176
+ onClick: () => onEdit(note),
1177
+ children: "\u270E"
1178
+ }
1179
+ ),
1180
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1181
+ "button",
1182
+ {
1183
+ className: "skw-icon-btn",
1184
+ type: "button",
1185
+ title: note.archived ? t("restore") : t("archive"),
1186
+ "aria-label": note.archived ? t("restore") : t("archive"),
1187
+ onPointerDown: (event) => event.stopPropagation(),
1188
+ onClick: () => onPatch(note, { archived: !note.archived }),
1189
+ children: note.archived ? "\u21A9" : "\u25A4"
1190
+ }
1191
+ ),
1192
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1193
+ "button",
1194
+ {
1195
+ className: "skw-icon-btn",
1196
+ type: "button",
1197
+ title: t("delete"),
1198
+ "aria-label": t("delete"),
1199
+ onPointerDown: (event) => event.stopPropagation(),
1200
+ onClick: () => onRemove(note),
1201
+ children: "\u2715"
1202
+ }
1203
+ )
1204
+ ] })
1205
+ ] }),
1206
+ note.title !== "" && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "skw-note-head", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "skw-note-title", children: note.title }) }),
1207
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "skw-note-body is-clamped", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Markdown, { source: note.body, compact: true }) }),
1208
+ note.tags.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "skw-note-tags", children: note.tags.map((tag) => /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { className: "skw-tag", children: [
1209
+ "#",
1210
+ tag
1211
+ ] }, tag)) }),
1212
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "skw-note-foot", children: [
1213
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { children: formatTime(note.updatedAt, locale) }),
1214
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "skw-spacer" }),
1215
+ note.archived && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "skw-note-badge", children: t("archive") }),
1216
+ note.pinned && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { className: "skw-note-badge", "aria-hidden": "true", children: [
1217
+ "\u2605 ",
1218
+ t("pin")
1219
+ ] })
1220
+ ] })
1221
+ ]
1222
+ }
1223
+ );
1224
+ }
1225
+ function NoteRow({ note, t, locale, onEdit }) {
1226
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("button", { className: "skw-row", type: "button", "data-color": note.color, "data-archived": note.archived, onClick: () => onEdit(note), children: [
1227
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "skw-row-dot", "aria-hidden": "true" }),
1228
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "skw-row-title", children: note.title || note.body.split("\n")[0] }),
1229
+ note.tags.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "skw-row-tags", children: note.tags.map((tag) => `#${tag}`).join(" ") }),
1230
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "skw-spacer" }),
1231
+ note.pinned && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "skw-row-badge", "aria-hidden": "true", children: "\u2605" }),
1232
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "skw-row-time", children: formatTime(note.updatedAt, locale) })
1233
+ ] });
1234
+ }
1235
+ function WallPage({ api, locale, t }) {
1236
+ const [notes, setNotes] = (0, import_react.useState)([]);
1237
+ const [filter, setFilter] = (0, import_react.useState)("all");
1238
+ const [sort, setSort] = (0, import_react.useState)("pinned");
1239
+ const [view, setView] = (0, import_react.useState)("canvas");
1240
+ const [query, setQuery] = (0, import_react.useState)("");
1241
+ const [showArchived, setShowArchived] = (0, import_react.useState)(false);
1242
+ const [loading, setLoading] = (0, import_react.useState)(true);
1243
+ const [busy, setBusy] = (0, import_react.useState)(false);
1244
+ const [error, setError] = (0, import_react.useState)("");
1245
+ const [draft, setDraft] = (0, import_react.useState)(null);
1246
+ const [draftError, setDraftError] = (0, import_react.useState)("");
1247
+ const [confirming, setConfirming] = (0, import_react.useState)(null);
1248
+ const [camera, setCamera] = (0, import_react.useState)({ zoom: 1 });
1249
+ const [, setLocaleRevision] = (0, import_react.useState)(0);
1250
+ const alive = (0, import_react.useRef)(true);
1251
+ const life = (0, import_react.useRef)(new AbortController());
1252
+ const sequence = (0, import_react.useRef)(0);
1253
+ const inFlight = (0, import_react.useRef)(false);
1254
+ const drag = (0, import_react.useRef)(null);
1255
+ const canvasRef = (0, import_react.useRef)(null);
1256
+ const activeLocale = locale.getSnapshot().active;
1257
+ const stableIndex = (0, import_react.useMemo)(() => {
1258
+ const map = /* @__PURE__ */ new Map();
1259
+ notes.forEach((note, index) => map.set(note.id, index));
1260
+ return map;
1261
+ }, [notes]);
1262
+ const indexOf = (note) => stableIndex.get(note.id) ?? 0;
1263
+ (0, import_react.useEffect)(() => {
1264
+ const unsubscribe = locale.subscribe(() => {
1265
+ if (alive.current) setLocaleRevision((value) => value + 1);
1266
+ });
1267
+ return unsubscribe;
1268
+ }, [locale]);
1269
+ const reload = (0, import_react.useMemo)(() => async (options) => {
1270
+ const seq = ++sequence.current;
1271
+ setLoading(true);
1272
+ try {
1273
+ const next = await api.list(options.includeArchived, options.sort, life.current.signal);
1274
+ if (alive.current && seq === sequence.current) {
1275
+ setNotes(next);
1276
+ if (options.clearError !== false) setError("");
1277
+ }
1278
+ } catch (cause) {
1279
+ if (alive.current && seq === sequence.current && !life.current.signal.aborted) setError(errorText(cause, t));
1280
+ } finally {
1281
+ if (alive.current && seq === sequence.current) setLoading(false);
1282
+ }
1283
+ }, [api, t]);
1284
+ const includeArchived = showArchived || filter === "archived";
1285
+ (0, import_react.useEffect)(() => {
1286
+ alive.current = true;
1287
+ life.current = new AbortController();
1288
+ void reload({ includeArchived, sort });
1289
+ return () => {
1290
+ alive.current = false;
1291
+ life.current.abort();
1292
+ sequence.current += 1;
1293
+ };
1294
+ }, [reload, includeArchived, sort]);
1295
+ async function mutate(run) {
1296
+ if (inFlight.current) return;
1297
+ inFlight.current = true;
1298
+ setBusy(true);
1299
+ setError("");
1300
+ try {
1301
+ await run();
1302
+ } catch (cause) {
1303
+ if (alive.current && !life.current.signal.aborted) setError(errorText(cause, t));
1304
+ } finally {
1305
+ if (alive.current) {
1306
+ await reload({ includeArchived, sort, clearError: false }).catch(() => {
1307
+ });
1308
+ setBusy(false);
1309
+ }
1310
+ inFlight.current = false;
1311
+ }
1312
+ }
1313
+ function openComposer(note) {
1314
+ setDraftError("");
1315
+ setDraft(note ? { id: note.id, title: note.title, body: note.body, tags: note.tags.join(" "), color: note.color, width: layoutOf(note, 0).width } : { id: null, title: "", body: "", tags: "", color: "yellow", width: "md" });
1316
+ }
1317
+ async function saveDraft() {
1318
+ if (!draft || inFlight.current) return;
1319
+ const body = draft.body.trim();
1320
+ if (body === "") {
1321
+ setDraftError(t("emptyBody"));
1322
+ return;
1323
+ }
1324
+ setDraftError("");
1325
+ await mutate(async () => {
1326
+ const tags = parseTags(draft.tags);
1327
+ if (draft.id === null) {
1328
+ await api.create({ body, title: draft.title.trim(), tags, color: draft.color }, life.current.signal);
1329
+ } else {
1330
+ await api.update(draft.id, { body, title: draft.title.trim(), tags, color: draft.color }, life.current.signal);
1331
+ const current = notes.find((note) => note.id === draft.id);
1332
+ if (current !== void 0) {
1333
+ const layout = layoutOf(current, stableIndex.get(current.id) ?? 0);
1334
+ if (layout.width !== draft.width) await api.move(current.id, { ...layout, width: draft.width }, life.current.signal);
1335
+ }
1336
+ }
1337
+ setDraft(null);
1338
+ });
1339
+ }
1340
+ const visible = (0, import_react.useMemo)(() => {
1341
+ const needle = query.trim().toLowerCase();
1342
+ return notes.filter((note) => {
1343
+ if (filter === "archived" && !note.archived) return false;
1344
+ if (filter === "pinned" && !note.pinned) return false;
1345
+ if (filter === "all" && note.archived && !showArchived) return false;
1346
+ if (needle === "") return true;
1347
+ return `${note.title} ${note.body} ${note.tags.join(" ")}`.toLowerCase().includes(needle);
1348
+ });
1349
+ }, [notes, filter, query, showArchived]);
1350
+ const archivedCount = notes.filter((note) => note.archived).length;
1351
+ const extent = (0, import_react.useMemo)(() => {
1352
+ let maxX = 0;
1353
+ let maxY = 0;
1354
+ for (const [index, note] of visible.entries()) {
1355
+ const layout = layoutOf(note, index);
1356
+ maxX = Math.max(maxX, layout.x + WIDTH_PX[layout.width]);
1357
+ maxY = Math.max(maxY, layout.y + 220);
1358
+ }
1359
+ return { width: maxX + 420, height: maxY + 320 };
1360
+ }, [visible]);
1361
+ function startDrag(note, event) {
1362
+ if (event.button !== 0 || event.isPrimary === false) return;
1363
+ const layout = layoutOf(note, indexOf(note));
1364
+ const node = event.currentTarget;
1365
+ node.setAttribute("data-dragging", "true");
1366
+ drag.current = {
1367
+ id: note.id,
1368
+ pointerId: event.pointerId,
1369
+ node,
1370
+ startX: event.clientX,
1371
+ startY: event.clientY,
1372
+ originX: layout.x,
1373
+ originY: layout.y,
1374
+ rotate: layout.rotate,
1375
+ width: layout.width,
1376
+ moved: false,
1377
+ captured: false,
1378
+ x: layout.x,
1379
+ y: layout.y,
1380
+ frame: 0,
1381
+ lastX: event.clientX,
1382
+ lastY: event.clientY
1383
+ };
1384
+ try {
1385
+ node.setPointerCapture?.(event.pointerId);
1386
+ drag.current.captured = true;
1387
+ } catch {
1388
+ }
1389
+ }
1390
+ (0, import_react.useEffect)(() => {
1391
+ const paint = (current) => {
1392
+ current.frame = 0;
1393
+ const node = current.node;
1394
+ if (node === null) return;
1395
+ const dx = (current.lastX - current.startX) / camera.zoom;
1396
+ const dy = (current.lastY - current.startY) / camera.zoom;
1397
+ node.style.transform = `translate3d(${dx}px, ${dy}px, 0) rotate(${current.rotate}deg)`;
1398
+ };
1399
+ const move = (event) => {
1400
+ const current = drag.current;
1401
+ if (current === null || event.pointerId !== current.pointerId) return;
1402
+ current.lastX = event.clientX;
1403
+ current.lastY = event.clientY;
1404
+ if (!current.moved) {
1405
+ if (Math.hypot(event.clientX - current.startX, event.clientY - current.startY) < DRAG_THRESHOLD) return;
1406
+ current.moved = true;
1407
+ }
1408
+ if (current.frame === 0) current.frame = requestAnimationFrame(() => paint(current));
1409
+ };
1410
+ const finish = (event) => {
1411
+ const current = drag.current;
1412
+ if (current === null || event.pointerId !== current.pointerId) return;
1413
+ drag.current = null;
1414
+ if (current.frame !== 0) cancelAnimationFrame(current.frame);
1415
+ const node = current.node;
1416
+ if (node !== null) {
1417
+ node.removeAttribute("data-dragging");
1418
+ node.style.transform = `translate3d(0, 0, 0) rotate(${current.rotate}deg)`;
1419
+ if (current.captured) {
1420
+ try {
1421
+ node.releasePointerCapture?.(event.pointerId);
1422
+ } catch {
1423
+ }
1424
+ }
1425
+ }
1426
+ if (!current.moved) return;
1427
+ const dx = (current.lastX - current.startX) / camera.zoom;
1428
+ const dy = (current.lastY - current.startY) / camera.zoom;
1429
+ const layout = {
1430
+ x: current.originX + dx,
1431
+ y: current.originY + dy,
1432
+ rotate: current.rotate,
1433
+ width: current.width
1434
+ };
1435
+ void mutate(async () => {
1436
+ await api.move(current.id, layout, life.current.signal);
1437
+ });
1438
+ };
1439
+ window.addEventListener("pointermove", move, { passive: true });
1440
+ window.addEventListener("pointerup", finish);
1441
+ window.addEventListener("pointercancel", finish);
1442
+ return () => {
1443
+ window.removeEventListener("pointermove", move);
1444
+ window.removeEventListener("pointerup", finish);
1445
+ window.removeEventListener("pointercancel", finish);
1446
+ const pending = drag.current;
1447
+ if (pending !== null && pending.frame !== 0) cancelAnimationFrame(pending.frame);
1448
+ };
1449
+ }, [camera.zoom, api, mutate]);
1450
+ function onWheel(event) {
1451
+ if (!event.ctrlKey && !event.metaKey) return;
1452
+ event.preventDefault();
1453
+ const next = Math.min(ZOOM_MAX, Math.max(ZOOM_MIN, camera.zoom - event.deltaY * 2e-3));
1454
+ setCamera((current) => ({ ...current, zoom: next }));
1455
+ }
1456
+ const zoomBy = (delta) => {
1457
+ setCamera((current) => ({ ...current, zoom: Math.min(ZOOM_MAX, Math.max(ZOOM_MIN, current.zoom + delta)) }));
1458
+ };
1459
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("section", { className: "skw-page", "aria-label": t("title"), children: [
1460
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("style", { children: WALL_CSS }),
1461
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("header", { className: "skw-head", children: [
1462
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "skw-head-text", children: [
1463
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("h1", { className: "skw-title", children: [
1464
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "skw-title-mark", "aria-hidden": "true", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(SidebarGlyph, { size: 15 }) }),
1465
+ t("title")
1466
+ ] }),
1467
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "skw-subtitle", children: t("subtitle") })
1468
+ ] }),
1469
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "skw-actions", children: [
1470
+ archivedCount > 0 && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { className: "skw-btn skw-btn-quiet", type: "button", onClick: () => setConfirming("clear"), disabled: busy, children: t("clearArchived") }),
1471
+ view === "canvas" && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1472
+ "button",
1473
+ {
1474
+ className: "skw-btn skw-btn-quiet",
1475
+ type: "button",
1476
+ disabled: busy || visible.length === 0,
1477
+ onClick: () => void mutate(async () => {
1478
+ await api.arrange(life.current.signal);
1479
+ }),
1480
+ children: t("arrange")
1481
+ }
1482
+ ),
1483
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("button", { className: "skw-btn skw-btn-primary", type: "button", onClick: () => openComposer(), disabled: busy, children: [
1484
+ "+ ",
1485
+ t("new")
1486
+ ] })
1487
+ ] })
1488
+ ] }),
1489
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "skw-toolbar", children: [
1490
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "skw-search-wrap", children: [
1491
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "skw-search-icon", "aria-hidden": "true", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", children: [
1492
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("circle", { cx: "10.5", cy: "10.5", r: "6.2", stroke: "currentColor", strokeWidth: "1.8" }),
1493
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M15.4 15.4L20 20", stroke: "currentColor", strokeWidth: "1.8", strokeLinecap: "round" })
1494
+ ] }) }),
1495
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1496
+ "input",
1497
+ {
1498
+ className: "skw-search",
1499
+ type: "search",
1500
+ value: query,
1501
+ placeholder: t("search"),
1502
+ "aria-label": t("search"),
1503
+ onChange: (event) => setQuery(event.target.value)
1504
+ }
1505
+ )
1506
+ ] }),
1507
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "skw-seg", role: "group", "aria-label": t("filters"), children: ["all", "pinned", "archived"].map((key) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { type: "button", "data-on": filter === key, onClick: () => setFilter(key), children: key === "all" ? t("all") : key === "pinned" ? t("pin") : t("archive") }, key)) }),
1508
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1509
+ "button",
1510
+ {
1511
+ className: "skw-toggle",
1512
+ type: "button",
1513
+ "data-on": showArchived,
1514
+ "aria-pressed": showArchived,
1515
+ onClick: () => setShowArchived((value) => !value),
1516
+ children: showArchived ? t("hideArchived") : t("showArchived")
1517
+ }
1518
+ ),
1519
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "skw-seg", role: "group", "aria-label": t("sortPinned"), children: ["pinned", "created", "updated"].map((key) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { type: "button", "data-on": sort === key, onClick: () => setSort(key), children: key === "pinned" ? t("sortPinned") : key === "created" ? t("sortCreated") : t("sortUpdated") }, key)) }),
1520
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "skw-seg skw-view-seg", role: "group", "aria-label": t("viewLabel"), children: ["canvas", "grid", "list"].map((mode) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { type: "button", "data-on": view === mode, onClick: () => setView(mode), "aria-label": t(`view_${mode}`), children: t(`view_${mode}`) }, mode)) }),
1521
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "skw-count", children: t("count", { count: visible.length }) })
1522
+ ] }),
1523
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "skw-body", "data-view": view, children: [
1524
+ error !== "" && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("p", { className: "skw-error", role: "alert", children: [
1525
+ error,
1526
+ " ",
1527
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { className: "skw-btn skw-btn-quiet", type: "button", onClick: () => void reload({ includeArchived, sort }), children: t("retry") })
1528
+ ] }),
1529
+ loading && notes.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "skw-empty", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { children: t("loading") }) }) : visible.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "skw-empty", children: [
1530
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "skw-empty-icon", "aria-hidden": "true", children: "\u{1F5D2}\uFE0F" }),
1531
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { children: filter === "archived" ? t("emptyArchived") : query.trim() !== "" ? t("noResults") : t("empty") }),
1532
+ filter !== "archived" && query.trim() === "" && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { className: "skw-btn", type: "button", onClick: () => openComposer(), children: t("new") })
1533
+ ] }) : view === "list" ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "skw-list", children: visible.map((note) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(NoteRow, { note, t, locale: activeLocale, onEdit: openComposer }, note.id)) }) : view === "grid" ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "skw-grid", children: visible.map((note) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1534
+ NoteCard,
1535
+ {
1536
+ note,
1537
+ index: indexOf(note),
1538
+ t,
1539
+ locale: activeLocale,
1540
+ onEdit: openComposer,
1541
+ onPatch: (target, patch) => void mutate(async () => {
1542
+ await api.update(target.id, patch, life.current.signal);
1543
+ }),
1544
+ onRemove: (target) => setConfirming(target),
1545
+ onResize: (target, width) => void mutate(async () => {
1546
+ const current = notes.find((candidate) => candidate.id === target.id);
1547
+ if (current !== void 0) await api.move(target.id, { ...layoutOf(current, stableIndex.get(current.id) ?? 0), width }, life.current.signal);
1548
+ }),
1549
+ onDragStart: () => {
1550
+ }
1551
+ },
1552
+ note.id
1553
+ )) }) : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "skw-canvas-scroll", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1554
+ "div",
1555
+ {
1556
+ ref: canvasRef,
1557
+ className: "skw-canvas",
1558
+ "data-zoomed": camera.zoom !== 1,
1559
+ style: { width: `${extent.width}px`, height: `${extent.height}px` },
1560
+ onWheel,
1561
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "skw-world", style: { transform: `scale(${camera.zoom})`, transformOrigin: "0 0" }, children: visible.map((note) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "skw-note-slot", "data-note-id": note.id, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1562
+ NoteCard,
1563
+ {
1564
+ note,
1565
+ index: indexOf(note),
1566
+ t,
1567
+ locale: activeLocale,
1568
+ onEdit: openComposer,
1569
+ onPatch: (target, patch) => void mutate(async () => {
1570
+ await api.update(target.id, patch, life.current.signal);
1571
+ }),
1572
+ onRemove: (target) => setConfirming(target),
1573
+ onResize: (target, width) => void mutate(async () => {
1574
+ const current = notes.find((candidate) => candidate.id === target.id);
1575
+ if (current !== void 0) await api.move(target.id, { ...layoutOf(current, stableIndex.get(current.id) ?? 0), width }, life.current.signal);
1576
+ }),
1577
+ onDragStart: startDrag
1578
+ }
1579
+ ) }, note.id)) })
1580
+ }
1581
+ ) })
1582
+ ] }),
1583
+ view === "canvas" && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "skw-cameras", role: "group", "aria-label": t("zoomLabel"), children: [
1584
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { className: "skw-camera-btn", type: "button", "aria-label": t("zoomOut"), onClick: () => zoomBy(-0.15), children: "\u2212" }),
1585
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { className: "skw-camera-value", children: [
1586
+ Math.round(camera.zoom * 100),
1587
+ "%"
1588
+ ] }),
1589
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { className: "skw-camera-btn", type: "button", "aria-label": t("zoomIn"), onClick: () => zoomBy(0.15), children: "+" }),
1590
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1591
+ "button",
1592
+ {
1593
+ className: "skw-camera-btn",
1594
+ type: "button",
1595
+ "aria-label": t("zoomReset"),
1596
+ onClick: () => setCamera({ zoom: 1 }),
1597
+ children: "\u2316"
1598
+ }
1599
+ )
1600
+ ] }),
1601
+ draft && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1602
+ Composer,
1603
+ {
1604
+ draft,
1605
+ t,
1606
+ busy,
1607
+ error: draftError,
1608
+ onChange: (patch) => setDraft((current) => current === null ? current : { ...current, ...patch }),
1609
+ onSave: () => void saveDraft(),
1610
+ onCancel: () => {
1611
+ if (!busy) {
1612
+ setDraft(null);
1613
+ setDraftError("");
1614
+ }
1615
+ }
1616
+ }
1617
+ ),
1618
+ confirming !== null && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1619
+ ConfirmDialog,
1620
+ {
1621
+ title: confirming === "clear" ? t("clearArchived") : t("delete"),
1622
+ message: confirming === "clear" ? t("confirmClear") : t("confirmDelete"),
1623
+ confirmLabel: t("delete"),
1624
+ cancelLabel: t("cancel"),
1625
+ busy,
1626
+ onConfirm: () => {
1627
+ const target = confirming;
1628
+ setConfirming(null);
1629
+ void mutate(async () => {
1630
+ if (target === "clear") await api.clearArchived(life.current.signal);
1631
+ else await api.remove([target.id], life.current.signal);
1632
+ });
1633
+ },
1634
+ onCancel: () => setConfirming(null)
1635
+ }
1636
+ )
1637
+ ] });
1638
+ }
1639
+ function errorText(cause, t) {
1640
+ if (cause instanceof ActionError) {
1641
+ const known = {
1642
+ "sticky-wall/empty-body": "emptyBody",
1643
+ "sticky-wall/wall-full": "wallFull",
1644
+ "sticky-wall/body-too-long": "tooLong",
1645
+ "sticky-wall/title-too-long": "tooLong"
1646
+ };
1647
+ const key = known[cause.code];
1648
+ return key === void 0 ? cause.message : t(key);
1649
+ }
1650
+ return cause instanceof Error ? cause.message : String(cause);
1651
+ }
1652
+
1653
+ // src/client/index.tsx
1654
+ var import_jsx_runtime3 = require("react/jsx-runtime");
1655
+ var inject = ["slots", "locale", "connection"];
1656
+ var SIDEBAR_SLOT = "sidebar.panellist";
1657
+ var MAIN_SLOT = "main";
1658
+ function registerDictionaries(ctx) {
1659
+ try {
1660
+ return ctx.locale.register(NAMESPACE, { zh: { ...zh }, en: { ...en } });
1661
+ } catch {
1662
+ return void 0;
1663
+ }
1664
+ }
1665
+ function apply(ctx) {
1666
+ const lifetime = new AbortController();
1667
+ const t = ctx.locale.bind(NAMESPACE);
1668
+ const api = createApi(ctx.connection, lifetime.signal);
1669
+ ctx.effect(() => {
1670
+ const disposeDictionary = registerDictionaries(ctx);
1671
+ return () => {
1672
+ lifetime.abort();
1673
+ try {
1674
+ disposeDictionary?.();
1675
+ } catch {
1676
+ }
1677
+ };
1678
+ }, "skillre-sticky-wall client lifetime");
1679
+ ctx.effect(
1680
+ () => ctx.slots.inject(
1681
+ SIDEBAR_SLOT,
1682
+ () => ctx.slots.register({ name: SIDEBAR_SLOT, id: PANEL_ID, order: 30, label: () => t("nav") }, SidebarGlyph)
1683
+ ),
1684
+ "skillre-sticky-wall sidebar entry"
1685
+ );
1686
+ ctx.effect(
1687
+ () => ctx.slots.inject(MAIN_SLOT, () => ctx.slots.register({ name: MAIN_SLOT, key: PANEL_ID }, () => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(WallPage, { api, locale: ctx.locale, t }))),
1688
+ "skillre-sticky-wall wall page"
1689
+ );
1690
+ }
1691
+
1692
+ return module.exports;
1693
+ }
1694
+ });