@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.
Files changed (206) hide show
  1. package/dist/app.js +50 -25
  2. package/dist/db/schema.js +1 -1
  3. package/dist/i18n/locales/en.js +1 -1
  4. package/dist/i18n/locales/zh-Hans.js +1 -1
  5. package/dist/i18n/locales/zh-Hant.js +1 -1
  6. package/dist/index.js +3 -9
  7. package/dist/lib/constants.js +1 -0
  8. package/dist/lib/nav-reorder.js +1 -1
  9. package/dist/lib/navigation.js +26 -1
  10. package/dist/lib/pagination.js +44 -0
  11. package/dist/lib/render.js +7 -11
  12. package/dist/lib/schemas.js +3 -3
  13. package/dist/lib/theme.js +4 -4
  14. package/dist/lib/timeline.js +24 -48
  15. package/dist/lib/view.js +2 -2
  16. package/dist/routes/api/collections.js +124 -0
  17. package/dist/routes/api/nav-items.js +104 -0
  18. package/dist/routes/api/pages.js +91 -0
  19. package/dist/routes/api/posts.js +2 -2
  20. package/dist/routes/api/search.js +2 -2
  21. package/dist/routes/api/settings.js +68 -0
  22. package/dist/routes/compose.js +48 -0
  23. package/dist/routes/dash/collections.js +2 -2
  24. package/dist/routes/dash/index.js +1 -1
  25. package/dist/routes/dash/media.js +2 -2
  26. package/dist/routes/dash/pages.js +411 -62
  27. package/dist/routes/dash/posts.js +3 -5
  28. package/dist/routes/dash/redirects.js +2 -2
  29. package/dist/routes/dash/settings.js +79 -5
  30. package/dist/routes/feed/rss.js +2 -2
  31. package/dist/routes/feed/sitemap.js +1 -1
  32. package/dist/routes/pages/archive.js +3 -6
  33. package/dist/routes/pages/collection.js +3 -6
  34. package/dist/routes/pages/collections.js +28 -0
  35. package/dist/routes/pages/featured.js +32 -0
  36. package/dist/routes/pages/home.js +9 -50
  37. package/dist/routes/pages/page.js +29 -32
  38. package/dist/routes/pages/post.js +3 -6
  39. package/dist/routes/pages/search.js +3 -6
  40. package/dist/services/page.js +5 -1
  41. package/dist/services/post.js +40 -6
  42. package/dist/services/search.js +1 -1
  43. package/dist/ui/compose/ComposeDialog.js +452 -0
  44. package/dist/ui/compose/ComposePrompt.js +55 -0
  45. package/dist/{theme/components/TypeBadge.js → ui/dash/FormatBadge.js} +1 -2
  46. package/dist/{theme/components → ui/dash}/PostForm.js +0 -27
  47. package/dist/{theme/components → ui/dash}/PostList.js +6 -6
  48. package/dist/{theme/components/VisibilityBadge.js → ui/dash/StatusBadge.js} +1 -2
  49. package/dist/{theme/components → ui/dash}/index.js +3 -6
  50. package/dist/{themes/threads/timeline → ui/feed}/LinkCard.js +6 -2
  51. package/dist/{themes/threads/timeline → ui/feed}/NoteCard.js +11 -6
  52. package/dist/{themes/threads/timeline → ui/feed}/QuoteCard.js +10 -6
  53. package/dist/{themes/threads/timeline → ui/feed}/ThreadPreview.js +7 -9
  54. package/dist/ui/feed/TimelineFeed.js +41 -0
  55. package/dist/ui/feed/TimelineItem.js +27 -0
  56. package/dist/{theme → ui}/layouts/BaseLayout.js +10 -0
  57. package/dist/{theme → ui}/layouts/DashLayout.js +0 -8
  58. package/dist/ui/layouts/SiteLayout.js +141 -0
  59. package/dist/{themes/threads → ui}/pages/ArchivePage.js +16 -14
  60. package/dist/{themes/threads → ui}/pages/CollectionPage.js +6 -1
  61. package/dist/ui/pages/CollectionsPage.js +76 -0
  62. package/dist/ui/pages/FeaturedPage.js +24 -0
  63. package/dist/ui/pages/HomePage.js +24 -0
  64. package/dist/{themes/threads → ui}/pages/PostPage.js +13 -8
  65. package/dist/{themes/threads → ui}/pages/SearchPage.js +9 -7
  66. package/dist/{themes/threads → ui}/pages/SinglePage.js +3 -2
  67. package/dist/{theme/components → ui/shared}/MediaGallery.js +1 -1
  68. package/dist/{theme/components → ui/shared}/Pagination.js +41 -2
  69. package/dist/{theme/components → ui/shared}/ThreadView.js +2 -2
  70. package/dist/ui/shared/index.js +5 -0
  71. package/package.json +1 -9
  72. package/src/__tests__/helpers/db.ts +3 -0
  73. package/src/app.tsx +57 -27
  74. package/src/db/migrations/0006_rename_slug_to_path.sql +5 -0
  75. package/src/db/migrations/meta/_journal.json +7 -0
  76. package/src/db/schema.ts +1 -1
  77. package/src/i18n/locales/en.po +332 -181
  78. package/src/i18n/locales/en.ts +1 -1
  79. package/src/i18n/locales/zh-Hans.po +332 -181
  80. package/src/i18n/locales/zh-Hans.ts +1 -1
  81. package/src/i18n/locales/zh-Hant.po +332 -181
  82. package/src/i18n/locales/zh-Hant.ts +1 -1
  83. package/src/index.ts +7 -36
  84. package/src/lib/__tests__/schemas.test.ts +60 -19
  85. package/src/lib/__tests__/timeline.test.ts +45 -81
  86. package/src/lib/__tests__/view.test.ts +13 -7
  87. package/src/lib/constants.ts +1 -0
  88. package/src/lib/nav-reorder.ts +1 -1
  89. package/src/lib/navigation.ts +40 -2
  90. package/src/lib/pagination.ts +50 -0
  91. package/src/lib/render.tsx +7 -14
  92. package/src/lib/schemas.ts +8 -6
  93. package/src/lib/theme.ts +5 -5
  94. package/src/lib/timeline.ts +28 -57
  95. package/src/lib/view.ts +2 -2
  96. package/src/preset.css +2 -1
  97. package/src/routes/__tests__/compose.test.ts +199 -0
  98. package/src/routes/api/__tests__/collections.test.ts +249 -0
  99. package/src/routes/api/__tests__/nav-items.test.ts +222 -0
  100. package/src/routes/api/__tests__/pages.test.ts +218 -0
  101. package/src/routes/api/__tests__/settings.test.ts +132 -0
  102. package/src/routes/api/collections.ts +143 -0
  103. package/src/routes/api/nav-items.ts +115 -0
  104. package/src/routes/api/pages.ts +101 -0
  105. package/src/routes/api/posts.ts +2 -2
  106. package/src/routes/api/search.ts +2 -2
  107. package/src/routes/api/settings.ts +91 -0
  108. package/src/routes/compose.ts +63 -0
  109. package/src/routes/dash/__tests__/pages.test.ts +225 -0
  110. package/src/routes/dash/collections.tsx +2 -2
  111. package/src/routes/dash/index.tsx +1 -1
  112. package/src/routes/dash/media.tsx +2 -2
  113. package/src/routes/dash/pages.tsx +443 -70
  114. package/src/routes/dash/posts.tsx +3 -7
  115. package/src/routes/dash/redirects.tsx +2 -2
  116. package/src/routes/dash/settings.tsx +83 -5
  117. package/src/routes/feed/rss.ts +2 -2
  118. package/src/routes/feed/sitemap.ts +1 -1
  119. package/src/routes/pages/__tests__/collections.test.ts +94 -0
  120. package/src/routes/pages/__tests__/featured.test.ts +94 -0
  121. package/src/routes/pages/archive.tsx +2 -6
  122. package/src/routes/pages/collection.tsx +2 -6
  123. package/src/routes/pages/collections.tsx +36 -0
  124. package/src/routes/pages/featured.tsx +38 -0
  125. package/src/routes/pages/home.tsx +9 -55
  126. package/src/routes/pages/page.tsx +28 -30
  127. package/src/routes/pages/post.tsx +2 -5
  128. package/src/routes/pages/search.tsx +2 -6
  129. package/src/services/__tests__/page.test.ts +106 -0
  130. package/src/services/__tests__/post.test.ts +114 -15
  131. package/src/services/page.ts +13 -1
  132. package/src/services/post.ts +57 -7
  133. package/src/services/search.ts +2 -2
  134. package/src/styles/tokens.css +47 -0
  135. package/src/styles/ui.css +491 -0
  136. package/src/types.ts +29 -159
  137. package/src/ui/compose/ComposeDialog.tsx +395 -0
  138. package/src/ui/compose/ComposePrompt.tsx +55 -0
  139. package/src/{theme/components/TypeBadge.tsx → ui/dash/FormatBadge.tsx} +2 -3
  140. package/src/{theme/components → ui/dash}/PostForm.tsx +0 -25
  141. package/src/{theme/components → ui/dash}/PostList.tsx +7 -7
  142. package/src/{theme/components/VisibilityBadge.tsx → ui/dash/StatusBadge.tsx} +2 -3
  143. package/src/ui/dash/index.ts +10 -0
  144. package/src/{themes/threads/timeline → ui/feed}/LinkCard.tsx +9 -4
  145. package/src/{themes/threads/timeline → ui/feed}/NoteCard.tsx +13 -8
  146. package/src/{themes/threads/timeline → ui/feed}/QuoteCard.tsx +13 -8
  147. package/src/{themes/threads/timeline → ui/feed}/ThreadPreview.tsx +7 -8
  148. package/src/ui/feed/TimelineFeed.tsx +49 -0
  149. package/src/ui/feed/TimelineItem.tsx +45 -0
  150. package/src/{theme → ui}/layouts/BaseLayout.tsx +11 -1
  151. package/src/{theme → ui}/layouts/DashLayout.tsx +0 -10
  152. package/src/ui/layouts/SiteLayout.tsx +150 -0
  153. package/src/{themes/threads → ui}/pages/ArchivePage.tsx +22 -17
  154. package/src/{themes/threads → ui}/pages/CollectionPage.tsx +14 -5
  155. package/src/ui/pages/CollectionsPage.tsx +73 -0
  156. package/src/ui/pages/FeaturedPage.tsx +31 -0
  157. package/src/{themes/threads → ui}/pages/HomePage.tsx +11 -15
  158. package/src/{themes/threads → ui}/pages/PostPage.tsx +23 -14
  159. package/src/{themes/threads → ui}/pages/SearchPage.tsx +13 -11
  160. package/src/{themes/threads → ui}/pages/SinglePage.tsx +4 -4
  161. package/src/{theme/components → ui/shared}/MediaGallery.tsx +1 -1
  162. package/src/{theme/components → ui/shared}/Pagination.tsx +67 -4
  163. package/src/{theme/components → ui/shared}/ThreadView.tsx +2 -2
  164. package/src/ui/shared/__tests__/pagination.test.ts +46 -0
  165. package/src/ui/shared/index.ts +12 -0
  166. package/bin/jant.js +0 -185
  167. package/dist/lib/theme-components.js +0 -46
  168. package/dist/routes/dash/navigation.js +0 -289
  169. package/dist/theme/index.js +0 -18
  170. package/dist/theme/layouts/index.js +0 -2
  171. package/dist/themes/threads/ThreadsSiteLayout.js +0 -172
  172. package/dist/themes/threads/index.js +0 -81
  173. package/dist/themes/threads/pages/HomePage.js +0 -25
  174. package/dist/themes/threads/timeline/TimelineFeed.js +0 -58
  175. package/dist/themes/threads/timeline/TimelineItem.js +0 -36
  176. package/dist/themes/threads/timeline/TimelineLoadMore.js +0 -23
  177. package/dist/themes/threads/timeline/groupByDate.js +0 -22
  178. package/dist/themes/threads/timeline/timelineMore.js +0 -107
  179. package/src/lib/__tests__/theme-components.test.ts +0 -105
  180. package/src/lib/theme-components.ts +0 -65
  181. package/src/routes/dash/navigation.tsx +0 -317
  182. package/src/theme/components/index.ts +0 -23
  183. package/src/theme/index.ts +0 -22
  184. package/src/theme/layouts/index.ts +0 -7
  185. package/src/themes/threads/ThreadsSiteLayout.tsx +0 -194
  186. package/src/themes/threads/index.ts +0 -100
  187. package/src/themes/threads/style.css +0 -336
  188. package/src/themes/threads/timeline/TimelineFeed.tsx +0 -62
  189. package/src/themes/threads/timeline/TimelineItem.tsx +0 -67
  190. package/src/themes/threads/timeline/TimelineLoadMore.tsx +0 -35
  191. package/src/themes/threads/timeline/groupByDate.ts +0 -30
  192. package/src/themes/threads/timeline/timelineMore.tsx +0 -130
  193. /package/dist/{theme → ui}/color-themes.js +0 -0
  194. /package/dist/{theme/components → ui/dash}/ActionButtons.js +0 -0
  195. /package/dist/{theme/components → ui/dash}/CrudPageHeader.js +0 -0
  196. /package/dist/{theme/components → ui/dash}/DangerZone.js +0 -0
  197. /package/dist/{theme/components → ui/dash}/ListItemRow.js +0 -0
  198. /package/dist/{theme/components → ui/dash}/PageForm.js +0 -0
  199. /package/dist/{theme/components → ui/shared}/EmptyState.js +0 -0
  200. /package/src/{theme → ui}/color-themes.ts +0 -0
  201. /package/src/{theme/components → ui/dash}/ActionButtons.tsx +0 -0
  202. /package/src/{theme/components → ui/dash}/CrudPageHeader.tsx +0 -0
  203. /package/src/{theme/components → ui/dash}/DangerZone.tsx +0 -0
  204. /package/src/{theme/components → ui/dash}/ListItemRow.tsx +0 -0
  205. /package/src/{theme/components → ui/dash}/PageForm.tsx +0 -0
  206. /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
- slug: string | null;
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
- slug?: string;
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
- slug?: string | null;
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: "/{slug}" if slug set, otherwise "/p/{sqid}" */
357
+ /** Pre-computed permalink: "/{path}" if path set, otherwise "/p/{sqid}" */
358
358
  permalink: string;
359
- /** Custom URL slug, if set */
360
- slug?: string;
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
- // Timeline Load-More Types
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 { FC, PropsWithChildren } from "hono/jsx";
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
- hasMore: boolean;
560
- nextCursor?: number;
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
- hasMore: boolean;
661
- nextCursor?: number;
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 compile-time customization (theme components)
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
- /** Theme configuration (components, CSS overrides) */
778
- theme?: JantTheme;
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
+ };