@jant/core 0.3.46 → 0.3.47
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/bin/commands/db/execute-file.js +12 -4
- package/bin/commands/db/rehearse.js +2 -2
- package/bin/commands/export.js +12 -4
- package/bin/commands/import-site.js +60 -267
- package/bin/commands/migrate.js +36 -69
- package/bin/commands/reset-password.js +10 -4
- package/bin/commands/site/export.js +59 -248
- package/bin/commands/site/snapshot/export.js +58 -45
- package/bin/commands/site/snapshot/import.js +104 -52
- package/bin/lib/node-env.js +100 -0
- package/bin/lib/runtime-target.js +64 -0
- package/bin/lib/site-snapshot.js +185 -54
- package/bin/lib/sql-export.js +19 -2
- package/dist/{app-DB-P66E5.js → app-3REcR-3U.js} +331 -189
- package/dist/app-B67XOEyo.js +6 -0
- package/dist/client/.vite/manifest.json +2 -2
- package/dist/client/_assets/{client-auth-BLCUje4M.js → client-auth-Ce5WEAVS.js} +102 -49
- package/dist/client/_assets/client-s71Js1Cu.css +2 -0
- package/dist/{github-sync-CQ1x271f.js → export-ZBlfKSKm.js} +12 -439
- package/dist/github-sync-C593r22F.js +4 -0
- package/dist/github-sync-bL1hnx3Q.js +428 -0
- package/dist/index.js +3 -2
- package/dist/node.js +5 -4
- package/package.json +3 -2
- package/src/__tests__/helpers/export-fixtures.ts +0 -1
- package/src/client/components/__tests__/jant-settings-avatar.test.ts +2 -0
- package/src/client/components/__tests__/jant-settings-general.test.ts +70 -0
- package/src/client/components/jant-settings-general.ts +164 -22
- package/src/client/components/settings-types.ts +4 -6
- package/src/client-auth.ts +1 -1
- package/src/db/__tests__/demo-canonical-snapshot.test.ts +1 -1
- package/src/db/__tests__/migration-rehearsal.test.ts +2 -5
- package/src/db/backfills/0004_register_apple_touch_media_rows.sql +65 -0
- package/src/db/migrations/0021_thankful_phalanx.sql +16 -0
- package/src/db/migrations/meta/0021_snapshot.json +2121 -0
- package/src/db/migrations/meta/_journal.json +7 -0
- package/src/db/migrations/pg/0019_gray_natasha_romanoff.sql +20 -0
- package/src/db/migrations/pg/meta/0019_snapshot.json +2718 -0
- package/src/db/migrations/pg/meta/_journal.json +7 -0
- package/src/db/pg/schema.ts +21 -26
- package/src/db/rehearsal-fixtures/demo-current.json +1 -1
- package/src/db/schema.ts +16 -20
- package/src/i18n/__tests__/middleware.test.ts +43 -1
- package/src/i18n/coverage.generated.ts +17 -0
- package/src/i18n/i18n.ts +18 -2
- package/src/i18n/index.ts +3 -0
- package/src/i18n/locales/settings/en.po +16 -11
- package/src/i18n/locales/settings/en.ts +1 -1
- package/src/i18n/locales/settings/zh-Hans.po +17 -12
- package/src/i18n/locales/settings/zh-Hans.ts +1 -1
- package/src/i18n/locales/settings/zh-Hant.po +16 -11
- package/src/i18n/locales/settings/zh-Hant.ts +1 -1
- package/src/i18n/locales.ts +84 -2
- package/src/i18n/middleware.ts +25 -16
- package/src/i18n/supported-locales.ts +153 -0
- package/src/lib/__tests__/csp-builder.test.ts +19 -2
- package/src/lib/__tests__/feed.test.ts +242 -1
- package/src/lib/__tests__/post-meta.test.ts +0 -1
- package/src/lib/__tests__/view.test.ts +0 -1
- package/src/lib/csp-builder.ts +28 -10
- package/src/lib/feed.ts +153 -3
- package/src/middleware/__tests__/secure-headers.test.ts +89 -0
- package/src/middleware/auth.ts +1 -1
- package/src/middleware/secure-headers.ts +47 -1
- package/src/node/__tests__/cli-runtime-target.test.ts +110 -2
- package/src/node/__tests__/cli-site-snapshot.test.ts +308 -13
- package/src/node/__tests__/cli-site-token-env.test.ts +2 -7
- package/src/node/__tests__/cli-snapshot-meta.test.ts +85 -0
- package/src/node/__tests__/cli-sql-export.test.ts +49 -0
- package/src/node/index.ts +1 -0
- package/src/preset.css +8 -2
- package/src/routes/api/__tests__/settings.test.ts +3 -2
- package/src/routes/api/github-sync.tsx +1 -1
- package/src/routes/api/settings.ts +4 -1
- package/src/routes/auth/signin.tsx +6 -0
- package/src/routes/pages/archive.tsx +4 -2
- package/src/services/__tests__/post.test.ts +19 -19
- package/src/services/__tests__/search.test.ts +0 -1
- package/src/services/__tests__/settings.test.ts +22 -3
- package/src/services/bootstrap.ts +7 -3
- package/src/services/collection.ts +3 -3
- package/src/services/export.ts +0 -3
- package/src/services/navigation.ts +0 -2
- package/src/services/path.ts +1 -38
- package/src/services/post.ts +32 -66
- package/src/services/search.ts +0 -6
- package/src/services/settings.ts +47 -6
- package/src/services/site-admin.ts +6 -1
- package/src/styles/ui.css +12 -23
- package/src/types/entities.ts +0 -1
- package/src/ui/color-themes.ts +1 -1
- package/src/ui/dash/settings/GeneralContent.tsx +17 -19
- package/src/ui/dash/settings/SettingsRootContent.tsx +17 -28
- package/src/ui/feed/NoteCard.tsx +1 -11
- package/src/ui/feed/__tests__/timeline-cards.test.ts +1 -1
- package/src/ui/pages/PostPage.tsx +2 -0
- package/bin/commands/collections.js +0 -268
- package/bin/commands/media.js +0 -302
- package/bin/commands/posts.js +0 -262
- package/bin/commands/search.js +0 -53
- package/bin/commands/settings.js +0 -93
- package/bin/lib/http-api.js +0 -223
- package/bin/lib/media-upload.js +0 -206
- package/dist/app-CM7sb3xO.js +0 -5
- package/dist/client/_assets/client-DDs6NzB3.css +0 -2
- package/src/__tests__/bin/content-cli.test.ts +0 -179
- package/src/__tests__/bin/media-cli.test.ts +0 -192
- /package/dist/{github-api-BkRWnqMx.js → github-api-Bh0PH3zr.js} +0 -0
- /package/dist/{github-app-WeadXMb8.js → github-app-D0GvNnqp.js} +0 -0
package/src/services/post.ts
CHANGED
|
@@ -99,7 +99,6 @@ export interface PostFilters {
|
|
|
99
99
|
excludeLatestHidden?: boolean;
|
|
100
100
|
/** Exclude private posts from results */
|
|
101
101
|
excludePrivate?: boolean;
|
|
102
|
-
includeDeleted?: boolean;
|
|
103
102
|
threadId?: string;
|
|
104
103
|
/** Unix timestamp (inclusive) — only posts published at or after this time */
|
|
105
104
|
publishedAfter?: number;
|
|
@@ -572,13 +571,7 @@ export function createPostService(
|
|
|
572
571
|
),
|
|
573
572
|
})
|
|
574
573
|
.from(posts)
|
|
575
|
-
.where(
|
|
576
|
-
and(
|
|
577
|
-
eq(posts.siteId, siteId),
|
|
578
|
-
eq(posts.threadId, rootId),
|
|
579
|
-
isNull(posts.deletedAt),
|
|
580
|
-
),
|
|
581
|
-
);
|
|
574
|
+
.where(and(eq(posts.siteId, siteId), eq(posts.threadId, rootId)));
|
|
582
575
|
|
|
583
576
|
const latestPublishedAt = rootRows[0]?.latestPublishedAt ?? null;
|
|
584
577
|
const root = await db
|
|
@@ -761,9 +754,6 @@ export function createPostService(
|
|
|
761
754
|
if (filters.excludeReplies) {
|
|
762
755
|
conditions.push(isNull(posts.replyToId));
|
|
763
756
|
}
|
|
764
|
-
if (!filters.includeDeleted) {
|
|
765
|
-
conditions.push(isNull(posts.deletedAt));
|
|
766
|
-
}
|
|
767
757
|
if (filters.publishedAfter !== undefined) {
|
|
768
758
|
conditions.push(sql`${posts.publishedAt} >= ${filters.publishedAfter}`);
|
|
769
759
|
}
|
|
@@ -820,13 +810,7 @@ export function createPostService(
|
|
|
820
810
|
const rows = await db
|
|
821
811
|
.select({ id: posts.id })
|
|
822
812
|
.from(posts)
|
|
823
|
-
.where(
|
|
824
|
-
and(
|
|
825
|
-
eq(posts.siteId, siteId),
|
|
826
|
-
eq(posts.threadId, threadId),
|
|
827
|
-
isNull(posts.deletedAt),
|
|
828
|
-
),
|
|
829
|
-
)
|
|
813
|
+
.where(and(eq(posts.siteId, siteId), eq(posts.threadId, threadId)))
|
|
830
814
|
.orderBy(desc(posts.createdAt), desc(posts.id))
|
|
831
815
|
.limit(1);
|
|
832
816
|
|
|
@@ -997,7 +981,6 @@ export function createPostService(
|
|
|
997
981
|
previewProvider: row.previewProvider,
|
|
998
982
|
replyToId: row.replyToId,
|
|
999
983
|
threadId: row.threadId,
|
|
1000
|
-
deletedAt: row.deletedAt,
|
|
1001
984
|
publishedAt: row.publishedAt,
|
|
1002
985
|
lastActivityAt: row.lastActivityAt ?? row.publishedAt ?? row.updatedAt,
|
|
1003
986
|
createdAt: row.createdAt,
|
|
@@ -1054,7 +1037,6 @@ export function createPostService(
|
|
|
1054
1037
|
eq(posts.siteId, siteId),
|
|
1055
1038
|
inArray(posts.id, chunk),
|
|
1056
1039
|
eq(posts.status, "published"),
|
|
1057
|
-
isNull(posts.deletedAt),
|
|
1058
1040
|
),
|
|
1059
1041
|
),
|
|
1060
1042
|
);
|
|
@@ -1090,7 +1072,7 @@ export function createPostService(
|
|
|
1090
1072
|
}
|
|
1091
1073
|
|
|
1092
1074
|
function buildThreadRootPageConditions(options?: ThreadRootPageOptions) {
|
|
1093
|
-
const conditions = [
|
|
1075
|
+
const conditions: SQL[] = [];
|
|
1094
1076
|
const status = options?.status;
|
|
1095
1077
|
|
|
1096
1078
|
if (status) {
|
|
@@ -1230,13 +1212,7 @@ export function createPostService(
|
|
|
1230
1212
|
const result = await db
|
|
1231
1213
|
.select()
|
|
1232
1214
|
.from(posts)
|
|
1233
|
-
.where(
|
|
1234
|
-
and(
|
|
1235
|
-
eq(posts.siteId, siteId),
|
|
1236
|
-
eq(posts.id, id),
|
|
1237
|
-
isNull(posts.deletedAt),
|
|
1238
|
-
),
|
|
1239
|
-
)
|
|
1215
|
+
.where(and(eq(posts.siteId, siteId), eq(posts.id, id)))
|
|
1240
1216
|
.limit(1);
|
|
1241
1217
|
return hydratePost(result[0]);
|
|
1242
1218
|
},
|
|
@@ -2442,17 +2418,35 @@ export function createPostService(
|
|
|
2442
2418
|
}
|
|
2443
2419
|
}
|
|
2444
2420
|
|
|
2445
|
-
const timestamp = now();
|
|
2446
|
-
|
|
2447
2421
|
if (isRoot) {
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2422
|
+
// Delete the entire thread atomically. SQLite/D1's self-referential
|
|
2423
|
+
// FK on thread_id triggers a violation when the root is removed (its
|
|
2424
|
+
// own thread_id points to itself), so wrap the cascade in a
|
|
2425
|
+
// transaction with PRAGMA defer_foreign_keys to push the FK check to
|
|
2426
|
+
// commit time, by which point every referencing row is gone too.
|
|
2427
|
+
if (databaseDialect === "pg") {
|
|
2428
|
+
await db.transaction(async (tx) => {
|
|
2429
|
+
await tx
|
|
2430
|
+
.delete(posts)
|
|
2431
|
+
.where(and(eq(posts.siteId, siteId), eq(posts.threadId, id)));
|
|
2432
|
+
});
|
|
2433
|
+
} else {
|
|
2434
|
+
await db.batch([
|
|
2435
|
+
db.run(sql`PRAGMA defer_foreign_keys = ON`),
|
|
2436
|
+
db
|
|
2437
|
+
.delete(posts)
|
|
2438
|
+
.where(and(eq(posts.siteId, siteId), eq(posts.threadId, id))),
|
|
2439
|
+
]);
|
|
2440
|
+
}
|
|
2452
2441
|
} else {
|
|
2442
|
+
// Re-parent any direct children of this reply onto its own parent so
|
|
2443
|
+
// the thread chain stays connected after the reply is removed.
|
|
2453
2444
|
await db
|
|
2454
2445
|
.update(posts)
|
|
2455
|
-
.set({
|
|
2446
|
+
.set({ replyToId: existing.replyToId })
|
|
2447
|
+
.where(and(eq(posts.siteId, siteId), eq(posts.replyToId, id)));
|
|
2448
|
+
await db
|
|
2449
|
+
.delete(posts)
|
|
2456
2450
|
.where(and(eq(posts.siteId, siteId), eq(posts.id, id)));
|
|
2457
2451
|
await recalculateThreadLastActivity(existing.threadId);
|
|
2458
2452
|
}
|
|
@@ -2461,33 +2455,14 @@ export function createPostService(
|
|
|
2461
2455
|
},
|
|
2462
2456
|
|
|
2463
2457
|
async deleteThreadDraft(id, deps) {
|
|
2464
|
-
|
|
2465
|
-
if (!deleted) return false;
|
|
2466
|
-
|
|
2467
|
-
// Release path_registry entries for all posts in the thread so slugs
|
|
2468
|
-
// can be reused by the replacement thread.
|
|
2469
|
-
const threadRows = await db
|
|
2470
|
-
.select({ id: posts.id })
|
|
2471
|
-
.from(posts)
|
|
2472
|
-
.where(and(eq(posts.siteId, siteId), eq(posts.threadId, id)));
|
|
2473
|
-
for (const row of threadRows) {
|
|
2474
|
-
await resolvedPaths.deleteByPostId(row.id);
|
|
2475
|
-
}
|
|
2476
|
-
|
|
2477
|
-
return true;
|
|
2458
|
+
return this.delete(id, deps);
|
|
2478
2459
|
},
|
|
2479
2460
|
|
|
2480
2461
|
async getThread(rootId) {
|
|
2481
2462
|
const rows = await db
|
|
2482
2463
|
.select()
|
|
2483
2464
|
.from(posts)
|
|
2484
|
-
.where(
|
|
2485
|
-
and(
|
|
2486
|
-
eq(posts.siteId, siteId),
|
|
2487
|
-
eq(posts.threadId, rootId),
|
|
2488
|
-
isNull(posts.deletedAt),
|
|
2489
|
-
),
|
|
2490
|
-
)
|
|
2465
|
+
.where(and(eq(posts.siteId, siteId), eq(posts.threadId, rootId)))
|
|
2491
2466
|
.orderBy(posts.createdAt);
|
|
2492
2467
|
|
|
2493
2468
|
return hydratePosts(rows);
|
|
@@ -2575,7 +2550,6 @@ export function createPostService(
|
|
|
2575
2550
|
inArray(posts.threadId, postIds),
|
|
2576
2551
|
eq(posts.status, "published"),
|
|
2577
2552
|
isNotNull(posts.replyToId),
|
|
2578
|
-
isNull(posts.deletedAt),
|
|
2579
2553
|
),
|
|
2580
2554
|
)
|
|
2581
2555
|
.groupBy(posts.threadId);
|
|
@@ -2607,7 +2581,6 @@ export function createPostService(
|
|
|
2607
2581
|
inArray(posts.threadId, rootIds),
|
|
2608
2582
|
eq(posts.status, "published"),
|
|
2609
2583
|
isNotNull(posts.replyToId),
|
|
2610
|
-
isNull(posts.deletedAt),
|
|
2611
2584
|
),
|
|
2612
2585
|
)
|
|
2613
2586
|
.as("ranked_replies");
|
|
@@ -2671,7 +2644,6 @@ export function createPostService(
|
|
|
2671
2644
|
inArray(posts.threadId, rootIds),
|
|
2672
2645
|
eq(posts.status, "published"),
|
|
2673
2646
|
isNotNull(posts.replyToId),
|
|
2674
|
-
isNull(posts.deletedAt),
|
|
2675
2647
|
),
|
|
2676
2648
|
)
|
|
2677
2649
|
.as("ranked_replies");
|
|
@@ -2994,7 +2966,6 @@ export function createPostService(
|
|
|
2994
2966
|
eq(posts.siteId, siteId),
|
|
2995
2967
|
inArray(posts.threadId, unique),
|
|
2996
2968
|
eq(posts.status, "published"),
|
|
2997
|
-
isNull(posts.deletedAt),
|
|
2998
2969
|
),
|
|
2999
2970
|
)
|
|
3000
2971
|
.orderBy(posts.threadId, posts.createdAt, posts.id);
|
|
@@ -3043,7 +3014,6 @@ export function createPostService(
|
|
|
3043
3014
|
buildCollectionMembershipCondition(collectionIds),
|
|
3044
3015
|
inArray(posts.threadId, chunk),
|
|
3045
3016
|
eq(posts.status, "published"),
|
|
3046
|
-
isNull(posts.deletedAt),
|
|
3047
3017
|
),
|
|
3048
3018
|
)
|
|
3049
3019
|
.groupBy(posts.threadId, posts.id)
|
|
@@ -3078,7 +3048,6 @@ export function createPostService(
|
|
|
3078
3048
|
eq(posts.siteId, siteId),
|
|
3079
3049
|
inArray(posts.threadId, unique),
|
|
3080
3050
|
eq(posts.status, "published"),
|
|
3081
|
-
isNull(posts.deletedAt),
|
|
3082
3051
|
),
|
|
3083
3052
|
)
|
|
3084
3053
|
.orderBy(posts.threadId, desc(posts.createdAt), desc(posts.id));
|
|
@@ -3115,10 +3084,7 @@ export function createPostService(
|
|
|
3115
3084
|
const limit = Math.min(Math.max(Math.trunc(requested), 1), 500);
|
|
3116
3085
|
const cursor = options.cursor;
|
|
3117
3086
|
|
|
3118
|
-
const whereConditions = [
|
|
3119
|
-
eq(posts.siteId, siteId),
|
|
3120
|
-
isNull(posts.deletedAt),
|
|
3121
|
-
];
|
|
3087
|
+
const whereConditions = [eq(posts.siteId, siteId)];
|
|
3122
3088
|
if (cursor) whereConditions.push(gt(posts.id, cursor));
|
|
3123
3089
|
|
|
3124
3090
|
// Fetch one extra row to detect end-of-data without a separate COUNT.
|
package/src/services/search.ts
CHANGED
|
@@ -53,7 +53,6 @@ interface RawSearchRow {
|
|
|
53
53
|
collection_id: string | null;
|
|
54
54
|
reply_to_id: string | null;
|
|
55
55
|
thread_id: string;
|
|
56
|
-
deleted_at: number | null;
|
|
57
56
|
published_at: number | null;
|
|
58
57
|
last_activity_at: number | null;
|
|
59
58
|
created_at: number;
|
|
@@ -87,7 +86,6 @@ function mapRow(row: RawSearchRow): SearchResult {
|
|
|
87
86
|
previewProvider: null,
|
|
88
87
|
replyToId: row.reply_to_id,
|
|
89
88
|
threadId: row.thread_id,
|
|
90
|
-
deletedAt: row.deleted_at,
|
|
91
89
|
publishedAt: row.published_at,
|
|
92
90
|
lastActivityAt:
|
|
93
91
|
row.last_activity_at ?? row.published_at ?? row.updated_at,
|
|
@@ -189,7 +187,6 @@ export function createSearchService(
|
|
|
189
187
|
AND path_registry.kind = 'slug'
|
|
190
188
|
WHERE post_fts MATCH ?
|
|
191
189
|
AND post.site_id = ?
|
|
192
|
-
AND post.deleted_at IS NULL
|
|
193
190
|
AND post.status IN (${statusPlaceholders})
|
|
194
191
|
${formatFilter}
|
|
195
192
|
ORDER BY post_fts.rank
|
|
@@ -264,7 +261,6 @@ export function createSearchService(
|
|
|
264
261
|
AND path_registry.kind = 'slug'
|
|
265
262
|
WHERE post.search_document @@ search_query.tsq
|
|
266
263
|
AND post.site_id = ?
|
|
267
|
-
AND post.deleted_at IS NULL
|
|
268
264
|
AND post.status IN (${statusPlaceholders})
|
|
269
265
|
${formatFilter}
|
|
270
266
|
ORDER BY rank DESC, post.published_at DESC NULLS LAST, post.id DESC
|
|
@@ -311,7 +307,6 @@ export function createSearchService(
|
|
|
311
307
|
AND path_registry.kind = 'slug'
|
|
312
308
|
WHERE post.search_text ILIKE ?
|
|
313
309
|
AND post.site_id = ?
|
|
314
|
-
AND post.deleted_at IS NULL
|
|
315
310
|
AND post.status IN (${statusPlaceholders})
|
|
316
311
|
${formatFilter}
|
|
317
312
|
ORDER BY rank DESC, post.published_at DESC NULLS LAST, post.id DESC
|
|
@@ -359,7 +354,6 @@ export function createSearchService(
|
|
|
359
354
|
post.url ${likeOperator} ?
|
|
360
355
|
)
|
|
361
356
|
AND post.site_id = ?
|
|
362
|
-
AND post.deleted_at IS NULL
|
|
363
357
|
AND post.status IN (${statusPlaceholders})
|
|
364
358
|
${formatFilter}
|
|
365
359
|
${likeOrderBy}
|
package/src/services/settings.ts
CHANGED
|
@@ -17,7 +17,11 @@ import {
|
|
|
17
17
|
ONBOARDING_STATUS,
|
|
18
18
|
type SettingsKey,
|
|
19
19
|
} from "../lib/constants.js";
|
|
20
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
baseLocale,
|
|
22
|
+
isValidContentLanguage,
|
|
23
|
+
normalizeContentLanguage,
|
|
24
|
+
} from "../i18n/locales.js";
|
|
21
25
|
import { isCjkSerifFont } from "../i18n/detect.js";
|
|
22
26
|
import type { StorageDriver } from "../lib/storage.js";
|
|
23
27
|
import type { MediaService } from "./media.js";
|
|
@@ -132,7 +136,10 @@ export interface SettingsService {
|
|
|
132
136
|
*
|
|
133
137
|
* @param storage - Optional storage driver for deleting the apple-touch-icon file
|
|
134
138
|
*/
|
|
135
|
-
removeAvatar(
|
|
139
|
+
removeAvatar(
|
|
140
|
+
storage?: StorageDriver | null,
|
|
141
|
+
deps?: { media?: MediaService; storageProvider?: string },
|
|
142
|
+
): Promise<void>;
|
|
136
143
|
}
|
|
137
144
|
|
|
138
145
|
export function createSettingsService(
|
|
@@ -284,10 +291,15 @@ export function createSettingsService(
|
|
|
284
291
|
|
|
285
292
|
async updateLocaleSettings(data, opts) {
|
|
286
293
|
const trimmedLanguage = data.siteLanguage.trim() || baseLocale;
|
|
287
|
-
if (!
|
|
288
|
-
throw new ValidationError(
|
|
294
|
+
if (!isValidContentLanguage(trimmedLanguage)) {
|
|
295
|
+
throw new ValidationError(
|
|
296
|
+
"Enter a valid BCP 47 language tag (e.g. en, zh-Hans, fi, ja, fr-CA).",
|
|
297
|
+
);
|
|
289
298
|
}
|
|
290
|
-
await this.set(
|
|
299
|
+
await this.set(
|
|
300
|
+
"SITE_LANGUAGE",
|
|
301
|
+
normalizeContentLanguage(trimmedLanguage),
|
|
302
|
+
);
|
|
291
303
|
|
|
292
304
|
// CJK serif font setting
|
|
293
305
|
const cjkFont = data.cjkSerifFont?.trim() ?? "";
|
|
@@ -419,11 +431,30 @@ export function createSettingsService(
|
|
|
419
431
|
"favicon",
|
|
420
432
|
"apple-touch-icon.png",
|
|
421
433
|
);
|
|
434
|
+
|
|
435
|
+
// The storage key is fixed across uploads, so an existing media
|
|
436
|
+
// row would violate the (provider, storage_key) unique index.
|
|
437
|
+
const existing = await deps.media.getByStorageKey(
|
|
438
|
+
appleTouchKey,
|
|
439
|
+
deps.storageProvider,
|
|
440
|
+
);
|
|
441
|
+
if (existing) {
|
|
442
|
+
await deps.media.delete(existing.id);
|
|
443
|
+
}
|
|
444
|
+
|
|
422
445
|
await deps.storage.put(
|
|
423
446
|
appleTouchKey,
|
|
424
447
|
new Uint8Array(data.appleTouchIcon),
|
|
425
448
|
{ contentType: "image/png" },
|
|
426
449
|
);
|
|
450
|
+
await deps.media.create({
|
|
451
|
+
filename: "apple-touch-icon.png",
|
|
452
|
+
originalName: "apple-touch-icon.png",
|
|
453
|
+
mimeType: "image/png",
|
|
454
|
+
size: data.appleTouchIcon.byteLength,
|
|
455
|
+
storageKey: appleTouchKey,
|
|
456
|
+
provider: deps.storageProvider,
|
|
457
|
+
});
|
|
427
458
|
await this.set("SITE_FAVICON_APPLE_TOUCH", appleTouchKey);
|
|
428
459
|
}
|
|
429
460
|
|
|
@@ -438,12 +469,22 @@ export function createSettingsService(
|
|
|
438
469
|
await this.set("SITE_FAVICON_VERSION", version);
|
|
439
470
|
},
|
|
440
471
|
|
|
441
|
-
async removeAvatar(storage) {
|
|
472
|
+
async removeAvatar(storage, deps) {
|
|
442
473
|
const appleTouchKey = await this.get("SITE_FAVICON_APPLE_TOUCH");
|
|
443
474
|
if (storage && appleTouchKey) {
|
|
444
475
|
await storage.delete(appleTouchKey);
|
|
445
476
|
}
|
|
446
477
|
|
|
478
|
+
if (deps?.media && deps.storageProvider && appleTouchKey) {
|
|
479
|
+
const existing = await deps.media.getByStorageKey(
|
|
480
|
+
appleTouchKey,
|
|
481
|
+
deps.storageProvider,
|
|
482
|
+
);
|
|
483
|
+
if (existing) {
|
|
484
|
+
await deps.media.delete(existing.id);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
447
488
|
await this.remove("SITE_AVATAR");
|
|
448
489
|
await this.remove("SITE_FAVICON_ICO");
|
|
449
490
|
await this.remove("SITE_FAVICON_APPLE_TOUCH");
|
|
@@ -29,7 +29,11 @@ import {
|
|
|
29
29
|
} from "../ui/font-themes.js";
|
|
30
30
|
import type { Site, SiteDomain } from "../types.js";
|
|
31
31
|
import { createCollectionService } from "./collection.js";
|
|
32
|
-
|
|
32
|
+
// Note: `./export.js` is loaded lazily inside `exportManagedSite` because it
|
|
33
|
+
// pulls in Vite-specific `?raw` asset imports (CSS/HTML/JS templates) at
|
|
34
|
+
// module-load time. Importing it eagerly here would force every consumer of
|
|
35
|
+
// `services/index.ts` — including Node-only dev scripts run under tsx — to
|
|
36
|
+
// resolve those Vite-only imports just to construct the services bundle.
|
|
33
37
|
import { createMediaService } from "./media.js";
|
|
34
38
|
import { createNavItemService } from "./navigation.js";
|
|
35
39
|
import { createPathService } from "./path.js";
|
|
@@ -675,6 +679,7 @@ export function createSiteAdminService(
|
|
|
675
679
|
);
|
|
676
680
|
const navItemList = await navItems.list();
|
|
677
681
|
const appleTouchKey = allSettings[SETTINGS_KEYS.SITE_FAVICON_APPLE_TOUCH];
|
|
682
|
+
const { createExportService } = await import("./export.js");
|
|
678
683
|
const exportService = createExportService(
|
|
679
684
|
{
|
|
680
685
|
collections,
|
package/src/styles/ui.css
CHANGED
|
@@ -2285,17 +2285,6 @@
|
|
|
2285
2285
|
text-decoration: underline;
|
|
2286
2286
|
}
|
|
2287
2287
|
|
|
2288
|
-
.post-header-actions {
|
|
2289
|
-
display: flex;
|
|
2290
|
-
align-items: center;
|
|
2291
|
-
flex-shrink: 0;
|
|
2292
|
-
margin-left: auto;
|
|
2293
|
-
}
|
|
2294
|
-
|
|
2295
|
-
.post-header-menu-trigger {
|
|
2296
|
-
margin-right: -0.1rem;
|
|
2297
|
-
}
|
|
2298
|
-
|
|
2299
2288
|
/*
|
|
2300
2289
|
* Detail pages get a denser reading ladder than the surrounding UI chrome.
|
|
2301
2290
|
* This keeps long-form text feeling grounded without making global UI copy
|
|
@@ -2349,18 +2338,6 @@
|
|
|
2349
2338
|
color: var(--site-reading-body);
|
|
2350
2339
|
}
|
|
2351
2340
|
|
|
2352
|
-
[data-page="post"] .post-header-actions .post-menu-trigger {
|
|
2353
|
-
color: var(--site-reading-meta);
|
|
2354
|
-
}
|
|
2355
|
-
|
|
2356
|
-
[data-page="post"] .post-header-actions .post-menu-trigger:hover,
|
|
2357
|
-
[data-page="post"] .post-header-actions .post-menu-trigger:focus-visible,
|
|
2358
|
-
[data-page="post"]
|
|
2359
|
-
.post-header-actions
|
|
2360
|
-
.post-menu-trigger[aria-expanded="true"] {
|
|
2361
|
-
color: var(--site-reading-body);
|
|
2362
|
-
}
|
|
2363
|
-
|
|
2364
2341
|
[data-page="post"] .feed-quote {
|
|
2365
2342
|
border-left-color: color-mix(
|
|
2366
2343
|
in srgb,
|
|
@@ -4415,6 +4392,7 @@
|
|
|
4415
4392
|
.compose-dialog jant-compose-dialog {
|
|
4416
4393
|
display: block;
|
|
4417
4394
|
position: relative;
|
|
4395
|
+
border-radius: inherit;
|
|
4418
4396
|
}
|
|
4419
4397
|
|
|
4420
4398
|
.compose-dialog:has(.compose-dialog-inner-suspended) {
|
|
@@ -4523,6 +4501,8 @@
|
|
|
4523
4501
|
min-height: 3.6rem;
|
|
4524
4502
|
padding: 12px 14px;
|
|
4525
4503
|
background-color: var(--compose-paper-bg);
|
|
4504
|
+
border-top-left-radius: inherit;
|
|
4505
|
+
border-top-right-radius: inherit;
|
|
4526
4506
|
position: relative;
|
|
4527
4507
|
z-index: 4;
|
|
4528
4508
|
}
|
|
@@ -5378,6 +5358,13 @@
|
|
|
5378
5358
|
position: relative;
|
|
5379
5359
|
z-index: 3;
|
|
5380
5360
|
background-color: var(--compose-paper-bg);
|
|
5361
|
+
border-bottom-left-radius: inherit;
|
|
5362
|
+
border-bottom-right-radius: inherit;
|
|
5363
|
+
}
|
|
5364
|
+
|
|
5365
|
+
.compose-action-row:has(+ .compose-quick-actions-row) {
|
|
5366
|
+
border-bottom-left-radius: 0;
|
|
5367
|
+
border-bottom-right-radius: 0;
|
|
5381
5368
|
}
|
|
5382
5369
|
|
|
5383
5370
|
.compose-action-row-overlay-open {
|
|
@@ -5801,6 +5788,8 @@
|
|
|
5801
5788
|
justify-content: flex-end;
|
|
5802
5789
|
padding: 2px 12px 12px;
|
|
5803
5790
|
background-color: var(--compose-paper-bg);
|
|
5791
|
+
border-bottom-left-radius: inherit;
|
|
5792
|
+
border-bottom-right-radius: inherit;
|
|
5804
5793
|
}
|
|
5805
5794
|
|
|
5806
5795
|
.compose-action-row:has(+ .compose-quick-actions-row) {
|
package/src/types/entities.ts
CHANGED
package/src/ui/color-themes.ts
CHANGED
|
@@ -37,7 +37,7 @@ interface ThemeModeColors {
|
|
|
37
37
|
searchMarkBg?: string;
|
|
38
38
|
/** Search highlight text color */
|
|
39
39
|
searchMarkColor?: string;
|
|
40
|
-
/**
|
|
40
|
+
/** Settings page background */
|
|
41
41
|
dashBg?: string;
|
|
42
42
|
/** Detail-page title color for long-form reading */
|
|
43
43
|
readingTitle?: string;
|
|
@@ -102,14 +102,28 @@ export function GeneralContent({
|
|
|
102
102
|
siteLanguage: i18n._(
|
|
103
103
|
msg({
|
|
104
104
|
message: "Language",
|
|
105
|
-
comment: "@context: Settings form field for site/
|
|
105
|
+
comment: "@context: Settings form field for site/admin language",
|
|
106
106
|
}),
|
|
107
107
|
),
|
|
108
108
|
siteLanguageHelp: i18n._(
|
|
109
109
|
msg({
|
|
110
110
|
message:
|
|
111
|
-
"
|
|
112
|
-
comment: "@context: Help text under the site language
|
|
111
|
+
"Sets the content language announced to readers (HTML lang, RSS) and the dashboard language. Any BCP 47 tag is accepted; tags without a dashboard translation fall back to English.",
|
|
112
|
+
comment: "@context: Help text under the site language input",
|
|
113
|
+
}),
|
|
114
|
+
),
|
|
115
|
+
siteLanguageSearchPlaceholder: i18n._(
|
|
116
|
+
msg({
|
|
117
|
+
message: "Search…",
|
|
118
|
+
comment:
|
|
119
|
+
"@context: Placeholder inside the language combobox search field",
|
|
120
|
+
}),
|
|
121
|
+
),
|
|
122
|
+
siteLanguageNoMatches: i18n._(
|
|
123
|
+
msg({
|
|
124
|
+
message: "No matches.",
|
|
125
|
+
comment:
|
|
126
|
+
"@context: Empty state shown when the language search filters out every entry",
|
|
113
127
|
}),
|
|
114
128
|
),
|
|
115
129
|
cjkFont: i18n._(
|
|
@@ -294,21 +308,6 @@ export function GeneralContent({
|
|
|
294
308
|
timezones.map((tz) => ({ value: tz.value, label: tz.label })),
|
|
295
309
|
).replace(/</g, "\\u003c");
|
|
296
310
|
|
|
297
|
-
// Language options — `value` must stay in sync with `src/i18n/locales.ts`.
|
|
298
|
-
// Labels are native names (untranslated) so they stay recognizable to any
|
|
299
|
-
// reader regardless of the currently active dashboard locale.
|
|
300
|
-
const languagesJson = JSON.stringify([
|
|
301
|
-
{ value: "en", label: "English" },
|
|
302
|
-
{
|
|
303
|
-
value: "zh-Hans",
|
|
304
|
-
label: "\u7B80\u4F53\u4E2D\u6587 (Simplified Chinese)",
|
|
305
|
-
},
|
|
306
|
-
{
|
|
307
|
-
value: "zh-Hant",
|
|
308
|
-
label: "\u7E41\u9AD4\u4E2D\u6587 (Traditional Chinese)",
|
|
309
|
-
},
|
|
310
|
-
]).replace(/</g, "\\u003c");
|
|
311
|
-
|
|
312
311
|
const cjkFontsJson = JSON.stringify([
|
|
313
312
|
{ value: "off", label: "None" },
|
|
314
313
|
{
|
|
@@ -342,7 +341,6 @@ export function GeneralContent({
|
|
|
342
341
|
labels={labels}
|
|
343
342
|
timezones={timezonesJson}
|
|
344
343
|
cjk-fonts={cjkFontsJson}
|
|
345
|
-
languages={languagesJson}
|
|
346
344
|
sitename-fallback={siteNameFallback}
|
|
347
345
|
sitedescription-fallback={siteDescriptionFallback}
|
|
348
346
|
main-feed-url={mainFeedUrl}
|
|
@@ -114,6 +114,23 @@ export function SettingsRootContent({
|
|
|
114
114
|
}),
|
|
115
115
|
)}
|
|
116
116
|
/>
|
|
117
|
+
<SettingsDirectoryLink
|
|
118
|
+
href={toPublicPath("/settings/github-sync", sitePathPrefix)}
|
|
119
|
+
icon={ICONS.gitBranch}
|
|
120
|
+
tone="subtle"
|
|
121
|
+
name={i18n._(
|
|
122
|
+
msg({
|
|
123
|
+
message: "GitHub Sync",
|
|
124
|
+
comment: "@context: Settings item — GitHub sync settings",
|
|
125
|
+
}),
|
|
126
|
+
)}
|
|
127
|
+
description={i18n._(
|
|
128
|
+
msg({
|
|
129
|
+
message: "Back up and sync content with a GitHub repository",
|
|
130
|
+
comment: "@context: Settings item description for GitHub sync",
|
|
131
|
+
}),
|
|
132
|
+
)}
|
|
133
|
+
/>
|
|
117
134
|
</SettingsDirectorySection>
|
|
118
135
|
|
|
119
136
|
<SettingsDirectorySection
|
|
@@ -208,34 +225,6 @@ export function SettingsRootContent({
|
|
|
208
225
|
/>
|
|
209
226
|
</SettingsDirectorySection>
|
|
210
227
|
|
|
211
|
-
<SettingsDirectorySection
|
|
212
|
-
title={i18n._(
|
|
213
|
-
msg({
|
|
214
|
-
message: "Integrations",
|
|
215
|
-
comment:
|
|
216
|
-
"@context: Settings group label for third-party integrations",
|
|
217
|
-
}),
|
|
218
|
-
)}
|
|
219
|
-
>
|
|
220
|
-
<SettingsDirectoryLink
|
|
221
|
-
href={toPublicPath("/settings/github-sync", sitePathPrefix)}
|
|
222
|
-
icon={ICONS.gitBranch}
|
|
223
|
-
tone="subtle"
|
|
224
|
-
name={i18n._(
|
|
225
|
-
msg({
|
|
226
|
-
message: "GitHub Sync",
|
|
227
|
-
comment: "@context: Settings item — GitHub sync settings",
|
|
228
|
-
}),
|
|
229
|
-
)}
|
|
230
|
-
description={i18n._(
|
|
231
|
-
msg({
|
|
232
|
-
message: "Back up and sync content with a GitHub repository",
|
|
233
|
-
comment: "@context: Settings item description for GitHub sync",
|
|
234
|
-
}),
|
|
235
|
-
)}
|
|
236
|
-
/>
|
|
237
|
-
</SettingsDirectorySection>
|
|
238
|
-
|
|
239
228
|
<SettingsDirectorySection
|
|
240
229
|
title={i18n._(
|
|
241
230
|
msg({
|
package/src/ui/feed/NoteCard.tsx
CHANGED
|
@@ -9,11 +9,7 @@ import type { FC } from "hono/jsx";
|
|
|
9
9
|
import type { TimelineCardProps } from "../../types.js";
|
|
10
10
|
import { MediaGallery } from "../shared/MediaGallery.js";
|
|
11
11
|
import { StarRating } from "../shared/StarRating.js";
|
|
12
|
-
import {
|
|
13
|
-
PostFooter,
|
|
14
|
-
PostMenuTriggerButton,
|
|
15
|
-
PostPublishedLink,
|
|
16
|
-
} from "../shared/PostFooter.js";
|
|
12
|
+
import { PostFooter, PostPublishedLink } from "../shared/PostFooter.js";
|
|
17
13
|
import { PostStatusBadges } from "./PostStatusBadges.js";
|
|
18
14
|
|
|
19
15
|
function stripContinueAnchor(html?: string): string | undefined {
|
|
@@ -44,7 +40,6 @@ export const NoteCard: FC<TimelineCardProps> = ({
|
|
|
44
40
|
const hasVisibleRating =
|
|
45
41
|
!!post.rating && post.rating > 0 && !display?.hideRating;
|
|
46
42
|
const showHeaderRating = isDetail && isArticle && hasVisibleRating;
|
|
47
|
-
const showHeaderActions = !display?.footer?.hideActions;
|
|
48
43
|
const footerDisplay =
|
|
49
44
|
isDetail && isArticle && display?.footer?.hideTimestamp === undefined
|
|
50
45
|
? { ...display?.footer, hideTimestamp: true }
|
|
@@ -77,11 +72,6 @@ export const NoteCard: FC<TimelineCardProps> = ({
|
|
|
77
72
|
post={post}
|
|
78
73
|
className="u-url post-header-meta-link"
|
|
79
74
|
/>
|
|
80
|
-
{showHeaderActions && (
|
|
81
|
-
<div class="post-header-actions">
|
|
82
|
-
<PostMenuTriggerButton className="post-menu-trigger post-header-menu-trigger" />
|
|
83
|
-
</div>
|
|
84
|
-
)}
|
|
85
75
|
</div>
|
|
86
76
|
{showHeaderRating && <StarRating rating={post.rating} />}
|
|
87
77
|
</div>
|
|
@@ -260,7 +260,7 @@ describe("timeline cards", () => {
|
|
|
260
260
|
expect(detailHtml).toContain('class="u-url post-header-meta-link"');
|
|
261
261
|
expect(detailHtml.match(/class="dt-published"/g)).toHaveLength(1);
|
|
262
262
|
expect(detailHtml).toContain("data-reply-trigger");
|
|
263
|
-
expect(detailHtml.match(/data-post-menu-trigger/g)).toHaveLength(
|
|
263
|
+
expect(detailHtml.match(/data-post-menu-trigger/g)).toHaveLength(1);
|
|
264
264
|
expect(detailHtml.indexOf('class="post-header-meta-row"')).toBeLessThan(
|
|
265
265
|
detailHtml.indexOf("data-post-body"),
|
|
266
266
|
);
|
|
@@ -49,6 +49,8 @@ export const PostPage: FC<PostPageProps> = ({ post, threadPosts }) => {
|
|
|
49
49
|
) : (
|
|
50
50
|
<TimelineItemFromPost post={post} mode="detail" />
|
|
51
51
|
)}
|
|
52
|
+
{/* Public integration slot — code injection (giscus, Webmentions, etc.) appends here. */}
|
|
53
|
+
<div data-post-end />
|
|
52
54
|
</div>
|
|
53
55
|
);
|
|
54
56
|
};
|