@jant/core 0.3.27 → 0.3.28
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/client/client.css +1 -0
- package/dist/client/client.js +31561 -0
- package/dist/index.js +15209 -15
- package/package.json +21 -15
- package/src/__tests__/helpers/app.ts +19 -3
- package/src/__tests__/helpers/db.ts +44 -0
- package/src/__tests__/helpers/lingui-core-macro-mock.ts +33 -0
- package/src/app.tsx +111 -174
- package/src/client.ts +13 -0
- package/src/db/migrations/0007_post_collections_m2m.sql +94 -0
- package/src/db/migrations/0008_add_collection_dividers.sql +8 -0
- package/src/db/migrations/0009_drop_collection_show_divider.sql +2 -0
- package/src/db/migrations/0010_add_performance_indexes.sql +16 -0
- package/src/db/schema.ts +24 -4
- package/src/i18n/locales/en.po +810 -385
- package/src/i18n/locales/en.ts +1 -1
- package/src/i18n/locales/zh-Hans.po +733 -522
- package/src/i18n/locales/zh-Hans.ts +1 -1
- package/src/i18n/locales/zh-Hant.po +733 -522
- package/src/i18n/locales/zh-Hant.ts +1 -1
- package/src/i18n/middleware.ts +7 -11
- package/src/index.ts +1 -1
- package/src/lib/__tests__/icons.test.ts +178 -0
- package/src/lib/__tests__/resolve-config.test.ts +184 -0
- package/src/lib/__tests__/schemas.test.ts +12 -6
- package/src/lib/__tests__/theme.test.ts +62 -0
- package/src/lib/__tests__/timezones.test.ts +1 -1
- package/src/lib/__tests__/url.test.ts +12 -0
- package/src/lib/__tests__/view.test.ts +1 -5
- package/src/lib/avatar-upload.ts +18 -10
- package/src/lib/collection-form-bridge.ts +52 -0
- package/src/lib/collections-reorder.ts +28 -0
- package/src/lib/compose-bridge.ts +251 -0
- package/src/lib/errors.ts +116 -0
- package/src/lib/excerpt.ts +1 -1
- package/src/lib/favicon.ts +3 -5
- package/src/lib/html.ts +22 -0
- package/src/lib/icon-catalog.ts +181 -0
- package/src/lib/icons.ts +202 -0
- package/src/lib/navigation.ts +18 -33
- package/src/lib/pagination.ts +3 -2
- package/src/lib/post-form-bridge.ts +136 -0
- package/src/lib/render.tsx +11 -4
- package/src/lib/resolve-config.ts +157 -0
- package/src/lib/schemas.ts +76 -12
- package/src/lib/settings-bridge.ts +139 -0
- package/src/lib/storage.ts +37 -16
- package/src/lib/theme.ts +5 -7
- package/src/lib/timeline.ts +4 -8
- package/src/lib/toast.ts +134 -0
- package/src/lib/upload.ts +71 -0
- package/src/lib/url.ts +9 -1
- package/src/lib/version.ts +16 -0
- package/src/lib/view.ts +9 -10
- package/src/middleware/__tests__/auth.test.ts +6 -28
- package/src/middleware/__tests__/onboarding.test.ts +1 -1
- package/src/middleware/auth.ts +6 -12
- package/src/middleware/config.ts +51 -0
- package/src/middleware/error-handler.ts +56 -0
- package/src/middleware/onboarding.ts +1 -1
- package/src/preset.css +6 -0
- package/src/routes/__tests__/compose.test.ts +104 -17
- package/src/routes/api/__tests__/collections.test.ts +93 -2
- package/src/routes/api/__tests__/posts.test.ts +2 -1
- package/src/routes/api/__tests__/settings.test.ts +1 -1
- package/src/routes/api/collections.ts +64 -68
- package/src/routes/api/nav-items.ts +21 -59
- package/src/routes/api/pages.ts +18 -46
- package/src/routes/api/posts.ts +64 -86
- package/src/routes/api/search.ts +6 -4
- package/src/routes/api/settings.ts +8 -24
- package/src/routes/api/upload.ts +55 -53
- package/src/routes/auth/__tests__/setup.test.ts +118 -0
- package/src/routes/auth/reset.tsx +17 -66
- package/src/routes/auth/setup.tsx +67 -11
- package/src/routes/auth/signin.tsx +44 -8
- package/src/routes/compose.tsx +194 -0
- package/src/routes/dash/__tests__/font-theme.test.ts +110 -0
- package/src/routes/dash/__tests__/pages.test.ts +2 -2
- package/src/routes/dash/__tests__/settings-avatar.test.ts +23 -12
- package/src/routes/dash/appearance.tsx +173 -0
- package/src/routes/dash/collections.tsx +80 -14
- package/src/routes/dash/index.tsx +12 -14
- package/src/routes/dash/media.tsx +46 -49
- package/src/routes/dash/pages.tsx +85 -37
- package/src/routes/dash/posts.tsx +60 -23
- package/src/routes/dash/redirects.tsx +43 -33
- package/src/routes/dash/settings.tsx +234 -214
- package/src/routes/feed/__tests__/rss.test.ts +7 -3
- package/src/routes/feed/rss.ts +11 -16
- package/src/routes/feed/sitemap.ts +15 -9
- package/src/routes/pages/__tests__/collections.test.ts +9 -8
- package/src/routes/pages/archive.tsx +2 -2
- package/src/routes/pages/collection.tsx +76 -9
- package/src/routes/pages/collections.tsx +3 -1
- package/src/routes/pages/featured.tsx +2 -2
- package/src/routes/pages/home.tsx +3 -3
- package/src/routes/pages/latest.tsx +2 -2
- package/src/routes/pages/page.tsx +2 -2
- package/src/routes/pages/post.tsx +2 -2
- package/src/routes/pages/search.tsx +2 -2
- package/src/services/__tests__/collection.test.ts +324 -34
- package/src/services/__tests__/media.test.ts +1 -1
- package/src/services/__tests__/page.test.ts +116 -1
- package/src/services/auth.ts +88 -0
- package/src/services/collection.ts +169 -30
- package/src/services/index.ts +8 -3
- package/src/services/media.ts +39 -12
- package/src/services/navigation.ts +17 -5
- package/src/services/page.ts +24 -4
- package/src/services/post.ts +87 -19
- package/src/services/search.ts +0 -1
- package/src/services/settings.ts +21 -13
- package/src/style.css +3 -0
- package/src/styles/components.css +42 -1
- package/src/styles/tokens.css +4 -0
- package/src/styles/ui.css +902 -73
- package/src/types/app-context.ts +25 -0
- package/src/types/bindings.ts +1 -0
- package/src/types/config.ts +60 -23
- package/src/types/entities.ts +12 -2
- package/src/types/lingui-react-macro.d.ts +3 -3
- package/src/types/operations.ts +2 -4
- package/src/types/views.ts +1 -3
- package/src/ui/__tests__/font-themes.test.ts +27 -8
- package/src/ui/color-themes.ts +1 -1
- package/src/ui/components/__tests__/jant-collection-form.test.ts +153 -0
- package/src/ui/components/__tests__/jant-compose-dialog.test.ts +512 -0
- package/src/ui/components/__tests__/jant-compose-editor.test.ts +272 -0
- package/src/ui/components/__tests__/jant-post-form.test.ts +172 -0
- package/src/ui/components/__tests__/jant-settings-avatar.test.ts +235 -0
- package/src/ui/components/__tests__/jant-settings-general.test.ts +319 -0
- package/src/ui/components/collection-types.ts +45 -0
- package/src/ui/components/compose-types.ts +75 -0
- package/src/ui/components/jant-collection-form.ts +512 -0
- package/src/ui/components/jant-compose-dialog.ts +494 -0
- package/src/ui/components/jant-compose-editor.ts +799 -0
- package/src/ui/components/jant-post-form.ts +290 -0
- package/src/ui/components/jant-settings-avatar.ts +231 -0
- package/src/ui/components/jant-settings-general.ts +436 -0
- package/src/ui/components/post-form-template.ts +260 -0
- package/src/ui/components/post-form-types.ts +87 -0
- package/src/ui/components/settings-types.ts +62 -0
- package/src/ui/compose/ComposeDialog.tsx +141 -385
- package/src/ui/compose/ComposePrompt.tsx +3 -3
- package/src/ui/dash/PostList.tsx +55 -61
- package/src/ui/dash/appearance/AdvancedContent.tsx +80 -0
- package/src/ui/dash/appearance/AppearanceNav.tsx +56 -0
- package/src/ui/dash/appearance/ColorThemeContent.tsx +129 -0
- package/src/ui/dash/appearance/FontThemeContent.tsx +98 -0
- package/src/ui/dash/collections/CollectionForm.tsx +130 -117
- package/src/ui/dash/collections/CollectionsListContent.tsx +102 -41
- package/src/ui/dash/collections/IconPickerGrid.tsx +50 -0
- package/src/ui/dash/collections/ViewCollectionContent.tsx +14 -3
- package/src/ui/dash/index.ts +1 -1
- package/src/ui/dash/posts/PostForm.tsx +248 -0
- package/src/ui/dash/settings/AccountContent.tsx +69 -80
- package/src/ui/dash/settings/GeneralContent.tsx +159 -478
- package/src/ui/dash/settings/SettingsNav.tsx +4 -4
- package/src/ui/font-themes.ts +115 -32
- package/src/ui/layouts/BaseLayout.tsx +49 -19
- package/src/ui/layouts/DashLayout.tsx +14 -9
- package/src/ui/layouts/SiteLayout.tsx +38 -23
- package/src/ui/pages/CollectionPage.tsx +12 -2
- package/src/ui/pages/CollectionsPage.tsx +27 -27
- package/src/ui/pages/HomePage.tsx +15 -6
- package/src/ui/pages/SearchPage.tsx +1 -2
- package/src/ui/shared/CollectionsSidebar.tsx +59 -0
- package/src/ui/shared/Pagination.tsx +2 -2
- package/dist/app.js +0 -267
- package/dist/auth.js +0 -39
- package/dist/client.js +0 -13
- package/dist/db/index.js +0 -10
- package/dist/db/schema.js +0 -224
- package/dist/i18n/Trans.js +0 -24
- package/dist/i18n/context.js +0 -58
- package/dist/i18n/detect.js +0 -26
- package/dist/i18n/i18n.js +0 -49
- package/dist/i18n/index.js +0 -44
- package/dist/i18n/locales/en.js +0 -1
- package/dist/i18n/locales/zh-Hans.js +0 -1
- package/dist/i18n/locales/zh-Hant.js +0 -1
- package/dist/i18n/locales.js +0 -13
- package/dist/i18n/middleware.js +0 -30
- package/dist/lib/avatar-upload.js +0 -134
- package/dist/lib/config.js +0 -143
- package/dist/lib/constants.js +0 -50
- package/dist/lib/excerpt.js +0 -76
- package/dist/lib/favicon.js +0 -102
- package/dist/lib/feed.js +0 -123
- package/dist/lib/image-processor.js +0 -187
- package/dist/lib/image.js +0 -97
- package/dist/lib/index.js +0 -7
- package/dist/lib/markdown.js +0 -83
- package/dist/lib/media-helpers.js +0 -49
- package/dist/lib/media-upload.js +0 -104
- package/dist/lib/nav-reorder.js +0 -27
- package/dist/lib/navigation.js +0 -79
- package/dist/lib/pagination.js +0 -44
- package/dist/lib/render.js +0 -53
- package/dist/lib/schemas.js +0 -174
- package/dist/lib/sqid.js +0 -72
- package/dist/lib/sse.js +0 -218
- package/dist/lib/storage.js +0 -164
- package/dist/lib/theme.js +0 -65
- package/dist/lib/time.js +0 -159
- package/dist/lib/timeline.js +0 -95
- package/dist/lib/timezones.js +0 -388
- package/dist/lib/url.js +0 -89
- package/dist/lib/view.js +0 -217
- package/dist/middleware/auth.js +0 -52
- package/dist/middleware/onboarding.js +0 -41
- package/dist/routes/api/collections.js +0 -124
- package/dist/routes/api/nav-items.js +0 -104
- package/dist/routes/api/pages.js +0 -91
- package/dist/routes/api/posts.js +0 -218
- package/dist/routes/api/search.js +0 -48
- package/dist/routes/api/settings.js +0 -68
- package/dist/routes/api/upload.js +0 -246
- package/dist/routes/auth/reset.js +0 -221
- package/dist/routes/auth/setup.js +0 -194
- package/dist/routes/auth/signin.js +0 -176
- package/dist/routes/compose.js +0 -48
- package/dist/routes/dash/collections.js +0 -115
- package/dist/routes/dash/index.js +0 -118
- package/dist/routes/dash/media.js +0 -106
- package/dist/routes/dash/pages.js +0 -294
- package/dist/routes/dash/posts.js +0 -244
- package/dist/routes/dash/redirects.js +0 -257
- package/dist/routes/dash/settings.js +0 -379
- package/dist/routes/feed/rss.js +0 -62
- package/dist/routes/feed/sitemap.js +0 -49
- package/dist/routes/pages/archive.js +0 -62
- package/dist/routes/pages/collection.js +0 -34
- package/dist/routes/pages/collections.js +0 -28
- package/dist/routes/pages/featured.js +0 -36
- package/dist/routes/pages/home.js +0 -64
- package/dist/routes/pages/latest.js +0 -45
- package/dist/routes/pages/page.js +0 -68
- package/dist/routes/pages/post.js +0 -44
- package/dist/routes/pages/search.js +0 -54
- package/dist/services/collection.js +0 -109
- package/dist/services/index.js +0 -24
- package/dist/services/media.js +0 -117
- package/dist/services/navigation.js +0 -91
- package/dist/services/page.js +0 -84
- package/dist/services/post.js +0 -229
- package/dist/services/redirect.js +0 -48
- package/dist/services/search.js +0 -67
- package/dist/services/settings.js +0 -68
- package/dist/types/bindings.js +0 -3
- package/dist/types/config.js +0 -147
- package/dist/types/constants.js +0 -27
- package/dist/types/entities.js +0 -3
- package/dist/types/lingui-react-macro.d.js +0 -9
- package/dist/types/operations.js +0 -3
- package/dist/types/props.js +0 -3
- package/dist/types/sortablejs.d.js +0 -5
- package/dist/types/views.js +0 -5
- package/dist/types.js +0 -11
- package/dist/ui/color-themes.js +0 -268
- package/dist/ui/compose/ComposeDialog.js +0 -467
- package/dist/ui/compose/ComposePrompt.js +0 -55
- package/dist/ui/dash/ActionButtons.js +0 -46
- package/dist/ui/dash/CrudPageHeader.js +0 -22
- package/dist/ui/dash/DangerZone.js +0 -36
- package/dist/ui/dash/FormatBadge.js +0 -27
- package/dist/ui/dash/ListItemRow.js +0 -21
- package/dist/ui/dash/PageForm.js +0 -195
- package/dist/ui/dash/PostForm.js +0 -395
- package/dist/ui/dash/PostList.js +0 -83
- package/dist/ui/dash/StatusBadge.js +0 -46
- package/dist/ui/dash/collections/CollectionForm.js +0 -152
- package/dist/ui/dash/collections/CollectionsListContent.js +0 -68
- package/dist/ui/dash/collections/ViewCollectionContent.js +0 -96
- package/dist/ui/dash/index.js +0 -10
- package/dist/ui/dash/media/MediaListContent.js +0 -166
- package/dist/ui/dash/media/ViewMediaContent.js +0 -212
- package/dist/ui/dash/pages/LinkFormContent.js +0 -130
- package/dist/ui/dash/pages/UnifiedPagesContent.js +0 -193
- package/dist/ui/dash/settings/AccountContent.js +0 -209
- package/dist/ui/dash/settings/AppearanceContent.js +0 -259
- package/dist/ui/dash/settings/GeneralContent.js +0 -536
- package/dist/ui/dash/settings/SettingsNav.js +0 -41
- package/dist/ui/feed/LinkCard.js +0 -72
- package/dist/ui/feed/NoteCard.js +0 -58
- package/dist/ui/feed/QuoteCard.js +0 -63
- package/dist/ui/feed/ThreadPreview.js +0 -48
- package/dist/ui/feed/TimelineFeed.js +0 -41
- package/dist/ui/feed/TimelineItem.js +0 -27
- package/dist/ui/font-themes.js +0 -36
- package/dist/ui/layouts/BaseLayout.js +0 -153
- package/dist/ui/layouts/DashLayout.js +0 -141
- package/dist/ui/layouts/SiteLayout.js +0 -169
- package/dist/ui/pages/ArchivePage.js +0 -143
- package/dist/ui/pages/CollectionPage.js +0 -70
- package/dist/ui/pages/CollectionsPage.js +0 -76
- package/dist/ui/pages/FeaturedPage.js +0 -24
- package/dist/ui/pages/HomePage.js +0 -24
- package/dist/ui/pages/PostPage.js +0 -55
- package/dist/ui/pages/SearchPage.js +0 -122
- package/dist/ui/pages/SinglePage.js +0 -23
- package/dist/ui/shared/EmptyState.js +0 -27
- package/dist/ui/shared/MediaGallery.js +0 -35
- package/dist/ui/shared/Pagination.js +0 -195
- package/dist/ui/shared/ThreadView.js +0 -108
- package/dist/ui/shared/index.js +0 -5
- package/dist/vendor/datastar.js +0 -1606
- package/src/lib/__tests__/config.test.ts +0 -192
- package/src/lib/config.ts +0 -167
- package/src/routes/compose.ts +0 -63
- package/src/ui/dash/PostForm.tsx +0 -360
- package/src/ui/dash/settings/AppearanceContent.tsx +0 -254
package/dist/lib/sse.js
DELETED
|
@@ -1,218 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Datastar response utilities for v1.0.0-RC.7
|
|
3
|
-
*
|
|
4
|
-
* Provides both SSE (multi-event) and plain HTTP (single-event) response helpers.
|
|
5
|
-
*
|
|
6
|
-
* **Non-SSE helpers** (preferred for single operations):
|
|
7
|
-
* - `dsRedirect(url)` — redirect via text/html
|
|
8
|
-
* - `dsToast(message, type)` — toast notification via text/html
|
|
9
|
-
* - `dsSignals(signals)` — signal patch via application/json
|
|
10
|
-
*
|
|
11
|
-
* **SSE** (for multiple operations in one response):
|
|
12
|
-
* - `sse(c, handler)` — streaming SSE with full stream API
|
|
13
|
-
*
|
|
14
|
-
* Datastar auto-detects response type by Content-Type:
|
|
15
|
-
* - `text/html` → dispatches as `datastar-patch-elements`
|
|
16
|
-
* - `application/json` → dispatches as `datastar-patch-signals`
|
|
17
|
-
*
|
|
18
|
-
* @see https://data-star.dev/
|
|
19
|
-
*/ // ---------------------------------------------------------------------------
|
|
20
|
-
// Shared internal helpers (used by both SSE and non-SSE response builders)
|
|
21
|
-
// ---------------------------------------------------------------------------
|
|
22
|
-
/** Build the redirect script tag for Datastar patch-elements */ function buildRedirectScript(url) {
|
|
23
|
-
const escapedUrl = url.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
|
|
24
|
-
return `<script data-effect="el.remove()">window.location.href='${escapedUrl}'</script>`;
|
|
25
|
-
}
|
|
26
|
-
/** Build a toast notification HTML element */ function buildToastHtml(message, type) {
|
|
27
|
-
const cls = type === "error" ? "toast-error" : "toast-success";
|
|
28
|
-
const icon = type === "error" ? '<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"><circle cx="12" cy="12" r="10"/><path d="m15 9-6 6M9 9l6 6"/></svg>' : '<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"><circle cx="12" cy="12" r="10"/><path d="m9 12 2 2 4-4"/></svg>';
|
|
29
|
-
const closeBtn = `<button class="toast-close" data-on:click="el.closest('.toast').classList.add('toast-out'); el.closest('.toast').addEventListener('animationend', () => el.closest('.toast').remove())"><svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"><path d="M18 6 6 18M6 6l12 12"/></svg></button>`;
|
|
30
|
-
const escapedMessage = message.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
31
|
-
return `<div class="toast ${cls}" data-init="setTimeout(() => { el.classList.add('toast-out'); el.addEventListener('animationend', () => el.remove()) }, 3000)">${icon}<span>${escapedMessage}</span>${closeBtn}</div>`;
|
|
32
|
-
}
|
|
33
|
-
// ---------------------------------------------------------------------------
|
|
34
|
-
// SSE helpers
|
|
35
|
-
// ---------------------------------------------------------------------------
|
|
36
|
-
/**
|
|
37
|
-
* Format a single SSE event string
|
|
38
|
-
*
|
|
39
|
-
* @param eventType - The Datastar event type (e.g. "datastar-patch-elements")
|
|
40
|
-
* @param dataLines - Array of "key value" data lines
|
|
41
|
-
* @returns Formatted SSE event string
|
|
42
|
-
*/ function formatEvent(eventType, dataLines) {
|
|
43
|
-
let event = `event: ${eventType}\n`;
|
|
44
|
-
for (const line of dataLines){
|
|
45
|
-
event += `data: ${line}\n`;
|
|
46
|
-
}
|
|
47
|
-
event += "\n";
|
|
48
|
-
return event;
|
|
49
|
-
}
|
|
50
|
-
/**
|
|
51
|
-
* Create an SSE response for Datastar
|
|
52
|
-
*
|
|
53
|
-
* @param c - Hono context
|
|
54
|
-
* @param handler - Async function that writes to the SSE stream
|
|
55
|
-
* @param options - Optional response options (e.g. headers for cookie forwarding)
|
|
56
|
-
* @returns Response with SSE content-type
|
|
57
|
-
*
|
|
58
|
-
* @example
|
|
59
|
-
* ```ts
|
|
60
|
-
* app.post("/api/upload", (c) => {
|
|
61
|
-
* return sse(c, async (stream) => {
|
|
62
|
-
* await stream.patchSignals({ uploading: false });
|
|
63
|
-
* await stream.patchElements('<div id="new-item">...</div>', {
|
|
64
|
-
* mode: 'append',
|
|
65
|
-
* selector: '#items'
|
|
66
|
-
* });
|
|
67
|
-
* });
|
|
68
|
-
* });
|
|
69
|
-
*
|
|
70
|
-
* // With cookie forwarding (for auth)
|
|
71
|
-
* app.post("/signin", (c) => {
|
|
72
|
-
* return sse(c, async (stream) => {
|
|
73
|
-
* await stream.redirect('/dash');
|
|
74
|
-
* }, { headers: { 'Set-Cookie': cookieValue } });
|
|
75
|
-
* });
|
|
76
|
-
* ```
|
|
77
|
-
*/ export function sse(c, handler, options) {
|
|
78
|
-
const encoder = new TextEncoder();
|
|
79
|
-
const body = new ReadableStream({
|
|
80
|
-
async start (controller) {
|
|
81
|
-
const stream = {
|
|
82
|
-
patchSignals (signals, opts) {
|
|
83
|
-
const dataLines = [
|
|
84
|
-
`signals ${JSON.stringify(signals)}`
|
|
85
|
-
];
|
|
86
|
-
if (opts?.onlyIfMissing) {
|
|
87
|
-
dataLines.push("onlyIfMissing true");
|
|
88
|
-
}
|
|
89
|
-
controller.enqueue(encoder.encode(formatEvent("datastar-patch-signals", dataLines)));
|
|
90
|
-
},
|
|
91
|
-
patchElements (html, opts) {
|
|
92
|
-
const dataLines = [];
|
|
93
|
-
// Each line of HTML gets its own "elements <line>" data line
|
|
94
|
-
for (const line of html.split("\n")){
|
|
95
|
-
dataLines.push(`elements ${line}`);
|
|
96
|
-
}
|
|
97
|
-
if (opts?.mode) {
|
|
98
|
-
dataLines.push(`mode ${opts.mode}`);
|
|
99
|
-
}
|
|
100
|
-
if (opts?.selector) {
|
|
101
|
-
dataLines.push(`selector ${opts.selector}`);
|
|
102
|
-
}
|
|
103
|
-
if (opts?.useViewTransition) {
|
|
104
|
-
dataLines.push("useViewTransition true");
|
|
105
|
-
}
|
|
106
|
-
controller.enqueue(encoder.encode(formatEvent("datastar-patch-elements", dataLines)));
|
|
107
|
-
},
|
|
108
|
-
redirect (url) {
|
|
109
|
-
const dataLines = [
|
|
110
|
-
`elements ${buildRedirectScript(url)}`,
|
|
111
|
-
"mode append",
|
|
112
|
-
"selector body"
|
|
113
|
-
];
|
|
114
|
-
controller.enqueue(encoder.encode(formatEvent("datastar-patch-elements", dataLines)));
|
|
115
|
-
},
|
|
116
|
-
remove (selector) {
|
|
117
|
-
controller.enqueue(encoder.encode(formatEvent("datastar-patch-elements", [
|
|
118
|
-
"elements ",
|
|
119
|
-
`mode remove`,
|
|
120
|
-
`selector ${selector}`
|
|
121
|
-
])));
|
|
122
|
-
},
|
|
123
|
-
toast (message, type = "success") {
|
|
124
|
-
const dataLines = [
|
|
125
|
-
`elements ${buildToastHtml(message, type)}`,
|
|
126
|
-
"mode append",
|
|
127
|
-
"selector #toast-container"
|
|
128
|
-
];
|
|
129
|
-
controller.enqueue(encoder.encode(formatEvent("datastar-patch-elements", dataLines)));
|
|
130
|
-
}
|
|
131
|
-
};
|
|
132
|
-
await handler(stream);
|
|
133
|
-
controller.close();
|
|
134
|
-
}
|
|
135
|
-
});
|
|
136
|
-
const headers = {
|
|
137
|
-
"Content-Type": "text/event-stream",
|
|
138
|
-
"Cache-Control": "no-cache",
|
|
139
|
-
Connection: "keep-alive",
|
|
140
|
-
...options?.headers
|
|
141
|
-
};
|
|
142
|
-
return new Response(body, {
|
|
143
|
-
headers
|
|
144
|
-
});
|
|
145
|
-
}
|
|
146
|
-
// ---------------------------------------------------------------------------
|
|
147
|
-
// Non-SSE Datastar helpers (for single-operation responses)
|
|
148
|
-
// ---------------------------------------------------------------------------
|
|
149
|
-
/**
|
|
150
|
-
* Datastar redirect via text/html
|
|
151
|
-
*
|
|
152
|
-
* Returns a plain HTML response that Datastar dispatches as `datastar-patch-elements`.
|
|
153
|
-
* Use instead of `sse()` when the only action is a redirect.
|
|
154
|
-
*
|
|
155
|
-
* @param url - The URL to redirect to
|
|
156
|
-
* @param options - Optional extra headers (accepts any `HeadersInit`)
|
|
157
|
-
* @returns Response with text/html content-type
|
|
158
|
-
*
|
|
159
|
-
* @example
|
|
160
|
-
* ```ts
|
|
161
|
-
* return dsRedirect("/dash/posts");
|
|
162
|
-
*
|
|
163
|
-
* // With cookie forwarding (for auth)
|
|
164
|
-
* return dsRedirect("/dash", { headers: authResponse.headers });
|
|
165
|
-
* ```
|
|
166
|
-
*/ export function dsRedirect(url, options) {
|
|
167
|
-
const headers = options?.headers ? new Headers(options.headers) : new Headers();
|
|
168
|
-
headers.set("Content-Type", "text/html");
|
|
169
|
-
headers.set("Datastar-Mode", "append");
|
|
170
|
-
headers.set("Datastar-Selector", "body");
|
|
171
|
-
return new Response(buildRedirectScript(url), {
|
|
172
|
-
headers
|
|
173
|
-
});
|
|
174
|
-
}
|
|
175
|
-
/**
|
|
176
|
-
* Datastar toast notification via text/html
|
|
177
|
-
*
|
|
178
|
-
* Returns a plain HTML response that Datastar dispatches as `datastar-patch-elements`.
|
|
179
|
-
* Use instead of `sse()` when the only action is showing a toast.
|
|
180
|
-
*
|
|
181
|
-
* @param message - The message to display
|
|
182
|
-
* @param type - Toast type: "success" (default) or "error"
|
|
183
|
-
* @returns Response with text/html content-type
|
|
184
|
-
*
|
|
185
|
-
* @example
|
|
186
|
-
* ```ts
|
|
187
|
-
* return dsToast("Settings saved successfully.");
|
|
188
|
-
* return dsToast("Something went wrong.", "error");
|
|
189
|
-
* ```
|
|
190
|
-
*/ export function dsToast(message, type = "success") {
|
|
191
|
-
return new Response(buildToastHtml(message, type), {
|
|
192
|
-
headers: {
|
|
193
|
-
"Content-Type": "text/html",
|
|
194
|
-
"Datastar-Mode": "append",
|
|
195
|
-
"Datastar-Selector": "#toast-container"
|
|
196
|
-
}
|
|
197
|
-
});
|
|
198
|
-
}
|
|
199
|
-
/**
|
|
200
|
-
* Datastar signal patch via application/json
|
|
201
|
-
*
|
|
202
|
-
* Returns a JSON response that Datastar dispatches as `datastar-patch-signals`.
|
|
203
|
-
* Use instead of `sse()` when the only action is updating signals.
|
|
204
|
-
*
|
|
205
|
-
* @param signals - Object containing signal values to update
|
|
206
|
-
* @returns Response with application/json content-type
|
|
207
|
-
*
|
|
208
|
-
* @example
|
|
209
|
-
* ```ts
|
|
210
|
-
* return dsSignals({ _uploadError: "File too large" });
|
|
211
|
-
* ```
|
|
212
|
-
*/ export function dsSignals(signals) {
|
|
213
|
-
return new Response(JSON.stringify(signals), {
|
|
214
|
-
headers: {
|
|
215
|
-
"Content-Type": "application/json"
|
|
216
|
-
}
|
|
217
|
-
});
|
|
218
|
-
}
|
package/dist/lib/storage.js
DELETED
|
@@ -1,164 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Storage Driver Abstraction
|
|
3
|
-
*
|
|
4
|
-
* Provides a common interface for file storage with R2 and S3-compatible backends.
|
|
5
|
-
*/ /**
|
|
6
|
-
* Creates an R2 storage driver that delegates to a Cloudflare R2 bucket binding.
|
|
7
|
-
*
|
|
8
|
-
* @param r2 - The R2 bucket binding from the Cloudflare Workers environment
|
|
9
|
-
* @returns A StorageDriver backed by R2
|
|
10
|
-
*/ export function createR2Driver(r2) {
|
|
11
|
-
return {
|
|
12
|
-
async put (key, body, opts) {
|
|
13
|
-
await r2.put(key, body, {
|
|
14
|
-
httpMetadata: opts?.contentType ? {
|
|
15
|
-
contentType: opts.contentType
|
|
16
|
-
} : undefined
|
|
17
|
-
});
|
|
18
|
-
},
|
|
19
|
-
async get (key) {
|
|
20
|
-
const object = await r2.get(key);
|
|
21
|
-
if (!object) return null;
|
|
22
|
-
return {
|
|
23
|
-
body: object.body,
|
|
24
|
-
contentType: object.httpMetadata?.contentType ?? undefined
|
|
25
|
-
};
|
|
26
|
-
},
|
|
27
|
-
async delete (key) {
|
|
28
|
-
await r2.delete(key);
|
|
29
|
-
}
|
|
30
|
-
};
|
|
31
|
-
}
|
|
32
|
-
/**
|
|
33
|
-
* Creates an S3-compatible storage driver using the AWS SDK.
|
|
34
|
-
*
|
|
35
|
-
* Supports any S3-compatible service: AWS S3, Backblaze B2, MinIO, etc.
|
|
36
|
-
* Uses path-style addressing for non-AWS endpoints.
|
|
37
|
-
*
|
|
38
|
-
* @param config - S3 connection configuration
|
|
39
|
-
* @returns A StorageDriver backed by S3
|
|
40
|
-
*/ export function createS3Driver(config) {
|
|
41
|
-
// Lazy-load the AWS SDK to avoid bundling it when using R2
|
|
42
|
-
let clientPromise = null;
|
|
43
|
-
function getClient() {
|
|
44
|
-
if (!clientPromise) {
|
|
45
|
-
clientPromise = import("@aws-sdk/client-s3").then((sdk)=>{
|
|
46
|
-
const forcePathStyle = !config.endpoint.includes("amazonaws.com");
|
|
47
|
-
const client = new sdk.S3Client({
|
|
48
|
-
endpoint: config.endpoint,
|
|
49
|
-
region: config.region,
|
|
50
|
-
credentials: {
|
|
51
|
-
accessKeyId: config.accessKeyId,
|
|
52
|
-
secretAccessKey: config.secretAccessKey
|
|
53
|
-
},
|
|
54
|
-
forcePathStyle
|
|
55
|
-
});
|
|
56
|
-
return {
|
|
57
|
-
send: (cmd)=>client.send(cmd),
|
|
58
|
-
S3Client: sdk.S3Client,
|
|
59
|
-
PutObjectCommand: sdk.PutObjectCommand,
|
|
60
|
-
GetObjectCommand: sdk.GetObjectCommand,
|
|
61
|
-
DeleteObjectCommand: sdk.DeleteObjectCommand,
|
|
62
|
-
bucket: config.bucket
|
|
63
|
-
};
|
|
64
|
-
});
|
|
65
|
-
}
|
|
66
|
-
return clientPromise;
|
|
67
|
-
}
|
|
68
|
-
return {
|
|
69
|
-
async put (key, body, opts) {
|
|
70
|
-
const s3 = await getClient();
|
|
71
|
-
// Buffer the stream to Uint8Array for the S3 SDK
|
|
72
|
-
let bodyBytes;
|
|
73
|
-
if (body instanceof Uint8Array) {
|
|
74
|
-
bodyBytes = body;
|
|
75
|
-
} else {
|
|
76
|
-
const reader = body.getReader();
|
|
77
|
-
const chunks = [];
|
|
78
|
-
for(;;){
|
|
79
|
-
const { done, value } = await reader.read();
|
|
80
|
-
if (done) break;
|
|
81
|
-
chunks.push(value);
|
|
82
|
-
}
|
|
83
|
-
let totalLength = 0;
|
|
84
|
-
for (const chunk of chunks)totalLength += chunk.length;
|
|
85
|
-
bodyBytes = new Uint8Array(totalLength);
|
|
86
|
-
let offset = 0;
|
|
87
|
-
for (const chunk of chunks){
|
|
88
|
-
bodyBytes.set(chunk, offset);
|
|
89
|
-
offset += chunk.length;
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
const command = new s3.PutObjectCommand({
|
|
93
|
-
Bucket: s3.bucket,
|
|
94
|
-
Key: key,
|
|
95
|
-
Body: bodyBytes,
|
|
96
|
-
ContentType: opts?.contentType
|
|
97
|
-
});
|
|
98
|
-
await s3.send(command);
|
|
99
|
-
},
|
|
100
|
-
async get (key) {
|
|
101
|
-
const s3 = await getClient();
|
|
102
|
-
try {
|
|
103
|
-
const command = new s3.GetObjectCommand({
|
|
104
|
-
Bucket: s3.bucket,
|
|
105
|
-
Key: key
|
|
106
|
-
});
|
|
107
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- AWS SDK response type
|
|
108
|
-
const response = await s3.send(command);
|
|
109
|
-
if (!response.Body) return null;
|
|
110
|
-
return {
|
|
111
|
-
body: response.Body.transformToWebStream(),
|
|
112
|
-
contentType: response.ContentType ?? undefined
|
|
113
|
-
};
|
|
114
|
-
} catch (err) {
|
|
115
|
-
// NoSuchKey → return null instead of throwing
|
|
116
|
-
if (err instanceof Error && (err.name === "NoSuchKey" || err.name === "NotFound")) {
|
|
117
|
-
return null;
|
|
118
|
-
}
|
|
119
|
-
throw err;
|
|
120
|
-
}
|
|
121
|
-
},
|
|
122
|
-
async delete (key) {
|
|
123
|
-
const s3 = await getClient();
|
|
124
|
-
const command = new s3.DeleteObjectCommand({
|
|
125
|
-
Bucket: s3.bucket,
|
|
126
|
-
Key: key
|
|
127
|
-
});
|
|
128
|
-
await s3.send(command);
|
|
129
|
-
}
|
|
130
|
-
};
|
|
131
|
-
}
|
|
132
|
-
/**
|
|
133
|
-
* Creates the appropriate storage driver based on environment configuration.
|
|
134
|
-
*
|
|
135
|
-
* Returns `null` if no storage is configured (no R2 binding and no S3 config).
|
|
136
|
-
*
|
|
137
|
-
* @param env - The Cloudflare Workers environment bindings
|
|
138
|
-
* @returns A StorageDriver instance or null
|
|
139
|
-
*
|
|
140
|
-
* @example
|
|
141
|
-
* ```ts
|
|
142
|
-
* const storage = createStorageDriver(c.env);
|
|
143
|
-
* if (storage) {
|
|
144
|
-
* await storage.put("media/file.jpg", stream, { contentType: "image/jpeg" });
|
|
145
|
-
* }
|
|
146
|
-
* ```
|
|
147
|
-
*/ export function createStorageDriver(env) {
|
|
148
|
-
const driver = env.STORAGE_DRIVER || "r2";
|
|
149
|
-
if (driver === "s3") {
|
|
150
|
-
if (!env.S3_ENDPOINT || !env.S3_BUCKET || !env.S3_ACCESS_KEY_ID || !env.S3_SECRET_ACCESS_KEY) {
|
|
151
|
-
return null;
|
|
152
|
-
}
|
|
153
|
-
return createS3Driver({
|
|
154
|
-
endpoint: env.S3_ENDPOINT,
|
|
155
|
-
bucket: env.S3_BUCKET,
|
|
156
|
-
accessKeyId: env.S3_ACCESS_KEY_ID,
|
|
157
|
-
secretAccessKey: env.S3_SECRET_ACCESS_KEY,
|
|
158
|
-
region: env.S3_REGION || "auto"
|
|
159
|
-
});
|
|
160
|
-
}
|
|
161
|
-
// Default: R2
|
|
162
|
-
if (!env.R2) return null;
|
|
163
|
-
return createR2Driver(env.R2);
|
|
164
|
-
}
|
package/dist/lib/theme.js
DELETED
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Theme Resolution Helpers
|
|
3
|
-
*
|
|
4
|
-
* Resolves the active color theme and builds CSS for injection into `<head>`.
|
|
5
|
-
*/ import { BUILTIN_COLOR_THEMES } from "../ui/color-themes.js";
|
|
6
|
-
/**
|
|
7
|
-
* Get the list of available color themes.
|
|
8
|
-
*
|
|
9
|
-
* Returns `config.colorThemes` if provided, otherwise the built-in list.
|
|
10
|
-
*
|
|
11
|
-
* @param config - The Jant configuration
|
|
12
|
-
* @returns Array of available color themes
|
|
13
|
-
*
|
|
14
|
-
* @example
|
|
15
|
-
* ```typescript
|
|
16
|
-
* const themes = getAvailableThemes(c.var.config);
|
|
17
|
-
* ```
|
|
18
|
-
*/ export function getAvailableThemes(config) {
|
|
19
|
-
return config.colorThemes ?? BUILTIN_COLOR_THEMES;
|
|
20
|
-
}
|
|
21
|
-
/**
|
|
22
|
-
* Build a `<style>` CSS string from a color theme and optional cssVariables overlay.
|
|
23
|
-
*
|
|
24
|
-
* Priority (lowest → highest):
|
|
25
|
-
* BaseCoat defaults → selected theme → cssVariables
|
|
26
|
-
*
|
|
27
|
-
* @param theme - The active color theme (undefined = no theme overrides)
|
|
28
|
-
* @param cssVariables - Extra CSS variable overrides from `createApp({ cssVariables })`
|
|
29
|
-
* @returns CSS string to inject in `<head>`, or empty string if nothing to inject
|
|
30
|
-
*
|
|
31
|
-
* Uses `:root:root` and `:root.dark` selectors for higher specificity than
|
|
32
|
-
* BaseCoat defaults (`:root` and `.dark`). This ensures theme overrides win
|
|
33
|
-
* regardless of source order — important because Vite dev mode injects CSS
|
|
34
|
-
* as `<style>` tags after the theme `<style>`.
|
|
35
|
-
*
|
|
36
|
-
* @example
|
|
37
|
-
* ```typescript
|
|
38
|
-
* const css = buildThemeStyle(blueTheme, { "--radius": "0.5rem" });
|
|
39
|
-
* // => ":root:root { --primary: oklch(...); ... }\n:root.dark { ... }"
|
|
40
|
-
* ```
|
|
41
|
-
*/ export function buildThemeStyle(theme, cssVariables) {
|
|
42
|
-
const lightVars = {
|
|
43
|
-
...theme?.light ?? {},
|
|
44
|
-
...cssVariables ?? {}
|
|
45
|
-
};
|
|
46
|
-
const darkVars = {
|
|
47
|
-
...theme?.dark ?? {},
|
|
48
|
-
...cssVariables ?? {}
|
|
49
|
-
};
|
|
50
|
-
const hasLight = Object.keys(lightVars).length > 0;
|
|
51
|
-
const hasDark = Object.keys(darkVars).length > 0;
|
|
52
|
-
if (!hasLight && !hasDark) return "";
|
|
53
|
-
const parts = [];
|
|
54
|
-
if (hasLight) {
|
|
55
|
-
const declarations = Object.entries(lightVars).map(([k, v])=>` ${k}: ${v};`).join("\n");
|
|
56
|
-
// :root:root has specificity (0,0,2) > BaseCoat's :root (0,0,1)
|
|
57
|
-
parts.push(`:root:root {\n${declarations}\n}`);
|
|
58
|
-
}
|
|
59
|
-
if (hasDark) {
|
|
60
|
-
const declarations = Object.entries(darkVars).map(([k, v])=>` ${k}: ${v};`).join("\n");
|
|
61
|
-
// :root.dark has specificity (0,1,1) > BaseCoat's .dark (0,1,0)
|
|
62
|
-
parts.push(`:root.dark {\n${declarations}\n}`);
|
|
63
|
-
}
|
|
64
|
-
return parts.join("\n");
|
|
65
|
-
}
|
package/dist/lib/time.js
DELETED
|
@@ -1,159 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Time Utilities
|
|
3
|
-
*/ /**
|
|
4
|
-
* Gets the current Unix timestamp in seconds.
|
|
5
|
-
*
|
|
6
|
-
* Returns the number of seconds since the Unix epoch (January 1, 1970 00:00:00 UTC).
|
|
7
|
-
* This is the standard time format used throughout the application for consistency
|
|
8
|
-
* and database storage.
|
|
9
|
-
*
|
|
10
|
-
* @returns Current Unix timestamp in seconds (not milliseconds)
|
|
11
|
-
*
|
|
12
|
-
* @example
|
|
13
|
-
* ```ts
|
|
14
|
-
* const timestamp = now();
|
|
15
|
-
* // Returns: 1706745600 (example value for Feb 1, 2024)
|
|
16
|
-
* ```
|
|
17
|
-
*/ export function now() {
|
|
18
|
-
return Math.floor(Date.now() / 1000);
|
|
19
|
-
}
|
|
20
|
-
/**
|
|
21
|
-
* One month in seconds
|
|
22
|
-
*/ const ONE_MONTH = 30 * 24 * 60 * 60;
|
|
23
|
-
/**
|
|
24
|
-
* Checks if a Unix timestamp is within the last 30 days.
|
|
25
|
-
*
|
|
26
|
-
* Compares the given timestamp to the current time to determine if it falls within
|
|
27
|
-
* the last month (defined as 30 days). Useful for highlighting recent posts or
|
|
28
|
-
* filtering time-sensitive content.
|
|
29
|
-
*
|
|
30
|
-
* @param timestamp - Unix timestamp in seconds to check
|
|
31
|
-
* @returns `true` if the timestamp is within the last 30 days, `false` otherwise
|
|
32
|
-
*
|
|
33
|
-
* @example
|
|
34
|
-
* ```ts
|
|
35
|
-
* const recentPost = 1706745600; // Recent timestamp
|
|
36
|
-
* if (isWithinMonth(recentPost)) {
|
|
37
|
-
* // Show "new" badge
|
|
38
|
-
* }
|
|
39
|
-
* ```
|
|
40
|
-
*/ export function isWithinMonth(timestamp) {
|
|
41
|
-
return now() - timestamp < ONE_MONTH;
|
|
42
|
-
}
|
|
43
|
-
/**
|
|
44
|
-
* Converts a Unix timestamp to an ISO 8601 date-time string.
|
|
45
|
-
*
|
|
46
|
-
* Formats a Unix timestamp (in seconds) as an ISO 8601 string suitable for HTML
|
|
47
|
-
* `datetime` attributes and API responses. The output includes full date, time,
|
|
48
|
-
* and timezone information in UTC.
|
|
49
|
-
*
|
|
50
|
-
* @param timestamp - Unix timestamp in seconds to convert
|
|
51
|
-
* @returns ISO 8601 formatted string (e.g., "2024-02-01T12:00:00.000Z")
|
|
52
|
-
*
|
|
53
|
-
* @example
|
|
54
|
-
* ```ts
|
|
55
|
-
* const isoDate = toISOString(1706745600);
|
|
56
|
-
* // Returns: "2024-02-01T00:00:00.000Z"
|
|
57
|
-
* ```
|
|
58
|
-
*/ export function toISOString(timestamp) {
|
|
59
|
-
return new Date(timestamp * 1000).toISOString();
|
|
60
|
-
}
|
|
61
|
-
/**
|
|
62
|
-
* Formats a Unix timestamp as a human-readable date string.
|
|
63
|
-
*
|
|
64
|
-
* Converts a Unix timestamp (in seconds) to a localized date string in the format
|
|
65
|
-
* "MMM DD, YYYY" (e.g., "Jan 15, 2024"). Always uses UTC timezone to ensure
|
|
66
|
-
* consistent display regardless of server or client location.
|
|
67
|
-
*
|
|
68
|
-
* @param timestamp - Unix timestamp in seconds to format
|
|
69
|
-
* @returns Formatted date string in "MMM DD, YYYY" format
|
|
70
|
-
*
|
|
71
|
-
* @example
|
|
72
|
-
* ```ts
|
|
73
|
-
* const readable = formatDate(1706745600);
|
|
74
|
-
* // Returns: "Feb 1, 2024"
|
|
75
|
-
* ```
|
|
76
|
-
*/ export function formatDate(timestamp) {
|
|
77
|
-
return new Date(timestamp * 1000).toLocaleDateString("en-US", {
|
|
78
|
-
year: "numeric",
|
|
79
|
-
month: "short",
|
|
80
|
-
day: "numeric",
|
|
81
|
-
timeZone: "UTC"
|
|
82
|
-
});
|
|
83
|
-
}
|
|
84
|
-
/**
|
|
85
|
-
* Formats a Unix timestamp as a year-month string for grouping.
|
|
86
|
-
*
|
|
87
|
-
* Converts a Unix timestamp (in seconds) to a "YYYY-MM" format string, useful for
|
|
88
|
-
* grouping posts by month in archives or creating month-based URLs. Always uses
|
|
89
|
-
* UTC timezone for consistency.
|
|
90
|
-
*
|
|
91
|
-
* @param timestamp - Unix timestamp in seconds to format
|
|
92
|
-
* @returns Year-month string in "YYYY-MM" format
|
|
93
|
-
*
|
|
94
|
-
* @example
|
|
95
|
-
* ```ts
|
|
96
|
-
* const yearMonth = formatYearMonth(1706745600);
|
|
97
|
-
* // Returns: "2024-02"
|
|
98
|
-
* ```
|
|
99
|
-
*/ /**
|
|
100
|
-
* Formats a Unix timestamp as a 24-hour time string (HH:MM).
|
|
101
|
-
*
|
|
102
|
-
* Converts a Unix timestamp (in seconds) to a zero-padded time string in
|
|
103
|
-
* 24-hour format. Always uses UTC timezone for consistency.
|
|
104
|
-
*
|
|
105
|
-
* @param timestamp - Unix timestamp in seconds to format
|
|
106
|
-
* @returns Formatted time string in "HH:MM" format
|
|
107
|
-
*
|
|
108
|
-
* @example
|
|
109
|
-
* ```ts
|
|
110
|
-
* const time = formatTime(1706745600);
|
|
111
|
-
* // Returns: "00:00"
|
|
112
|
-
* ```
|
|
113
|
-
*/ export function formatTime(timestamp) {
|
|
114
|
-
const date = new Date(timestamp * 1000);
|
|
115
|
-
const hours = String(date.getUTCHours()).padStart(2, "0");
|
|
116
|
-
const minutes = String(date.getUTCMinutes()).padStart(2, "0");
|
|
117
|
-
return `${hours}:${minutes}`;
|
|
118
|
-
}
|
|
119
|
-
/**
|
|
120
|
-
* Formats a Unix timestamp as a short relative time string.
|
|
121
|
-
*
|
|
122
|
-
* Returns compact labels like "1m", "5h", "3d" for recent timestamps,
|
|
123
|
-
* and falls back to "MMM D" (e.g. "Feb 1") for anything older than 7 days.
|
|
124
|
-
*
|
|
125
|
-
* @param timestamp - Unix timestamp in seconds
|
|
126
|
-
* @returns Short relative time string
|
|
127
|
-
*
|
|
128
|
-
* @example
|
|
129
|
-
* ```ts
|
|
130
|
-
* // Assuming current time is Feb 16, 2026
|
|
131
|
-
* formatRelativeTime(now() - 30); // "1m" (30 seconds → rounds up)
|
|
132
|
-
* formatRelativeTime(now() - 3600); // "1h"
|
|
133
|
-
* formatRelativeTime(now() - 86400); // "1d"
|
|
134
|
-
* formatRelativeTime(now() - 604800); // "7d"
|
|
135
|
-
* formatRelativeTime(now() - 864000); // "Feb 6"
|
|
136
|
-
* ```
|
|
137
|
-
*/ export function formatRelativeTime(timestamp) {
|
|
138
|
-
const seconds = now() - timestamp;
|
|
139
|
-
if (seconds < 60) return "1m";
|
|
140
|
-
const minutes = Math.floor(seconds / 60);
|
|
141
|
-
if (minutes < 60) return `${minutes}m`;
|
|
142
|
-
const hours = Math.floor(seconds / 3600);
|
|
143
|
-
if (hours < 24) return `${hours}h`;
|
|
144
|
-
const days = Math.floor(seconds / 86400);
|
|
145
|
-
if (days <= 7) return `${days}d`;
|
|
146
|
-
// Older than 7 days: show "MMM D" (e.g. "Feb 1")
|
|
147
|
-
const date = new Date(timestamp * 1000);
|
|
148
|
-
return date.toLocaleDateString("en-US", {
|
|
149
|
-
month: "short",
|
|
150
|
-
day: "numeric",
|
|
151
|
-
timeZone: "UTC"
|
|
152
|
-
});
|
|
153
|
-
}
|
|
154
|
-
export function formatYearMonth(timestamp) {
|
|
155
|
-
const date = new Date(timestamp * 1000);
|
|
156
|
-
const year = date.getUTCFullYear();
|
|
157
|
-
const month = String(date.getUTCMonth() + 1).padStart(2, "0");
|
|
158
|
-
return `${year}-${month}`;
|
|
159
|
-
}
|