@jant/core 0.6.11 → 0.6.12

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.
Files changed (72) hide show
  1. package/bin/commands/import-site.js +45 -20
  2. package/dist/app-B5P1b4TF.js +6 -0
  3. package/dist/{app-CpmficmQ.js → app-D-zAhayc.js} +331 -55
  4. package/dist/client/.vite/manifest.json +3 -3
  5. package/dist/client/_assets/client-BUxR-E8O.css +2 -0
  6. package/dist/client/_assets/{client-Dd9U383b.js → client-S1cdvulk.js} +1 -1
  7. package/dist/client/_assets/{client-auth-DkpSdIDz.js → client-auth-YW92ZH7u.js} +116 -55
  8. package/dist/{export-Ba7NJImL.js → export-Ckwh4BiE.js} +4 -4
  9. package/dist/{github-sync-BD4w2m8-.js → github-sync-BMmK3JZ0.js} +2 -2
  10. package/dist/{github-sync-Cb4_6_i7.js → github-sync-Dl9wQMar.js} +1 -1
  11. package/dist/index.js +3 -3
  12. package/dist/node.js +4 -4
  13. package/package.json +1 -1
  14. package/src/__tests__/import-site-command.test.ts +75 -0
  15. package/src/__tests__/vite-dev-plugins.test.ts +34 -0
  16. package/src/client/__tests__/compose-launch.test.ts +50 -1
  17. package/src/client/__tests__/compose-shortcuts.test.ts +74 -18
  18. package/src/client/components/__tests__/jant-command-palette.test.ts +63 -0
  19. package/src/client/components/__tests__/jant-compose-dialog.test.ts +33 -0
  20. package/src/client/components/__tests__/jant-compose-editor-rehost-notice.test.ts +20 -1
  21. package/src/client/components/__tests__/jant-nav-manager.test.ts +77 -1
  22. package/src/client/components/__tests__/jant-settings-avatar.test.ts +6 -0
  23. package/src/client/components/__tests__/jant-settings-general.test.ts +71 -0
  24. package/src/client/components/jant-command-palette.ts +67 -2
  25. package/src/client/components/jant-compose-dialog.ts +8 -2
  26. package/src/client/components/jant-nav-manager.ts +142 -2
  27. package/src/client/components/jant-settings-general.ts +56 -0
  28. package/src/client/components/nav-manager-types.ts +14 -0
  29. package/src/client/components/settings-types.ts +30 -0
  30. package/src/client/compose-launch.ts +30 -0
  31. package/src/client/compose-shortcuts.ts +46 -3
  32. package/src/client/tiptap/__tests__/rehost-images.test.ts +66 -13
  33. package/src/client/tiptap/rehost-images.ts +5 -1
  34. package/src/i18n/__tests__/fallback.test.ts +24 -0
  35. package/src/i18n/locales/public/zh-Hans.po +2 -2
  36. package/src/i18n/locales/public/zh-Hans.ts +1 -1
  37. package/src/i18n/locales/public/zh-Hant.po +2 -2
  38. package/src/i18n/locales/public/zh-Hant.ts +1 -1
  39. package/src/i18n/locales/settings/en.po +54 -0
  40. package/src/i18n/locales/settings/en.ts +1 -1
  41. package/src/i18n/locales/settings/zh-Hans.po +64 -10
  42. package/src/i18n/locales/settings/zh-Hans.ts +1 -1
  43. package/src/i18n/locales/settings/zh-Hant.po +56 -2
  44. package/src/i18n/locales/settings/zh-Hant.ts +1 -1
  45. package/src/lib/__tests__/feed.test.ts +96 -10
  46. package/src/lib/feed.ts +63 -3
  47. package/src/lib/post-display.ts +14 -8
  48. package/src/preset.css +60 -18
  49. package/src/routes/dash/settings.tsx +41 -2
  50. package/src/routes/pages/__tests__/draft-about-edit.test.ts +65 -0
  51. package/src/routes/pages/page.tsx +44 -12
  52. package/src/services/__tests__/about-page.test.ts +116 -0
  53. package/src/services/__tests__/navigation.test.ts +190 -1
  54. package/src/services/__tests__/post-timeline.test.ts +119 -2
  55. package/src/services/about-page.ts +143 -0
  56. package/src/services/export-theme/layouts/_default/rss.xml +2 -1
  57. package/src/services/export-theme/layouts/partials/feed-post-content.xml +25 -1
  58. package/src/services/export-theme/styles/main.css +46 -14
  59. package/src/services/index.ts +27 -17
  60. package/src/services/navigation.ts +155 -1
  61. package/src/services/post.ts +24 -7
  62. package/src/styles/components.css +20 -0
  63. package/src/styles/tokens.css +26 -8
  64. package/src/styles/ui.css +149 -7
  65. package/src/types/views.ts +13 -0
  66. package/src/ui/dash/appearance/NavigationContent.tsx +57 -0
  67. package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +19 -0
  68. package/src/ui/dash/settings/GeneralContent.tsx +44 -0
  69. package/src/ui/dash/settings/__tests__/GeneralContent.test.tsx +8 -0
  70. package/src/ui/feed/__tests__/timeline-cards.test.ts +162 -1
  71. package/dist/app-DqKkZenB.js +0 -6
  72. package/dist/client/_assets/client-BhHHVvSY.css +0 -2
