@jant/core 0.3.24 → 0.3.25
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/app.js +50 -25
- package/dist/db/schema.js +1 -1
- package/dist/i18n/locales/en.js +1 -1
- package/dist/i18n/locales/zh-Hans.js +1 -1
- package/dist/i18n/locales/zh-Hant.js +1 -1
- package/dist/index.js +3 -9
- package/dist/lib/constants.js +1 -0
- package/dist/lib/nav-reorder.js +1 -1
- package/dist/lib/navigation.js +26 -1
- package/dist/lib/pagination.js +44 -0
- package/dist/lib/render.js +7 -11
- package/dist/lib/schemas.js +3 -3
- package/dist/lib/theme.js +4 -4
- package/dist/lib/timeline.js +24 -48
- package/dist/lib/view.js +2 -2
- package/dist/routes/api/collections.js +124 -0
- package/dist/routes/api/nav-items.js +104 -0
- package/dist/routes/api/pages.js +91 -0
- package/dist/routes/api/posts.js +2 -2
- package/dist/routes/api/search.js +2 -2
- package/dist/routes/api/settings.js +68 -0
- package/dist/routes/compose.js +48 -0
- package/dist/routes/dash/collections.js +2 -2
- package/dist/routes/dash/index.js +1 -1
- package/dist/routes/dash/media.js +2 -2
- package/dist/routes/dash/pages.js +411 -62
- package/dist/routes/dash/posts.js +3 -5
- package/dist/routes/dash/redirects.js +2 -2
- package/dist/routes/dash/settings.js +79 -5
- package/dist/routes/feed/rss.js +2 -2
- package/dist/routes/feed/sitemap.js +1 -1
- package/dist/routes/pages/archive.js +3 -6
- package/dist/routes/pages/collection.js +3 -6
- package/dist/routes/pages/collections.js +28 -0
- package/dist/routes/pages/featured.js +32 -0
- package/dist/routes/pages/home.js +9 -50
- package/dist/routes/pages/page.js +29 -32
- package/dist/routes/pages/post.js +3 -6
- package/dist/routes/pages/search.js +3 -6
- package/dist/services/page.js +5 -1
- package/dist/services/post.js +40 -6
- package/dist/services/search.js +1 -1
- package/dist/ui/compose/ComposeDialog.js +452 -0
- package/dist/ui/compose/ComposePrompt.js +55 -0
- package/dist/{theme/components/TypeBadge.js → ui/dash/FormatBadge.js} +1 -2
- package/dist/{theme/components → ui/dash}/PostForm.js +0 -27
- package/dist/{theme/components → ui/dash}/PostList.js +6 -6
- package/dist/{theme/components/VisibilityBadge.js → ui/dash/StatusBadge.js} +1 -2
- package/dist/{theme/components → ui/dash}/index.js +3 -6
- package/dist/{themes/threads/timeline → ui/feed}/LinkCard.js +6 -2
- package/dist/{themes/threads/timeline → ui/feed}/NoteCard.js +11 -6
- package/dist/{themes/threads/timeline → ui/feed}/QuoteCard.js +10 -6
- package/dist/{themes/threads/timeline → ui/feed}/ThreadPreview.js +7 -9
- package/dist/ui/feed/TimelineFeed.js +41 -0
- package/dist/ui/feed/TimelineItem.js +27 -0
- package/dist/{theme → ui}/layouts/BaseLayout.js +10 -0
- package/dist/{theme → ui}/layouts/DashLayout.js +0 -8
- package/dist/ui/layouts/SiteLayout.js +141 -0
- package/dist/{themes/threads → ui}/pages/ArchivePage.js +16 -14
- package/dist/{themes/threads → ui}/pages/CollectionPage.js +6 -1
- package/dist/ui/pages/CollectionsPage.js +76 -0
- package/dist/ui/pages/FeaturedPage.js +24 -0
- package/dist/ui/pages/HomePage.js +24 -0
- package/dist/{themes/threads → ui}/pages/PostPage.js +13 -8
- package/dist/{themes/threads → ui}/pages/SearchPage.js +9 -7
- package/dist/{themes/threads → ui}/pages/SinglePage.js +3 -2
- package/dist/{theme/components → ui/shared}/MediaGallery.js +1 -1
- package/dist/{theme/components → ui/shared}/Pagination.js +41 -2
- package/dist/{theme/components → ui/shared}/ThreadView.js +2 -2
- package/dist/ui/shared/index.js +5 -0
- package/package.json +1 -9
- package/src/__tests__/helpers/db.ts +3 -0
- package/src/app.tsx +57 -27
- package/src/db/migrations/0006_rename_slug_to_path.sql +5 -0
- package/src/db/migrations/meta/_journal.json +7 -0
- package/src/db/schema.ts +1 -1
- package/src/i18n/locales/en.po +332 -181
- package/src/i18n/locales/en.ts +1 -1
- package/src/i18n/locales/zh-Hans.po +332 -181
- package/src/i18n/locales/zh-Hans.ts +1 -1
- package/src/i18n/locales/zh-Hant.po +332 -181
- package/src/i18n/locales/zh-Hant.ts +1 -1
- package/src/index.ts +7 -36
- package/src/lib/__tests__/schemas.test.ts +60 -19
- package/src/lib/__tests__/timeline.test.ts +45 -81
- package/src/lib/__tests__/view.test.ts +13 -7
- package/src/lib/constants.ts +1 -0
- package/src/lib/nav-reorder.ts +1 -1
- package/src/lib/navigation.ts +40 -2
- package/src/lib/pagination.ts +50 -0
- package/src/lib/render.tsx +7 -14
- package/src/lib/schemas.ts +8 -6
- package/src/lib/theme.ts +5 -5
- package/src/lib/timeline.ts +28 -57
- package/src/lib/view.ts +2 -2
- package/src/preset.css +2 -1
- package/src/routes/__tests__/compose.test.ts +199 -0
- package/src/routes/api/__tests__/collections.test.ts +249 -0
- package/src/routes/api/__tests__/nav-items.test.ts +222 -0
- package/src/routes/api/__tests__/pages.test.ts +218 -0
- package/src/routes/api/__tests__/settings.test.ts +132 -0
- package/src/routes/api/collections.ts +143 -0
- package/src/routes/api/nav-items.ts +115 -0
- package/src/routes/api/pages.ts +101 -0
- package/src/routes/api/posts.ts +2 -2
- package/src/routes/api/search.ts +2 -2
- package/src/routes/api/settings.ts +91 -0
- package/src/routes/compose.ts +63 -0
- package/src/routes/dash/__tests__/pages.test.ts +225 -0
- package/src/routes/dash/collections.tsx +2 -2
- package/src/routes/dash/index.tsx +1 -1
- package/src/routes/dash/media.tsx +2 -2
- package/src/routes/dash/pages.tsx +443 -70
- package/src/routes/dash/posts.tsx +3 -7
- package/src/routes/dash/redirects.tsx +2 -2
- package/src/routes/dash/settings.tsx +83 -5
- package/src/routes/feed/rss.ts +2 -2
- package/src/routes/feed/sitemap.ts +1 -1
- package/src/routes/pages/__tests__/collections.test.ts +94 -0
- package/src/routes/pages/__tests__/featured.test.ts +94 -0
- package/src/routes/pages/archive.tsx +2 -6
- package/src/routes/pages/collection.tsx +2 -6
- package/src/routes/pages/collections.tsx +36 -0
- package/src/routes/pages/featured.tsx +38 -0
- package/src/routes/pages/home.tsx +9 -55
- package/src/routes/pages/page.tsx +28 -30
- package/src/routes/pages/post.tsx +2 -5
- package/src/routes/pages/search.tsx +2 -6
- package/src/services/__tests__/page.test.ts +106 -0
- package/src/services/__tests__/post.test.ts +114 -15
- package/src/services/page.ts +13 -1
- package/src/services/post.ts +57 -7
- package/src/services/search.ts +2 -2
- package/src/styles/tokens.css +47 -0
- package/src/styles/ui.css +491 -0
- package/src/types.ts +29 -159
- package/src/ui/compose/ComposeDialog.tsx +395 -0
- package/src/ui/compose/ComposePrompt.tsx +55 -0
- package/src/{theme/components/TypeBadge.tsx → ui/dash/FormatBadge.tsx} +2 -3
- package/src/{theme/components → ui/dash}/PostForm.tsx +0 -25
- package/src/{theme/components → ui/dash}/PostList.tsx +7 -7
- package/src/{theme/components/VisibilityBadge.tsx → ui/dash/StatusBadge.tsx} +2 -3
- package/src/ui/dash/index.ts +10 -0
- package/src/{themes/threads/timeline → ui/feed}/LinkCard.tsx +9 -4
- package/src/{themes/threads/timeline → ui/feed}/NoteCard.tsx +13 -8
- package/src/{themes/threads/timeline → ui/feed}/QuoteCard.tsx +13 -8
- package/src/{themes/threads/timeline → ui/feed}/ThreadPreview.tsx +7 -8
- package/src/ui/feed/TimelineFeed.tsx +49 -0
- package/src/ui/feed/TimelineItem.tsx +45 -0
- package/src/{theme → ui}/layouts/BaseLayout.tsx +11 -1
- package/src/{theme → ui}/layouts/DashLayout.tsx +0 -10
- package/src/ui/layouts/SiteLayout.tsx +150 -0
- package/src/{themes/threads → ui}/pages/ArchivePage.tsx +22 -17
- package/src/{themes/threads → ui}/pages/CollectionPage.tsx +14 -5
- package/src/ui/pages/CollectionsPage.tsx +73 -0
- package/src/ui/pages/FeaturedPage.tsx +31 -0
- package/src/{themes/threads → ui}/pages/HomePage.tsx +11 -15
- package/src/{themes/threads → ui}/pages/PostPage.tsx +23 -14
- package/src/{themes/threads → ui}/pages/SearchPage.tsx +13 -11
- package/src/{themes/threads → ui}/pages/SinglePage.tsx +4 -4
- package/src/{theme/components → ui/shared}/MediaGallery.tsx +1 -1
- package/src/{theme/components → ui/shared}/Pagination.tsx +67 -4
- package/src/{theme/components → ui/shared}/ThreadView.tsx +2 -2
- package/src/ui/shared/__tests__/pagination.test.ts +46 -0
- package/src/ui/shared/index.ts +12 -0
- package/bin/jant.js +0 -185
- package/dist/lib/theme-components.js +0 -46
- package/dist/routes/dash/navigation.js +0 -289
- package/dist/theme/index.js +0 -18
- package/dist/theme/layouts/index.js +0 -2
- package/dist/themes/threads/ThreadsSiteLayout.js +0 -172
- package/dist/themes/threads/index.js +0 -81
- package/dist/themes/threads/pages/HomePage.js +0 -25
- package/dist/themes/threads/timeline/TimelineFeed.js +0 -58
- package/dist/themes/threads/timeline/TimelineItem.js +0 -36
- package/dist/themes/threads/timeline/TimelineLoadMore.js +0 -23
- package/dist/themes/threads/timeline/groupByDate.js +0 -22
- package/dist/themes/threads/timeline/timelineMore.js +0 -107
- package/src/lib/__tests__/theme-components.test.ts +0 -105
- package/src/lib/theme-components.ts +0 -65
- package/src/routes/dash/navigation.tsx +0 -317
- package/src/theme/components/index.ts +0 -23
- package/src/theme/index.ts +0 -22
- package/src/theme/layouts/index.ts +0 -7
- package/src/themes/threads/ThreadsSiteLayout.tsx +0 -194
- package/src/themes/threads/index.ts +0 -100
- package/src/themes/threads/style.css +0 -336
- package/src/themes/threads/timeline/TimelineFeed.tsx +0 -62
- package/src/themes/threads/timeline/TimelineItem.tsx +0 -67
- package/src/themes/threads/timeline/TimelineLoadMore.tsx +0 -35
- package/src/themes/threads/timeline/groupByDate.ts +0 -30
- package/src/themes/threads/timeline/timelineMore.tsx +0 -130
- /package/dist/{theme → ui}/color-themes.js +0 -0
- /package/dist/{theme/components → ui/dash}/ActionButtons.js +0 -0
- /package/dist/{theme/components → ui/dash}/CrudPageHeader.js +0 -0
- /package/dist/{theme/components → ui/dash}/DangerZone.js +0 -0
- /package/dist/{theme/components → ui/dash}/ListItemRow.js +0 -0
- /package/dist/{theme/components → ui/dash}/PageForm.js +0 -0
- /package/dist/{theme/components → ui/shared}/EmptyState.js +0 -0
- /package/src/{theme → ui}/color-themes.ts +0 -0
- /package/src/{theme/components → ui/dash}/ActionButtons.tsx +0 -0
- /package/src/{theme/components → ui/dash}/CrudPageHeader.tsx +0 -0
- /package/src/{theme/components → ui/dash}/DangerZone.tsx +0 -0
- /package/src/{theme/components → ui/dash}/ListItemRow.tsx +0 -0
- /package/src/{theme/components → ui/dash}/PageForm.tsx +0 -0
- /package/src/{theme/components → ui/shared}/EmptyState.tsx +0 -0
package/src/types.ts
CHANGED
|
@@ -158,7 +158,7 @@ export interface Post {
|
|
|
158
158
|
status: Status;
|
|
159
159
|
featured: number; // 0 | 1
|
|
160
160
|
pinned: number; // 0 | 1
|
|
161
|
-
|
|
161
|
+
path: string | null;
|
|
162
162
|
title: string | null;
|
|
163
163
|
url: string | null;
|
|
164
164
|
body: string | null;
|
|
@@ -265,7 +265,7 @@ export interface CreatePost {
|
|
|
265
265
|
status?: Status;
|
|
266
266
|
featured?: boolean;
|
|
267
267
|
pinned?: boolean;
|
|
268
|
-
|
|
268
|
+
path?: string;
|
|
269
269
|
title?: string;
|
|
270
270
|
url?: string;
|
|
271
271
|
body?: string;
|
|
@@ -282,7 +282,7 @@ export interface UpdatePost {
|
|
|
282
282
|
status?: Status;
|
|
283
283
|
featured?: boolean;
|
|
284
284
|
pinned?: boolean;
|
|
285
|
-
|
|
285
|
+
path?: string | null;
|
|
286
286
|
title?: string | null;
|
|
287
287
|
url?: string | null;
|
|
288
288
|
body?: string | null;
|
|
@@ -354,10 +354,10 @@ export interface UpdateCollection {
|
|
|
354
354
|
export interface PostView {
|
|
355
355
|
// Identity
|
|
356
356
|
id: number;
|
|
357
|
-
/** Pre-computed permalink: "/{
|
|
357
|
+
/** Pre-computed permalink: "/{path}" if path set, otherwise "/p/{sqid}" */
|
|
358
358
|
permalink: string;
|
|
359
|
-
/** Custom URL
|
|
360
|
-
|
|
359
|
+
/** Custom URL path, if set. Supports multi-level paths (e.g. "2024/my-post") */
|
|
360
|
+
path?: string;
|
|
361
361
|
|
|
362
362
|
// Content
|
|
363
363
|
title?: string;
|
|
@@ -487,45 +487,14 @@ export interface ArchiveGroup {
|
|
|
487
487
|
}
|
|
488
488
|
|
|
489
489
|
// =============================================================================
|
|
490
|
-
//
|
|
490
|
+
// Page-Based Pagination Types
|
|
491
491
|
// =============================================================================
|
|
492
492
|
|
|
493
|
-
/** A date-based group of timeline items (shared utility type) */
|
|
494
|
-
export interface DateGroup {
|
|
495
|
-
dateKey: string;
|
|
496
|
-
label: string;
|
|
497
|
-
items: TimelineItemView[];
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
/** A single SSE DOM patch instruction returned by timelineMore */
|
|
501
|
-
export interface TimelinePatch {
|
|
502
|
-
selector: string;
|
|
503
|
-
content: string;
|
|
504
|
-
mode?:
|
|
505
|
-
| "append"
|
|
506
|
-
| "prepend"
|
|
507
|
-
| "inner"
|
|
508
|
-
| "outer"
|
|
509
|
-
| "before"
|
|
510
|
-
| "after"
|
|
511
|
-
| "remove";
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
/** Props passed to the theme's timelineMore renderer */
|
|
515
|
-
export interface TimelineMoreProps {
|
|
516
|
-
items: TimelineItemView[];
|
|
517
|
-
lastDate?: string;
|
|
518
|
-
hasMore: boolean;
|
|
519
|
-
nextCursor?: number;
|
|
520
|
-
theme?: ThemeComponents;
|
|
521
|
-
}
|
|
522
|
-
|
|
523
493
|
// =============================================================================
|
|
524
494
|
// Configuration Types
|
|
525
495
|
// =============================================================================
|
|
526
496
|
|
|
527
|
-
import type {
|
|
528
|
-
import type { ColorTheme } from "./theme/color-themes.js";
|
|
497
|
+
import type { ColorTheme } from "./ui/color-themes.js";
|
|
529
498
|
|
|
530
499
|
/**
|
|
531
500
|
* Search result from FTS5
|
|
@@ -544,8 +513,11 @@ export interface SearchResult {
|
|
|
544
513
|
|
|
545
514
|
export interface SiteLayoutProps {
|
|
546
515
|
siteName: string;
|
|
516
|
+
siteDescription?: string;
|
|
547
517
|
links: NavItemView[];
|
|
548
518
|
currentPath: string;
|
|
519
|
+
isAuthenticated?: boolean;
|
|
520
|
+
collections?: Collection[];
|
|
549
521
|
}
|
|
550
522
|
|
|
551
523
|
// =============================================================================
|
|
@@ -556,29 +528,23 @@ export interface SiteLayoutProps {
|
|
|
556
528
|
export interface HomePageProps {
|
|
557
529
|
items: TimelineItemView[];
|
|
558
530
|
pinnedItems: PostView[];
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
theme?: ThemeComponents;
|
|
531
|
+
currentPage: number;
|
|
532
|
+
totalPages: number;
|
|
562
533
|
}
|
|
563
534
|
|
|
564
535
|
/** Props for the single post page component */
|
|
565
536
|
export interface PostPageProps {
|
|
566
537
|
post: PostView;
|
|
567
|
-
theme?: ThemeComponents;
|
|
568
538
|
}
|
|
569
539
|
|
|
570
540
|
/** Props for the custom page component */
|
|
571
541
|
export interface SinglePageProps {
|
|
572
542
|
page: PageView;
|
|
573
|
-
theme?: ThemeComponents;
|
|
574
543
|
}
|
|
575
544
|
|
|
576
545
|
/** Props for the featured page component */
|
|
577
546
|
export interface FeaturedPageProps {
|
|
578
547
|
items: TimelineItemView[];
|
|
579
|
-
hasMore: boolean;
|
|
580
|
-
nextCursor?: number;
|
|
581
|
-
theme?: ThemeComponents;
|
|
582
548
|
}
|
|
583
549
|
|
|
584
550
|
/** Props for the archive page component */
|
|
@@ -588,7 +554,6 @@ export interface ArchivePageProps {
|
|
|
588
554
|
nextCursor?: number;
|
|
589
555
|
format?: Format;
|
|
590
556
|
featured?: boolean;
|
|
591
|
-
theme?: ThemeComponents;
|
|
592
557
|
}
|
|
593
558
|
|
|
594
559
|
/** Props for the search page component */
|
|
@@ -598,7 +563,6 @@ export interface SearchPageProps {
|
|
|
598
563
|
error?: string;
|
|
599
564
|
hasMore: boolean;
|
|
600
565
|
page: number;
|
|
601
|
-
theme?: ThemeComponents;
|
|
602
566
|
}
|
|
603
567
|
|
|
604
568
|
/** Props for the single collection page component */
|
|
@@ -607,13 +571,11 @@ export interface CollectionPageProps {
|
|
|
607
571
|
posts: PostView[];
|
|
608
572
|
hasMore: boolean;
|
|
609
573
|
nextCursor?: number;
|
|
610
|
-
theme?: ThemeComponents;
|
|
611
574
|
}
|
|
612
575
|
|
|
613
576
|
/** Props for the collections list page component */
|
|
614
577
|
export interface CollectionsPageProps {
|
|
615
578
|
collections: (Collection & { postCount: number })[];
|
|
616
|
-
theme?: ThemeComponents;
|
|
617
579
|
}
|
|
618
580
|
|
|
619
581
|
// =============================================================================
|
|
@@ -651,116 +613,13 @@ export interface ThreadPreviewProps {
|
|
|
651
613
|
rootPost: PostView;
|
|
652
614
|
previewReplies: PostView[];
|
|
653
615
|
totalReplyCount: number;
|
|
654
|
-
theme?: ThemeComponents;
|
|
655
616
|
}
|
|
656
617
|
|
|
657
618
|
/** Props for the timeline feed wrapper */
|
|
658
619
|
export interface TimelineFeedProps {
|
|
659
620
|
items: TimelineItemView[];
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
theme?: ThemeComponents;
|
|
663
|
-
}
|
|
664
|
-
|
|
665
|
-
/** Props for the timeline load-more button */
|
|
666
|
-
export interface TimelineLoadMoreProps {
|
|
667
|
-
nextCursor: number;
|
|
668
|
-
/** Last visible date key (YYYY-MM-DD) for merging groups across pages */
|
|
669
|
-
lastDate?: string;
|
|
670
|
-
theme?: ThemeComponents;
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
/**
|
|
674
|
-
* Theme component overrides
|
|
675
|
-
*/
|
|
676
|
-
export interface ThemeComponents {
|
|
677
|
-
// Layout
|
|
678
|
-
SiteLayout?: FC<PropsWithChildren<SiteLayoutProps>>;
|
|
679
|
-
|
|
680
|
-
// Pages
|
|
681
|
-
HomePage?: FC<HomePageProps>;
|
|
682
|
-
PostPage?: FC<PostPageProps>;
|
|
683
|
-
SinglePage?: FC<SinglePageProps>;
|
|
684
|
-
FeaturedPage?: FC<FeaturedPageProps>;
|
|
685
|
-
ArchivePage?: FC<ArchivePageProps>;
|
|
686
|
-
SearchPage?: FC<SearchPageProps>;
|
|
687
|
-
CollectionPage?: FC<CollectionPageProps>;
|
|
688
|
-
CollectionsPage?: FC<CollectionsPageProps>;
|
|
689
|
-
|
|
690
|
-
// Timeline sub-components (by format)
|
|
691
|
-
NoteCard?: FC<TimelineCardProps>;
|
|
692
|
-
LinkCard?: FC<TimelineCardProps>;
|
|
693
|
-
QuoteCard?: FC<TimelineCardProps>;
|
|
694
|
-
ThreadPreview?: FC<ThreadPreviewProps>;
|
|
695
|
-
TimelineFeed?: FC<TimelineFeedProps>;
|
|
696
|
-
TimelineLoadMore?: FC<TimelineLoadMoreProps>;
|
|
697
|
-
|
|
698
|
-
// Shared sub-components
|
|
699
|
-
Pagination?: FC<PaginationComponentProps>;
|
|
700
|
-
PagePagination?: FC<PagePaginationComponentProps>;
|
|
701
|
-
EmptyState?: FC<EmptyStateComponentProps>;
|
|
702
|
-
MediaGallery?: FC<MediaGalleryComponentProps>;
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
/**
|
|
706
|
-
* Real component prop types (re-exported from component files via index.ts).
|
|
707
|
-
* These are provided here as aliases to avoid circular imports in types.ts.
|
|
708
|
-
* The canonical definitions live in the component files.
|
|
709
|
-
*/
|
|
710
|
-
|
|
711
|
-
/** @see Pagination component in theme/components/Pagination.tsx */
|
|
712
|
-
export interface PaginationComponentProps {
|
|
713
|
-
baseUrl: string;
|
|
714
|
-
hasMore: boolean;
|
|
715
|
-
nextCursor?: number | string;
|
|
716
|
-
prevCursor?: number | string;
|
|
717
|
-
cursorParam?: string;
|
|
718
|
-
}
|
|
719
|
-
|
|
720
|
-
/** @see PagePagination component in theme/components/Pagination.tsx */
|
|
721
|
-
export interface PagePaginationComponentProps {
|
|
722
|
-
baseUrl: string;
|
|
723
|
-
currentPage: number;
|
|
724
|
-
hasMore: boolean;
|
|
725
|
-
pageParam?: string;
|
|
726
|
-
}
|
|
727
|
-
|
|
728
|
-
/** @see EmptyState component in theme/components/EmptyState.tsx */
|
|
729
|
-
export interface EmptyStateComponentProps {
|
|
730
|
-
message: string;
|
|
731
|
-
ctaText?: string;
|
|
732
|
-
ctaHref?: string;
|
|
733
|
-
centered?: boolean;
|
|
734
|
-
}
|
|
735
|
-
|
|
736
|
-
/** @see MediaGallery component in theme/components/MediaGallery.tsx */
|
|
737
|
-
export interface MediaGalleryComponentProps {
|
|
738
|
-
attachments: MediaView[];
|
|
739
|
-
}
|
|
740
|
-
|
|
741
|
-
/**
|
|
742
|
-
* Theme configuration
|
|
743
|
-
*/
|
|
744
|
-
export interface JantTheme {
|
|
745
|
-
/** Theme name */
|
|
746
|
-
name?: string;
|
|
747
|
-
/** Component overrides */
|
|
748
|
-
components?: ThemeComponents;
|
|
749
|
-
/** Feed renderer overrides (RSS, Atom, Sitemap) */
|
|
750
|
-
feed?: {
|
|
751
|
-
/** Custom RSS 2.0 renderer -- returns XML string */
|
|
752
|
-
rss?: (data: FeedData) => string;
|
|
753
|
-
/** Custom Atom renderer -- returns XML string */
|
|
754
|
-
atom?: (data: FeedData) => string;
|
|
755
|
-
/** Custom Sitemap renderer -- returns XML string */
|
|
756
|
-
sitemap?: (data: SitemapData) => string;
|
|
757
|
-
};
|
|
758
|
-
/** Renders SSE patches for timeline load-more responses */
|
|
759
|
-
timelineMore?: (props: TimelineMoreProps) => TimelinePatch[];
|
|
760
|
-
/** CSS variable overrides (highest priority, always applied) */
|
|
761
|
-
cssVariables?: Record<string, string>;
|
|
762
|
-
/** Replace built-in color themes with a custom list */
|
|
763
|
-
colorThemes?: ColorTheme[];
|
|
621
|
+
currentPage?: number;
|
|
622
|
+
totalPages?: number;
|
|
764
623
|
}
|
|
765
624
|
|
|
766
625
|
/**
|
|
@@ -768,12 +627,23 @@ export interface JantTheme {
|
|
|
768
627
|
*
|
|
769
628
|
* Configuration Philosophy:
|
|
770
629
|
* - Use environment variables for runtime config (API keys, feature flags, site settings)
|
|
771
|
-
* - Use code config (this object) for
|
|
630
|
+
* - Use code config (this object) for CSS customization and feed overrides
|
|
772
631
|
*
|
|
773
632
|
* Site-level settings (name, description, language) are configured via
|
|
774
633
|
* environment variables, not here. See lib/config.ts for details.
|
|
775
634
|
*/
|
|
776
635
|
export interface JantConfig {
|
|
777
|
-
/**
|
|
778
|
-
|
|
636
|
+
/** CSS variable overrides (highest priority after custom CSS) */
|
|
637
|
+
cssVariables?: Record<string, string>;
|
|
638
|
+
/** Replace built-in color themes with custom list */
|
|
639
|
+
colorThemes?: ColorTheme[];
|
|
640
|
+
/** Custom feed renderers */
|
|
641
|
+
feed?: {
|
|
642
|
+
/** Custom RSS 2.0 renderer -- returns XML string */
|
|
643
|
+
rss?: (data: FeedData) => string;
|
|
644
|
+
/** Custom Atom renderer -- returns XML string */
|
|
645
|
+
atom?: (data: FeedData) => string;
|
|
646
|
+
/** Custom Sitemap renderer -- returns XML string */
|
|
647
|
+
sitemap?: (data: SitemapData) => string;
|
|
648
|
+
};
|
|
779
649
|
}
|
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compose Dialog
|
|
3
|
+
*
|
|
4
|
+
* Full-screen compose dialog for quick post creation.
|
|
5
|
+
* Rendered server-side as part of SiteLayout for authenticated users.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { FC } from "hono/jsx";
|
|
9
|
+
import type { Collection } from "../../types.js";
|
|
10
|
+
import { useLingui } from "@lingui/react/macro";
|
|
11
|
+
|
|
12
|
+
export interface ComposeDialogProps {
|
|
13
|
+
collections?: Collection[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const ComposeDialog: FC<ComposeDialogProps> = ({ collections }) => {
|
|
17
|
+
const { t } = useLingui();
|
|
18
|
+
|
|
19
|
+
const signals = JSON.stringify({
|
|
20
|
+
format: "note",
|
|
21
|
+
title: "",
|
|
22
|
+
body: "",
|
|
23
|
+
url: "",
|
|
24
|
+
quoteText: "",
|
|
25
|
+
status: "published",
|
|
26
|
+
featured: false,
|
|
27
|
+
pinned: false,
|
|
28
|
+
rating: 0,
|
|
29
|
+
collectionId: 0,
|
|
30
|
+
mediaIds: [],
|
|
31
|
+
_composeLoading: false,
|
|
32
|
+
_showRating: false,
|
|
33
|
+
_showCollection: false,
|
|
34
|
+
}).replace(/</g, "\\u003c");
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<dialog
|
|
38
|
+
id="compose-dialog"
|
|
39
|
+
class="compose-dialog backdrop:bg-black/50"
|
|
40
|
+
onclick="event.target === this && this.close()"
|
|
41
|
+
>
|
|
42
|
+
<div class="compose-dialog-inner">
|
|
43
|
+
{/* Header */}
|
|
44
|
+
<header class="compose-dialog-header">
|
|
45
|
+
<button
|
|
46
|
+
type="button"
|
|
47
|
+
class="compose-dialog-close"
|
|
48
|
+
onclick="this.closest('dialog').close()"
|
|
49
|
+
>
|
|
50
|
+
<svg
|
|
51
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
52
|
+
width="20"
|
|
53
|
+
height="20"
|
|
54
|
+
viewBox="0 0 24 24"
|
|
55
|
+
fill="none"
|
|
56
|
+
stroke="currentColor"
|
|
57
|
+
stroke-width="2"
|
|
58
|
+
stroke-linecap="round"
|
|
59
|
+
stroke-linejoin="round"
|
|
60
|
+
>
|
|
61
|
+
<path d="M18 6 6 18" />
|
|
62
|
+
<path d="M6 6l12 12" />
|
|
63
|
+
</svg>
|
|
64
|
+
</button>
|
|
65
|
+
<h2 class="compose-dialog-title">
|
|
66
|
+
{t({
|
|
67
|
+
message: "New Post",
|
|
68
|
+
comment: "@context: Compose dialog title",
|
|
69
|
+
})}
|
|
70
|
+
</h2>
|
|
71
|
+
<div class="w-5" />
|
|
72
|
+
</header>
|
|
73
|
+
|
|
74
|
+
{/* Form */}
|
|
75
|
+
<section class="compose-dialog-body">
|
|
76
|
+
<form
|
|
77
|
+
data-signals={signals}
|
|
78
|
+
data-on:submit__prevent="@post('/compose')"
|
|
79
|
+
data-indicator="_composeLoading"
|
|
80
|
+
class="flex flex-col gap-3"
|
|
81
|
+
>
|
|
82
|
+
{/* Format tabs */}
|
|
83
|
+
<div class="compose-format-tabs">
|
|
84
|
+
<button
|
|
85
|
+
type="button"
|
|
86
|
+
class="compose-format-tab"
|
|
87
|
+
data-class-compose-format-tab-active="$format === 'note'"
|
|
88
|
+
data-on:click="$format = 'note'"
|
|
89
|
+
>
|
|
90
|
+
{t({
|
|
91
|
+
message: "Note",
|
|
92
|
+
comment: "@context: Compose format tab",
|
|
93
|
+
})}
|
|
94
|
+
</button>
|
|
95
|
+
<button
|
|
96
|
+
type="button"
|
|
97
|
+
class="compose-format-tab"
|
|
98
|
+
data-class-compose-format-tab-active="$format === 'link'"
|
|
99
|
+
data-on:click="$format = 'link'"
|
|
100
|
+
>
|
|
101
|
+
{t({
|
|
102
|
+
message: "Link",
|
|
103
|
+
comment: "@context: Compose format tab",
|
|
104
|
+
})}
|
|
105
|
+
</button>
|
|
106
|
+
<button
|
|
107
|
+
type="button"
|
|
108
|
+
class="compose-format-tab"
|
|
109
|
+
data-class-compose-format-tab-active="$format === 'quote'"
|
|
110
|
+
data-on:click="$format = 'quote'"
|
|
111
|
+
>
|
|
112
|
+
{t({
|
|
113
|
+
message: "Quote",
|
|
114
|
+
comment: "@context: Compose format tab",
|
|
115
|
+
})}
|
|
116
|
+
</button>
|
|
117
|
+
</div>
|
|
118
|
+
|
|
119
|
+
{/* Title input */}
|
|
120
|
+
<input
|
|
121
|
+
type="text"
|
|
122
|
+
data-bind="title"
|
|
123
|
+
class="compose-title-input"
|
|
124
|
+
placeholder={t({
|
|
125
|
+
message: "Title (optional)",
|
|
126
|
+
comment: "@context: Compose title placeholder",
|
|
127
|
+
})}
|
|
128
|
+
/>
|
|
129
|
+
|
|
130
|
+
{/* Body textarea */}
|
|
131
|
+
<textarea
|
|
132
|
+
data-bind="body"
|
|
133
|
+
class="compose-body-input"
|
|
134
|
+
placeholder={t({
|
|
135
|
+
message: "What's on your mind?",
|
|
136
|
+
comment: "@context: Compose body placeholder",
|
|
137
|
+
})}
|
|
138
|
+
rows={4}
|
|
139
|
+
/>
|
|
140
|
+
|
|
141
|
+
{/* URL input (link/quote) */}
|
|
142
|
+
<div data-show="$format === 'link' || $format === 'quote'">
|
|
143
|
+
<input
|
|
144
|
+
type="url"
|
|
145
|
+
data-bind="url"
|
|
146
|
+
class="input text-sm"
|
|
147
|
+
placeholder="https://..."
|
|
148
|
+
/>
|
|
149
|
+
</div>
|
|
150
|
+
|
|
151
|
+
{/* Quote text (quote format) */}
|
|
152
|
+
<div data-show="$format === 'quote'">
|
|
153
|
+
<textarea
|
|
154
|
+
data-bind="quoteText"
|
|
155
|
+
class="textarea text-sm"
|
|
156
|
+
placeholder={t({
|
|
157
|
+
message: "The text being quoted...",
|
|
158
|
+
comment: "@context: Compose quote text placeholder",
|
|
159
|
+
})}
|
|
160
|
+
rows={2}
|
|
161
|
+
/>
|
|
162
|
+
</div>
|
|
163
|
+
|
|
164
|
+
{/* Rating picker (toggleable) */}
|
|
165
|
+
<div data-show="$_showRating" class="field">
|
|
166
|
+
<label class="label text-sm">
|
|
167
|
+
{t({
|
|
168
|
+
message: "Rating",
|
|
169
|
+
comment: "@context: Compose rating field",
|
|
170
|
+
})}
|
|
171
|
+
</label>
|
|
172
|
+
<select data-bind="rating" class="select text-sm">
|
|
173
|
+
<option value="0">
|
|
174
|
+
{t({
|
|
175
|
+
message: "None",
|
|
176
|
+
comment: "@context: No rating selected",
|
|
177
|
+
})}
|
|
178
|
+
</option>
|
|
179
|
+
<option value="1">1</option>
|
|
180
|
+
<option value="2">2</option>
|
|
181
|
+
<option value="3">3</option>
|
|
182
|
+
<option value="4">4</option>
|
|
183
|
+
<option value="5">5</option>
|
|
184
|
+
</select>
|
|
185
|
+
</div>
|
|
186
|
+
|
|
187
|
+
{/* Collection picker (toggleable) */}
|
|
188
|
+
{collections && collections.length > 0 && (
|
|
189
|
+
<div data-show="$_showCollection" class="field">
|
|
190
|
+
<label class="label text-sm">
|
|
191
|
+
{t({
|
|
192
|
+
message: "Collection",
|
|
193
|
+
comment: "@context: Compose collection field",
|
|
194
|
+
})}
|
|
195
|
+
</label>
|
|
196
|
+
<select data-bind="collectionId" class="select text-sm">
|
|
197
|
+
<option value="0">
|
|
198
|
+
{t({
|
|
199
|
+
message: "None",
|
|
200
|
+
comment: "@context: No collection selected",
|
|
201
|
+
})}
|
|
202
|
+
</option>
|
|
203
|
+
{collections.map((col) => (
|
|
204
|
+
<option key={col.id} value={col.id}>
|
|
205
|
+
{col.title}
|
|
206
|
+
</option>
|
|
207
|
+
))}
|
|
208
|
+
</select>
|
|
209
|
+
</div>
|
|
210
|
+
)}
|
|
211
|
+
|
|
212
|
+
{/* Toolbar */}
|
|
213
|
+
<div class="compose-toolbar">
|
|
214
|
+
<div class="flex gap-1">
|
|
215
|
+
{/* Media button */}
|
|
216
|
+
<button
|
|
217
|
+
type="button"
|
|
218
|
+
class="compose-toolbar-btn"
|
|
219
|
+
title={t({
|
|
220
|
+
message: "Add Media",
|
|
221
|
+
comment: "@context: Compose toolbar - add media",
|
|
222
|
+
})}
|
|
223
|
+
data-on:click="document.getElementById('compose-media-picker').showModal(); fetch('/dash/media/picker').then(r => r.text()).then(html => document.getElementById('compose-media-grid').innerHTML = html)"
|
|
224
|
+
>
|
|
225
|
+
<svg
|
|
226
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
227
|
+
width="18"
|
|
228
|
+
height="18"
|
|
229
|
+
viewBox="0 0 24 24"
|
|
230
|
+
fill="none"
|
|
231
|
+
stroke="currentColor"
|
|
232
|
+
stroke-width="2"
|
|
233
|
+
stroke-linecap="round"
|
|
234
|
+
stroke-linejoin="round"
|
|
235
|
+
>
|
|
236
|
+
<rect width="18" height="18" x="3" y="3" rx="2" ry="2" />
|
|
237
|
+
<circle cx="9" cy="9" r="2" />
|
|
238
|
+
<path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21" />
|
|
239
|
+
</svg>
|
|
240
|
+
</button>
|
|
241
|
+
|
|
242
|
+
{/* Rating toggle */}
|
|
243
|
+
<button
|
|
244
|
+
type="button"
|
|
245
|
+
class="compose-toolbar-btn"
|
|
246
|
+
title={t({
|
|
247
|
+
message: "Rating",
|
|
248
|
+
comment: "@context: Compose toolbar - toggle rating",
|
|
249
|
+
})}
|
|
250
|
+
data-on:click="$_showRating = !$_showRating"
|
|
251
|
+
>
|
|
252
|
+
<svg
|
|
253
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
254
|
+
width="18"
|
|
255
|
+
height="18"
|
|
256
|
+
viewBox="0 0 24 24"
|
|
257
|
+
fill="none"
|
|
258
|
+
stroke="currentColor"
|
|
259
|
+
stroke-width="2"
|
|
260
|
+
stroke-linecap="round"
|
|
261
|
+
stroke-linejoin="round"
|
|
262
|
+
>
|
|
263
|
+
<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2" />
|
|
264
|
+
</svg>
|
|
265
|
+
</button>
|
|
266
|
+
|
|
267
|
+
{/* Collection toggle */}
|
|
268
|
+
{collections && collections.length > 0 && (
|
|
269
|
+
<button
|
|
270
|
+
type="button"
|
|
271
|
+
class="compose-toolbar-btn"
|
|
272
|
+
title={t({
|
|
273
|
+
message: "Collection",
|
|
274
|
+
comment: "@context: Compose toolbar - toggle collection",
|
|
275
|
+
})}
|
|
276
|
+
data-on:click="$_showCollection = !$_showCollection"
|
|
277
|
+
>
|
|
278
|
+
<svg
|
|
279
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
280
|
+
width="18"
|
|
281
|
+
height="18"
|
|
282
|
+
viewBox="0 0 24 24"
|
|
283
|
+
fill="none"
|
|
284
|
+
stroke="currentColor"
|
|
285
|
+
stroke-width="2"
|
|
286
|
+
stroke-linecap="round"
|
|
287
|
+
stroke-linejoin="round"
|
|
288
|
+
>
|
|
289
|
+
<path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z" />
|
|
290
|
+
</svg>
|
|
291
|
+
</button>
|
|
292
|
+
)}
|
|
293
|
+
</div>
|
|
294
|
+
</div>
|
|
295
|
+
|
|
296
|
+
{/* Footer: checkboxes + submit */}
|
|
297
|
+
<div class="compose-dialog-footer">
|
|
298
|
+
<div class="flex gap-3">
|
|
299
|
+
<label class="flex items-center gap-1.5 text-xs text-muted-foreground">
|
|
300
|
+
<input
|
|
301
|
+
type="checkbox"
|
|
302
|
+
class="checkbox"
|
|
303
|
+
data-bind="featured"
|
|
304
|
+
/>
|
|
305
|
+
{t({
|
|
306
|
+
message: "Featured",
|
|
307
|
+
comment: "@context: Compose checkbox - mark as featured",
|
|
308
|
+
})}
|
|
309
|
+
</label>
|
|
310
|
+
<label class="flex items-center gap-1.5 text-xs text-muted-foreground">
|
|
311
|
+
<input type="checkbox" class="checkbox" data-bind="pinned" />
|
|
312
|
+
{t({
|
|
313
|
+
message: "Pinned",
|
|
314
|
+
comment: "@context: Compose checkbox - pin to top",
|
|
315
|
+
})}
|
|
316
|
+
</label>
|
|
317
|
+
</div>
|
|
318
|
+
<div class="flex gap-2">
|
|
319
|
+
<button
|
|
320
|
+
type="button"
|
|
321
|
+
class="btn-outline text-sm"
|
|
322
|
+
data-attr-disabled="$_composeLoading"
|
|
323
|
+
data-on:click="$status = 'draft'; document.querySelector('#compose-dialog form').requestSubmit()"
|
|
324
|
+
>
|
|
325
|
+
<span data-show="!$_composeLoading">
|
|
326
|
+
{t({
|
|
327
|
+
message: "Draft",
|
|
328
|
+
comment: "@context: Compose button - save as draft",
|
|
329
|
+
})}
|
|
330
|
+
</span>
|
|
331
|
+
<span data-show="$_composeLoading">...</span>
|
|
332
|
+
</button>
|
|
333
|
+
<button
|
|
334
|
+
type="submit"
|
|
335
|
+
class="btn text-sm"
|
|
336
|
+
data-attr-disabled="$_composeLoading"
|
|
337
|
+
>
|
|
338
|
+
<span data-show="!$_composeLoading">
|
|
339
|
+
{t({
|
|
340
|
+
message: "Post",
|
|
341
|
+
comment: "@context: Compose button - publish post",
|
|
342
|
+
})}
|
|
343
|
+
</span>
|
|
344
|
+
<span data-show="$_composeLoading">
|
|
345
|
+
{t({
|
|
346
|
+
message: "Posting...",
|
|
347
|
+
comment: "@context: Compose loading text while posting",
|
|
348
|
+
})}
|
|
349
|
+
</span>
|
|
350
|
+
</button>
|
|
351
|
+
</div>
|
|
352
|
+
</div>
|
|
353
|
+
</form>
|
|
354
|
+
</section>
|
|
355
|
+
</div>
|
|
356
|
+
|
|
357
|
+
{/* Nested media picker dialog */}
|
|
358
|
+
<dialog
|
|
359
|
+
id="compose-media-picker"
|
|
360
|
+
class="p-6 rounded-lg max-w-2xl w-full backdrop:bg-black/50"
|
|
361
|
+
onclick="event.target === this && this.close()"
|
|
362
|
+
>
|
|
363
|
+
<div class="flex items-center justify-between mb-4">
|
|
364
|
+
<h2 class="text-lg font-semibold">
|
|
365
|
+
{t({
|
|
366
|
+
message: "Select Media",
|
|
367
|
+
comment: "@context: Media picker dialog title",
|
|
368
|
+
})}
|
|
369
|
+
</h2>
|
|
370
|
+
<button
|
|
371
|
+
type="button"
|
|
372
|
+
class="btn-outline text-sm"
|
|
373
|
+
onclick="this.closest('dialog').close()"
|
|
374
|
+
>
|
|
375
|
+
{t({
|
|
376
|
+
message: "Done",
|
|
377
|
+
comment: "@context: Close media picker button",
|
|
378
|
+
})}
|
|
379
|
+
</button>
|
|
380
|
+
</div>
|
|
381
|
+
<div
|
|
382
|
+
id="compose-media-grid"
|
|
383
|
+
class="grid grid-cols-4 gap-2 max-h-96 overflow-y-auto"
|
|
384
|
+
>
|
|
385
|
+
<p class="text-muted-foreground text-sm col-span-4">
|
|
386
|
+
{t({
|
|
387
|
+
message: "Loading...",
|
|
388
|
+
comment: "@context: Loading state for media picker",
|
|
389
|
+
})}
|
|
390
|
+
</p>
|
|
391
|
+
</div>
|
|
392
|
+
</dialog>
|
|
393
|
+
</dialog>
|
|
394
|
+
);
|
|
395
|
+
};
|