@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
|
@@ -5,7 +5,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "hono/jsx/jsx-
|
|
|
5
5
|
* Sub-pages: General, Appearance, Account
|
|
6
6
|
*/ import { Hono } from "hono";
|
|
7
7
|
import { useLingui as $_useLingui } from "@jant/core/i18n";
|
|
8
|
-
import { DashLayout } from "../../
|
|
8
|
+
import { DashLayout } from "../../ui/layouts/DashLayout.js";
|
|
9
9
|
import { sse, dsRedirect, dsToast } from "../../lib/sse.js";
|
|
10
10
|
import { getSiteLanguage, getSiteName, getConfigFallback } from "../../lib/config.js";
|
|
11
11
|
import { SETTINGS_KEYS } from "../../lib/constants.js";
|
|
@@ -272,11 +272,14 @@ function ThemeCard({ theme, selected }) {
|
|
|
272
272
|
})
|
|
273
273
|
});
|
|
274
274
|
}
|
|
275
|
-
function AppearanceContent({ themes, currentThemeId }) {
|
|
275
|
+
function AppearanceContent({ themes, currentThemeId, customCSS }) {
|
|
276
276
|
const { i18n: $__i18n, _: $__ } = $_useLingui();
|
|
277
|
-
const
|
|
277
|
+
const themeSignals = JSON.stringify({
|
|
278
278
|
theme: currentThemeId
|
|
279
279
|
}).replace(/</g, "\\u003c");
|
|
280
|
+
const cssSignals = JSON.stringify({
|
|
281
|
+
customCSS
|
|
282
|
+
}).replace(/</g, "\\u003c");
|
|
280
283
|
return /*#__PURE__*/ _jsxs(_Fragment, {
|
|
281
284
|
children: [
|
|
282
285
|
/*#__PURE__*/ _jsx("h1", {
|
|
@@ -290,7 +293,7 @@ function AppearanceContent({ themes, currentThemeId }) {
|
|
|
290
293
|
currentTab: "appearance"
|
|
291
294
|
}),
|
|
292
295
|
/*#__PURE__*/ _jsx("div", {
|
|
293
|
-
"data-signals":
|
|
296
|
+
"data-signals": themeSignals,
|
|
294
297
|
"data-on:change": "@post('/dash/settings/appearance')",
|
|
295
298
|
class: "max-w-3xl",
|
|
296
299
|
children: /*#__PURE__*/ _jsxs("fieldset", {
|
|
@@ -318,6 +321,63 @@ function AppearanceContent({ themes, currentThemeId }) {
|
|
|
318
321
|
})
|
|
319
322
|
]
|
|
320
323
|
})
|
|
324
|
+
}),
|
|
325
|
+
/*#__PURE__*/ _jsxs("form", {
|
|
326
|
+
"data-signals": cssSignals,
|
|
327
|
+
"data-on:submit__prevent": "@post('/dash/settings/custom-css')",
|
|
328
|
+
"data-indicator": "_cssLoading",
|
|
329
|
+
class: "max-w-3xl mt-8",
|
|
330
|
+
children: [
|
|
331
|
+
/*#__PURE__*/ _jsxs("fieldset", {
|
|
332
|
+
children: [
|
|
333
|
+
/*#__PURE__*/ _jsx("legend", {
|
|
334
|
+
class: "text-lg font-semibold",
|
|
335
|
+
children: $__i18n._({
|
|
336
|
+
id: "9+vGLh",
|
|
337
|
+
message: "Custom CSS"
|
|
338
|
+
})
|
|
339
|
+
}),
|
|
340
|
+
/*#__PURE__*/ _jsx("p", {
|
|
341
|
+
class: "text-sm text-muted-foreground mb-4",
|
|
342
|
+
children: $__i18n._({
|
|
343
|
+
id: "vmQmHx",
|
|
344
|
+
message: "Add custom CSS to override any styles. Use data attributes like [data-page], [data-post], [data-format] to target specific elements."
|
|
345
|
+
})
|
|
346
|
+
}),
|
|
347
|
+
/*#__PURE__*/ _jsx("textarea", {
|
|
348
|
+
"data-bind": "customCSS",
|
|
349
|
+
class: "textarea font-mono text-sm min-h-32",
|
|
350
|
+
rows: 8,
|
|
351
|
+
placeholder: $__i18n._({
|
|
352
|
+
id: "wc+17X",
|
|
353
|
+
message: "/* Your custom CSS here */"
|
|
354
|
+
}),
|
|
355
|
+
children: customCSS
|
|
356
|
+
})
|
|
357
|
+
]
|
|
358
|
+
}),
|
|
359
|
+
/*#__PURE__*/ _jsxs("button", {
|
|
360
|
+
type: "submit",
|
|
361
|
+
class: "btn mt-4",
|
|
362
|
+
"data-attr-disabled": "$_cssLoading",
|
|
363
|
+
children: [
|
|
364
|
+
/*#__PURE__*/ _jsx("span", {
|
|
365
|
+
"data-show": "!$_cssLoading",
|
|
366
|
+
children: $__i18n._({
|
|
367
|
+
id: "NU2Fqi",
|
|
368
|
+
message: "Save CSS"
|
|
369
|
+
})
|
|
370
|
+
}),
|
|
371
|
+
/*#__PURE__*/ _jsx("span", {
|
|
372
|
+
"data-show": "$_cssLoading",
|
|
373
|
+
children: $__i18n._({
|
|
374
|
+
id: "k1ifdL",
|
|
375
|
+
message: "Processing..."
|
|
376
|
+
})
|
|
377
|
+
})
|
|
378
|
+
]
|
|
379
|
+
})
|
|
380
|
+
]
|
|
321
381
|
})
|
|
322
382
|
]
|
|
323
383
|
});
|
|
@@ -583,6 +643,7 @@ settingsRoutes.get("/appearance", async (c)=>{
|
|
|
583
643
|
const { settings } = c.var.services;
|
|
584
644
|
const siteName = await getSiteName(c);
|
|
585
645
|
const currentThemeId = await settings.get(SETTINGS_KEYS.THEME) ?? "default";
|
|
646
|
+
const customCSS = await settings.get(SETTINGS_KEYS.CUSTOM_CSS) ?? "";
|
|
586
647
|
const themes = getAvailableThemes(c.var.config);
|
|
587
648
|
const saved = c.req.query("saved") !== undefined;
|
|
588
649
|
return c.html(/*#__PURE__*/ _jsx(DashLayout, {
|
|
@@ -595,7 +656,8 @@ settingsRoutes.get("/appearance", async (c)=>{
|
|
|
595
656
|
} : undefined,
|
|
596
657
|
children: /*#__PURE__*/ _jsx(AppearanceContent, {
|
|
597
658
|
themes: themes,
|
|
598
|
-
currentThemeId: currentThemeId
|
|
659
|
+
currentThemeId: currentThemeId,
|
|
660
|
+
customCSS: customCSS
|
|
599
661
|
})
|
|
600
662
|
}));
|
|
601
663
|
});
|
|
@@ -615,6 +677,18 @@ settingsRoutes.post("/appearance", async (c)=>{
|
|
|
615
677
|
}
|
|
616
678
|
return dsRedirect("/dash/settings/appearance?saved");
|
|
617
679
|
});
|
|
680
|
+
// Save custom CSS
|
|
681
|
+
settingsRoutes.post("/custom-css", async (c)=>{
|
|
682
|
+
const body = await c.req.json();
|
|
683
|
+
const { settings } = c.var.services;
|
|
684
|
+
const css = body.customCSS?.trim() ?? "";
|
|
685
|
+
if (css) {
|
|
686
|
+
await settings.set(SETTINGS_KEYS.CUSTOM_CSS, css);
|
|
687
|
+
} else {
|
|
688
|
+
await settings.remove(SETTINGS_KEYS.CUSTOM_CSS);
|
|
689
|
+
}
|
|
690
|
+
return dsToast("Custom CSS saved successfully.");
|
|
691
|
+
});
|
|
618
692
|
// Account page
|
|
619
693
|
settingsRoutes.get("/account", async (c)=>{
|
|
620
694
|
const siteName = await getSiteName(c);
|
package/dist/routes/feed/rss.js
CHANGED
|
@@ -40,7 +40,7 @@ export const rssRoutes = new Hono();
|
|
|
40
40
|
// RSS 2.0 Feed - main feed at /feed
|
|
41
41
|
rssRoutes.get("/", async (c)=>{
|
|
42
42
|
const feedData = await buildFeedData(c);
|
|
43
|
-
const renderer = c.var.config.
|
|
43
|
+
const renderer = c.var.config.feed?.rss ?? defaultRssRenderer;
|
|
44
44
|
const xml = renderer(feedData);
|
|
45
45
|
return new Response(xml, {
|
|
46
46
|
headers: {
|
|
@@ -51,7 +51,7 @@ rssRoutes.get("/", async (c)=>{
|
|
|
51
51
|
// Atom Feed
|
|
52
52
|
rssRoutes.get("/atom.xml", async (c)=>{
|
|
53
53
|
const feedData = await buildFeedData(c);
|
|
54
|
-
const renderer = c.var.config.
|
|
54
|
+
const renderer = c.var.config.feed?.atom ?? defaultAtomRenderer;
|
|
55
55
|
const xml = renderer(feedData);
|
|
56
56
|
return new Response(xml, {
|
|
57
57
|
headers: {
|
|
@@ -19,7 +19,7 @@ sitemapRoutes.get("/sitemap.xml", async (c)=>{
|
|
|
19
19
|
const mediaCtx = createMediaContext(c);
|
|
20
20
|
const postViews = toPostViewsFromPosts(posts, mediaCtx);
|
|
21
21
|
const pageViews = publishedPages.map(toPageView);
|
|
22
|
-
const renderer = c.var.config.
|
|
22
|
+
const renderer = c.var.config.feed?.sitemap ?? defaultSitemapRenderer;
|
|
23
23
|
const xml = renderer({
|
|
24
24
|
siteUrl,
|
|
25
25
|
posts: postViews,
|
|
@@ -5,7 +5,7 @@ import { jsx as _jsx } from "hono/jsx/jsx-runtime";
|
|
|
5
5
|
* Shows all posts, optionally filtered by format or featured status
|
|
6
6
|
*/ import { Hono } from "hono";
|
|
7
7
|
import { FORMATS } from "../../types.js";
|
|
8
|
-
import { ArchivePage
|
|
8
|
+
import { ArchivePage } from "../../ui/pages/ArchivePage.js";
|
|
9
9
|
import { getNavigationData } from "../../lib/navigation.js";
|
|
10
10
|
import { renderPublicPage } from "../../lib/render.js";
|
|
11
11
|
import { createMediaContext, toArchiveGroups } from "../../lib/view.js";
|
|
@@ -48,18 +48,15 @@ archiveRoutes.get("/", async (c)=>{
|
|
|
48
48
|
// Transform to View Models
|
|
49
49
|
const mediaCtx = createMediaContext(c);
|
|
50
50
|
const groups = toArchiveGroups(grouped, mediaCtx);
|
|
51
|
-
const components = c.var.config.theme?.components;
|
|
52
|
-
const Page = components?.ArchivePage ?? DefaultArchivePage;
|
|
53
51
|
return renderPublicPage(c, {
|
|
54
52
|
title: `Archive - ${navData.siteName}`,
|
|
55
53
|
navData,
|
|
56
|
-
content: /*#__PURE__*/ _jsx(
|
|
54
|
+
content: /*#__PURE__*/ _jsx(ArchivePage, {
|
|
57
55
|
groups: groups,
|
|
58
56
|
hasMore: hasMore,
|
|
59
57
|
nextCursor: nextCursor,
|
|
60
58
|
format: format,
|
|
61
|
-
featured: featured
|
|
62
|
-
theme: components
|
|
59
|
+
featured: featured
|
|
63
60
|
})
|
|
64
61
|
});
|
|
65
62
|
});
|
|
@@ -2,7 +2,7 @@ import { jsx as _jsx } from "hono/jsx/jsx-runtime";
|
|
|
2
2
|
/**
|
|
3
3
|
* Collection Page Route
|
|
4
4
|
*/ import { Hono } from "hono";
|
|
5
|
-
import { CollectionPage
|
|
5
|
+
import { CollectionPage } from "../../ui/pages/CollectionPage.js";
|
|
6
6
|
import { getNavigationData } from "../../lib/navigation.js";
|
|
7
7
|
import { renderPublicPage } from "../../lib/render.js";
|
|
8
8
|
import { createMediaContext, toPostViewsFromPosts } from "../../lib/view.js";
|
|
@@ -21,17 +21,14 @@ collectionRoutes.get("/:slug", async (c)=>{
|
|
|
21
21
|
// Transform to View Models
|
|
22
22
|
const mediaCtx = createMediaContext(c);
|
|
23
23
|
const postViews = toPostViewsFromPosts(posts, mediaCtx);
|
|
24
|
-
const components = c.var.config.theme?.components;
|
|
25
|
-
const Page = components?.CollectionPage ?? DefaultCollectionPage;
|
|
26
24
|
return renderPublicPage(c, {
|
|
27
25
|
title: `${collection.title} - ${navData.siteName}`,
|
|
28
26
|
description: collection.description ?? undefined,
|
|
29
27
|
navData,
|
|
30
|
-
content: /*#__PURE__*/ _jsx(
|
|
28
|
+
content: /*#__PURE__*/ _jsx(CollectionPage, {
|
|
31
29
|
collection: collection,
|
|
32
30
|
posts: postViews,
|
|
33
|
-
hasMore: false
|
|
34
|
-
theme: components
|
|
31
|
+
hasMore: false
|
|
35
32
|
})
|
|
36
33
|
});
|
|
37
34
|
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { jsx as _jsx } from "hono/jsx/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Collections Listing Page Route
|
|
4
|
+
*
|
|
5
|
+
* Lists all collections with their post counts.
|
|
6
|
+
*/ import { Hono } from "hono";
|
|
7
|
+
import { getNavigationData } from "../../lib/navigation.js";
|
|
8
|
+
import { renderPublicPage } from "../../lib/render.js";
|
|
9
|
+
import { CollectionsPage } from "../../ui/pages/CollectionsPage.js";
|
|
10
|
+
export const collectionsPageRoutes = new Hono();
|
|
11
|
+
collectionsPageRoutes.get("/", async (c)=>{
|
|
12
|
+
const [allCollections, postCounts] = await Promise.all([
|
|
13
|
+
c.var.services.collections.list(),
|
|
14
|
+
c.var.services.collections.getPostCounts()
|
|
15
|
+
]);
|
|
16
|
+
const collections = allCollections.map((col)=>({
|
|
17
|
+
...col,
|
|
18
|
+
postCount: postCounts.get(col.id) ?? 0
|
|
19
|
+
}));
|
|
20
|
+
const navData = await getNavigationData(c);
|
|
21
|
+
return renderPublicPage(c, {
|
|
22
|
+
title: `Collections - ${navData.siteName}`,
|
|
23
|
+
navData,
|
|
24
|
+
content: /*#__PURE__*/ _jsx(CollectionsPage, {
|
|
25
|
+
collections: collections
|
|
26
|
+
})
|
|
27
|
+
});
|
|
28
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { jsx as _jsx } from "hono/jsx/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Featured Page Route
|
|
4
|
+
*
|
|
5
|
+
* Shows featured posts as a timeline feed.
|
|
6
|
+
*/ import { Hono } from "hono";
|
|
7
|
+
import { getNavigationData } from "../../lib/navigation.js";
|
|
8
|
+
import { renderPublicPage } from "../../lib/render.js";
|
|
9
|
+
import { createMediaContext, toPostViewsFromPosts } from "../../lib/view.js";
|
|
10
|
+
import { FeaturedPage } from "../../ui/pages/FeaturedPage.js";
|
|
11
|
+
export const featuredRoutes = new Hono();
|
|
12
|
+
featuredRoutes.get("/", async (c)=>{
|
|
13
|
+
const posts = await c.var.services.posts.list({
|
|
14
|
+
featured: true,
|
|
15
|
+
status: "published",
|
|
16
|
+
excludeReplies: true
|
|
17
|
+
});
|
|
18
|
+
const navData = await getNavigationData(c);
|
|
19
|
+
const mediaCtx = createMediaContext(c);
|
|
20
|
+
const postViews = toPostViewsFromPosts(posts, mediaCtx);
|
|
21
|
+
// Convert to timeline items (simple — no thread previews)
|
|
22
|
+
const items = postViews.map((post)=>({
|
|
23
|
+
post
|
|
24
|
+
}));
|
|
25
|
+
return renderPublicPage(c, {
|
|
26
|
+
title: `Featured - ${navData.siteName}`,
|
|
27
|
+
navData,
|
|
28
|
+
content: /*#__PURE__*/ _jsx(FeaturedPage, {
|
|
29
|
+
items: items
|
|
30
|
+
})
|
|
31
|
+
});
|
|
32
|
+
});
|
|
@@ -3,58 +3,20 @@ import { jsx as _jsx } from "hono/jsx/jsx-runtime";
|
|
|
3
3
|
* Home Page Route
|
|
4
4
|
*
|
|
5
5
|
* Timeline feed with per-type card components and thread previews.
|
|
6
|
-
*
|
|
6
|
+
* Uses page-based pagination.
|
|
7
7
|
*/ import { Hono } from "hono";
|
|
8
8
|
import { getNavigationData } from "../../lib/navigation.js";
|
|
9
9
|
import { renderPublicPage } from "../../lib/render.js";
|
|
10
10
|
import { assembleTimeline } from "../../lib/timeline.js";
|
|
11
|
-
import { sse } from "../../lib/sse.js";
|
|
12
11
|
import { createMediaContext, toPostViewsFromPosts } from "../../lib/view.js";
|
|
13
|
-
import { HomePage
|
|
12
|
+
import { HomePage } from "../../ui/pages/HomePage.js";
|
|
14
13
|
export const homeRoutes = new Hono();
|
|
15
14
|
homeRoutes.get("/", async (c)=>{
|
|
16
|
-
const
|
|
17
|
-
const
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
cursor: cursor && !isNaN(cursor) ? cursor : undefined
|
|
15
|
+
const pageParam = c.req.query("page");
|
|
16
|
+
const page = pageParam ? Math.max(1, parseInt(pageParam, 10) || 1) : 1;
|
|
17
|
+
const { items, currentPage, totalPages } = await assembleTimeline(c, {
|
|
18
|
+
page
|
|
21
19
|
});
|
|
22
|
-
// SSE load-more response
|
|
23
|
-
if (cursor && !isNaN(cursor)) {
|
|
24
|
-
if (items.length === 0) {
|
|
25
|
-
return sse(c, async (stream)=>{
|
|
26
|
-
stream.remove("#load-more-container");
|
|
27
|
-
});
|
|
28
|
-
}
|
|
29
|
-
const themeConfig = c.var.config.theme;
|
|
30
|
-
const renderMore = themeConfig?.timelineMore;
|
|
31
|
-
if (!renderMore) {
|
|
32
|
-
// Should never happen — default theme always provides timelineMore
|
|
33
|
-
return sse(c, async (stream)=>{
|
|
34
|
-
stream.remove("#load-more-container");
|
|
35
|
-
});
|
|
36
|
-
}
|
|
37
|
-
const patches = renderMore({
|
|
38
|
-
items,
|
|
39
|
-
lastDate: lastDate ?? undefined,
|
|
40
|
-
hasMore,
|
|
41
|
-
nextCursor,
|
|
42
|
-
theme: themeConfig?.components
|
|
43
|
-
});
|
|
44
|
-
return sse(c, async (stream)=>{
|
|
45
|
-
for (const patch of patches){
|
|
46
|
-
if (patch.mode === "remove") {
|
|
47
|
-
stream.remove(patch.selector);
|
|
48
|
-
} else {
|
|
49
|
-
stream.patchElements(patch.content, {
|
|
50
|
-
mode: patch.mode,
|
|
51
|
-
selector: patch.selector
|
|
52
|
-
});
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
// Full page render
|
|
58
20
|
const navData = await getNavigationData(c);
|
|
59
21
|
// Fetch pinned posts
|
|
60
22
|
const pinnedPosts = await c.var.services.posts.list({
|
|
@@ -64,17 +26,14 @@ homeRoutes.get("/", async (c)=>{
|
|
|
64
26
|
});
|
|
65
27
|
const mediaCtx = createMediaContext(c);
|
|
66
28
|
const pinnedItems = toPostViewsFromPosts(pinnedPosts, mediaCtx);
|
|
67
|
-
const components = c.var.config.theme?.components;
|
|
68
|
-
const Page = components?.HomePage ?? DefaultHomePage;
|
|
69
29
|
return renderPublicPage(c, {
|
|
70
30
|
title: navData.siteName,
|
|
71
31
|
navData,
|
|
72
|
-
content: /*#__PURE__*/ _jsx(
|
|
32
|
+
content: /*#__PURE__*/ _jsx(HomePage, {
|
|
73
33
|
items: items,
|
|
74
34
|
pinnedItems: pinnedItems,
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
theme: components
|
|
35
|
+
currentPage: currentPage,
|
|
36
|
+
totalPages: totalPages
|
|
78
37
|
})
|
|
79
38
|
});
|
|
80
39
|
});
|
|
@@ -2,44 +2,44 @@ import { jsx as _jsx } from "hono/jsx/jsx-runtime";
|
|
|
2
2
|
/**
|
|
3
3
|
* Custom Page Route
|
|
4
4
|
*
|
|
5
|
-
* Serves pages from the pages table and posts with custom
|
|
5
|
+
* Serves pages from the pages table and posts with custom paths.
|
|
6
6
|
* This is a catch-all route mounted at "/" - must be registered last.
|
|
7
|
+
* Supports multi-level paths (e.g. /2024/my-post) for posts.
|
|
7
8
|
*/ import { Hono } from "hono";
|
|
8
|
-
import { SinglePage
|
|
9
|
-
import { PostPage
|
|
9
|
+
import { SinglePage } from "../../ui/pages/SinglePage.js";
|
|
10
|
+
import { PostPage } from "../../ui/pages/PostPage.js";
|
|
10
11
|
import { getNavigationData } from "../../lib/navigation.js";
|
|
11
12
|
import { renderPublicPage } from "../../lib/render.js";
|
|
12
13
|
import { buildMediaMap } from "../../lib/media-helpers.js";
|
|
13
14
|
import { createMediaContext, toPageView, toPostView } from "../../lib/view.js";
|
|
14
15
|
export const pageRoutes = new Hono();
|
|
15
|
-
// Catch-all for custom page
|
|
16
|
-
pageRoutes.get("
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
16
|
+
// Catch-all for custom page slugs and post paths (including multi-level)
|
|
17
|
+
pageRoutes.get("/*", async (c)=>{
|
|
18
|
+
const fullPath = c.req.path.slice(1); // Remove leading /
|
|
19
|
+
if (!fullPath) return c.notFound();
|
|
20
|
+
const isMultiSegment = fullPath.includes("/");
|
|
21
|
+
// Pages only have single-level slugs; skip page lookup for multi-segment paths
|
|
22
|
+
if (!isMultiSegment) {
|
|
23
|
+
const page = await c.var.services.pages.getBySlug(fullPath);
|
|
24
|
+
if (page) {
|
|
25
|
+
if (page.status === "draft") {
|
|
26
|
+
return c.notFound();
|
|
27
|
+
}
|
|
28
|
+
const navData = await getNavigationData(c);
|
|
29
|
+
const pageView = toPageView(page);
|
|
30
|
+
return renderPublicPage(c, {
|
|
31
|
+
title: `${page.title || fullPath} - ${navData.siteName}`,
|
|
32
|
+
description: page.body?.slice(0, 160),
|
|
33
|
+
navData,
|
|
34
|
+
content: /*#__PURE__*/ _jsx(SinglePage, {
|
|
35
|
+
page: pageView
|
|
36
|
+
})
|
|
37
|
+
});
|
|
24
38
|
}
|
|
25
|
-
const navData = await getNavigationData(c);
|
|
26
|
-
const pageView = toPageView(page);
|
|
27
|
-
const components = c.var.config.theme?.components;
|
|
28
|
-
const Page = components?.SinglePage ?? DefaultSinglePage;
|
|
29
|
-
return renderPublicPage(c, {
|
|
30
|
-
title: `${page.title || slug} - ${navData.siteName}`,
|
|
31
|
-
description: page.body?.slice(0, 160),
|
|
32
|
-
navData,
|
|
33
|
-
content: /*#__PURE__*/ _jsx(Page, {
|
|
34
|
-
page: pageView,
|
|
35
|
-
theme: components
|
|
36
|
-
})
|
|
37
|
-
});
|
|
38
39
|
}
|
|
39
|
-
//
|
|
40
|
-
const post = await c.var.services.posts.
|
|
40
|
+
// Posts support multi-level paths
|
|
41
|
+
const post = await c.var.services.posts.getByPath(fullPath);
|
|
41
42
|
if (post) {
|
|
42
|
-
// Don't show draft posts
|
|
43
43
|
if (post.status === "draft") {
|
|
44
44
|
return c.notFound();
|
|
45
45
|
}
|
|
@@ -55,15 +55,12 @@ pageRoutes.get("/:slug", async (c)=>{
|
|
|
55
55
|
}, mediaCtx);
|
|
56
56
|
const navData = await getNavigationData(c);
|
|
57
57
|
const title = post.title || navData.siteName;
|
|
58
|
-
const components = c.var.config.theme?.components;
|
|
59
|
-
const PostPage = components?.PostPage ?? DefaultPostPage;
|
|
60
58
|
return renderPublicPage(c, {
|
|
61
59
|
title,
|
|
62
60
|
description: post.body?.slice(0, 160),
|
|
63
61
|
navData,
|
|
64
62
|
content: /*#__PURE__*/ _jsx(PostPage, {
|
|
65
|
-
post: postView
|
|
66
|
-
theme: components
|
|
63
|
+
post: postView
|
|
67
64
|
})
|
|
68
65
|
});
|
|
69
66
|
}
|
|
@@ -2,7 +2,7 @@ import { jsx as _jsx } from "hono/jsx/jsx-runtime";
|
|
|
2
2
|
/**
|
|
3
3
|
* Single Post Page Route
|
|
4
4
|
*/ import { Hono } from "hono";
|
|
5
|
-
import { PostPage
|
|
5
|
+
import { PostPage } from "../../ui/pages/PostPage.js";
|
|
6
6
|
import * as sqid from "../../lib/sqid.js";
|
|
7
7
|
import { getNavigationData } from "../../lib/navigation.js";
|
|
8
8
|
import { renderPublicPage } from "../../lib/render.js";
|
|
@@ -33,15 +33,12 @@ postRoutes.get("/:id", async (c)=>{
|
|
|
33
33
|
}, mediaCtx);
|
|
34
34
|
const navData = await getNavigationData(c);
|
|
35
35
|
const title = post.title || navData.siteName;
|
|
36
|
-
const components = c.var.config.theme?.components;
|
|
37
|
-
const Page = components?.PostPage ?? DefaultPostPage;
|
|
38
36
|
return renderPublicPage(c, {
|
|
39
37
|
title,
|
|
40
38
|
description: post.body?.slice(0, 160),
|
|
41
39
|
navData,
|
|
42
|
-
content: /*#__PURE__*/ _jsx(
|
|
43
|
-
post: postView
|
|
44
|
-
theme: components
|
|
40
|
+
content: /*#__PURE__*/ _jsx(PostPage, {
|
|
41
|
+
post: postView
|
|
45
42
|
})
|
|
46
43
|
});
|
|
47
44
|
});
|
|
@@ -2,7 +2,7 @@ import { jsx as _jsx } from "hono/jsx/jsx-runtime";
|
|
|
2
2
|
/**
|
|
3
3
|
* Search Page Route
|
|
4
4
|
*/ import { Hono } from "hono";
|
|
5
|
-
import { SearchPage
|
|
5
|
+
import { SearchPage } from "../../ui/pages/SearchPage.js";
|
|
6
6
|
import { getNavigationData } from "../../lib/navigation.js";
|
|
7
7
|
import { renderPublicPage } from "../../lib/render.js";
|
|
8
8
|
import { createMediaContext, toSearchResultViews } from "../../lib/view.js";
|
|
@@ -40,18 +40,15 @@ searchRoutes.get("/", async (c)=>{
|
|
|
40
40
|
// Transform to View Models
|
|
41
41
|
const mediaCtx = createMediaContext(c);
|
|
42
42
|
const resultViews = toSearchResultViews(results, mediaCtx);
|
|
43
|
-
const components = c.var.config.theme?.components;
|
|
44
|
-
const Page = components?.SearchPage ?? DefaultSearchPage;
|
|
45
43
|
return renderPublicPage(c, {
|
|
46
44
|
title: query ? `Search: ${query} - ${navData.siteName}` : `Search - ${navData.siteName}`,
|
|
47
45
|
navData,
|
|
48
|
-
content: /*#__PURE__*/ _jsx(
|
|
46
|
+
content: /*#__PURE__*/ _jsx(SearchPage, {
|
|
49
47
|
query: query,
|
|
50
48
|
results: resultViews,
|
|
51
49
|
error: error,
|
|
52
50
|
hasMore: hasMore,
|
|
53
|
-
page: page
|
|
54
|
-
theme: components
|
|
51
|
+
page: page
|
|
55
52
|
})
|
|
56
53
|
});
|
|
57
54
|
});
|
package/dist/services/page.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Page Service
|
|
3
3
|
*
|
|
4
4
|
* CRUD operations for standalone pages (about, now, etc.)
|
|
5
|
-
*/ import { eq, desc } from "drizzle-orm";
|
|
5
|
+
*/ import { eq, desc, sql } from "drizzle-orm";
|
|
6
6
|
import { pages, navItems } from "../db/schema.js";
|
|
7
7
|
import { now } from "../lib/time.js";
|
|
8
8
|
import { render as renderMarkdown } from "../lib/markdown.js";
|
|
@@ -32,6 +32,10 @@ export function createPageService(db) {
|
|
|
32
32
|
const rows = await db.select().from(pages).orderBy(desc(pages.createdAt));
|
|
33
33
|
return rows.map(toPage);
|
|
34
34
|
},
|
|
35
|
+
async listNotInNav () {
|
|
36
|
+
const rows = await db.select().from(pages).where(sql`${pages.id} NOT IN (SELECT ${navItems.pageId} FROM ${navItems} WHERE ${navItems.pageId} IS NOT NULL)`).orderBy(desc(pages.createdAt));
|
|
37
|
+
return rows.map(toPage);
|
|
38
|
+
},
|
|
35
39
|
async create (data) {
|
|
36
40
|
const timestamp = now();
|
|
37
41
|
const bodyHtml = data.body ? renderMarkdown(data.body) : null;
|
package/dist/services/post.js
CHANGED
|
@@ -16,7 +16,7 @@ export function createPostService(db) {
|
|
|
16
16
|
status: row.status,
|
|
17
17
|
featured: row.featured,
|
|
18
18
|
pinned: row.pinned,
|
|
19
|
-
|
|
19
|
+
path: row.path,
|
|
20
20
|
title: row.title,
|
|
21
21
|
url: row.url,
|
|
22
22
|
body: row.body,
|
|
@@ -37,8 +37,8 @@ export function createPostService(db) {
|
|
|
37
37
|
const result = await db.select().from(posts).where(and(eq(posts.id, id), isNull(posts.deletedAt))).limit(1);
|
|
38
38
|
return result[0] ? toPost(result[0]) : null;
|
|
39
39
|
},
|
|
40
|
-
async
|
|
41
|
-
const result = await db.select().from(posts).where(and(eq(posts.
|
|
40
|
+
async getByPath (path) {
|
|
41
|
+
const result = await db.select().from(posts).where(and(eq(posts.path, path), isNull(posts.deletedAt))).limit(1);
|
|
42
42
|
return result[0] ? toPost(result[0]) : null;
|
|
43
43
|
},
|
|
44
44
|
async list (filters = {}) {
|
|
@@ -70,10 +70,44 @@ export function createPostService(db) {
|
|
|
70
70
|
if (filters.cursor) {
|
|
71
71
|
conditions.push(sql`${posts.id} < ${filters.cursor}`);
|
|
72
72
|
}
|
|
73
|
-
|
|
73
|
+
let query = db.select().from(posts).where(conditions.length > 0 ? and(...conditions) : undefined).orderBy(desc(posts.publishedAt), desc(posts.id)).limit(filters.limit ?? 100);
|
|
74
|
+
if (filters.offset !== undefined) {
|
|
75
|
+
query = query.offset(filters.offset);
|
|
76
|
+
}
|
|
74
77
|
const rows = await query;
|
|
75
78
|
return rows.map(toPost);
|
|
76
79
|
},
|
|
80
|
+
async count (filters = {}) {
|
|
81
|
+
const conditions = [];
|
|
82
|
+
if (filters.status) {
|
|
83
|
+
conditions.push(eq(posts.status, filters.status));
|
|
84
|
+
}
|
|
85
|
+
if (filters.featured !== undefined) {
|
|
86
|
+
conditions.push(eq(posts.featured, filters.featured ? 1 : 0));
|
|
87
|
+
}
|
|
88
|
+
if (filters.pinned !== undefined) {
|
|
89
|
+
conditions.push(eq(posts.pinned, filters.pinned ? 1 : 0));
|
|
90
|
+
}
|
|
91
|
+
if (filters.format) {
|
|
92
|
+
conditions.push(eq(posts.format, filters.format));
|
|
93
|
+
}
|
|
94
|
+
if (filters.collectionId !== undefined) {
|
|
95
|
+
conditions.push(eq(posts.collectionId, filters.collectionId));
|
|
96
|
+
}
|
|
97
|
+
if (filters.threadId) {
|
|
98
|
+
conditions.push(eq(posts.threadId, filters.threadId));
|
|
99
|
+
}
|
|
100
|
+
if (filters.excludeReplies) {
|
|
101
|
+
conditions.push(isNull(posts.threadId));
|
|
102
|
+
}
|
|
103
|
+
if (!filters.includeDeleted) {
|
|
104
|
+
conditions.push(isNull(posts.deletedAt));
|
|
105
|
+
}
|
|
106
|
+
const result = await db.select({
|
|
107
|
+
count: sql`count(*)`.as("count")
|
|
108
|
+
}).from(posts).where(conditions.length > 0 ? and(...conditions) : undefined);
|
|
109
|
+
return result[0]?.count ?? 0;
|
|
110
|
+
},
|
|
77
111
|
async create (data) {
|
|
78
112
|
const timestamp = now();
|
|
79
113
|
const bodyHtml = data.body ? renderMarkdown(data.body) : null;
|
|
@@ -98,7 +132,7 @@ export function createPostService(db) {
|
|
|
98
132
|
status,
|
|
99
133
|
featured: featured ? 1 : 0,
|
|
100
134
|
pinned: data.pinned ? 1 : 0,
|
|
101
|
-
|
|
135
|
+
path: data.path ?? null,
|
|
102
136
|
title: data.title ?? null,
|
|
103
137
|
url: data.url ?? null,
|
|
104
138
|
body: data.body ?? null,
|
|
@@ -123,7 +157,7 @@ export function createPostService(db) {
|
|
|
123
157
|
updatedAt: timestamp
|
|
124
158
|
};
|
|
125
159
|
if (data.format !== undefined) updates.format = data.format;
|
|
126
|
-
if (data.
|
|
160
|
+
if (data.path !== undefined) updates.path = data.path;
|
|
127
161
|
if (data.title !== undefined) updates.title = data.title;
|
|
128
162
|
if (data.url !== undefined) updates.url = data.url;
|
|
129
163
|
if (data.quoteText !== undefined) updates.quoteText = data.quoteText;
|