@jant/core 0.3.23 → 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 -26
- package/dist/db/schema.js +72 -47
- 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 +5 -11
- package/dist/lib/constants.js +2 -4
- package/dist/lib/excerpt.js +76 -0
- package/dist/lib/feed.js +18 -7
- package/dist/lib/nav-reorder.js +1 -1
- package/dist/lib/navigation.js +30 -6
- package/dist/lib/pagination.js +44 -0
- package/dist/lib/render.js +7 -11
- package/dist/lib/schemas.js +80 -38
- package/dist/lib/theme.js +4 -4
- package/dist/lib/time.js +56 -1
- package/dist/lib/timeline.js +95 -0
- package/dist/lib/view.js +61 -72
- 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 +27 -33
- package/dist/routes/api/search.js +4 -5
- package/dist/routes/api/settings.js +68 -0
- package/dist/routes/api/upload.js +13 -13
- package/dist/routes/compose.js +48 -0
- package/dist/routes/dash/collections.js +24 -42
- package/dist/routes/dash/index.js +3 -3
- package/dist/routes/dash/media.js +2 -2
- package/dist/routes/dash/pages.js +440 -106
- package/dist/routes/dash/posts.js +27 -37
- package/dist/routes/dash/redirects.js +2 -2
- package/dist/routes/dash/settings.js +79 -5
- package/dist/routes/feed/rss.js +4 -6
- package/dist/routes/feed/sitemap.js +11 -8
- package/dist/routes/pages/archive.js +13 -15
- package/dist/routes/pages/collection.js +12 -9
- package/dist/routes/pages/collections.js +28 -0
- package/dist/routes/pages/featured.js +32 -0
- package/dist/routes/pages/home.js +19 -68
- package/dist/routes/pages/page.js +57 -29
- package/dist/routes/pages/post.js +7 -17
- package/dist/routes/pages/search.js +5 -9
- package/dist/services/collection.js +52 -64
- package/dist/services/index.js +5 -3
- package/dist/services/navigation.js +29 -53
- package/dist/services/page.js +84 -0
- package/dist/services/post.js +102 -69
- package/dist/services/search.js +24 -18
- package/dist/types.js +24 -40
- 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} +3 -15
- package/dist/{theme/components → ui/dash}/PageForm.js +15 -15
- package/dist/{theme/components → ui/dash}/PostForm.js +117 -137
- package/dist/{theme/components → ui/dash}/PostList.js +18 -13
- package/dist/ui/dash/StatusBadge.js +46 -0
- package/dist/{theme/components → ui/dash}/index.js +3 -6
- package/dist/ui/feed/LinkCard.js +72 -0
- package/dist/ui/feed/NoteCard.js +58 -0
- package/dist/{themes/minimal/timeline → ui/feed}/QuoteCard.js +29 -14
- package/dist/{themes/minimal/timeline → ui/feed}/ThreadPreview.js +20 -18
- 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/minimal → ui}/pages/ArchivePage.js +37 -50
- package/dist/ui/pages/CollectionPage.js +70 -0
- 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/minimal → ui}/pages/PostPage.js +20 -12
- package/dist/{themes/minimal → ui}/pages/SearchPage.js +19 -18
- package/dist/{themes/minimal → ui}/pages/SinglePage.js +5 -4
- package/dist/ui/shared/MediaGallery.js +35 -0
- package/dist/{theme/components → ui/shared}/Pagination.js +41 -2
- package/dist/{theme/components → ui/shared}/ThreadView.js +3 -3
- package/dist/ui/shared/index.js +5 -0
- package/package.json +2 -9
- package/src/__tests__/helpers/app.ts +4 -0
- package/src/__tests__/helpers/db.ts +53 -73
- package/src/app.tsx +56 -28
- package/src/db/migrations/0005_v2_schema_migration.sql +268 -0
- package/src/db/migrations/0006_rename_slug_to_path.sql +5 -0
- package/src/db/migrations/meta/_journal.json +14 -0
- package/src/db/schema.ts +63 -46
- package/src/i18n/locales/en.po +443 -240
- package/src/i18n/locales/en.ts +1 -1
- package/src/i18n/locales/zh-Hans.po +443 -240
- package/src/i18n/locales/zh-Hans.ts +1 -1
- package/src/i18n/locales/zh-Hant.po +443 -240
- package/src/i18n/locales/zh-Hant.ts +1 -1
- package/src/index.ts +29 -42
- package/src/lib/__tests__/excerpt.test.ts +125 -0
- package/src/lib/__tests__/schemas.test.ts +201 -99
- package/src/lib/__tests__/time.test.ts +62 -0
- package/src/{routes/api → lib}/__tests__/timeline.test.ts +81 -75
- package/src/lib/__tests__/view.test.ts +204 -50
- package/src/lib/constants.ts +2 -4
- package/src/lib/excerpt.ts +87 -0
- package/src/lib/feed.ts +22 -7
- package/src/lib/nav-reorder.ts +1 -1
- package/src/lib/navigation.ts +45 -8
- package/src/lib/pagination.ts +50 -0
- package/src/lib/render.tsx +7 -14
- package/src/lib/schemas.ts +119 -51
- package/src/lib/theme.ts +5 -5
- package/src/lib/time.ts +64 -0
- package/src/lib/timeline.ts +141 -0
- package/src/lib/view.ts +80 -82
- package/src/preset.css +46 -0
- 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__/posts.test.ts +50 -108
- package/src/routes/api/__tests__/search.test.ts +2 -3
- 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 +28 -28
- package/src/routes/api/search.ts +3 -3
- package/src/routes/api/settings.ts +91 -0
- package/src/routes/api/upload.ts +16 -6
- package/src/routes/compose.ts +63 -0
- package/src/routes/dash/__tests__/pages.test.ts +225 -0
- package/src/routes/dash/collections.tsx +20 -42
- package/src/routes/dash/index.tsx +3 -3
- package/src/routes/dash/media.tsx +2 -2
- package/src/routes/dash/pages.tsx +480 -122
- package/src/routes/dash/posts.tsx +42 -54
- package/src/routes/dash/redirects.tsx +2 -2
- package/src/routes/dash/settings.tsx +83 -5
- package/src/routes/feed/rss.ts +4 -3
- package/src/routes/feed/sitemap.ts +15 -5
- 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 +15 -15
- package/src/routes/pages/collection.tsx +16 -9
- package/src/routes/pages/collections.tsx +36 -0
- package/src/routes/pages/featured.tsx +38 -0
- package/src/routes/pages/home.tsx +21 -92
- package/src/routes/pages/page.tsx +62 -27
- package/src/routes/pages/post.tsx +6 -18
- package/src/routes/pages/search.tsx +3 -7
- package/src/services/__tests__/collection.test.ts +257 -158
- package/src/services/__tests__/media.test.ts +18 -18
- package/src/services/__tests__/navigation.test.ts +161 -87
- package/src/services/__tests__/page.test.ts +106 -0
- package/src/services/__tests__/post-timeline.test.ts +92 -88
- package/src/services/__tests__/post.test.ts +432 -197
- package/src/services/__tests__/search.test.ts +19 -25
- package/src/services/collection.ts +71 -113
- package/src/services/index.ts +9 -8
- package/src/services/navigation.ts +38 -71
- package/src/services/page.ts +136 -0
- package/src/services/post.ts +141 -101
- package/src/services/search.ts +38 -27
- package/src/styles/tokens.css +47 -0
- package/src/styles/ui.css +491 -0
- package/src/types.ts +212 -198
- package/src/ui/compose/ComposeDialog.tsx +395 -0
- package/src/ui/compose/ComposePrompt.tsx +55 -0
- package/src/ui/dash/FormatBadge.tsx +28 -0
- package/src/{theme/components → ui/dash}/PageForm.tsx +21 -21
- package/src/{theme/components → ui/dash}/PostForm.tsx +110 -131
- package/src/ui/dash/PostList.tsx +101 -0
- package/src/ui/dash/StatusBadge.tsx +61 -0
- package/src/ui/dash/index.ts +10 -0
- package/src/ui/feed/LinkCard.tsx +72 -0
- package/src/ui/feed/NoteCard.tsx +63 -0
- package/src/ui/feed/QuoteCard.tsx +68 -0
- package/src/ui/feed/ThreadPreview.tsx +48 -0
- 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/ui/pages/ArchivePage.tsx +162 -0
- package/src/ui/pages/CollectionPage.tsx +70 -0
- package/src/ui/pages/CollectionsPage.tsx +73 -0
- package/src/ui/pages/FeaturedPage.tsx +31 -0
- package/src/ui/pages/HomePage.tsx +37 -0
- package/src/ui/pages/PostPage.tsx +56 -0
- package/src/{themes/minimal → ui}/pages/SearchPage.tsx +24 -20
- package/src/{themes/minimal → ui}/pages/SinglePage.tsx +5 -5
- package/src/ui/shared/MediaGallery.tsx +59 -0
- package/src/{theme/components → ui/shared}/Pagination.tsx +67 -4
- package/src/{theme/components → ui/shared}/ThreadView.tsx +6 -3
- 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 -49
- package/dist/routes/api/timeline.js +0 -120
- package/dist/routes/dash/navigation.js +0 -288
- package/dist/theme/components/MediaGallery.js +0 -107
- package/dist/theme/components/VisibilityBadge.js +0 -37
- package/dist/theme/index.js +0 -18
- package/dist/theme/layouts/index.js +0 -2
- package/dist/themes/minimal/MinimalSiteLayout.js +0 -83
- package/dist/themes/minimal/index.js +0 -65
- package/dist/themes/minimal/pages/CollectionPage.js +0 -65
- package/dist/themes/minimal/pages/HomePage.js +0 -25
- package/dist/themes/minimal/timeline/ArticleCard.js +0 -36
- package/dist/themes/minimal/timeline/ImageCard.js +0 -67
- package/dist/themes/minimal/timeline/LinkCard.js +0 -47
- package/dist/themes/minimal/timeline/NoteCard.js +0 -34
- package/dist/themes/minimal/timeline/TimelineFeed.js +0 -48
- package/dist/themes/minimal/timeline/TimelineItem.js +0 -44
- package/src/lib/__tests__/theme-components.test.ts +0 -126
- package/src/lib/theme-components.ts +0 -68
- package/src/routes/api/timeline.tsx +0 -159
- package/src/routes/dash/navigation.tsx +0 -316
- package/src/theme/components/MediaGallery.tsx +0 -128
- package/src/theme/components/PostList.tsx +0 -92
- package/src/theme/components/TypeBadge.tsx +0 -37
- package/src/theme/components/VisibilityBadge.tsx +0 -45
- 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/minimal/MinimalSiteLayout.tsx +0 -100
- package/src/themes/minimal/index.ts +0 -83
- package/src/themes/minimal/pages/ArchivePage.tsx +0 -157
- package/src/themes/minimal/pages/CollectionPage.tsx +0 -60
- package/src/themes/minimal/pages/HomePage.tsx +0 -41
- package/src/themes/minimal/pages/PostPage.tsx +0 -43
- package/src/themes/minimal/timeline/ArticleCard.tsx +0 -37
- package/src/themes/minimal/timeline/ImageCard.tsx +0 -63
- package/src/themes/minimal/timeline/LinkCard.tsx +0 -48
- package/src/themes/minimal/timeline/NoteCard.tsx +0 -35
- package/src/themes/minimal/timeline/QuoteCard.tsx +0 -49
- package/src/themes/minimal/timeline/ThreadPreview.tsx +0 -47
- package/src/themes/minimal/timeline/TimelineFeed.tsx +0 -57
- package/src/themes/minimal/timeline/TimelineItem.tsx +0 -75
- /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/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/shared}/EmptyState.tsx +0 -0
|
@@ -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
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compose Prompt
|
|
3
|
+
*
|
|
4
|
+
* "What's new?" prompt bar at the top of the content area.
|
|
5
|
+
* Clicking it opens the compose dialog.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { FC } from "hono/jsx";
|
|
9
|
+
import { useLingui } from "@lingui/react/macro";
|
|
10
|
+
|
|
11
|
+
export const ComposePrompt: FC = () => {
|
|
12
|
+
const { t } = useLingui();
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<div class="compose-prompt">
|
|
16
|
+
<button
|
|
17
|
+
type="button"
|
|
18
|
+
class="compose-prompt-trigger"
|
|
19
|
+
onclick="document.getElementById('compose-dialog').showModal()"
|
|
20
|
+
>
|
|
21
|
+
<span class="compose-prompt-avatar">
|
|
22
|
+
<svg
|
|
23
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
24
|
+
width="16"
|
|
25
|
+
height="16"
|
|
26
|
+
viewBox="0 0 24 24"
|
|
27
|
+
fill="none"
|
|
28
|
+
stroke="currentColor"
|
|
29
|
+
stroke-width="2"
|
|
30
|
+
stroke-linecap="round"
|
|
31
|
+
stroke-linejoin="round"
|
|
32
|
+
>
|
|
33
|
+
<path d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 1 1 3.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z" />
|
|
34
|
+
</svg>
|
|
35
|
+
</span>
|
|
36
|
+
<span class="compose-prompt-text">
|
|
37
|
+
{t({
|
|
38
|
+
message: "What's new?",
|
|
39
|
+
comment: "@context: Compose prompt placeholder text",
|
|
40
|
+
})}
|
|
41
|
+
</span>
|
|
42
|
+
</button>
|
|
43
|
+
<button
|
|
44
|
+
type="button"
|
|
45
|
+
class="compose-prompt-post-btn"
|
|
46
|
+
onclick="document.getElementById('compose-dialog').showModal()"
|
|
47
|
+
>
|
|
48
|
+
{t({
|
|
49
|
+
message: "Post",
|
|
50
|
+
comment: "@context: Compose prompt post button",
|
|
51
|
+
})}
|
|
52
|
+
</button>
|
|
53
|
+
</div>
|
|
54
|
+
);
|
|
55
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Format Badge Component
|
|
3
|
+
*
|
|
4
|
+
* Displays a badge indicating the format of a post (note, link, quote).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { FC } from "hono/jsx";
|
|
8
|
+
import { useLingui } from "@lingui/react/macro";
|
|
9
|
+
import type { Format } from "../../types.js";
|
|
10
|
+
|
|
11
|
+
export interface FormatBadgeProps {
|
|
12
|
+
type: Format;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const FormatBadge: FC<FormatBadgeProps> = ({ type }) => {
|
|
16
|
+
const { t } = useLingui();
|
|
17
|
+
|
|
18
|
+
const labels: Record<Format, string> = {
|
|
19
|
+
note: t({ message: "Note", comment: "@context: Post format badge - note" }),
|
|
20
|
+
link: t({ message: "Link", comment: "@context: Post format badge - link" }),
|
|
21
|
+
quote: t({
|
|
22
|
+
message: "Quote",
|
|
23
|
+
comment: "@context: Post format badge - quote",
|
|
24
|
+
}),
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
return <span class="badge-outline">{labels[type]}</span>;
|
|
28
|
+
};
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Page Creation/Edit Form
|
|
3
3
|
*
|
|
4
|
-
* For managing
|
|
4
|
+
* For managing standalone pages (about, now, etc.)
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import type { FC } from "hono/jsx";
|
|
8
|
-
import type {
|
|
8
|
+
import type { Page } from "../../types.js";
|
|
9
9
|
import { useLingui } from "@lingui/react/macro";
|
|
10
10
|
|
|
11
11
|
export interface PageFormProps {
|
|
12
|
-
page?:
|
|
12
|
+
page?: Page;
|
|
13
13
|
action: string;
|
|
14
14
|
cancelUrl?: string;
|
|
15
15
|
}
|
|
@@ -24,9 +24,9 @@ export const PageForm: FC<PageFormProps> = ({
|
|
|
24
24
|
|
|
25
25
|
const signals = JSON.stringify({
|
|
26
26
|
title: page?.title ?? "",
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
slug: page?.slug ?? "",
|
|
28
|
+
body: page?.body ?? "",
|
|
29
|
+
status: page?.status ?? "published",
|
|
30
30
|
}).replace(/</g, "\\u003c");
|
|
31
31
|
|
|
32
32
|
return (
|
|
@@ -58,25 +58,25 @@ export const PageForm: FC<PageFormProps> = ({
|
|
|
58
58
|
/>
|
|
59
59
|
</div>
|
|
60
60
|
|
|
61
|
-
{/*
|
|
61
|
+
{/* Slug */}
|
|
62
62
|
<div class="field">
|
|
63
63
|
<label class="label">
|
|
64
64
|
{t({
|
|
65
|
-
message: "
|
|
66
|
-
comment: "@context: Page form field label - URL
|
|
65
|
+
message: "Slug",
|
|
66
|
+
comment: "@context: Page form field label - URL slug",
|
|
67
67
|
})}
|
|
68
68
|
</label>
|
|
69
69
|
<div class="flex items-center gap-2">
|
|
70
70
|
<span class="text-muted-foreground">/</span>
|
|
71
71
|
<input
|
|
72
72
|
type="text"
|
|
73
|
-
data-bind="
|
|
73
|
+
data-bind="slug"
|
|
74
74
|
class="input flex-1"
|
|
75
75
|
placeholder="about"
|
|
76
76
|
pattern="[a-z0-9\-]+"
|
|
77
77
|
title={t({
|
|
78
78
|
message: "Lowercase letters, numbers, and hyphens only",
|
|
79
|
-
comment: "@context: Page
|
|
79
|
+
comment: "@context: Page slug validation message",
|
|
80
80
|
})}
|
|
81
81
|
required
|
|
82
82
|
/>
|
|
@@ -85,12 +85,12 @@ export const PageForm: FC<PageFormProps> = ({
|
|
|
85
85
|
{t({
|
|
86
86
|
message:
|
|
87
87
|
"The URL path for this page. Use lowercase letters, numbers, and hyphens.",
|
|
88
|
-
comment: "@context: Page
|
|
88
|
+
comment: "@context: Page slug helper text",
|
|
89
89
|
})}
|
|
90
90
|
</p>
|
|
91
91
|
</div>
|
|
92
92
|
|
|
93
|
-
{/*
|
|
93
|
+
{/* Body */}
|
|
94
94
|
<div class="field">
|
|
95
95
|
<label class="label">
|
|
96
96
|
{t({
|
|
@@ -99,7 +99,7 @@ export const PageForm: FC<PageFormProps> = ({
|
|
|
99
99
|
})}
|
|
100
100
|
</label>
|
|
101
101
|
<textarea
|
|
102
|
-
data-bind="
|
|
102
|
+
data-bind="body"
|
|
103
103
|
class="textarea min-h-48"
|
|
104
104
|
placeholder={t({
|
|
105
105
|
message: "Page content (Markdown supported)...",
|
|
@@ -107,11 +107,11 @@ export const PageForm: FC<PageFormProps> = ({
|
|
|
107
107
|
})}
|
|
108
108
|
required
|
|
109
109
|
>
|
|
110
|
-
{page?.
|
|
110
|
+
{page?.body ?? ""}
|
|
111
111
|
</textarea>
|
|
112
112
|
</div>
|
|
113
113
|
|
|
114
|
-
{/*
|
|
114
|
+
{/* Status */}
|
|
115
115
|
<div class="field">
|
|
116
116
|
<label class="label">
|
|
117
117
|
{t({
|
|
@@ -119,17 +119,17 @@ export const PageForm: FC<PageFormProps> = ({
|
|
|
119
119
|
comment: "@context: Page form field label - publish status",
|
|
120
120
|
})}
|
|
121
121
|
</label>
|
|
122
|
-
<select data-bind="
|
|
122
|
+
<select data-bind="status" class="select">
|
|
123
123
|
<option
|
|
124
|
-
value="
|
|
125
|
-
selected={page?.
|
|
124
|
+
value="published"
|
|
125
|
+
selected={page?.status === "published" || !page}
|
|
126
126
|
>
|
|
127
127
|
{t({
|
|
128
128
|
message: "Published",
|
|
129
129
|
comment: "@context: Page status option - published",
|
|
130
130
|
})}
|
|
131
131
|
</option>
|
|
132
|
-
<option value="draft" selected={page?.
|
|
132
|
+
<option value="draft" selected={page?.status === "draft"}>
|
|
133
133
|
{t({
|
|
134
134
|
message: "Draft",
|
|
135
135
|
comment: "@context: Page status option - draft",
|
|
@@ -139,7 +139,7 @@ export const PageForm: FC<PageFormProps> = ({
|
|
|
139
139
|
<p class="text-xs text-muted-foreground mt-1">
|
|
140
140
|
{t({
|
|
141
141
|
message:
|
|
142
|
-
"Published pages are accessible via their
|
|
142
|
+
"Published pages are accessible via their slug. Drafts are not visible.",
|
|
143
143
|
comment: "@context: Page status helper text",
|
|
144
144
|
})}
|
|
145
145
|
</p>
|