@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/LICENSE +21 -0
- package/README.en.md +154 -0
- package/README.md +164 -2
- package/UNINSTALL.md +26 -0
- package/compatibility.json +83 -0
- package/cordis.patch.yml +10 -0
- package/docs/DETAIL.md +1 -0
- package/docs/DETAIL.zh.md +1 -0
- package/docs/PREVIEW.md +39 -0
- package/docs/preview.svg +170 -0
- package/docs/preview.zh.svg +170 -0
- package/icon.svg +15 -0
- package/lib/client.js +1694 -0
- package/lib/config.d.ts +22 -0
- package/lib/config.js +21 -0
- package/lib/index.d.ts +23 -0
- package/lib/index.js +184 -0
- package/lib/layout.d.ts +76 -0
- package/lib/layout.js +259 -0
- package/lib/store.d.ts +182 -0
- package/lib/store.js +452 -0
- package/lib/transport.d.ts +48 -0
- package/lib/transport.js +174 -0
- package/lib/types.d.ts +172 -0
- package/lib/types.js +64 -0
- package/locale/en.json +7 -0
- package/locale/zh.json +7 -0
- package/package.json +116 -4
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
|
+
});
|