@jant/core 0.3.35 → 0.3.36

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (156) hide show
  1. package/dist/client/assets/module-RjUF93sV.js +716 -0
  2. package/dist/client/assets/native-48B9X9Wg.js +1 -0
  3. package/dist/client/assets/url-8Dj-5CLW.js +1 -0
  4. package/dist/client/client.css +1 -1
  5. package/dist/client/client.js +3109 -2294
  6. package/dist/index.js +3026 -2778
  7. package/package.json +13 -4
  8. package/src/__tests__/helpers/app.ts +1 -1
  9. package/src/__tests__/helpers/db.ts +6 -0
  10. package/src/app.tsx +1 -5
  11. package/src/{lib → client}/avatar-upload.ts +1 -1
  12. package/src/{lib → client}/collection-form-bridge.ts +2 -2
  13. package/src/{ui → client}/components/__tests__/jant-collection-form.test.ts +26 -9
  14. package/src/{ui → client}/components/__tests__/jant-compose-dialog.test.ts +46 -14
  15. package/src/{ui → client}/components/__tests__/jant-compose-editor.test.ts +64 -24
  16. package/src/{ui → client}/components/__tests__/jant-post-form.test.ts +24 -14
  17. package/src/{ui → client}/components/__tests__/jant-settings-general.test.ts +3 -3
  18. package/src/client/components/collection-sidebar-types.ts +45 -0
  19. package/src/{ui → client}/components/collection-types.ts +3 -4
  20. package/src/{ui → client}/components/compose-types.ts +3 -1
  21. package/src/{ui → client}/components/jant-collection-form.ts +301 -182
  22. package/src/client/components/jant-collection-sidebar.ts +801 -0
  23. package/src/{ui → client}/components/jant-compose-dialog.ts +231 -1
  24. package/src/client/components/jant-compose-editor.ts +1249 -0
  25. package/src/client/components/jant-compose-fullscreen.ts +338 -0
  26. package/src/client/components/jant-media-lightbox.ts +257 -0
  27. package/src/{ui → client}/components/jant-nav-manager.ts +143 -84
  28. package/src/{ui → client}/components/jant-post-form.ts +57 -8
  29. package/src/{ui → client}/components/jant-settings-general.ts +2 -2
  30. package/src/{ui → client}/components/nav-manager-types.ts +3 -0
  31. package/src/{ui → client}/components/post-form-template.ts +35 -31
  32. package/src/{ui → client}/components/post-form-types.ts +7 -3
  33. package/src/{lib → client}/compose-bridge.ts +9 -7
  34. package/src/client/lazy-slugify.ts +51 -0
  35. package/src/{lib → client}/media-upload.ts +16 -3
  36. package/src/{lib → client}/nav-manager-bridge.ts +1 -1
  37. package/src/client/page-slug-bridge.ts +42 -0
  38. package/src/{lib → client}/post-form-bridge.ts +2 -2
  39. package/src/{lib → client}/settings-bridge.ts +3 -3
  40. package/src/client/tiptap/bubble-menu.ts +205 -0
  41. package/src/client/tiptap/create-editor.ts +40 -0
  42. package/src/client/tiptap/exitable-marks.ts +73 -0
  43. package/src/client/tiptap/extensions.ts +60 -0
  44. package/src/client/tiptap/image-node.ts +488 -0
  45. package/src/client/tiptap/link-toolbar.ts +371 -0
  46. package/src/client/tiptap/more-break.ts +50 -0
  47. package/src/client/tiptap/paste-image.ts +140 -0
  48. package/src/client/tiptap/slash-commands.ts +328 -0
  49. package/src/{types → client/types}/sortablejs.d.ts +1 -1
  50. package/src/client.ts +24 -17
  51. package/src/db/migrations/0012_add_tiptap_columns.sql +2 -0
  52. package/src/db/migrations/0013_replace_featured_with_visibility.sql +8 -0
  53. package/src/db/schema.ts +6 -1
  54. package/src/i18n/locales/en.po +641 -215
  55. package/src/i18n/locales/en.ts +1 -1
  56. package/src/i18n/locales/zh-Hans.po +642 -204
  57. package/src/i18n/locales/zh-Hans.ts +1 -1
  58. package/src/i18n/locales/zh-Hant.po +642 -204
  59. package/src/i18n/locales/zh-Hant.ts +1 -1
  60. package/src/lib/__tests__/resolve-config.test.ts +2 -2
  61. package/src/lib/__tests__/schemas.test.ts +9 -6
  62. package/src/lib/__tests__/url.test.ts +2 -2
  63. package/src/lib/__tests__/view.test.ts +9 -9
  64. package/src/lib/emoji-catalog.ts +146 -0
  65. package/src/lib/feed.ts +1 -1
  66. package/src/lib/media-helpers.ts +10 -9
  67. package/src/lib/render.tsx +4 -3
  68. package/src/lib/resolve-config.ts +8 -1
  69. package/src/lib/schemas.ts +2 -3
  70. package/src/lib/summary.ts +92 -0
  71. package/src/lib/timeline.ts +2 -0
  72. package/src/lib/tiptap-render.ts +196 -0
  73. package/src/lib/upload.ts +97 -9
  74. package/src/lib/url.ts +7 -23
  75. package/src/lib/view.ts +33 -19
  76. package/src/middleware/error-handler.ts +3 -3
  77. package/src/preset.css +38 -0
  78. package/src/routes/api/collections.ts +20 -3
  79. package/src/routes/api/posts.ts +48 -33
  80. package/src/routes/api/upload.ts +7 -5
  81. package/src/routes/auth/reset.tsx +5 -4
  82. package/src/routes/auth/setup.tsx +26 -11
  83. package/src/routes/auth/signin.tsx +10 -7
  84. package/src/routes/compose.tsx +20 -11
  85. package/src/routes/dash/__tests__/settings-avatar.test.ts +43 -8
  86. package/src/routes/dash/index.tsx +7 -1
  87. package/src/routes/dash/media.tsx +3 -0
  88. package/src/routes/dash/pages.tsx +8 -2
  89. package/src/routes/dash/posts.tsx +6 -2
  90. package/src/routes/dash/redirects.tsx +15 -9
  91. package/src/routes/dash/settings.tsx +336 -32
  92. package/src/routes/feed/__tests__/rss.test.ts +7 -7
  93. package/src/routes/feed/rss.ts +8 -6
  94. package/src/routes/pages/__tests__/featured.test.ts +6 -7
  95. package/src/routes/pages/archive.tsx +11 -7
  96. package/src/routes/pages/collection.tsx +32 -15
  97. package/src/routes/pages/collections.tsx +11 -2
  98. package/src/routes/pages/featured.tsx +1 -1
  99. package/src/routes/pages/home.tsx +1 -1
  100. package/src/services/__tests__/post.test.ts +124 -33
  101. package/src/services/__tests__/settings.test.ts +3 -3
  102. package/src/services/page.ts +16 -3
  103. package/src/services/post.ts +96 -37
  104. package/src/services/search.ts +4 -2
  105. package/src/services/settings.ts +6 -2
  106. package/src/styles/components.css +240 -60
  107. package/src/styles/tokens.css +10 -0
  108. package/src/styles/ui.css +1157 -81
  109. package/src/types/bindings.ts +5 -0
  110. package/src/types/config.ts +23 -1
  111. package/src/types/constants.ts +3 -0
  112. package/src/types/entities.ts +9 -2
  113. package/src/types/operations.ts +9 -3
  114. package/src/types/props.ts +3 -3
  115. package/src/types/views.ts +3 -2
  116. package/src/ui/compose/ComposeDialog.tsx +24 -7
  117. package/src/ui/dash/PageForm.tsx +2 -0
  118. package/src/ui/dash/PostList.tsx +5 -5
  119. package/src/ui/dash/StatusBadge.tsx +13 -5
  120. package/src/ui/dash/appearance/AdvancedContent.tsx +52 -61
  121. package/src/ui/dash/appearance/ColorThemeContent.tsx +30 -35
  122. package/src/ui/dash/appearance/FontThemeContent.tsx +65 -73
  123. package/src/ui/dash/appearance/NavigationContent.tsx +107 -96
  124. package/src/ui/dash/media/MediaListContent.tsx +9 -4
  125. package/src/ui/dash/media/ViewMediaContent.tsx +2 -2
  126. package/src/ui/dash/pages/PagesContent.tsx +2 -1
  127. package/src/ui/dash/posts/PostForm.tsx +19 -7
  128. package/src/ui/dash/settings/AccountContent.tsx +133 -138
  129. package/src/ui/dash/settings/AvatarContent.tsx +70 -0
  130. package/src/ui/dash/settings/GeneralContent.tsx +3 -62
  131. package/src/ui/dash/settings/SettingsRootContent.tsx +236 -0
  132. package/src/ui/layouts/DashLayout.tsx +157 -75
  133. package/src/ui/layouts/SiteLayout.tsx +13 -13
  134. package/src/ui/pages/ArchivePage.tsx +10 -7
  135. package/src/ui/pages/CollectionPage.tsx +6 -35
  136. package/src/ui/pages/CollectionsPage.tsx +2 -1
  137. package/src/ui/pages/FeaturedPage.tsx +2 -1
  138. package/src/ui/pages/HomePage.tsx +1 -1
  139. package/src/ui/pages/SearchPage.tsx +1 -1
  140. package/src/ui/shared/CollectionsSidebar.tsx +228 -3
  141. package/src/ui/shared/MediaGallery.tsx +179 -41
  142. package/src/lib/collections-reorder.ts +0 -28
  143. package/src/routes/dash/appearance.tsx +0 -240
  144. package/src/routes/dash/collections.tsx +0 -211
  145. package/src/ui/components/jant-compose-editor.ts +0 -814
  146. package/src/ui/dash/appearance/AppearanceNav.tsx +0 -60
  147. package/src/ui/dash/collections/CollectionForm.tsx +0 -166
  148. package/src/ui/dash/collections/CollectionsListContent.tsx +0 -146
  149. package/src/ui/dash/collections/IconPickerGrid.tsx +0 -50
  150. package/src/ui/dash/collections/ViewCollectionContent.tsx +0 -103
  151. package/src/ui/dash/settings/SettingsNav.tsx +0 -52
  152. /package/src/{ui → client}/components/__tests__/jant-settings-avatar.test.ts +0 -0
  153. /package/src/{ui → client}/components/jant-settings-avatar.ts +0 -0
  154. /package/src/{ui → client}/components/settings-types.ts +0 -0
  155. /package/src/{lib → client}/image-processor.ts +0 -0
  156. /package/src/{lib → client}/toast.ts +0 -0