@@ -35,6 +35,7 @@ import {
35
35
  createSiteProfileService,
36
36
  type SiteProfileService,
37
37
  } from "./site-profile.js";
38
+ import { createAboutPageService, type AboutPageService } from "./about-page.js";
38
39
  import {
39
40
  createUploadSessionService,
40
41
  type UploadSessionService,
@@ -64,6 +65,7 @@ export interface Services {
64
65
  siteAdmin: SiteAdminService;
65
66
  siteMembers: SiteMemberService;
66
67
  siteProfile: SiteProfileService;
68
+ aboutPage: AboutPageService;
67
69
  githubAppInstallations: GitHubAppInstallationsService;
68
70
  telegram: TelegramService;
69
71
  }
@@ -88,6 +90,23 @@ export function createServices(
88
90
  const settings = createSettingsService(db, siteId, databaseSchema, dialect);
89
91
  const paths = createPathService(db, siteId, databaseSchema);
90
92
  const navItems = createNavItemService(db, siteId, databaseSchema);
93
+ const posts = createPostService(
94
+ db,
95
+ {
96
+ slugIdLength: config?.slugIdLength ?? 5,
97
+ databaseDialect: dialect,
98
+ },
99
+ siteId,
100
+ paths,
101
+ databaseSchema,
102
+ );
103
+ const collections = createCollectionService(
104
+ db,
105
+ siteId,
106
+ paths,
107
+ databaseSchema,
108
+ dialect,
109
+ );
91
110
  const media = createMediaService(db, siteId, databaseSchema, dialect, {
92
111
  enforceHostedQuota: config?.enforceHostedMediaQuota ?? false,
93
112
  hostedControlPlane: config?.hostedControlPlane ?? null,
@@ -97,26 +116,11 @@ export function createServices(
97
116
  settings,
98
117
  site,
99
118
  paths,
100
- posts: createPostService(
101
- db,
102
- {
103
- slugIdLength: config?.slugIdLength ?? 5,
104
- databaseDialect: dialect,
105
- },
106
- siteId,
107
- paths,
108
- databaseSchema,
109
- ),
119
+ posts,
110
120
  customUrls: createCustomUrlService(db, siteId, paths, databaseSchema),
111
121
  media,
112
122
  uploads: createUploadSessionService(db, siteId, media, databaseSchema),
113
- collections: createCollectionService(
114
- db,
115
- siteId,
116
- paths,
117
- databaseSchema,
118
- dialect,
119
- ),
123
+ collections,
120
124
  search: createSearchService(rawQuery, siteId, dialect),
121
125
  navItems,
122
126
  auth: createAuthService(
@@ -148,6 +152,11 @@ export function createServices(
148
152
  );
149
153
  },
150
154
  }),
155
+ aboutPage: createAboutPageService({
156
+ paths,
157
+ posts,
158
+ collections,
159
+ }),
151
160
  githubAppInstallations: createGitHubAppInstallationsService(
152
161
  db,
153
162
  databaseSchema,
@@ -166,6 +175,7 @@ export type { UploadSessionService } from "./upload-session.js";
166
175
  export type { CollectionService } from "./collection.js";
167
176
  export type { SearchService, SearchResult, SearchOptions } from "./search.js";
168
177
  export type { NavItemService } from "./navigation.js";
178
+ export type { AboutPageService, AboutPageStatus } from "./about-page.js";
169
179
  export type { AuthService, DeleteAccountDeps } from "./auth.js";
170
180
  export type { ApiTokenService } from "./api-token.js";
171
181
  export type {
@@ -15,6 +15,7 @@ import {
15
15
  import { createEntityId } from "../lib/ids.js";
16
16
  import { ValidationError } from "../lib/errors.js";
17
17
  import { now } from "../lib/time.js";
18
+ import { normalizePath } from "../lib/url.js";
18
19
  import { COLLECTION_FRESHNESS_WINDOW_SECONDS } from "../types.js";
19
20
  import type {
20
21
  NavItem,
@@ -23,10 +24,15 @@ import type {
23
24
  CreateNavItem,
24
25
  UpdateNavItem,
25
26
  SystemNavKey,
27
+ SuggestedNavLink,
26
28
  } from "../types.js";
27
29
  import { SYSTEM_NAV_KEYS } from "../types.js";
28
30
 
29
31
  const POSITION_RETRY_ATTEMPTS = 5;
32
+ const SUGGESTED_NAV_LINK_CANDIDATES = [
33
+ { key: "about", path: "/about", label: "About" },
34
+ { key: "now", path: "/now", label: "Now" },
35
+ ] as const;
30
36
 
31
37
  // Re-export shared constraint detection — see db/dialect.ts
32
38
  import { isUniqueConstraintError } from "../db/dialect.js";
@@ -45,6 +51,7 @@ export interface NavItemService {
45
51
  afterId: string | null,
46
52
  beforeId: string | null,
47
53
  ): Promise<NavItem | null>;
54
+ listSuggestedLinks(): Promise<SuggestedNavLink[]>;
48
55
  getCollectionFreshness(collectionIds: string[]): Promise<Map<string, number>>;
49
56
  }
50
57
 
@@ -53,7 +60,8 @@ export function createNavItemService(
53
60
  siteId: string,
54
61
  databaseSchema: DatabaseSchema = sqliteSchemaBundle,
55
62
  ): NavItemService {
56
- const { navItems, postCollections, posts } = databaseSchema;
63
+ const { navItems, postCollections, posts, pathRegistry, collections } =
64
+ databaseSchema;
57
65
 
58
66
  const defaultSystemOrder = [
59
67
  "latest",
@@ -121,6 +129,39 @@ export function createNavItemService(
121
129
  };
122
130
  }
123
131
 
132
+ function withLeadingSlash(path: string): string {
133
+ const normalized = normalizePath(path);
134
+ return normalized ? `/${normalized}` : "/";
135
+ }
136
+
137
+ function getComparableInternalPath(url: string): string | null {
138
+ if (
139
+ url.startsWith("http://") ||
140
+ url.startsWith("https://") ||
141
+ url.startsWith("//") ||
142
+ url.startsWith("mailto:") ||
143
+ url.startsWith("tel:") ||
144
+ url.startsWith("#")
145
+ ) {
146
+ return null;
147
+ }
148
+
149
+ try {
150
+ const pathname = new URL(url, "https://jant.invalid").pathname;
151
+ return withLeadingSlash(pathname);
152
+ } catch {
153
+ return null;
154
+ }
155
+ }
156
+
157
+ function normalizeSuggestedLabel(
158
+ label: string | null | undefined,
159
+ fallback: string,
160
+ ): string {
161
+ const trimmed = label?.trim();
162
+ return trimmed && trimmed.length > 0 ? trimmed : fallback;
163
+ }
164
+
124
165
  async function getLastPosition(): Promise<string | null> {
125
166
  const rows = await db
126
167
  .select({ position: navItems.position })
@@ -414,6 +455,119 @@ export function createNavItemService(
414
455
  throw new Error("Failed to assign a unique nav item position");
415
456
  },
416
457
 
458
+ async listSuggestedLinks() {
459
+ const existingRows = await db
460
+ .select({
461
+ url: navItems.url,
462
+ collectionId: navItems.collectionId,
463
+ })
464
+ .from(navItems)
465
+ .where(eq(navItems.siteId, siteId));
466
+ const existingPaths = new Set(
467
+ existingRows.flatMap((item) => {
468
+ const path = getComparableInternalPath(item.url);
469
+ return path ? [path] : [];
470
+ }),
471
+ );
472
+ const existingCollectionIds = new Set(
473
+ existingRows.flatMap((item) =>
474
+ item.collectionId ? [item.collectionId] : [],
475
+ ),
476
+ );
477
+
478
+ const suggestions: SuggestedNavLink[] = [];
479
+
480
+ for (const candidate of SUGGESTED_NAV_LINK_CANDIDATES) {
481
+ const path = withLeadingSlash(candidate.path);
482
+ if (existingPaths.has(path)) continue;
483
+
484
+ const pathRows = await db
485
+ .select()
486
+ .from(pathRegistry)
487
+ .where(
488
+ and(
489
+ eq(pathRegistry.siteId, siteId),
490
+ eq(pathRegistry.path, normalizePath(candidate.path)),
491
+ ),
492
+ )
493
+ .limit(1);
494
+ const record = pathRows[0];
495
+ if (!record || record.kind === "redirect") continue;
496
+
497
+ if (record.postId) {
498
+ const postRows = await db
499
+ .select({
500
+ title: posts.title,
501
+ status: posts.status,
502
+ visibility: posts.visibility,
503
+ })
504
+ .from(posts)
505
+ .where(and(eq(posts.siteId, siteId), eq(posts.id, record.postId)))
506
+ .limit(1);
507
+ const post = postRows[0];
508
+ if (
509
+ !post ||
510
+ post.status !== "published" ||
511
+ post.visibility === "private"
512
+ ) {
513
+ continue;
514
+ }
515
+
516
+ suggestions.push({
517
+ key: candidate.key,
518
+ label: normalizeSuggestedLabel(post.title, candidate.label),
519
+ url: path,
520
+ targetType: "page",
521
+ navItemType: "link",
522
+ });
523
+ continue;
524
+ }
525
+
526
+ if (record.collectionId) {
527
+ if (existingCollectionIds.has(record.collectionId)) continue;
528
+
529
+ const collectionRows = await db
530
+ .select({
531
+ title: collections.title,
532
+ })
533
+ .from(collections)
534
+ .where(
535
+ and(
536
+ eq(collections.siteId, siteId),
537
+ eq(collections.id, record.collectionId),
538
+ ),
539
+ )
540
+ .limit(1);
541
+ const collection = collectionRows[0];
542
+ if (!collection) continue;
543
+
544
+ suggestions.push({
545
+ key: candidate.key,
546
+ label: normalizeSuggestedLabel(collection.title, candidate.label),
547
+ url: path,
548
+ targetType: "collection",
549
+ navItemType: record.kind === "slug" ? "collection" : "link",
550
+ ...(record.kind === "slug" && {
551
+ collectionId: record.collectionId,
552
+ }),
553
+ });
554
+ continue;
555
+ }
556
+
557
+ if (record.kind === "archive") {
558
+ suggestions.push({
559
+ key: candidate.key,
560
+ label: candidate.label,
561
+ url: path,
562
+ targetType: "archive",
563
+ navItemType: "link",
564
+ });
565
+ }
566
+ }
567
+
568
+ return suggestions;
569
+ },
570
+
417
571
  async getCollectionFreshness(collectionIds) {
418
572
  if (collectionIds.length === 0) return new Map<string, number>();
419
573
 
@@ -1105,6 +1105,21 @@ export function createPostService(
1105
1105
  return conditions;
1106
1106
  }
1107
1107
 
1108
+ function buildCollectionThreadActivityExpr(alias: string) {
1109
+ return sql<number>`MAX(
1110
+ COALESCE(
1111
+ (
1112
+ SELECT root.last_activity_at
1113
+ FROM post AS root
1114
+ WHERE root.site_id = ${siteId}
1115
+ AND root.id = ${posts.threadId}
1116
+ ),
1117
+ ${posts.publishedAt},
1118
+ ${posts.updatedAt}
1119
+ )
1120
+ )`.as(alias);
1121
+ }
1122
+
1108
1123
  function isMediaAttachmentInput(
1109
1124
  attachment: PostAttachmentInput,
1110
1125
  ): attachment is Extract<PostAttachmentInput, { type: "media" }> {
@@ -2845,6 +2860,8 @@ export function createPostService(
2845
2860
  sortOrder === "oldest"
2846
2861
  ? sql<number>`MIN(${posts.publishedAt})`.as("published_at")
2847
2862
  : sql<number>`MAX(${posts.publishedAt})`.as("published_at");
2863
+ const threadActivityAt =
2864
+ buildCollectionThreadActivityExpr("thread_activity_at");
2848
2865
  const ratingPresence = sql<number>`MAX(
2849
2866
  CASE
2850
2867
  WHEN ${posts.rating} IS NULL THEN 0
@@ -2864,6 +2881,7 @@ export function createPostService(
2864
2881
  .select({
2865
2882
  threadId: posts.threadId,
2866
2883
  publishedAt,
2884
+ threadActivityAt,
2867
2885
  collectionPinnedAt,
2868
2886
  ratingPresence,
2869
2887
  ratingValue,
@@ -2891,12 +2909,12 @@ export function createPostService(
2891
2909
  desc(collectionPinnedAt),
2892
2910
  desc(ratingPresence),
2893
2911
  desc(ratingValue),
2894
- desc(publishedAt),
2912
+ desc(threadActivityAt),
2895
2913
  desc(posts.threadId),
2896
2914
  )
2897
2915
  : baseQuery.orderBy(
2898
2916
  desc(collectionPinnedAt),
2899
- desc(publishedAt),
2917
+ desc(threadActivityAt),
2900
2918
  desc(posts.threadId),
2901
2919
  );
2902
2920
 
@@ -2923,9 +2941,8 @@ export function createPostService(
2923
2941
  ...buildThreadRootPageConditions(options),
2924
2942
  buildCollectionMembershipCondition(collectionIds),
2925
2943
  ];
2926
- const publishedAt = sql<number>`MAX(${posts.publishedAt})`.as(
2927
- "published_at",
2928
- );
2944
+ const threadActivityAt =
2945
+ buildCollectionThreadActivityExpr("thread_activity_at");
2929
2946
  const collectedAt = sql<number>`MAX(${postCollections.createdAt})`.as(
2930
2947
  "collected_at",
2931
2948
  );
@@ -2937,7 +2954,7 @@ export function createPostService(
2937
2954
  let query = db
2938
2955
  .select({
2939
2956
  threadId: posts.threadId,
2940
- publishedAt,
2957
+ threadActivityAt,
2941
2958
  collectedAt,
2942
2959
  collectionPinnedAt,
2943
2960
  })
@@ -2953,7 +2970,7 @@ export function createPostService(
2953
2970
  .groupBy(posts.threadId)
2954
2971
  .orderBy(
2955
2972
  desc(collectionPinnedAt),
2956
- desc(publishedAt),
2973
+ desc(threadActivityAt),
2957
2974
  desc(posts.threadId),
2958
2975
  );
2959
2976
 
@@ -1163,6 +1163,26 @@ svg[stroke-width].icon-fine {
1163
1163
  border-color: var(--color-border);
1164
1164
  }
1165
1165
 
1166
+ .nav-suggestions-list {
1167
+ @apply flex flex-col gap-2;
1168
+ }
1169
+
1170
+ .nav-suggestion-item {
1171
+ @apply flex items-center justify-between gap-3 rounded-lg border px-3 py-2;
1172
+ }
1173
+
1174
+ .nav-suggestion-info {
1175
+ @apply flex min-w-0 flex-col;
1176
+ }
1177
+
1178
+ .nav-suggestion-title {
1179
+ @apply truncate text-sm font-medium;
1180
+ }
1181
+
1182
+ .nav-suggestion-meta {
1183
+ @apply truncate text-xs text-muted-foreground;
1184
+ }
1185
+
1166
1186
  /* Collection picker dropdown menu */
1167
1187
  .collection-picker {
1168
1188
  @apply absolute left-0 z-10 mt-1.5 w-64 overflow-y-auto rounded-lg border py-1.5;
@@ -65,8 +65,8 @@
65
65
  * --type-subtitle 1.8rem (27px) — h3
66
66
  * --type-body 1.4rem (21px) — running text, form inputs
67
67
  * --type-secondary 1.1rem (16.5px) — meta, nav, sidenotes, UI labels
68
- * --type-base 1.0rem (15px) — code, UI buttons, controls
69
- * --type-sm 0.9rem (13.5px) — code blocks, small UI text
68
+ * --type-base 1.0rem (15px) — UI buttons, controls
69
+ * --type-sm 0.9rem (13.5px) — small UI text
70
70
  * --type-xs 0.8rem (12px) — captions, descriptions, chips
71
71
  * --type-2xs 0.65rem (9.75px) — tiny labels, file sizes, badges
72
72
  */
@@ -84,18 +84,20 @@
84
84
  mobile overrides below cap sizes for small screens.
85
85
  --type-content-scale uniformly scales all content sizes
86
86
  without affecting UI controls. */
87
- --type-content-scale: 0.78;
87
+ --type-content-scale: 0.8;
88
88
  --type-content-display: calc(var(--type-display) * var(--type-content-scale));
89
89
  --type-content-title: calc(var(--type-title) * var(--type-content-scale));
90
90
  --type-content-subtitle: calc(
91
91
  var(--type-subtitle) * var(--type-content-scale)
92
92
  );
93
93
  --type-content-body: calc(var(--type-body) * var(--type-content-scale));
94
+ --type-content-quote: calc(var(--type-content-body) * 1.16);
95
+ --type-content-quote-leading: 1.4;
94
96
 
95
97
  /* Semantic aliases */
96
- --type-code: var(--type-base);
97
- --type-code-block: var(--type-sm);
98
98
  --type-body-size: var(--type-content-body);
99
+ --type-code: calc(var(--type-body-size) * 0.94);
100
+ --type-code-block: calc(var(--type-body-size) * 0.9);
99
101
  --type-body-tracking: 0;
100
102
  --type-ui-title: var(--type-secondary);
101
103
  --type-ui-control: var(--type-base);
@@ -107,9 +109,9 @@
107
109
  --type-thread-context: var(--type-base);
108
110
  --type-thread-context-title: var(--type-secondary);
109
111
  --type-thread-context-meta: var(--type-sm);
110
- --feed-note-title-size: var(--type-content-title);
112
+ --feed-note-title-size: calc(var(--type-content-body) * 1.36);
111
113
  --feed-note-title-leading: var(--type-heading-leading);
112
- --type-body-leading: 1.5;
114
+ --type-body-leading: 1.7;
113
115
  --type-display-leading: 1.15;
114
116
  --type-heading-leading: 1.15;
115
117
  --type-heading-weight: var(--fw-regular);
@@ -264,6 +266,22 @@
264
266
  var(--site-text-primary) 12%,
265
267
  transparent
266
268
  );
269
+ --site-code-text: color-mix(
270
+ in srgb,
271
+ var(--site-reading-heading) 86%,
272
+ var(--site-reading-body)
273
+ );
274
+ --site-code-bg: color-mix(in srgb, var(--site-reading-meta) 12%, transparent);
275
+ --site-code-block-bg: color-mix(
276
+ in srgb,
277
+ var(--site-elevated-bg) 94%,
278
+ var(--site-nav-hover-bg)
279
+ );
280
+ --site-code-block-border: color-mix(
281
+ in srgb,
282
+ var(--site-divider) 62%,
283
+ transparent
284
+ );
267
285
  --site-feed-divider-color: color-mix(
268
286
  in srgb,
269
287
  var(--site-text-secondary) 30%,
@@ -484,7 +502,7 @@
484
502
  :root {
485
503
  /* Content layer — tighter scale for mobile reading.
486
504
  Ratio ≈ 1.88 : 1.42 : 1.15 : 1 (display:title:subtitle:body).
487
- At 15px root × 0.78 content-scale: 28.7 / 21.6 / 17.6 / 15.2px.
505
+ At 15px root × 0.8 content-scale: 29.4 / 22.2 / 18 / 15.6px.
488
506
  Body is floored to a 16px minimum below (max()) for readability;
489
507
  the floor still yields to calc() when the user zooms the root up. */
490
508
  --type-content-display: calc(2.45rem * var(--type-content-scale));