@jant/core 0.3.36 → 0.3.38
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/bin/commands/export.js +1 -1
- package/bin/commands/import-site.js +529 -0
- package/bin/commands/reset-password.js +3 -2
- package/dist/client/assets/heic-to-DIRPI3VF.js +1 -0
- package/dist/client/assets/url-FWFqPJPb.js +1 -0
- package/dist/client/client.css +1 -1
- package/dist/client/client.js +4012 -3276
- package/dist/index.js +10285 -5809
- package/package.json +11 -3
- package/src/__tests__/helpers/app.ts +9 -9
- package/src/__tests__/helpers/db.ts +91 -93
- package/src/app.tsx +157 -27
- package/src/auth.ts +20 -2
- package/src/client/archive-nav.js +187 -0
- package/src/client/audio-player.ts +478 -0
- package/src/client/audio-processor.ts +84 -0
- package/src/client/avatar-upload.ts +3 -2
- package/src/client/components/__tests__/jant-compose-dialog.test.ts +645 -49
- package/src/client/components/__tests__/jant-compose-editor.test.ts +208 -16
- package/src/client/components/__tests__/jant-post-form.test.ts +19 -9
- package/src/client/components/__tests__/jant-settings-avatar.test.ts +2 -2
- package/src/client/components/__tests__/jant-settings-general.test.ts +3 -3
- package/src/client/components/collection-sidebar-types.ts +7 -9
- package/src/client/components/compose-types.ts +101 -4
- package/src/client/components/jant-collection-form.ts +43 -7
- package/src/client/components/jant-collection-sidebar.ts +88 -84
- package/src/client/components/jant-compose-dialog.ts +1655 -219
- package/src/client/components/jant-compose-editor.ts +732 -168
- package/src/client/components/jant-compose-fullscreen.ts +23 -78
- package/src/client/components/jant-media-lightbox.ts +2 -0
- package/src/client/components/jant-nav-manager.ts +24 -284
- package/src/client/components/jant-post-form.ts +89 -9
- package/src/client/components/jant-post-menu.ts +1019 -0
- package/src/client/components/jant-settings-avatar.ts +3 -3
- package/src/client/components/jant-settings-general.ts +38 -4
- package/src/client/components/jant-text-preview.ts +232 -0
- package/src/client/components/nav-manager-types.ts +4 -19
- package/src/client/components/post-form-template.ts +107 -12
- package/src/client/components/post-form-types.ts +11 -4
- package/src/client/compose-bridge.ts +410 -109
- package/src/client/image-processor.ts +26 -8
- package/src/client/media-metadata.ts +247 -0
- package/src/client/multipart-upload.ts +160 -0
- package/src/client/post-form-bridge.ts +52 -1
- package/src/client/settings-bridge.ts +0 -12
- package/src/client/thread-context.ts +140 -0
- package/src/client/tiptap/create-editor.ts +46 -0
- package/src/client/tiptap/extensions.ts +5 -0
- package/src/client/tiptap/image-node.ts +2 -8
- package/src/client/tiptap/paste-image.ts +2 -13
- package/src/client/tiptap/slash-commands.ts +173 -63
- package/src/client/toast.ts +101 -3
- package/src/client/types/sortablejs.d.ts +15 -0
- package/src/client/upload-with-metadata.ts +54 -0
- package/src/client/video-processor.ts +207 -0
- package/src/client.ts +5 -2
- package/src/db/__tests__/migrations.test.ts +118 -0
- package/src/db/index.ts +52 -0
- package/src/db/migrations/0000_baseline.sql +269 -0
- package/src/db/migrations/0001_fts_setup.sql +31 -0
- package/src/db/migrations/meta/0000_snapshot.json +703 -119
- package/src/db/migrations/meta/0001_snapshot.json +1337 -0
- package/src/db/migrations/meta/_journal.json +4 -39
- package/src/db/schema.ts +409 -145
- package/src/i18n/__tests__/detect.test.ts +115 -0
- package/src/i18n/context.tsx +2 -2
- package/src/i18n/detect.ts +85 -1
- package/src/i18n/i18n.ts +1 -1
- package/src/i18n/index.ts +2 -1
- package/src/i18n/locales/en.po +487 -1217
- package/src/i18n/locales/en.ts +1 -1
- package/src/i18n/locales/zh-Hans.po +613 -996
- package/src/i18n/locales/zh-Hans.ts +1 -1
- package/src/i18n/locales/zh-Hant.po +624 -1007
- package/src/i18n/locales/zh-Hant.ts +1 -1
- package/src/i18n/middleware.ts +6 -0
- package/src/index.ts +5 -7
- package/src/lib/__tests__/blurhash-placeholder.test.ts +75 -0
- package/src/lib/__tests__/constants.test.ts +0 -1
- package/src/lib/__tests__/markdown-to-tiptap.test.ts +358 -0
- package/src/lib/__tests__/nanoid.test.ts +26 -0
- package/src/lib/__tests__/schemas.test.ts +181 -63
- package/src/lib/__tests__/slug.test.ts +126 -0
- package/src/lib/__tests__/sse.test.ts +6 -6
- package/src/lib/__tests__/summary.test.ts +264 -0
- package/src/lib/__tests__/theme.test.ts +1 -1
- package/src/lib/__tests__/timeline.test.ts +33 -30
- package/src/lib/__tests__/tiptap-to-markdown.test.ts +346 -0
- package/src/lib/__tests__/view.test.ts +141 -66
- package/src/lib/blurhash-placeholder.ts +102 -0
- package/src/lib/constants.ts +3 -1
- package/src/lib/emoji-catalog.ts +885 -68
- package/src/lib/errors.ts +11 -8
- package/src/lib/feed.ts +78 -32
- package/src/lib/html.ts +2 -1
- package/src/lib/icon-catalog.ts +5033 -1
- package/src/lib/icons.ts +3 -2
- package/src/lib/index.ts +0 -1
- package/src/lib/markdown-to-tiptap.ts +286 -0
- package/src/lib/media-helpers.ts +12 -3
- package/src/lib/nanoid.ts +29 -0
- package/src/lib/navigation.ts +1 -1
- package/src/lib/render.tsx +20 -2
- package/src/lib/resolve-config.ts +6 -2
- package/src/lib/schemas.ts +224 -55
- package/src/lib/search-snippet.ts +34 -0
- package/src/lib/slug.ts +96 -0
- package/src/lib/sse.ts +6 -6
- package/src/lib/storage.ts +115 -7
- package/src/lib/summary.ts +66 -0
- package/src/lib/theme.ts +11 -8
- package/src/lib/timeline.ts +74 -34
- package/src/lib/tiptap-render.ts +5 -10
- package/src/lib/tiptap-to-markdown.ts +305 -0
- package/src/lib/upload.ts +190 -29
- package/src/lib/url.ts +31 -0
- package/src/lib/view.ts +204 -37
- package/src/middleware/__tests__/auth.test.ts +191 -11
- package/src/middleware/__tests__/onboarding.test.ts +12 -10
- package/src/middleware/auth.ts +63 -9
- package/src/middleware/onboarding.ts +1 -1
- package/src/middleware/secure-headers.ts +40 -0
- package/src/preset.css +45 -2
- package/src/routes/__tests__/compose.test.ts +17 -24
- package/src/routes/api/__tests__/collections.test.ts +109 -61
- package/src/routes/api/__tests__/nav-items.test.ts +46 -29
- package/src/routes/api/__tests__/posts.test.ts +132 -68
- package/src/routes/api/__tests__/search.test.ts +15 -2
- package/src/routes/api/__tests__/upload-multipart.test.ts +534 -0
- package/src/routes/api/collections.ts +51 -42
- package/src/routes/api/custom-urls.ts +80 -0
- package/src/routes/api/export.ts +31 -0
- package/src/routes/api/nav-items.ts +23 -19
- package/src/routes/api/posts.ts +43 -39
- package/src/routes/api/search.ts +3 -4
- package/src/routes/api/upload-multipart.ts +245 -0
- package/src/routes/api/upload.ts +85 -19
- package/src/routes/auth/__tests__/setup.test.ts +20 -60
- package/src/routes/auth/setup.tsx +26 -33
- package/src/routes/auth/signin.tsx +3 -7
- package/src/routes/compose.tsx +10 -55
- package/src/routes/dash/__tests__/settings-avatar.test.ts +1 -1
- package/src/routes/dash/custom-urls.tsx +414 -0
- package/src/routes/dash/settings.tsx +304 -232
- package/src/routes/feed/__tests__/rss.test.ts +27 -28
- package/src/routes/feed/rss.ts +6 -4
- package/src/routes/feed/sitemap.ts +2 -12
- package/src/routes/pages/__tests__/collections.test.ts +5 -6
- package/src/routes/pages/__tests__/featured.test.ts +41 -22
- package/src/routes/pages/archive.tsx +175 -39
- package/src/routes/pages/collection.tsx +22 -10
- package/src/routes/pages/collections.tsx +3 -3
- package/src/routes/pages/featured.tsx +28 -4
- package/src/routes/pages/home.tsx +16 -15
- package/src/routes/pages/latest.tsx +1 -11
- package/src/routes/pages/new.tsx +39 -0
- package/src/routes/pages/page.tsx +95 -49
- package/src/routes/pages/search.tsx +1 -1
- package/src/services/__tests__/api-token.test.ts +135 -0
- package/src/services/__tests__/collection.test.ts +275 -227
- package/src/services/__tests__/custom-url.test.ts +213 -0
- package/src/services/__tests__/media.test.ts +162 -22
- package/src/services/__tests__/navigation.test.ts +109 -68
- package/src/services/__tests__/post-timeline.test.ts +205 -32
- package/src/services/__tests__/post.test.ts +713 -234
- package/src/services/__tests__/search.test.ts +67 -10
- package/src/services/api-token.ts +166 -0
- package/src/services/auth.ts +17 -2
- package/src/services/collection.ts +397 -131
- package/src/services/custom-url.ts +188 -0
- package/src/services/export.ts +802 -0
- package/src/services/index.ts +26 -19
- package/src/services/media.ts +100 -22
- package/src/services/navigation.ts +158 -47
- package/src/services/path.ts +339 -0
- package/src/services/post.ts +687 -154
- package/src/services/search.ts +160 -75
- package/src/styles/components.css +58 -7
- package/src/styles/tokens.css +84 -6
- package/src/styles/ui.css +2964 -457
- package/src/types/bindings.ts +4 -1
- package/src/types/config.ts +12 -4
- package/src/types/constants.ts +15 -3
- package/src/types/entities.ts +74 -35
- package/src/types/operations.ts +11 -24
- package/src/types/props.ts +51 -16
- package/src/types/views.ts +45 -22
- package/src/ui/color-themes.ts +133 -23
- package/src/ui/compose/ComposeDialog.tsx +239 -17
- package/src/ui/compose/ComposePrompt.tsx +1 -1
- package/src/ui/dash/CrudPageHeader.tsx +1 -1
- package/src/ui/dash/ListItemRow.tsx +1 -1
- package/src/ui/dash/StatusBadge.tsx +3 -1
- package/src/ui/dash/appearance/AdvancedContent.tsx +22 -1
- package/src/ui/dash/appearance/ColorThemeContent.tsx +22 -2
- package/src/ui/dash/appearance/FontThemeContent.tsx +1 -1
- package/src/ui/dash/appearance/NavigationContent.tsx +5 -45
- package/src/ui/dash/index.ts +0 -3
- package/src/ui/dash/settings/AccountContent.tsx +3 -57
- package/src/ui/dash/settings/AccountMenuContent.tsx +147 -0
- package/src/ui/dash/settings/ApiTokensContent.tsx +232 -0
- package/src/ui/dash/settings/AvatarContent.tsx +8 -0
- package/src/ui/dash/settings/SessionsContent.tsx +159 -0
- package/src/ui/dash/settings/SettingsRootContent.tsx +45 -15
- package/src/ui/feed/LinkCard.tsx +89 -40
- package/src/ui/feed/NoteCard.tsx +39 -25
- package/src/ui/feed/PostStatusBadges.tsx +67 -0
- package/src/ui/feed/QuoteCard.tsx +38 -23
- package/src/ui/feed/ThreadPreview.tsx +90 -26
- package/src/ui/feed/TimelineFeed.tsx +3 -2
- package/src/ui/feed/TimelineItem.tsx +15 -6
- package/src/ui/feed/__tests__/thread-preview.test.ts +107 -0
- package/src/ui/feed/thread-preview-state.ts +61 -0
- package/src/ui/font-themes.ts +2 -2
- package/src/ui/layouts/BaseLayout.tsx +2 -2
- package/src/ui/layouts/SiteLayout.tsx +105 -92
- package/src/ui/pages/ArchivePage.tsx +923 -98
- package/src/ui/pages/ComposePage.tsx +54 -0
- package/src/ui/pages/PostPage.tsx +30 -45
- package/src/ui/pages/SearchPage.tsx +181 -37
- package/src/ui/shared/AdminBreadcrumb.tsx +29 -0
- package/src/ui/shared/CollectionsSidebar.tsx +47 -37
- package/src/ui/shared/MediaGallery.tsx +445 -149
- package/src/ui/shared/PostFooter.tsx +204 -0
- package/src/ui/shared/StarRating.tsx +27 -0
- package/src/ui/shared/__tests__/format-chars.test.ts +35 -0
- package/src/ui/shared/index.ts +0 -1
- package/dist/client/assets/url-8Dj-5CLW.js +0 -1
- package/src/client/media-upload.ts +0 -161
- package/src/client/page-slug-bridge.ts +0 -42
- package/src/db/migrations/0000_square_wallflower.sql +0 -118
- package/src/db/migrations/0001_add_search_fts.sql +0 -34
- package/src/db/migrations/0002_add_media_attachments.sql +0 -3
- package/src/db/migrations/0003_add_navigation_links.sql +0 -8
- package/src/db/migrations/0004_add_storage_provider.sql +0 -3
- package/src/db/migrations/0005_v2_schema_migration.sql +0 -268
- package/src/db/migrations/0006_rename_slug_to_path.sql +0 -5
- package/src/db/migrations/0007_post_collections_m2m.sql +0 -94
- package/src/db/migrations/0008_add_collection_dividers.sql +0 -8
- package/src/db/migrations/0009_drop_collection_show_divider.sql +0 -2
- package/src/db/migrations/0010_add_performance_indexes.sql +0 -16
- package/src/db/migrations/0011_add_path_registry.sql +0 -23
- package/src/db/migrations/0012_add_tiptap_columns.sql +0 -2
- package/src/db/migrations/0013_replace_featured_with_visibility.sql +0 -8
- package/src/db/migrations/meta/0003_snapshot.json +0 -821
- package/src/lib/__tests__/sqid.test.ts +0 -65
- package/src/lib/sqid.ts +0 -79
- package/src/routes/api/__tests__/pages.test.ts +0 -218
- package/src/routes/api/pages.ts +0 -73
- package/src/routes/dash/__tests__/pages.test.ts +0 -226
- package/src/routes/dash/index.tsx +0 -109
- package/src/routes/dash/media.tsx +0 -135
- package/src/routes/dash/pages.tsx +0 -245
- package/src/routes/dash/posts.tsx +0 -338
- package/src/routes/dash/redirects.tsx +0 -263
- package/src/routes/pages/post.tsx +0 -59
- package/src/services/__tests__/page.test.ts +0 -298
- package/src/services/__tests__/path-registry.test.ts +0 -165
- package/src/services/__tests__/redirect.test.ts +0 -159
- package/src/services/page.ts +0 -216
- package/src/services/path-registry.ts +0 -160
- package/src/services/redirect.ts +0 -97
- package/src/ui/dash/PageForm.tsx +0 -187
- package/src/ui/dash/PostList.tsx +0 -95
- package/src/ui/dash/media/MediaListContent.tsx +0 -206
- package/src/ui/dash/media/ViewMediaContent.tsx +0 -208
- package/src/ui/dash/pages/PagesContent.tsx +0 -75
- package/src/ui/dash/posts/PostForm.tsx +0 -260
- package/src/ui/layouts/DashLayout.tsx +0 -247
- package/src/ui/pages/SinglePage.tsx +0 -23
- package/src/ui/shared/ThreadView.tsx +0 -136
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session management: view active sessions and revoke them
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { useLingui } from "@lingui/react/macro";
|
|
6
|
+
import { formatDate } from "../../../lib/time.js";
|
|
7
|
+
|
|
8
|
+
export interface SessionInfo {
|
|
9
|
+
token: string;
|
|
10
|
+
ipAddress: string | null;
|
|
11
|
+
userAgent: string | null;
|
|
12
|
+
createdAt: number;
|
|
13
|
+
isCurrent: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Parse a user-agent string into a human-readable device description.
|
|
18
|
+
*
|
|
19
|
+
* @param ua - Raw User-Agent header value
|
|
20
|
+
* @returns Short description like "Chrome on macOS" or "Safari on iPhone"
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```ts
|
|
24
|
+
* parseDevice("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) ... Chrome/120")
|
|
25
|
+
* // "Chrome on macOS"
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
function parseDevice(ua: string | null): string | null {
|
|
29
|
+
if (!ua) return null;
|
|
30
|
+
|
|
31
|
+
let browser = "Unknown browser";
|
|
32
|
+
if (ua.includes("Firefox/")) browser = "Firefox";
|
|
33
|
+
else if (ua.includes("Edg/")) browser = "Edge";
|
|
34
|
+
else if (ua.includes("OPR/") || ua.includes("Opera/")) browser = "Opera";
|
|
35
|
+
else if (ua.includes("Chrome/") && ua.includes("Safari/")) browser = "Chrome";
|
|
36
|
+
else if (ua.includes("Safari/") && !ua.includes("Chrome/"))
|
|
37
|
+
browser = "Safari";
|
|
38
|
+
else if (ua.includes("curl/")) browser = "curl";
|
|
39
|
+
|
|
40
|
+
let os = "";
|
|
41
|
+
if (ua.includes("iPhone")) os = "iPhone";
|
|
42
|
+
else if (ua.includes("iPad")) os = "iPad";
|
|
43
|
+
else if (ua.includes("Android")) os = "Android";
|
|
44
|
+
else if (ua.includes("Macintosh") || ua.includes("Mac OS X")) os = "macOS";
|
|
45
|
+
else if (ua.includes("Windows")) os = "Windows";
|
|
46
|
+
else if (ua.includes("Linux")) os = "Linux";
|
|
47
|
+
else if (ua.includes("CrOS")) os = "ChromeOS";
|
|
48
|
+
|
|
49
|
+
return os ? `${browser} on ${os}` : browser;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Monitor icon for desktop sessions */
|
|
53
|
+
const ICON_DESKTOP = `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="20" height="14" x="2" y="3" rx="2"/><line x1="8" x2="16" y1="21" y2="21"/><line x1="12" x2="12" y1="17" y2="21"/></svg>`;
|
|
54
|
+
|
|
55
|
+
/** Smartphone icon for mobile sessions */
|
|
56
|
+
const ICON_MOBILE = `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="14" height="20" x="5" y="2" rx="2" ry="2"/><path d="M12 18h.01"/></svg>`;
|
|
57
|
+
|
|
58
|
+
function isMobileUA(ua: string | null): boolean {
|
|
59
|
+
if (!ua) return false;
|
|
60
|
+
return /iPhone|iPad|iPod|Android|Mobile/i.test(ua);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function SessionRow({ session }: { session: SessionInfo }) {
|
|
64
|
+
const { t } = useLingui();
|
|
65
|
+
const device = parseDevice(session.userAgent);
|
|
66
|
+
const icon = isMobileUA(session.userAgent) ? ICON_MOBILE : ICON_DESKTOP;
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<div class="py-4 flex items-start gap-4 border-b border-border last:border-b-0">
|
|
70
|
+
<span
|
|
71
|
+
class="text-muted-foreground mt-0.5 shrink-0"
|
|
72
|
+
dangerouslySetInnerHTML={{ __html: icon }}
|
|
73
|
+
/>
|
|
74
|
+
<div class="flex-1 min-w-0">
|
|
75
|
+
<div class="font-medium flex items-center gap-2">
|
|
76
|
+
{device ??
|
|
77
|
+
t({
|
|
78
|
+
message: "Unknown device",
|
|
79
|
+
comment:
|
|
80
|
+
"@context: Fallback label when session device can't be identified",
|
|
81
|
+
})}
|
|
82
|
+
{session.isCurrent && (
|
|
83
|
+
<span class="badge text-xs">
|
|
84
|
+
{t({
|
|
85
|
+
message: "Current",
|
|
86
|
+
comment:
|
|
87
|
+
"@context: Badge indicating the current active session",
|
|
88
|
+
})}
|
|
89
|
+
</span>
|
|
90
|
+
)}
|
|
91
|
+
</div>
|
|
92
|
+
<div class="text-sm text-muted-foreground mt-0.5">
|
|
93
|
+
{session.ipAddress && (
|
|
94
|
+
<>
|
|
95
|
+
<span>{session.ipAddress}</span>
|
|
96
|
+
<span class="mx-2">·</span>
|
|
97
|
+
</>
|
|
98
|
+
)}
|
|
99
|
+
{t({
|
|
100
|
+
message: `Signed in ${formatDate(session.createdAt)}`,
|
|
101
|
+
comment: "@context: Session creation date",
|
|
102
|
+
})}
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
105
|
+
{!session.isCurrent && (
|
|
106
|
+
<button
|
|
107
|
+
type="button"
|
|
108
|
+
class="btn-sm-ghost text-destructive"
|
|
109
|
+
data-on:click__prevent={`@post('/settings/account/sessions/${session.token}/revoke')`}
|
|
110
|
+
>
|
|
111
|
+
{t({
|
|
112
|
+
message: "Revoke",
|
|
113
|
+
comment: "@context: Button to revoke a session",
|
|
114
|
+
})}
|
|
115
|
+
</button>
|
|
116
|
+
)}
|
|
117
|
+
</div>
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function SessionsContent({ sessions }: { sessions: SessionInfo[] }) {
|
|
122
|
+
const { t } = useLingui();
|
|
123
|
+
|
|
124
|
+
return (
|
|
125
|
+
<div class="flex flex-col gap-6 max-w-2xl">
|
|
126
|
+
<div>
|
|
127
|
+
<h2 class="text-lg font-medium mb-1">
|
|
128
|
+
{t({
|
|
129
|
+
message: "Active Sessions",
|
|
130
|
+
comment: "@context: Settings section heading for active sessions",
|
|
131
|
+
})}
|
|
132
|
+
</h2>
|
|
133
|
+
<p class="text-sm text-muted-foreground mb-4">
|
|
134
|
+
{t({
|
|
135
|
+
message:
|
|
136
|
+
"These devices are currently signed in to your account. Revoke any session you don't recognize.",
|
|
137
|
+
comment: "@context: Description for session management",
|
|
138
|
+
})}
|
|
139
|
+
</p>
|
|
140
|
+
</div>
|
|
141
|
+
|
|
142
|
+
{sessions.length > 0 ? (
|
|
143
|
+
<div class="border border-border rounded-lg px-4">
|
|
144
|
+
{sessions.map((session) => (
|
|
145
|
+
<SessionRow key={session.token} session={session} />
|
|
146
|
+
))}
|
|
147
|
+
</div>
|
|
148
|
+
) : (
|
|
149
|
+
<p class="text-sm text-muted-foreground">
|
|
150
|
+
{t({
|
|
151
|
+
message: "No active sessions found.",
|
|
152
|
+
comment:
|
|
153
|
+
"@context: Empty state when no sessions exist (shouldn't normally appear)",
|
|
154
|
+
})}
|
|
155
|
+
</p>
|
|
156
|
+
)}
|
|
157
|
+
</div>
|
|
158
|
+
);
|
|
159
|
+
}
|
|
@@ -60,7 +60,9 @@ const ICONS = {
|
|
|
60
60
|
type: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="4 7 4 4 20 4 20 7"/><line x1="9" x2="15" y1="20" y2="20"/><line x1="12" x2="12" y1="4" y2="20"/></svg>`,
|
|
61
61
|
code: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg>`,
|
|
62
62
|
arrowRightLeft: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m16 3 4 4-4 4"/><path d="M20 7H4"/><path d="m8 21-4-4 4-4"/><path d="M4 17h16"/></svg>`,
|
|
63
|
-
|
|
63
|
+
lock: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="11" x="3" y="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>`,
|
|
64
|
+
key: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m15.5 7.5 2.3 2.3a1 1 0 0 0 1.4 0l2.1-2.1a1 1 0 0 0 0-1.4L19 4"/><path d="m21 2-9.6 9.6"/><circle cx="7.5" cy="15.5" r="5.5"/></svg>`,
|
|
65
|
+
shield: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z"/></svg>`,
|
|
64
66
|
};
|
|
65
67
|
|
|
66
68
|
// oklch-based colors for icon backgrounds
|
|
@@ -72,6 +74,7 @@ const COLORS = {
|
|
|
72
74
|
pink: "oklch(0.6 0.2 350)",
|
|
73
75
|
indigo: "oklch(0.5 0.2 275)",
|
|
74
76
|
amber: "oklch(0.6 0.16 75)",
|
|
77
|
+
teal: "oklch(0.55 0.15 185)",
|
|
75
78
|
gray: "oklch(0.55 0.01 250)",
|
|
76
79
|
};
|
|
77
80
|
|
|
@@ -90,7 +93,7 @@ export function SettingsRootContent() {
|
|
|
90
93
|
</div>
|
|
91
94
|
<div class="settings-group">
|
|
92
95
|
<SettingsItem
|
|
93
|
-
href="/
|
|
96
|
+
href="/settings/general"
|
|
94
97
|
icon={ICONS.settings}
|
|
95
98
|
color={COLORS.blue}
|
|
96
99
|
name={t({
|
|
@@ -115,7 +118,7 @@ export function SettingsRootContent() {
|
|
|
115
118
|
</div>
|
|
116
119
|
<div class="settings-group">
|
|
117
120
|
<SettingsItem
|
|
118
|
-
href="/
|
|
121
|
+
href="/settings/avatar"
|
|
119
122
|
icon={ICONS.image}
|
|
120
123
|
color={COLORS.purple}
|
|
121
124
|
name={t({
|
|
@@ -128,7 +131,7 @@ export function SettingsRootContent() {
|
|
|
128
131
|
})}
|
|
129
132
|
/>
|
|
130
133
|
<SettingsItem
|
|
131
|
-
href="/
|
|
134
|
+
href="/settings/navigation"
|
|
132
135
|
icon={ICONS.menu}
|
|
133
136
|
color={COLORS.green}
|
|
134
137
|
name={t({
|
|
@@ -141,7 +144,7 @@ export function SettingsRootContent() {
|
|
|
141
144
|
})}
|
|
142
145
|
/>
|
|
143
146
|
<SettingsItem
|
|
144
|
-
href="/
|
|
147
|
+
href="/settings/color-theme"
|
|
145
148
|
icon={ICONS.palette}
|
|
146
149
|
color={COLORS.orange}
|
|
147
150
|
name={t({
|
|
@@ -154,7 +157,7 @@ export function SettingsRootContent() {
|
|
|
154
157
|
})}
|
|
155
158
|
/>
|
|
156
159
|
<SettingsItem
|
|
157
|
-
href="/
|
|
160
|
+
href="/settings/font-theme"
|
|
158
161
|
icon={ICONS.type}
|
|
159
162
|
color={COLORS.pink}
|
|
160
163
|
name={t({
|
|
@@ -167,7 +170,7 @@ export function SettingsRootContent() {
|
|
|
167
170
|
})}
|
|
168
171
|
/>
|
|
169
172
|
<SettingsItem
|
|
170
|
-
href="/
|
|
173
|
+
href="/settings/custom-css"
|
|
171
174
|
icon={ICONS.code}
|
|
172
175
|
color={COLORS.indigo}
|
|
173
176
|
name={t({
|
|
@@ -192,16 +195,29 @@ export function SettingsRootContent() {
|
|
|
192
195
|
</div>
|
|
193
196
|
<div class="settings-group">
|
|
194
197
|
<SettingsItem
|
|
195
|
-
href="/
|
|
198
|
+
href="/settings/custom-urls"
|
|
196
199
|
icon={ICONS.arrowRightLeft}
|
|
197
200
|
color={COLORS.amber}
|
|
198
201
|
name={t({
|
|
199
|
-
message: "
|
|
200
|
-
comment: "@context: Settings item —
|
|
202
|
+
message: "Custom URLs",
|
|
203
|
+
comment: "@context: Settings item — custom URL settings",
|
|
201
204
|
})}
|
|
202
205
|
description={t({
|
|
203
|
-
message: "
|
|
204
|
-
comment: "@context: Settings item description for
|
|
206
|
+
message: "Redirects and custom paths",
|
|
207
|
+
comment: "@context: Settings item description for custom URLs",
|
|
208
|
+
})}
|
|
209
|
+
/>
|
|
210
|
+
<SettingsItem
|
|
211
|
+
href="/settings/api-tokens"
|
|
212
|
+
icon={ICONS.key}
|
|
213
|
+
color={COLORS.teal}
|
|
214
|
+
name={t({
|
|
215
|
+
message: "API Tokens",
|
|
216
|
+
comment: "@context: Settings item — API token settings",
|
|
217
|
+
})}
|
|
218
|
+
description={t({
|
|
219
|
+
message: "Bearer tokens for scripts and automation",
|
|
220
|
+
comment: "@context: Settings item description for API tokens",
|
|
205
221
|
})}
|
|
206
222
|
/>
|
|
207
223
|
</div>
|
|
@@ -217,20 +233,34 @@ export function SettingsRootContent() {
|
|
|
217
233
|
</div>
|
|
218
234
|
<div class="settings-group">
|
|
219
235
|
<SettingsItem
|
|
220
|
-
href="/
|
|
221
|
-
icon={ICONS.
|
|
236
|
+
href="/settings/account"
|
|
237
|
+
icon={ICONS.shield}
|
|
222
238
|
color={COLORS.gray}
|
|
223
239
|
name={t({
|
|
224
240
|
message: "Account",
|
|
225
241
|
comment: "@context: Settings item — account settings",
|
|
226
242
|
})}
|
|
227
243
|
description={t({
|
|
228
|
-
message: "
|
|
244
|
+
message: "Sessions, password, export",
|
|
229
245
|
comment: "@context: Settings item description for account",
|
|
230
246
|
})}
|
|
231
247
|
/>
|
|
232
248
|
</div>
|
|
233
249
|
</div>
|
|
250
|
+
|
|
251
|
+
{/* Sign Out */}
|
|
252
|
+
<div class="pt-2 text-center">
|
|
253
|
+
<button
|
|
254
|
+
type="button"
|
|
255
|
+
data-on:click__prevent="@post('/signout')"
|
|
256
|
+
class="text-sm text-destructive hover:text-destructive/80 transition-colors"
|
|
257
|
+
>
|
|
258
|
+
{t({
|
|
259
|
+
message: "Sign Out",
|
|
260
|
+
comment: "@context: Settings link — sign out action",
|
|
261
|
+
})}
|
|
262
|
+
</button>
|
|
263
|
+
</div>
|
|
234
264
|
</div>
|
|
235
265
|
);
|
|
236
266
|
}
|
package/src/ui/feed/LinkCard.tsx
CHANGED
|
@@ -6,13 +6,25 @@
|
|
|
6
6
|
|
|
7
7
|
import type { FC } from "hono/jsx";
|
|
8
8
|
import type { TimelineCardProps } from "../../types.js";
|
|
9
|
+
import { StarRating } from "../shared/StarRating.js";
|
|
10
|
+
import { PostFooter } from "../shared/PostFooter.js";
|
|
11
|
+
import { PostStatusBadges } from "./PostStatusBadges.js";
|
|
12
|
+
import { sanitizeUrl } from "../../lib/url.js";
|
|
9
13
|
|
|
10
|
-
export const LinkCard: FC<TimelineCardProps> = ({
|
|
11
|
-
|
|
14
|
+
export const LinkCard: FC<TimelineCardProps> = ({
|
|
15
|
+
post,
|
|
16
|
+
mode = "feed",
|
|
17
|
+
display,
|
|
18
|
+
}) => {
|
|
19
|
+
const isCompact = mode === "compact";
|
|
20
|
+
const isDetail = mode === "detail";
|
|
21
|
+
const articleClass = `h-entry post-menu-target${isCompact ? " feed-compact" : isDetail ? " py-6" : " feed-card feed-card-link"}`;
|
|
22
|
+
|
|
23
|
+
const safeUrl = post.url ? sanitizeUrl(post.url) : "";
|
|
12
24
|
let domain: string | undefined;
|
|
13
|
-
if (
|
|
25
|
+
if (safeUrl) {
|
|
14
26
|
try {
|
|
15
|
-
domain = new URL(
|
|
27
|
+
domain = new URL(safeUrl).hostname.replace(/^www\./, "");
|
|
16
28
|
} catch {
|
|
17
29
|
// Invalid URL, skip domain display
|
|
18
30
|
}
|
|
@@ -20,53 +32,90 @@ export const LinkCard: FC<TimelineCardProps> = ({ post, compact }) => {
|
|
|
20
32
|
|
|
21
33
|
return (
|
|
22
34
|
<article
|
|
23
|
-
class={
|
|
35
|
+
class={articleClass}
|
|
36
|
+
{...(isDetail ? { "data-page": "post" } : {})}
|
|
24
37
|
data-post
|
|
25
38
|
data-format="link"
|
|
39
|
+
data-post-id={post.id}
|
|
40
|
+
data-post-permalink={post.permalink}
|
|
41
|
+
{...(post.pinned ? { "data-post-pinned": "" } : {})}
|
|
42
|
+
{...(post.featured ? { "data-post-featured": "" } : {})}
|
|
43
|
+
data-post-visibility={post.visibility}
|
|
44
|
+
{...(!isDetail && post.threadRootId ? { "data-post-reply": "" } : {})}
|
|
26
45
|
>
|
|
27
|
-
{
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
class="size-3"
|
|
31
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
32
|
-
fill="none"
|
|
33
|
-
viewBox="0 0 24 24"
|
|
34
|
-
stroke-width="2"
|
|
35
|
-
stroke="currentColor"
|
|
36
|
-
>
|
|
37
|
-
<path d="M13.5 6H5.25A2.25 2.25 0 0 0 3 8.25v10.5A2.25 2.25 0 0 0 5.25 21h10.5A2.25 2.25 0 0 0 18 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25" />
|
|
38
|
-
</svg>
|
|
39
|
-
<span>{domain}</span>
|
|
40
|
-
</div>
|
|
41
|
-
)}
|
|
42
|
-
{post.title && (
|
|
43
|
-
<h2
|
|
44
|
-
class={`p-name font-semibold ${compact ? "text-sm" : "text-base"} mb-1`}
|
|
45
|
-
>
|
|
46
|
+
{!isCompact && !display?.hideStatusBadges && <PostStatusBadges />}
|
|
47
|
+
{domain &&
|
|
48
|
+
(safeUrl ? (
|
|
46
49
|
<a
|
|
47
|
-
href={
|
|
48
|
-
class="
|
|
49
|
-
target=
|
|
50
|
-
rel=
|
|
50
|
+
href={safeUrl}
|
|
51
|
+
class="feed-link-domain"
|
|
52
|
+
target="_blank"
|
|
53
|
+
rel="noopener noreferrer"
|
|
51
54
|
>
|
|
52
|
-
|
|
55
|
+
<svg
|
|
56
|
+
class="feed-link-domain-icon"
|
|
57
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
58
|
+
fill="none"
|
|
59
|
+
viewBox="0 0 24 24"
|
|
60
|
+
stroke-width="2"
|
|
61
|
+
stroke="currentColor"
|
|
62
|
+
>
|
|
63
|
+
<path d="M13.5 6H5.25A2.25 2.25 0 0 0 3 8.25v10.5A2.25 2.25 0 0 0 5.25 21h10.5A2.25 2.25 0 0 0 18 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25" />
|
|
64
|
+
</svg>
|
|
65
|
+
<span>{domain}</span>
|
|
53
66
|
</a>
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
67
|
+
) : (
|
|
68
|
+
<div class="feed-link-domain">
|
|
69
|
+
<svg
|
|
70
|
+
class="feed-link-domain-icon"
|
|
71
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
72
|
+
fill="none"
|
|
73
|
+
viewBox="0 0 24 24"
|
|
74
|
+
stroke-width="2"
|
|
75
|
+
stroke="currentColor"
|
|
76
|
+
>
|
|
77
|
+
<path d="M13.5 6H5.25A2.25 2.25 0 0 0 3 8.25v10.5A2.25 2.25 0 0 0 5.25 21h10.5A2.25 2.25 0 0 0 18 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25" />
|
|
78
|
+
</svg>
|
|
79
|
+
<span>{domain}</span>
|
|
80
|
+
</div>
|
|
81
|
+
))}
|
|
82
|
+
{post.title &&
|
|
83
|
+
(isDetail ? (
|
|
84
|
+
<h1 class="p-name feed-link-title text-2xl font-semibold mb-4">
|
|
85
|
+
<a
|
|
86
|
+
href={safeUrl || post.permalink}
|
|
87
|
+
class="u-url feed-link-title-link"
|
|
88
|
+
target={safeUrl ? "_blank" : undefined}
|
|
89
|
+
rel={safeUrl ? "noopener noreferrer" : undefined}
|
|
90
|
+
>
|
|
91
|
+
{post.title}
|
|
92
|
+
</a>
|
|
93
|
+
</h1>
|
|
94
|
+
) : (
|
|
95
|
+
<h2
|
|
96
|
+
class={`p-name feed-link-title font-semibold ${isCompact ? "text-sm" : ""} mb-1`}
|
|
97
|
+
>
|
|
98
|
+
<a
|
|
99
|
+
href={safeUrl || post.permalink}
|
|
100
|
+
class="u-url feed-link-title-link"
|
|
101
|
+
target={safeUrl ? "_blank" : undefined}
|
|
102
|
+
rel={safeUrl ? "noopener noreferrer" : undefined}
|
|
103
|
+
>
|
|
104
|
+
{post.title}
|
|
105
|
+
</a>
|
|
106
|
+
</h2>
|
|
107
|
+
))}
|
|
108
|
+
{!isCompact && post.bodyHtml && (
|
|
57
109
|
<div
|
|
58
|
-
class="e-content prose text-muted-foreground"
|
|
110
|
+
class="e-content prose text-muted-foreground feed-link-summary"
|
|
59
111
|
data-post-body
|
|
60
112
|
dangerouslySetInnerHTML={{ __html: post.bodyHtml }}
|
|
61
113
|
/>
|
|
62
114
|
)}
|
|
63
|
-
|
|
64
|
-
<
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
</time>
|
|
68
|
-
</a>
|
|
69
|
-
</footer>
|
|
115
|
+
{!isCompact && !display?.hideRating && (
|
|
116
|
+
<StarRating rating={post.rating} />
|
|
117
|
+
)}
|
|
118
|
+
<PostFooter post={post} detail={isDetail} display={display?.footer} />
|
|
70
119
|
</article>
|
|
71
120
|
);
|
|
72
121
|
};
|
package/src/ui/feed/NoteCard.tsx
CHANGED
|
@@ -8,39 +8,59 @@
|
|
|
8
8
|
import type { FC } from "hono/jsx";
|
|
9
9
|
import type { TimelineCardProps } from "../../types.js";
|
|
10
10
|
import { MediaGallery } from "../shared/MediaGallery.js";
|
|
11
|
+
import { StarRating } from "../shared/StarRating.js";
|
|
12
|
+
import { PostFooter } from "../shared/PostFooter.js";
|
|
13
|
+
import { PostStatusBadges } from "./PostStatusBadges.js";
|
|
11
14
|
|
|
12
|
-
export const NoteCard: FC<TimelineCardProps> = ({
|
|
15
|
+
export const NoteCard: FC<TimelineCardProps> = ({
|
|
16
|
+
post,
|
|
17
|
+
mode = "feed",
|
|
18
|
+
display,
|
|
19
|
+
}) => {
|
|
20
|
+
const isCompact = mode === "compact";
|
|
21
|
+
const isDetail = mode === "detail";
|
|
13
22
|
const isArticle = !!post.title;
|
|
14
|
-
const displayHtml = isArticle ? post.
|
|
23
|
+
const displayHtml = isDetail || !isArticle ? post.bodyHtml : post.summaryHtml;
|
|
15
24
|
|
|
16
25
|
return (
|
|
17
26
|
<article
|
|
18
|
-
class={`h-entry${
|
|
27
|
+
class={`h-entry post-menu-target${isCompact ? " feed-compact" : isDetail ? " py-6" : ""}`}
|
|
28
|
+
{...(isDetail ? { "data-page": "post" } : {})}
|
|
19
29
|
data-post
|
|
20
30
|
data-format="note"
|
|
31
|
+
data-post-id={post.id}
|
|
32
|
+
data-post-permalink={post.permalink}
|
|
33
|
+
{...(post.pinned ? { "data-post-pinned": "" } : {})}
|
|
34
|
+
{...(post.featured ? { "data-post-featured": "" } : {})}
|
|
35
|
+
data-post-visibility={post.visibility}
|
|
36
|
+
{...(!isDetail && post.threadRootId ? { "data-post-reply": "" } : {})}
|
|
21
37
|
>
|
|
22
|
-
{
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
38
|
+
{!isCompact && !display?.hideStatusBadges && <PostStatusBadges />}
|
|
39
|
+
{isArticle &&
|
|
40
|
+
(isDetail ? (
|
|
41
|
+
<h1 class="p-name text-2xl font-semibold mb-4">{post.title}</h1>
|
|
42
|
+
) : (
|
|
43
|
+
<h2
|
|
44
|
+
class={`p-name font-semibold ${isCompact ? "text-sm" : "text-lg"} mb-1`}
|
|
45
|
+
>
|
|
46
|
+
<a href={post.permalink} class="u-url hover:underline">
|
|
47
|
+
{post.title}
|
|
48
|
+
</a>
|
|
49
|
+
</h2>
|
|
50
|
+
))}
|
|
31
51
|
{displayHtml && (
|
|
32
52
|
<div
|
|
33
|
-
class={`e-content prose ${
|
|
53
|
+
class={`e-content prose ${isCompact ? "prose-sm" : isArticle && !isDetail ? "text-muted-foreground" : ""}`}
|
|
34
54
|
data-post-body
|
|
35
55
|
dangerouslySetInnerHTML={{ __html: displayHtml }}
|
|
36
56
|
/>
|
|
37
57
|
)}
|
|
38
|
-
{!
|
|
58
|
+
{!isCompact && post.media.length > 0 && (
|
|
39
59
|
<div class="mt-3" data-post-media>
|
|
40
60
|
<MediaGallery attachments={post.media} />
|
|
41
61
|
</div>
|
|
42
62
|
)}
|
|
43
|
-
{!
|
|
63
|
+
{!isDetail && !isCompact && isArticle && post.summaryHasMore && (
|
|
44
64
|
<a
|
|
45
65
|
href={`${post.permalink}#continue`}
|
|
46
66
|
class="text-sm text-muted-foreground hover:underline mt-1 inline-block"
|
|
@@ -48,16 +68,10 @@ export const NoteCard: FC<TimelineCardProps> = ({ post, compact }) => {
|
|
|
48
68
|
Continue →
|
|
49
69
|
</a>
|
|
50
70
|
)}
|
|
51
|
-
|
|
52
|
-
<
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
>
|
|
56
|
-
<time class="dt-published" datetime={post.publishedAt}>
|
|
57
|
-
{post.publishedAtFormatted}
|
|
58
|
-
</time>
|
|
59
|
-
</a>
|
|
60
|
-
</footer>
|
|
71
|
+
{!isCompact && !display?.hideRating && (
|
|
72
|
+
<StarRating rating={post.rating} />
|
|
73
|
+
)}
|
|
74
|
+
<PostFooter post={post} detail={isDetail} display={display?.footer} />
|
|
61
75
|
</article>
|
|
62
76
|
);
|
|
63
77
|
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Post Status Badges
|
|
3
|
+
*
|
|
4
|
+
* Renders pinned / featured indicators at the top of a post card.
|
|
5
|
+
* All badges are always rendered in the DOM; visibility is driven by CSS
|
|
6
|
+
* selectors on the parent article's data attributes (data-post-pinned,
|
|
7
|
+
* data-post-featured). This lets the post menu toggle
|
|
8
|
+
* badges instantly without a page reload.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { FC } from "hono/jsx";
|
|
12
|
+
|
|
13
|
+
export const PostStatusBadges: FC = () => {
|
|
14
|
+
return (
|
|
15
|
+
<div class="post-status-badges">
|
|
16
|
+
<span class="post-status-badge post-status-pinned">
|
|
17
|
+
<svg
|
|
18
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
19
|
+
viewBox="0 0 24 24"
|
|
20
|
+
fill="none"
|
|
21
|
+
stroke="currentColor"
|
|
22
|
+
stroke-width="1.75"
|
|
23
|
+
stroke-linecap="round"
|
|
24
|
+
stroke-linejoin="round"
|
|
25
|
+
>
|
|
26
|
+
<line x1="12" x2="12" y1="17" y2="22" />
|
|
27
|
+
<path d="M5 17h14v-1.76a2 2 0 0 0-1.11-1.79l-1.78-.9A2 2 0 0 1 15 10.76V6h1a2 2 0 0 0 0-4H8a2 2 0 0 0 0 4h1v4.76a2 2 0 0 1-1.11 1.79l-1.78.9A2 2 0 0 0 5 15.24Z" />
|
|
28
|
+
</svg>
|
|
29
|
+
Pinned
|
|
30
|
+
</span>
|
|
31
|
+
<span class="post-status-separator" aria-hidden="true">
|
|
32
|
+
·
|
|
33
|
+
</span>
|
|
34
|
+
<span class="post-status-badge post-status-featured">
|
|
35
|
+
<svg
|
|
36
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
37
|
+
viewBox="0 0 24 24"
|
|
38
|
+
fill="none"
|
|
39
|
+
stroke="currentColor"
|
|
40
|
+
stroke-width="1.75"
|
|
41
|
+
stroke-linecap="round"
|
|
42
|
+
stroke-linejoin="round"
|
|
43
|
+
>
|
|
44
|
+
<path d="M19 14c1.49-1.46 3-3.21 3-5.5A5.5 5.5 0 0 0 16.5 3c-1.76 0-3 .5-4.5 2-1.5-1.5-2.74-2-4.5-2A5.5 5.5 0 0 0 2 8.5c0 2.3 1.5 4.05 3 5.5l7 7Z" />
|
|
45
|
+
</svg>
|
|
46
|
+
Featured
|
|
47
|
+
</span>
|
|
48
|
+
<span class="post-status-badge post-status-private">
|
|
49
|
+
<svg
|
|
50
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
51
|
+
viewBox="0 0 24 24"
|
|
52
|
+
fill="none"
|
|
53
|
+
stroke="currentColor"
|
|
54
|
+
stroke-width="1.75"
|
|
55
|
+
stroke-linecap="round"
|
|
56
|
+
stroke-linejoin="round"
|
|
57
|
+
>
|
|
58
|
+
<path d="M10.733 5.076a10.744 10.744 0 0 1 11.205 6.575 1 1 0 0 1 0 .696 10.747 10.747 0 0 1-1.444 2.49" />
|
|
59
|
+
<path d="M14.084 14.158a3 3 0 0 1-4.242-4.242" />
|
|
60
|
+
<path d="M17.479 17.499a10.75 10.75 0 0 1-15.417-5.151 1 1 0 0 1 0-.696 10.75 10.75 0 0 1 4.446-5.143" />
|
|
61
|
+
<path d="m2 2 20 20" />
|
|
62
|
+
</svg>
|
|
63
|
+
Private
|
|
64
|
+
</span>
|
|
65
|
+
</div>
|
|
66
|
+
);
|
|
67
|
+
};
|