@@ -1,60 +0,0 @@
1
- /**
2
- * Appearance sub-navigation tabs
3
- */
4
-
5
- import { useLingui } from "@lingui/react/macro";
6
-
7
- export type AppearanceTab = "navigation" | "color" | "fonts" | "advanced";
8
-
9
- export function AppearanceNav({ currentTab }: { currentTab: AppearanceTab }) {
10
- const { t } = useLingui();
11
-
12
- const tabs: { id: AppearanceTab; label: string; href: string }[] = [
13
- {
14
- id: "navigation",
15
- label: t({
16
- message: "Navigation",
17
- comment: "@context: Appearance sub-navigation tab",
18
- }),
19
- href: "/dash/appearance",
20
- },
21
- {
22
- id: "color",
23
- label: t({
24
- message: "Color Theme",
25
- comment: "@context: Appearance sub-navigation tab",
26
- }),
27
- href: "/dash/appearance/color",
28
- },
29
- {
30
- id: "fonts",
31
- label: t({
32
- message: "Font Theme",
33
- comment: "@context: Appearance sub-navigation tab",
34
- }),
35
- href: "/dash/appearance/fonts",
36
- },
37
- {
38
- id: "advanced",
39
- label: t({
40
- message: "Advanced",
41
- comment: "@context: Appearance sub-navigation tab",
42
- }),
43
- href: "/dash/appearance/advanced",
44
- },
45
- ];
46
-
47
- return (
48
- <nav class="dash-subnav">
49
- {tabs.map((tab) => (
50
- <a
51
- key={tab.id}
52
- href={tab.href}
53
- class={tab.id === currentTab ? "active" : ""}
54
- >
55
- {tab.label}
56
- </a>
57
- ))}
58
- </nav>
59
- );
60
- }
@@ -1,166 +0,0 @@
1
- /**
2
- * Collection Form
3
- *
4
- * Server-rendered shell that provides data/labels to the Lit component
5
- * `<jant-collection-form>`. Includes heading and SSR fallback skeleton.
6
- */
7
-
8
- import { useLingui } from "@lingui/react/macro";
9
- import type { FC } from "hono/jsx";
10
- import type { Collection } from "../../../types.js";
11
-
12
- interface CollectionFormProps {
13
- collection?: Collection;
14
- isEdit?: boolean;
15
- }
16
-
17
- export const CollectionForm: FC<CollectionFormProps> = ({
18
- collection,
19
- isEdit,
20
- }) => {
21
- const { t } = useLingui();
22
-
23
- const heading = isEdit
24
- ? t({ message: "Edit Collection", comment: "@context: Page heading" })
25
- : t({ message: "New Collection", comment: "@context: Page heading" });
26
-
27
- const submitLabel = isEdit
28
- ? t({
29
- message: "Update Collection",
30
- comment: "@context: Button to save collection changes",
31
- })
32
- : t({
33
- message: "Create Collection",
34
- comment: "@context: Button to save new collection",
35
- });
36
-
37
- const labels = JSON.stringify({
38
- titleLabel: t({
39
- message: "Title",
40
- comment: "@context: Collection form field",
41
- }),
42
- titlePlaceholder: t({
43
- message: "My Collection",
44
- comment: "@context: Collection title placeholder",
45
- }),
46
- slugLabel: t({
47
- message: "Slug",
48
- comment: "@context: Collection form field",
49
- }),
50
- slugHelp: t({
51
- message:
52
- "URL-safe identifier (lowercase, numbers, hyphens). For CJK titles, slug will be auto-generated on the server.",
53
- comment: "@context: Collection path help text",
54
- }),
55
- descriptionLabel: t({
56
- message: "Description (optional)",
57
- comment: "@context: Collection form field",
58
- }),
59
- descriptionPlaceholder: t({
60
- message: "What's this collection about?",
61
- comment: "@context: Collection description placeholder",
62
- }),
63
- iconLabel: t({
64
- message: "Icon (optional)",
65
- comment: "@context: Collection form field",
66
- }),
67
- chooseIcon: t({
68
- message: "Choose Icon",
69
- comment: "@context: Button to open icon picker",
70
- }),
71
- removeIcon: t({
72
- message: "Remove",
73
- comment: "@context: Button to remove icon",
74
- }),
75
- dialogTitle: t({
76
- message: "Choose Icon",
77
- comment: "@context: Icon picker dialog title",
78
- }),
79
- dialogClose: t({
80
- message: "Close",
81
- comment: "@context: Button to close icon picker",
82
- }),
83
- searchIconsPlaceholder: t({
84
- message: "Search icons...",
85
- comment: "@context: Icon picker search placeholder",
86
- }),
87
- sortOrderLabel: t({
88
- message: "Sort Order",
89
- comment: "@context: Collection form field",
90
- }),
91
- sortNewest: t({
92
- message: "Newest first",
93
- comment: "@context: Collection sort order option",
94
- }),
95
- sortOldest: t({
96
- message: "Oldest first",
97
- comment: "@context: Collection sort order option",
98
- }),
99
- sortRatingDesc: t({
100
- message: "Highest rated",
101
- comment: "@context: Collection sort order option",
102
- }),
103
- sortRatingAsc: t({
104
- message: "Lowest rated",
105
- comment: "@context: Collection sort order option",
106
- }),
107
- submitLabel,
108
- cancelLabel: t({
109
- message: "Cancel",
110
- comment: "@context: Button to cancel form",
111
- }),
112
- }).replace(/</g, "\\u003c");
113
-
114
- const initial = JSON.stringify({
115
- title: collection?.title ?? "",
116
- slug: collection?.slug ?? "",
117
- description: collection?.description ?? "",
118
- sortOrder: collection?.sortOrder ?? "newest",
119
- icon: collection?.icon ?? "",
120
- }).replace(/</g, "\\u003c");
121
-
122
- const action = isEdit
123
- ? `/dash/collections/${collection?.id}`
124
- : "/dash/collections";
125
-
126
- const cancelHref = isEdit
127
- ? `/dash/collections/${collection?.id}`
128
- : "/dash/collections";
129
-
130
- return (
131
- <>
132
- <h1 class="text-2xl font-semibold mb-6">{heading}</h1>
133
-
134
- <jant-collection-form
135
- labels={labels}
136
- initial={initial}
137
- action={action}
138
- cancel-href={cancelHref}
139
- is-edit={isEdit ? "true" : undefined}
140
- >
141
- <div class="flex flex-col gap-4 max-w-lg">
142
- <div class="field">
143
- <div class="label skel-label"></div>
144
- <div class="input skel-input"></div>
145
- </div>
146
- <div class="field">
147
- <div class="label skel-label"></div>
148
- <div class="input skel-input"></div>
149
- </div>
150
- <div class="field">
151
- <div class="label skel-label"></div>
152
- <div class="textarea skel-textarea"></div>
153
- </div>
154
- <div class="field">
155
- <div class="label skel-label"></div>
156
- <div class="input skel-input"></div>
157
- </div>
158
- <div class="flex gap-2">
159
- <div class="btn skel-input min-w-28"></div>
160
- <div class="btn-outline skel-input min-w-20"></div>
161
- </div>
162
- </div>
163
- </jant-collection-form>
164
- </>
165
- );
166
- };
@@ -1,146 +0,0 @@
1
- /**
2
- * Collections list view with drag-and-drop reordering
3
- */
4
-
5
- import { useLingui } from "@lingui/react/macro";
6
- import type { Collection, CollectionDivider } from "../../../types.js";
7
- import { EmptyState, ActionButtons, CrudPageHeader } from "../index.js";
8
- import { renderCollectionIcon } from "../../../lib/icons.js";
9
-
10
- type ListItem =
11
- | { type: "collection"; data: Collection }
12
- | { type: "divider"; data: CollectionDivider };
13
-
14
- export function CollectionsListContent({
15
- collections,
16
- dividers,
17
- postCounts,
18
- }: {
19
- collections: Collection[];
20
- dividers: CollectionDivider[];
21
- postCounts: Map<number, number>;
22
- }) {
23
- const { t } = useLingui();
24
-
25
- const items: ListItem[] = [
26
- ...collections.map((c) => ({ type: "collection", data: c }) as ListItem),
27
- ...dividers.map((d) => ({ type: "divider", data: d }) as ListItem),
28
- ].sort((a, b) => a.data.position - b.data.position);
29
-
30
- const hasItems = collections.length > 0 || dividers.length > 0;
31
-
32
- return (
33
- <>
34
- <CrudPageHeader
35
- title={t({
36
- message: "Collections",
37
- comment: "@context: Dashboard heading",
38
- })}
39
- >
40
- <div class="flex items-center gap-2">
41
- <form method="post" action="/dash/collections/dividers">
42
- <button type="submit" class="btn-sm-outline">
43
- {t({
44
- message: "New Divider",
45
- comment: "@context: Button to add divider between collections",
46
- })}
47
- </button>
48
- </form>
49
- <a href="/dash/collections/new" class="btn-sm">
50
- {t({
51
- message: "New Collection",
52
- comment: "@context: Button to create new collection",
53
- })}
54
- </a>
55
- </div>
56
- </CrudPageHeader>
57
-
58
- {!hasItems ? (
59
- <EmptyState
60
- message={t({
61
- message: "No collections yet.",
62
- comment: "@context: Empty state message",
63
- })}
64
- ctaText={t({
65
- message: "New Collection",
66
- comment: "@context: Button to create new collection",
67
- })}
68
- ctaHref="/dash/collections/new"
69
- />
70
- ) : (
71
- <div id="collections-list" class="flex flex-col">
72
- {items.map((item) => {
73
- if (item.type === "divider") {
74
- return (
75
- <div
76
- key={`d-${item.data.id}`}
77
- class="py-2 flex items-center gap-4"
78
- >
79
- <div
80
- class="flex-1 min-w-0 flex items-center gap-3 cursor-grab"
81
- data-id={`d-${item.data.id}`}
82
- >
83
- <span class="text-muted-foreground select-none">⠿</span>
84
- <hr class="flex-1 border-border" />
85
- </div>
86
- <form
87
- method="post"
88
- action={`/dash/collections/dividers/${item.data.id}/delete`}
89
- >
90
- <button
91
- type="submit"
92
- class="btn-sm-ghost text-muted-foreground hover:text-destructive"
93
- title={t({
94
- message: "Remove divider",
95
- comment: "@context: Button to delete a divider",
96
- })}
97
- >
98
-
99
- </button>
100
- </form>
101
- </div>
102
- );
103
- }
104
-
105
- const col = item.data;
106
- const count = postCounts.get(col.id) ?? 0;
107
- return (
108
- <div key={`c-${col.id}`} class="py-2 flex items-center gap-4">
109
- <div
110
- class="flex-1 min-w-0 flex items-center gap-3 cursor-grab"
111
- data-id={`c-${col.id}`}
112
- >
113
- <span class="text-muted-foreground select-none">⠿</span>
114
- {col.icon && (
115
- <span
116
- class="flex items-center justify-center w-5 h-5 shrink-0"
117
- dangerouslySetInnerHTML={{
118
- __html: renderCollectionIcon(col.icon, {
119
- size: 18,
120
- }),
121
- }}
122
- />
123
- )}
124
- <a
125
- href={`/dash/collections/${col.id}`}
126
- class="font-medium hover:underline"
127
- >
128
- {col.title}
129
- </a>
130
- <span class="badge-secondary">{count}</span>
131
- </div>
132
- <ActionButtons
133
- editHref={`/dash/collections/${col.id}/edit`}
134
- editLabel={t({
135
- message: "Edit",
136
- comment: "@context: Button to edit collection",
137
- })}
138
- />
139
- </div>
140
- );
141
- })}
142
- </div>
143
- )}
144
- </>
145
- );
146
- }
@@ -1,50 +0,0 @@
1
- /**
2
- * Icon Picker Grid
3
- *
4
- * HTML fragment returned by GET /dash/collections/icons.
5
- * Renders a grid of icon buttons organized by category.
6
- */
7
-
8
- import type { FC } from "hono/jsx";
9
- import { ICON_CATALOG } from "../../../lib/icon-catalog.js";
10
- import { getIconSvg } from "../../../lib/icons.js";
11
-
12
- export const IconPickerGrid: FC = () => {
13
- return (
14
- <div class="flex flex-col gap-4">
15
- {Object.entries(ICON_CATALOG).map(([category, names]) => (
16
- <div key={category} data-category={category}>
17
- <h3 class="text-xs font-medium text-muted-foreground uppercase tracking-wider mb-2">
18
- {category}
19
- </h3>
20
- <div class="grid grid-cols-8 gap-1">
21
- {names.map((name) => {
22
- const svg = getIconSvg(name);
23
- if (!svg) return null;
24
- return (
25
- <button
26
- key={name}
27
- type="button"
28
- class="flex items-center justify-center w-9 h-9 rounded-md hover:bg-accent transition-colors"
29
- data-icon-name={name}
30
- data-icon-svg={svg}
31
- title={name}
32
- data-on:click={`$iconName = el.dataset.iconName; $iconSvg = el.dataset.iconSvg; $icon = JSON.stringify({ name: $iconName, svg: $iconSvg, color: $iconColor }); const p = document.getElementById('icon-preview'); if (p) p.innerHTML = el.dataset.iconSvg; document.getElementById('icon-picker-dialog')?.close()`}
33
- >
34
- <span
35
- class="w-5 h-5 flex items-center justify-center"
36
- dangerouslySetInnerHTML={{
37
- __html: svg
38
- .replace(/width="24"/, 'width="20"')
39
- .replace(/height="24"/, 'height="20"'),
40
- }}
41
- />
42
- </button>
43
- );
44
- })}
45
- </div>
46
- </div>
47
- ))}
48
- </div>
49
- );
50
- };
@@ -1,103 +0,0 @@
1
- /**
2
- * Single collection detail view
3
- */
4
-
5
- import { useLingui } from "@lingui/react/macro";
6
- import type { Collection, PostView } from "../../../types.js";
7
- import { ActionButtons } from "../index.js";
8
- import { encode } from "../../../lib/sqid.js";
9
- import { renderCollectionIcon } from "../../../lib/icons.js";
10
-
11
- export function ViewCollectionContent({
12
- collection,
13
- posts,
14
- }: {
15
- collection: Collection;
16
- posts: PostView[];
17
- }) {
18
- const { t } = useLingui();
19
- const count = String(posts.length);
20
- const postsHeader = t({
21
- message: `Posts in Collection (${count})`,
22
- comment: "@context: Collection posts section heading",
23
- });
24
-
25
- return (
26
- <>
27
- <div class="flex items-center justify-between mb-6">
28
- <div>
29
- <h1 class="text-2xl font-semibold flex items-center gap-2">
30
- {collection.icon && (
31
- <span
32
- class="shrink-0"
33
- dangerouslySetInnerHTML={{
34
- __html: renderCollectionIcon(collection.icon, { size: 24 }),
35
- }}
36
- />
37
- )}
38
- {collection.title}
39
- </h1>
40
- <p class="text-sm text-muted-foreground">/{collection.slug}</p>
41
- </div>
42
- <ActionButtons
43
- editHref={`/dash/collections/${collection.id}/edit`}
44
- editLabel={t({
45
- message: "Edit",
46
- comment: "@context: Button to edit collection",
47
- })}
48
- viewHref={`/c/${collection.slug}`}
49
- viewLabel={t({
50
- message: "View",
51
- comment: "@context: Button to view collection",
52
- })}
53
- />
54
- </div>
55
-
56
- {collection.description && (
57
- <p class="text-muted-foreground mb-6">{collection.description}</p>
58
- )}
59
-
60
- <div class="card">
61
- <header>
62
- <h2>{postsHeader}</h2>
63
- </header>
64
- <section>
65
- {posts.length === 0 ? (
66
- <p class="text-muted-foreground">
67
- {t({
68
- message: "No posts in this collection.",
69
- comment: "@context: Empty state message",
70
- })}
71
- </p>
72
- ) : (
73
- <div class="flex flex-col divide-y">
74
- {posts.map((post) => (
75
- <div key={post.id} class="py-3 flex items-center gap-4">
76
- <div class="flex-1 min-w-0">
77
- <a
78
- href={`/dash/posts/${encode(post.id)}`}
79
- class="font-medium hover:underline"
80
- >
81
- {post.title ||
82
- post.excerpt?.slice(0, 50) ||
83
- `Post #${post.id}`}
84
- </a>
85
- </div>
86
- </div>
87
- ))}
88
- </div>
89
- )}
90
- </section>
91
- </div>
92
-
93
- <div class="mt-6">
94
- <a href="/dash/collections" class="text-sm hover:underline">
95
- {t({
96
- message: "\u2190 Back to Collections",
97
- comment: "@context: Navigation link",
98
- })}
99
- </a>
100
- </div>
101
- </>
102
- );
103
- }
@@ -1,52 +0,0 @@
1
- /**
2
- * Settings sub-navigation tabs
3
- */
4
-
5
- import { useLingui } from "@lingui/react/macro";
6
-
7
- export type SettingsTab = "general" | "redirects" | "account";
8
-
9
- export function SettingsNav({ currentTab }: { currentTab: SettingsTab }) {
10
- const { t } = useLingui();
11
-
12
- const tabs: { id: SettingsTab; label: string; href: string }[] = [
13
- {
14
- id: "general",
15
- label: t({
16
- message: "General",
17
- comment: "@context: Settings sub-navigation tab",
18
- }),
19
- href: "/dash/settings",
20
- },
21
- {
22
- id: "redirects",
23
- label: t({
24
- message: "Redirects",
25
- comment: "@context: Settings sub-navigation tab",
26
- }),
27
- href: "/dash/settings/redirects",
28
- },
29
- {
30
- id: "account",
31
- label: t({
32
- message: "Account",
33
- comment: "@context: Settings sub-navigation tab",
34
- }),
35
- href: "/dash/settings/account",
36
- },
37
- ];
38
-
39
- return (
40
- <nav class="dash-subnav">
41
- {tabs.map((tab) => (
42
- <a
43
- key={tab.id}
44
- href={tab.href}
45
- class={tab.id === currentTab ? "active" : ""}
46
- >
47
- {tab.label}
48
- </a>
49
- ))}
50
- </nav>
51
- );
52
- }
File without changes
File without changes
File without changes