@jant/core 0.3.35 → 0.3.36
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client/assets/module-RjUF93sV.js +716 -0
- package/dist/client/assets/native-48B9X9Wg.js +1 -0
- package/dist/client/assets/url-8Dj-5CLW.js +1 -0
- package/dist/client/client.css +1 -1
- package/dist/client/client.js +3109 -2294
- package/dist/index.js +3026 -2778
- package/package.json +13 -4
- package/src/__tests__/helpers/app.ts +1 -1
- package/src/__tests__/helpers/db.ts +6 -0
- package/src/app.tsx +1 -5
- package/src/{lib → client}/avatar-upload.ts +1 -1
- package/src/{lib → client}/collection-form-bridge.ts +2 -2
- package/src/{ui → client}/components/__tests__/jant-collection-form.test.ts +26 -9
- package/src/{ui → client}/components/__tests__/jant-compose-dialog.test.ts +46 -14
- package/src/{ui → client}/components/__tests__/jant-compose-editor.test.ts +64 -24
- package/src/{ui → client}/components/__tests__/jant-post-form.test.ts +24 -14
- package/src/{ui → client}/components/__tests__/jant-settings-general.test.ts +3 -3
- package/src/client/components/collection-sidebar-types.ts +45 -0
- package/src/{ui → client}/components/collection-types.ts +3 -4
- package/src/{ui → client}/components/compose-types.ts +3 -1
- package/src/{ui → client}/components/jant-collection-form.ts +301 -182
- package/src/client/components/jant-collection-sidebar.ts +801 -0
- package/src/{ui → client}/components/jant-compose-dialog.ts +231 -1
- package/src/client/components/jant-compose-editor.ts +1249 -0
- package/src/client/components/jant-compose-fullscreen.ts +338 -0
- package/src/client/components/jant-media-lightbox.ts +257 -0
- package/src/{ui → client}/components/jant-nav-manager.ts +143 -84
- package/src/{ui → client}/components/jant-post-form.ts +57 -8
- package/src/{ui → client}/components/jant-settings-general.ts +2 -2
- package/src/{ui → client}/components/nav-manager-types.ts +3 -0
- package/src/{ui → client}/components/post-form-template.ts +35 -31
- package/src/{ui → client}/components/post-form-types.ts +7 -3
- package/src/{lib → client}/compose-bridge.ts +9 -7
- package/src/client/lazy-slugify.ts +51 -0
- package/src/{lib → client}/media-upload.ts +16 -3
- package/src/{lib → client}/nav-manager-bridge.ts +1 -1
- package/src/client/page-slug-bridge.ts +42 -0
- package/src/{lib → client}/post-form-bridge.ts +2 -2
- package/src/{lib → client}/settings-bridge.ts +3 -3
- package/src/client/tiptap/bubble-menu.ts +205 -0
- package/src/client/tiptap/create-editor.ts +40 -0
- package/src/client/tiptap/exitable-marks.ts +73 -0
- package/src/client/tiptap/extensions.ts +60 -0
- package/src/client/tiptap/image-node.ts +488 -0
- package/src/client/tiptap/link-toolbar.ts +371 -0
- package/src/client/tiptap/more-break.ts +50 -0
- package/src/client/tiptap/paste-image.ts +140 -0
- package/src/client/tiptap/slash-commands.ts +328 -0
- package/src/{types → client/types}/sortablejs.d.ts +1 -1
- package/src/client.ts +24 -17
- package/src/db/migrations/0012_add_tiptap_columns.sql +2 -0
- package/src/db/migrations/0013_replace_featured_with_visibility.sql +8 -0
- package/src/db/schema.ts +6 -1
- package/src/i18n/locales/en.po +641 -215
- package/src/i18n/locales/en.ts +1 -1
- package/src/i18n/locales/zh-Hans.po +642 -204
- package/src/i18n/locales/zh-Hans.ts +1 -1
- package/src/i18n/locales/zh-Hant.po +642 -204
- package/src/i18n/locales/zh-Hant.ts +1 -1
- package/src/lib/__tests__/resolve-config.test.ts +2 -2
- package/src/lib/__tests__/schemas.test.ts +9 -6
- package/src/lib/__tests__/url.test.ts +2 -2
- package/src/lib/__tests__/view.test.ts +9 -9
- package/src/lib/emoji-catalog.ts +146 -0
- package/src/lib/feed.ts +1 -1
- package/src/lib/media-helpers.ts +10 -9
- package/src/lib/render.tsx +4 -3
- package/src/lib/resolve-config.ts +8 -1
- package/src/lib/schemas.ts +2 -3
- package/src/lib/summary.ts +92 -0
- package/src/lib/timeline.ts +2 -0
- package/src/lib/tiptap-render.ts +196 -0
- package/src/lib/upload.ts +97 -9
- package/src/lib/url.ts +7 -23
- package/src/lib/view.ts +33 -19
- package/src/middleware/error-handler.ts +3 -3
- package/src/preset.css +38 -0
- package/src/routes/api/collections.ts +20 -3
- package/src/routes/api/posts.ts +48 -33
- package/src/routes/api/upload.ts +7 -5
- package/src/routes/auth/reset.tsx +5 -4
- package/src/routes/auth/setup.tsx +26 -11
- package/src/routes/auth/signin.tsx +10 -7
- package/src/routes/compose.tsx +20 -11
- package/src/routes/dash/__tests__/settings-avatar.test.ts +43 -8
- package/src/routes/dash/index.tsx +7 -1
- package/src/routes/dash/media.tsx +3 -0
- package/src/routes/dash/pages.tsx +8 -2
- package/src/routes/dash/posts.tsx +6 -2
- package/src/routes/dash/redirects.tsx +15 -9
- package/src/routes/dash/settings.tsx +336 -32
- package/src/routes/feed/__tests__/rss.test.ts +7 -7
- package/src/routes/feed/rss.ts +8 -6
- package/src/routes/pages/__tests__/featured.test.ts +6 -7
- package/src/routes/pages/archive.tsx +11 -7
- package/src/routes/pages/collection.tsx +32 -15
- package/src/routes/pages/collections.tsx +11 -2
- package/src/routes/pages/featured.tsx +1 -1
- package/src/routes/pages/home.tsx +1 -1
- package/src/services/__tests__/post.test.ts +124 -33
- package/src/services/__tests__/settings.test.ts +3 -3
- package/src/services/page.ts +16 -3
- package/src/services/post.ts +96 -37
- package/src/services/search.ts +4 -2
- package/src/services/settings.ts +6 -2
- package/src/styles/components.css +240 -60
- package/src/styles/tokens.css +10 -0
- package/src/styles/ui.css +1157 -81
- package/src/types/bindings.ts +5 -0
- package/src/types/config.ts +23 -1
- package/src/types/constants.ts +3 -0
- package/src/types/entities.ts +9 -2
- package/src/types/operations.ts +9 -3
- package/src/types/props.ts +3 -3
- package/src/types/views.ts +3 -2
- package/src/ui/compose/ComposeDialog.tsx +24 -7
- package/src/ui/dash/PageForm.tsx +2 -0
- package/src/ui/dash/PostList.tsx +5 -5
- package/src/ui/dash/StatusBadge.tsx +13 -5
- package/src/ui/dash/appearance/AdvancedContent.tsx +52 -61
- package/src/ui/dash/appearance/ColorThemeContent.tsx +30 -35
- package/src/ui/dash/appearance/FontThemeContent.tsx +65 -73
- package/src/ui/dash/appearance/NavigationContent.tsx +107 -96
- package/src/ui/dash/media/MediaListContent.tsx +9 -4
- package/src/ui/dash/media/ViewMediaContent.tsx +2 -2
- package/src/ui/dash/pages/PagesContent.tsx +2 -1
- package/src/ui/dash/posts/PostForm.tsx +19 -7
- package/src/ui/dash/settings/AccountContent.tsx +133 -138
- package/src/ui/dash/settings/AvatarContent.tsx +70 -0
- package/src/ui/dash/settings/GeneralContent.tsx +3 -62
- package/src/ui/dash/settings/SettingsRootContent.tsx +236 -0
- package/src/ui/layouts/DashLayout.tsx +157 -75
- package/src/ui/layouts/SiteLayout.tsx +13 -13
- package/src/ui/pages/ArchivePage.tsx +10 -7
- package/src/ui/pages/CollectionPage.tsx +6 -35
- package/src/ui/pages/CollectionsPage.tsx +2 -1
- package/src/ui/pages/FeaturedPage.tsx +2 -1
- package/src/ui/pages/HomePage.tsx +1 -1
- package/src/ui/pages/SearchPage.tsx +1 -1
- package/src/ui/shared/CollectionsSidebar.tsx +228 -3
- package/src/ui/shared/MediaGallery.tsx +179 -41
- package/src/lib/collections-reorder.ts +0 -28
- package/src/routes/dash/appearance.tsx +0 -240
- package/src/routes/dash/collections.tsx +0 -211
- package/src/ui/components/jant-compose-editor.ts +0 -814
- package/src/ui/dash/appearance/AppearanceNav.tsx +0 -60
- package/src/ui/dash/collections/CollectionForm.tsx +0 -166
- package/src/ui/dash/collections/CollectionsListContent.tsx +0 -146
- package/src/ui/dash/collections/IconPickerGrid.tsx +0 -50
- package/src/ui/dash/collections/ViewCollectionContent.tsx +0 -103
- package/src/ui/dash/settings/SettingsNav.tsx +0 -52
- /package/src/{ui → client}/components/__tests__/jant-settings-avatar.test.ts +0 -0
- /package/src/{ui → client}/components/jant-settings-avatar.ts +0 -0
- /package/src/{ui → client}/components/settings-types.ts +0 -0
- /package/src/{lib → client}/image-processor.ts +0 -0
- /package/src/{lib → client}/toast.ts +0 -0
package/src/services/post.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* CRUD operations for posts with Thread support.
|
|
5
5
|
* Posts have format (note/link/quote), status (draft/published),
|
|
6
|
-
* featured
|
|
6
|
+
* visibility (listed/featured/unlisted), and pinned flag.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { eq, and, isNull, desc, or, inArray, sql } from "drizzle-orm";
|
|
@@ -11,10 +11,18 @@ import type { BatchItem } from "drizzle-orm/batch";
|
|
|
11
11
|
import type { Database } from "../db/index.js";
|
|
12
12
|
import { posts, postCollections } from "../db/schema.js";
|
|
13
13
|
import { now } from "../lib/time.js";
|
|
14
|
-
import {
|
|
14
|
+
import { renderTiptapJson } from "../lib/tiptap-render.js";
|
|
15
|
+
import { extractSummary } from "../lib/summary.js";
|
|
15
16
|
import type { StorageDriver } from "../lib/storage.js";
|
|
16
17
|
import type { MediaService } from "./media.js";
|
|
17
|
-
import type {
|
|
18
|
+
import type {
|
|
19
|
+
Format,
|
|
20
|
+
Status,
|
|
21
|
+
Visibility,
|
|
22
|
+
Post,
|
|
23
|
+
CreatePost,
|
|
24
|
+
UpdatePost,
|
|
25
|
+
} from "../types.js";
|
|
18
26
|
import type { PathRegistryService } from "./path-registry.js";
|
|
19
27
|
import { ConflictError } from "../lib/errors.js";
|
|
20
28
|
|
|
@@ -27,11 +35,13 @@ export interface PostDeleteDeps {
|
|
|
27
35
|
export interface PostFilters {
|
|
28
36
|
format?: Format;
|
|
29
37
|
status?: Status;
|
|
30
|
-
|
|
38
|
+
visibility?: Visibility;
|
|
31
39
|
pinned?: boolean;
|
|
32
40
|
collectionId?: number;
|
|
33
41
|
/** Exclude posts that are replies (have threadId set) */
|
|
34
42
|
excludeReplies?: boolean;
|
|
43
|
+
/** Exclude unlisted posts from results */
|
|
44
|
+
excludeUnlisted?: boolean;
|
|
35
45
|
includeDeleted?: boolean;
|
|
36
46
|
threadId?: number;
|
|
37
47
|
limit?: number;
|
|
@@ -39,14 +49,24 @@ export interface PostFilters {
|
|
|
39
49
|
offset?: number; // offset for page-based pagination
|
|
40
50
|
}
|
|
41
51
|
|
|
52
|
+
/** Config for automatic summary extraction */
|
|
53
|
+
export interface SummaryConfig {
|
|
54
|
+
maxParagraphs: number;
|
|
55
|
+
maxChars: number;
|
|
56
|
+
}
|
|
57
|
+
|
|
42
58
|
export interface PostService {
|
|
43
59
|
getById(id: number): Promise<Post | null>;
|
|
44
60
|
getByPath(path: string): Promise<Post | null>;
|
|
45
61
|
list(filters?: PostFilters): Promise<Post[]>;
|
|
46
62
|
/** Count posts matching filters (ignores cursor, offset, limit) */
|
|
47
63
|
count(filters?: PostFilters): Promise<number>;
|
|
48
|
-
create(data: CreatePost): Promise<Post>;
|
|
49
|
-
update(
|
|
64
|
+
create(data: CreatePost, summaryConfig?: SummaryConfig): Promise<Post>;
|
|
65
|
+
update(
|
|
66
|
+
id: number,
|
|
67
|
+
data: UpdatePost,
|
|
68
|
+
summaryConfig?: SummaryConfig,
|
|
69
|
+
): Promise<Post | null>;
|
|
50
70
|
/**
|
|
51
71
|
* Soft-delete a post and clean up its media (storage files + DB records).
|
|
52
72
|
* Thread roots cascade to all replies.
|
|
@@ -56,10 +76,10 @@ export interface PostService {
|
|
|
56
76
|
*/
|
|
57
77
|
delete(id: number, deps?: PostDeleteDeps): Promise<boolean>;
|
|
58
78
|
getThread(rootId: number): Promise<Post[]>;
|
|
59
|
-
|
|
79
|
+
updateThreadStatusAndVisibility(
|
|
60
80
|
rootId: number,
|
|
61
81
|
status: Status,
|
|
62
|
-
|
|
82
|
+
visibility: Visibility,
|
|
63
83
|
): Promise<void>;
|
|
64
84
|
/** Get reply counts for multiple posts */
|
|
65
85
|
getReplyCounts(postIds: number[]): Promise<Map<number, number>>;
|
|
@@ -70,10 +90,23 @@ export interface PostService {
|
|
|
70
90
|
): Promise<Map<number, Post[]>>;
|
|
71
91
|
}
|
|
72
92
|
|
|
73
|
-
/** Check if an error is a SQLite UNIQUE constraint violation
|
|
93
|
+
/** Check if an error (or any of its causes) is a SQLite UNIQUE constraint violation */
|
|
74
94
|
function isUniqueConstraintError(err: unknown): boolean {
|
|
75
|
-
|
|
76
|
-
|
|
95
|
+
let current: unknown = err;
|
|
96
|
+
while (current) {
|
|
97
|
+
const msg = String(current);
|
|
98
|
+
if (
|
|
99
|
+
msg.includes("UNIQUE constraint") ||
|
|
100
|
+
msg.includes("SQLITE_CONSTRAINT")
|
|
101
|
+
) {
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
current =
|
|
105
|
+
current instanceof Error && current.cause !== current
|
|
106
|
+
? current.cause
|
|
107
|
+
: undefined;
|
|
108
|
+
}
|
|
109
|
+
return false;
|
|
77
110
|
}
|
|
78
111
|
|
|
79
112
|
export function createPostService(
|
|
@@ -87,8 +120,11 @@ export function createPostService(
|
|
|
87
120
|
if (filters.status) {
|
|
88
121
|
conditions.push(eq(posts.status, filters.status));
|
|
89
122
|
}
|
|
90
|
-
if (filters.
|
|
91
|
-
conditions.push(eq(posts.
|
|
123
|
+
if (filters.visibility !== undefined) {
|
|
124
|
+
conditions.push(eq(posts.visibility, filters.visibility));
|
|
125
|
+
}
|
|
126
|
+
if (filters.excludeUnlisted) {
|
|
127
|
+
conditions.push(sql`${posts.visibility} != 'unlisted'`);
|
|
92
128
|
}
|
|
93
129
|
if (filters.pinned !== undefined) {
|
|
94
130
|
conditions.push(eq(posts.pinned, filters.pinned ? 1 : 0));
|
|
@@ -120,7 +156,7 @@ export function createPostService(
|
|
|
120
156
|
id: row.id,
|
|
121
157
|
format: row.format as Format,
|
|
122
158
|
status: row.status as Status,
|
|
123
|
-
|
|
159
|
+
visibility: row.visibility as Visibility,
|
|
124
160
|
pinned: row.pinned,
|
|
125
161
|
path: row.path,
|
|
126
162
|
title: row.title,
|
|
@@ -128,6 +164,7 @@ export function createPostService(
|
|
|
128
164
|
body: row.body,
|
|
129
165
|
bodyHtml: row.bodyHtml,
|
|
130
166
|
quoteText: row.quoteText,
|
|
167
|
+
summary: row.summary,
|
|
131
168
|
rating: row.rating,
|
|
132
169
|
replyToId: row.replyToId,
|
|
133
170
|
threadId: row.threadId,
|
|
@@ -190,27 +227,37 @@ export function createPostService(
|
|
|
190
227
|
return result[0]?.count ?? 0;
|
|
191
228
|
},
|
|
192
229
|
|
|
193
|
-
async create(data) {
|
|
230
|
+
async create(data, summaryConfig) {
|
|
194
231
|
const timestamp = now();
|
|
195
232
|
|
|
196
|
-
const bodyHtml = data.body ?
|
|
233
|
+
const bodyHtml = data.body ? renderTiptapJson(data.body) : null;
|
|
234
|
+
|
|
235
|
+
// Generate summary for titled notes with body content
|
|
236
|
+
let summary: string | null = null;
|
|
237
|
+
if (data.format === "note" && data.title && data.body && summaryConfig) {
|
|
238
|
+
summary = extractSummary(
|
|
239
|
+
data.body,
|
|
240
|
+
summaryConfig.maxParagraphs,
|
|
241
|
+
summaryConfig.maxChars,
|
|
242
|
+
);
|
|
243
|
+
}
|
|
197
244
|
|
|
198
245
|
// Handle thread relationship
|
|
199
246
|
let threadId: number | null = null;
|
|
200
247
|
let status: Status = data.status ?? "published";
|
|
201
|
-
let
|
|
248
|
+
let visibility: Visibility = data.visibility ?? "listed";
|
|
202
249
|
|
|
203
250
|
if (data.replyToId) {
|
|
204
251
|
const parent = await this.getById(data.replyToId);
|
|
205
252
|
if (parent) {
|
|
206
253
|
threadId = parent.threadId ?? parent.id;
|
|
207
|
-
// Inherit status and
|
|
254
|
+
// Inherit status and visibility from root
|
|
208
255
|
const root = parent.threadId
|
|
209
256
|
? await this.getById(parent.threadId)
|
|
210
257
|
: parent;
|
|
211
258
|
if (root) {
|
|
212
259
|
status = root.status as Status;
|
|
213
|
-
|
|
260
|
+
visibility = root.visibility as Visibility;
|
|
214
261
|
}
|
|
215
262
|
}
|
|
216
263
|
}
|
|
@@ -229,7 +276,7 @@ export function createPostService(
|
|
|
229
276
|
.values({
|
|
230
277
|
format: data.format,
|
|
231
278
|
status,
|
|
232
|
-
|
|
279
|
+
visibility,
|
|
233
280
|
pinned: data.pinned ? 1 : 0,
|
|
234
281
|
path: data.path ?? null,
|
|
235
282
|
title: data.title ?? null,
|
|
@@ -237,6 +284,7 @@ export function createPostService(
|
|
|
237
284
|
body: data.body ?? null,
|
|
238
285
|
bodyHtml,
|
|
239
286
|
quoteText: data.quoteText ?? null,
|
|
287
|
+
summary,
|
|
240
288
|
rating: data.rating ?? null,
|
|
241
289
|
replyToId: data.replyToId ?? null,
|
|
242
290
|
threadId,
|
|
@@ -275,7 +323,7 @@ export function createPostService(
|
|
|
275
323
|
return post;
|
|
276
324
|
},
|
|
277
325
|
|
|
278
|
-
async update(id, data) {
|
|
326
|
+
async update(id, data, summaryConfig) {
|
|
279
327
|
const existing = await this.getById(id);
|
|
280
328
|
if (!existing) return null;
|
|
281
329
|
|
|
@@ -310,22 +358,38 @@ export function createPostService(
|
|
|
310
358
|
|
|
311
359
|
if (data.body !== undefined) {
|
|
312
360
|
updates.body = data.body;
|
|
313
|
-
updates.bodyHtml = data.body ?
|
|
361
|
+
updates.bodyHtml = data.body ? renderTiptapJson(data.body) : null;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Recompute summary when body, title, or format change
|
|
365
|
+
if (summaryConfig) {
|
|
366
|
+
const format = data.format ?? (existing.format as Format);
|
|
367
|
+
const title = data.title !== undefined ? data.title : existing.title;
|
|
368
|
+
const body = data.body !== undefined ? data.body : existing.body;
|
|
369
|
+
if (format === "note" && title && body) {
|
|
370
|
+
updates.summary = extractSummary(
|
|
371
|
+
body,
|
|
372
|
+
summaryConfig.maxParagraphs,
|
|
373
|
+
summaryConfig.maxChars,
|
|
374
|
+
);
|
|
375
|
+
} else {
|
|
376
|
+
updates.summary = null;
|
|
377
|
+
}
|
|
314
378
|
}
|
|
315
379
|
|
|
316
|
-
// Handle status/
|
|
380
|
+
// Handle status/visibility change - cascade to thread if this is root
|
|
317
381
|
const statusChanged =
|
|
318
382
|
data.status !== undefined && data.status !== existing.status;
|
|
319
|
-
const
|
|
320
|
-
data.
|
|
321
|
-
|
|
383
|
+
const visibilityChanged =
|
|
384
|
+
data.visibility !== undefined &&
|
|
385
|
+
data.visibility !== existing.visibility;
|
|
322
386
|
|
|
323
387
|
if (statusChanged) updates.status = data.status;
|
|
324
|
-
if (
|
|
388
|
+
if (visibilityChanged) updates.visibility = data.visibility;
|
|
325
389
|
|
|
326
390
|
// Build all write queries for atomic execution via D1 batch
|
|
327
391
|
const needsCascade =
|
|
328
|
-
(statusChanged ||
|
|
392
|
+
(statusChanged || visibilityChanged) && !existing.threadId;
|
|
329
393
|
const needsCollectionSync = data.collectionIds !== undefined;
|
|
330
394
|
const hasExtraWrites = needsCascade || needsCollectionSync;
|
|
331
395
|
|
|
@@ -348,13 +412,8 @@ export function createPostService(
|
|
|
348
412
|
.update(posts)
|
|
349
413
|
.set({
|
|
350
414
|
status: data.status ?? (existing.status as Status),
|
|
351
|
-
|
|
352
|
-
data.
|
|
353
|
-
? data.featured
|
|
354
|
-
: existing.featured === 1
|
|
355
|
-
)
|
|
356
|
-
? 1
|
|
357
|
-
: 0,
|
|
415
|
+
visibility:
|
|
416
|
+
data.visibility ?? (existing.visibility as Visibility),
|
|
358
417
|
updatedAt: timestamp,
|
|
359
418
|
})
|
|
360
419
|
.where(eq(posts.threadId, id)),
|
|
@@ -467,11 +526,11 @@ export function createPostService(
|
|
|
467
526
|
return rows.map(toPost);
|
|
468
527
|
},
|
|
469
528
|
|
|
470
|
-
async
|
|
529
|
+
async updateThreadStatusAndVisibility(rootId, status, visibility) {
|
|
471
530
|
const timestamp = now();
|
|
472
531
|
await db
|
|
473
532
|
.update(posts)
|
|
474
|
-
.set({ status,
|
|
533
|
+
.set({ status, visibility, updatedAt: timestamp })
|
|
475
534
|
.where(eq(posts.threadId, rootId));
|
|
476
535
|
},
|
|
477
536
|
|
package/src/services/search.ts
CHANGED
|
@@ -27,7 +27,7 @@ interface RawSearchRow {
|
|
|
27
27
|
id: number;
|
|
28
28
|
format: string;
|
|
29
29
|
status: string;
|
|
30
|
-
|
|
30
|
+
visibility: string;
|
|
31
31
|
pinned: number;
|
|
32
32
|
path: string | null;
|
|
33
33
|
title: string | null;
|
|
@@ -35,6 +35,7 @@ interface RawSearchRow {
|
|
|
35
35
|
body: string | null;
|
|
36
36
|
body_html: string | null;
|
|
37
37
|
quote_text: string | null;
|
|
38
|
+
summary: string | null;
|
|
38
39
|
rating: number | null;
|
|
39
40
|
collection_id: number | null;
|
|
40
41
|
reply_to_id: number | null;
|
|
@@ -97,7 +98,7 @@ export function createSearchService(d1: D1Database): SearchService {
|
|
|
97
98
|
id: row.id,
|
|
98
99
|
format: row.format as Post["format"],
|
|
99
100
|
status: row.status as Post["status"],
|
|
100
|
-
|
|
101
|
+
visibility: row.visibility as Post["visibility"],
|
|
101
102
|
pinned: row.pinned,
|
|
102
103
|
path: row.path,
|
|
103
104
|
title: row.title,
|
|
@@ -105,6 +106,7 @@ export function createSearchService(d1: D1Database): SearchService {
|
|
|
105
106
|
body: row.body,
|
|
106
107
|
bodyHtml: row.body_html,
|
|
107
108
|
quoteText: row.quote_text,
|
|
109
|
+
summary: row.summary,
|
|
108
110
|
rating: row.rating,
|
|
109
111
|
replyToId: row.reply_to_id,
|
|
110
112
|
threadId: row.thread_id,
|
package/src/services/settings.ts
CHANGED
|
@@ -44,6 +44,7 @@ export interface AvatarUploadDeps {
|
|
|
44
44
|
media: MediaService;
|
|
45
45
|
storage: StorageDriver;
|
|
46
46
|
storageProvider: string;
|
|
47
|
+
maxFileSizeMB: number;
|
|
47
48
|
}
|
|
48
49
|
|
|
49
50
|
export interface SettingsService {
|
|
@@ -194,7 +195,7 @@ export function createSettingsService(db: Database): SettingsService {
|
|
|
194
195
|
// Header nav max visible: only update if provided (may be managed separately)
|
|
195
196
|
if (data.headerNavMaxVisible !== undefined) {
|
|
196
197
|
const navMax = parseInt(String(data.headerNavMaxVisible), 10);
|
|
197
|
-
if (!isNaN(navMax) && navMax !==
|
|
198
|
+
if (!isNaN(navMax) && navMax !== 2) {
|
|
198
199
|
await this.set("HEADER_NAV_MAX_VISIBLE", String(navMax));
|
|
199
200
|
} else {
|
|
200
201
|
await this.remove("HEADER_NAV_MAX_VISIBLE");
|
|
@@ -215,7 +216,10 @@ export function createSettingsService(db: Database): SettingsService {
|
|
|
215
216
|
},
|
|
216
217
|
|
|
217
218
|
async uploadAvatar(data, deps) {
|
|
218
|
-
const uploadError = validateUploadFile(data.file as unknown as File
|
|
219
|
+
const uploadError = validateUploadFile(data.file as unknown as File, {
|
|
220
|
+
imagesOnly: true,
|
|
221
|
+
maxFileSizeMB: deps.maxFileSizeMB,
|
|
222
|
+
});
|
|
219
223
|
if (uploadError) {
|
|
220
224
|
throw new ValidationError(uploadError);
|
|
221
225
|
}
|
|
@@ -23,16 +23,6 @@ svg[stroke-width].icon-fine {
|
|
|
23
23
|
padding-left: var(--site-padding);
|
|
24
24
|
padding-right: var(--site-padding);
|
|
25
25
|
}
|
|
26
|
-
|
|
27
|
-
.container-sidebar {
|
|
28
|
-
max-width: calc(
|
|
29
|
-
var(--sidebar-width) + var(--sidebar-gap) + var(--site-width)
|
|
30
|
-
);
|
|
31
|
-
margin-left: auto;
|
|
32
|
-
margin-right: auto;
|
|
33
|
-
padding-left: var(--site-padding);
|
|
34
|
-
padding-right: var(--site-padding);
|
|
35
|
-
}
|
|
36
26
|
}
|
|
37
27
|
|
|
38
28
|
/* Toast notifications */
|
|
@@ -121,29 +111,50 @@ svg[stroke-width].icon-fine {
|
|
|
121
111
|
padding-bottom: 12px;
|
|
122
112
|
}
|
|
123
113
|
|
|
124
|
-
.dash-header-
|
|
114
|
+
.dash-header-avatar-link {
|
|
115
|
+
flex-shrink: 0;
|
|
116
|
+
line-height: 0;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.dash-header-avatar {
|
|
120
|
+
width: 22px;
|
|
121
|
+
height: 22px;
|
|
122
|
+
border-radius: 5px;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.dash-header-avatar:is(img) {
|
|
126
|
+
object-fit: cover;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.dash-header-avatar-fallback {
|
|
125
130
|
display: flex;
|
|
126
131
|
align-items: center;
|
|
127
|
-
|
|
132
|
+
justify-content: center;
|
|
133
|
+
color: white;
|
|
134
|
+
font-size: 0.625rem;
|
|
135
|
+
font-weight: 600;
|
|
136
|
+
line-height: 1;
|
|
128
137
|
}
|
|
129
138
|
|
|
130
139
|
.dash-header-nav {
|
|
131
140
|
display: flex;
|
|
132
141
|
align-items: center;
|
|
133
|
-
|
|
134
|
-
gap: 4px;
|
|
135
|
-
flex: 1;
|
|
142
|
+
gap: 16px;
|
|
136
143
|
}
|
|
137
144
|
|
|
138
|
-
.dash-header-
|
|
139
|
-
|
|
140
|
-
font-weight: 800;
|
|
141
|
-
line-height: 1;
|
|
142
|
-
color: var(--color-foreground);
|
|
145
|
+
.dash-header-nav-sep {
|
|
146
|
+
display: none;
|
|
143
147
|
}
|
|
144
148
|
|
|
145
|
-
.dash-header-
|
|
146
|
-
|
|
149
|
+
.dash-header-right {
|
|
150
|
+
display: flex;
|
|
151
|
+
align-items: center;
|
|
152
|
+
gap: 8px;
|
|
153
|
+
margin-left: auto;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
.dash-header-link {
|
|
157
|
+
font-size: 0.8125rem;
|
|
147
158
|
color: var(--color-muted-foreground);
|
|
148
159
|
transition: color 0.15s;
|
|
149
160
|
|
|
@@ -152,33 +163,41 @@ svg[stroke-width].icon-fine {
|
|
|
152
163
|
}
|
|
153
164
|
}
|
|
154
165
|
|
|
155
|
-
.dash-header-link {
|
|
156
|
-
|
|
166
|
+
.dash-header-link-active {
|
|
167
|
+
color: var(--color-foreground);
|
|
168
|
+
font-weight: 500;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
.dash-header-visit {
|
|
172
|
+
@apply flex items-center gap-3;
|
|
157
173
|
font-size: 0.875rem;
|
|
158
|
-
padding: 6px 10px;
|
|
159
|
-
border-radius: var(--radius);
|
|
160
174
|
color: var(--color-muted-foreground);
|
|
161
|
-
transition:
|
|
162
|
-
color 0.15s,
|
|
163
|
-
background-color 0.15s;
|
|
175
|
+
transition: color 0.15s;
|
|
164
176
|
|
|
165
177
|
&:hover {
|
|
166
178
|
color: var(--color-foreground);
|
|
167
|
-
background-color: var(--color-accent);
|
|
168
179
|
}
|
|
169
180
|
}
|
|
170
181
|
|
|
171
|
-
.dash-header-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
182
|
+
.dash-header-visit-text {
|
|
183
|
+
display: none;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
@media (min-width: 700px) {
|
|
187
|
+
.dash-header-visit-text {
|
|
188
|
+
display: inline;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
.dash-header-visit-icon {
|
|
192
|
+
display: none;
|
|
193
|
+
}
|
|
175
194
|
}
|
|
176
195
|
|
|
177
196
|
.dash-header-menu-btn {
|
|
178
197
|
@apply flex items-center justify-center;
|
|
179
198
|
width: 32px;
|
|
180
199
|
height: 32px;
|
|
181
|
-
border-radius:
|
|
200
|
+
border-radius: var(--radius);
|
|
182
201
|
border: none;
|
|
183
202
|
background: transparent;
|
|
184
203
|
color: var(--color-muted-foreground);
|
|
@@ -198,33 +217,194 @@ svg[stroke-width].icon-fine {
|
|
|
198
217
|
}
|
|
199
218
|
}
|
|
200
219
|
|
|
201
|
-
/*
|
|
220
|
+
/* Breadcrumb — second row below header on settings sub-pages */
|
|
221
|
+
@layer components {
|
|
222
|
+
.dash-breadcrumb {
|
|
223
|
+
display: flex;
|
|
224
|
+
align-items: center;
|
|
225
|
+
gap: 8px;
|
|
226
|
+
padding: 8px 0;
|
|
227
|
+
font-size: 0.8125rem;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
.dash-breadcrumb-parent {
|
|
231
|
+
color: var(--color-muted-foreground);
|
|
232
|
+
transition: color 0.15s;
|
|
233
|
+
|
|
234
|
+
&:hover {
|
|
235
|
+
color: var(--color-foreground);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
.dash-breadcrumb-sep {
|
|
240
|
+
color: var(--color-muted-foreground);
|
|
241
|
+
opacity: 0.5;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
.dash-breadcrumb-current {
|
|
245
|
+
color: var(--color-foreground);
|
|
246
|
+
font-weight: 500;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/* Settings root — iOS-style grouped list */
|
|
202
251
|
@layer components {
|
|
203
|
-
.
|
|
204
|
-
@apply flex
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
background-color: var(--color-foreground);
|
|
225
|
-
}
|
|
226
|
-
}
|
|
252
|
+
.settings-root {
|
|
253
|
+
@apply flex flex-col gap-6;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
.settings-group-label {
|
|
257
|
+
@apply text-xs font-medium uppercase tracking-wider mb-2 px-1;
|
|
258
|
+
color: var(--color-muted-foreground);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
.settings-group {
|
|
262
|
+
@apply border rounded-xl overflow-hidden;
|
|
263
|
+
border-radius: var(--dash-card-radius);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
.settings-item {
|
|
267
|
+
@apply flex items-center gap-3 px-4 py-3;
|
|
268
|
+
transition: background-color 0.15s;
|
|
269
|
+
border-bottom: 1px solid var(--color-border);
|
|
270
|
+
|
|
271
|
+
&:last-child {
|
|
272
|
+
border-bottom: none;
|
|
227
273
|
}
|
|
274
|
+
|
|
275
|
+
&:hover {
|
|
276
|
+
background-color: var(--color-accent);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
.settings-item-icon {
|
|
281
|
+
@apply flex items-center justify-center shrink-0;
|
|
282
|
+
width: 28px;
|
|
283
|
+
height: 28px;
|
|
284
|
+
border-radius: 6px;
|
|
285
|
+
color: white;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
.settings-item-text {
|
|
289
|
+
@apply flex flex-col flex-1 min-w-0;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
.settings-item-name {
|
|
293
|
+
@apply text-sm font-medium;
|
|
294
|
+
color: var(--color-foreground);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
.settings-item-desc {
|
|
298
|
+
@apply text-xs;
|
|
299
|
+
color: var(--color-muted-foreground);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
.settings-item-chevron {
|
|
303
|
+
@apply shrink-0;
|
|
304
|
+
color: var(--color-muted-foreground);
|
|
305
|
+
opacity: 0.5;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/* Dashboard scoped font rules */
|
|
310
|
+
@layer components {
|
|
311
|
+
.dash-heading {
|
|
312
|
+
font-family: Georgia, "Times New Roman", serif;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/* Navigation preview — browser-chrome frame for nav preview */
|
|
317
|
+
@layer components {
|
|
318
|
+
.nav-preview {
|
|
319
|
+
@apply border rounded-lg overflow-hidden;
|
|
320
|
+
background-color: var(--color-card);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
.nav-preview-chrome {
|
|
324
|
+
@apply flex items-center gap-3 px-4 py-2.5 border-b;
|
|
325
|
+
background-color: var(--color-muted);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
.nav-preview-dots {
|
|
329
|
+
@apply flex gap-1.5;
|
|
330
|
+
|
|
331
|
+
> span {
|
|
332
|
+
@apply block size-2.5 rounded-full;
|
|
333
|
+
background-color: var(--color-muted-foreground);
|
|
334
|
+
opacity: 0.3;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
.nav-preview-label {
|
|
339
|
+
@apply text-xs;
|
|
340
|
+
color: var(--color-muted-foreground);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
.nav-preview-content {
|
|
344
|
+
@apply px-5 py-3;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/* Navigation items list — card-style draggable nav items */
|
|
349
|
+
@layer components {
|
|
350
|
+
.nav-items-list {
|
|
351
|
+
@apply flex flex-col gap-2;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
.nav-item {
|
|
355
|
+
@apply border rounded-lg transition-shadow;
|
|
356
|
+
|
|
357
|
+
&:hover {
|
|
358
|
+
@apply shadow-xs;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
.nav-item-editing {
|
|
363
|
+
@apply ring-1 ring-ring;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
.nav-item-row {
|
|
367
|
+
@apply flex items-center gap-1 px-1 py-1;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
.nav-item-handle {
|
|
371
|
+
@apply flex items-center justify-center w-8 h-8 shrink-0 cursor-grab rounded;
|
|
372
|
+
transition: background-color 0.15s;
|
|
373
|
+
|
|
374
|
+
&:hover {
|
|
375
|
+
background-color: var(--color-accent);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
&:active {
|
|
379
|
+
cursor: grabbing;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
.nav-item-info {
|
|
384
|
+
@apply flex flex-col flex-1 min-w-0 py-1.5 px-1.5 rounded cursor-pointer;
|
|
385
|
+
transition: background-color 0.15s;
|
|
386
|
+
|
|
387
|
+
&:hover {
|
|
388
|
+
background-color: var(--color-accent);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
.nav-item-toggle {
|
|
393
|
+
@apply flex items-center justify-center w-8 h-8 shrink-0 rounded border-0 bg-transparent cursor-pointer;
|
|
394
|
+
color: var(--color-muted-foreground);
|
|
395
|
+
transition:
|
|
396
|
+
color 0.15s,
|
|
397
|
+
background-color 0.15s;
|
|
398
|
+
|
|
399
|
+
&:hover {
|
|
400
|
+
color: var(--color-foreground);
|
|
401
|
+
background-color: var(--color-accent);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
.nav-item-edit {
|
|
406
|
+
@apply flex flex-col gap-3 px-4 pb-3 pt-3 border-t mx-1 mb-1;
|
|
407
|
+
border-color: var(--color-border);
|
|
228
408
|
}
|
|
229
409
|
}
|
|
230
410
|
|