@jant/core 0.3.35 → 0.3.37
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/module-RjUF93sV.js +716 -0
- package/dist/client/assets/native-48B9X9Wg.js +1 -0
- package/dist/client/assets/url-FWFqPJPb.js +1 -0
- package/dist/client/client.css +1 -1
- package/dist/client/client.js +4564 -3013
- package/dist/index.js +12885 -8161
- package/package.json +23 -6
- package/src/__tests__/helpers/app.ts +10 -10
- package/src/__tests__/helpers/db.ts +91 -87
- package/src/app.tsx +157 -31
- 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/{lib → client}/avatar-upload.ts +4 -3
- package/src/{lib → client}/collection-form-bridge.ts +2 -2
- package/src/{ui → client}/components/__tests__/jant-collection-form.test.ts +26 -9
- package/src/client/components/__tests__/jant-compose-dialog.test.ts +1140 -0
- package/src/client/components/__tests__/jant-compose-editor.test.ts +504 -0
- package/src/{ui → client}/components/__tests__/jant-post-form.test.ts +37 -17
- package/src/{ui → client}/components/__tests__/jant-settings-avatar.test.ts +2 -2
- package/src/{ui → client}/components/__tests__/jant-settings-general.test.ts +3 -3
- package/src/client/components/collection-sidebar-types.ts +43 -0
- package/src/{ui → client}/components/collection-types.ts +3 -4
- package/src/client/components/compose-types.ts +174 -0
- package/src/client/components/jant-collection-form.ts +667 -0
- package/src/client/components/jant-collection-sidebar.ts +805 -0
- package/src/client/components/jant-compose-dialog.ts +2161 -0
- package/src/client/components/jant-compose-editor.ts +1813 -0
- package/src/client/components/jant-compose-fullscreen.ts +283 -0
- package/src/client/components/jant-media-lightbox.ts +259 -0
- package/src/{ui → client}/components/jant-nav-manager.ts +97 -298
- package/src/{ui → client}/components/jant-post-form.ts +141 -12
- package/src/client/components/jant-post-menu.ts +1019 -0
- package/src/{ui → client}/components/jant-settings-avatar.ts +3 -3
- package/src/{ui → client}/components/jant-settings-general.ts +38 -4
- package/src/client/components/jant-text-preview.ts +232 -0
- package/src/{ui → client}/components/nav-manager-types.ts +6 -18
- package/src/{ui → client}/components/post-form-template.ts +137 -38
- package/src/{ui → client}/components/post-form-types.ts +15 -4
- package/src/client/compose-bridge.ts +583 -0
- package/src/{lib → client}/image-processor.ts +26 -8
- package/src/client/lazy-slugify.ts +51 -0
- package/src/client/media-metadata.ts +247 -0
- package/src/client/multipart-upload.ts +160 -0
- package/src/{lib → client}/nav-manager-bridge.ts +1 -1
- package/src/{lib → client}/post-form-bridge.ts +53 -2
- package/src/{lib → client}/settings-bridge.ts +3 -15
- package/src/client/thread-context.ts +140 -0
- package/src/client/tiptap/bubble-menu.ts +205 -0
- package/src/client/tiptap/create-editor.ts +86 -0
- package/src/client/tiptap/exitable-marks.ts +73 -0
- package/src/client/tiptap/extensions.ts +65 -0
- package/src/client/tiptap/image-node.ts +482 -0
- package/src/client/tiptap/link-toolbar.ts +371 -0
- package/src/client/tiptap/more-break.ts +50 -0
- package/src/client/tiptap/paste-image.ts +129 -0
- package/src/client/tiptap/slash-commands.ts +438 -0
- package/src/{lib → client}/toast.ts +101 -3
- package/src/client/types/sortablejs.d.ts +44 -0
- package/src/client/upload-with-metadata.ts +54 -0
- package/src/client/video-processor.ts +207 -0
- package/src/client.ts +27 -17
- 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 -140
- 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 +783 -1087
- package/src/i18n/locales/en.ts +1 -1
- package/src/i18n/locales/zh-Hans.po +867 -812
- package/src/i18n/locales/zh-Hans.ts +1 -1
- package/src/i18n/locales/zh-Hant.po +878 -823
- 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__/resolve-config.test.ts +2 -2
- package/src/lib/__tests__/schemas.test.ts +186 -65
- 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__/url.test.ts +2 -2
- package/src/lib/__tests__/view.test.ts +140 -65
- package/src/lib/blurhash-placeholder.ts +102 -0
- package/src/lib/constants.ts +3 -1
- package/src/lib/emoji-catalog.ts +963 -0
- package/src/lib/errors.ts +11 -8
- package/src/lib/feed.ts +77 -31
- 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 +22 -12
- package/src/lib/nanoid.ts +29 -0
- package/src/lib/navigation.ts +1 -1
- package/src/lib/render.tsx +24 -5
- package/src/lib/resolve-config.ts +13 -2
- package/src/lib/schemas.ts +226 -58
- 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 +158 -0
- package/src/lib/theme.ts +11 -8
- package/src/lib/timeline.ts +76 -34
- package/src/lib/tiptap-render.ts +191 -0
- package/src/lib/tiptap-to-markdown.ts +305 -0
- package/src/lib/upload.ts +263 -14
- package/src/lib/url.ts +37 -22
- package/src/lib/view.ts +236 -55
- 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/error-handler.ts +3 -3
- package/src/middleware/onboarding.ts +1 -1
- package/src/middleware/secure-headers.ts +40 -0
- package/src/preset.css +83 -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 +57 -31
- 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 +81 -62
- package/src/routes/api/search.ts +3 -4
- package/src/routes/api/upload-multipart.ts +245 -0
- package/src/routes/api/upload.ts +92 -24
- package/src/routes/auth/__tests__/setup.test.ts +20 -60
- package/src/routes/auth/reset.tsx +5 -4
- package/src/routes/auth/setup.tsx +39 -31
- package/src/routes/auth/signin.tsx +13 -14
- package/src/routes/compose.tsx +27 -63
- package/src/routes/dash/__tests__/settings-avatar.test.ts +44 -9
- package/src/routes/dash/custom-urls.tsx +414 -0
- package/src/routes/dash/settings.tsx +475 -99
- package/src/routes/feed/__tests__/rss.test.ts +22 -23
- package/src/routes/feed/rss.ts +6 -2
- 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 +36 -18
- package/src/routes/pages/archive.tsx +177 -37
- package/src/routes/pages/collection.tsx +43 -14
- package/src/routes/pages/collections.tsx +11 -2
- package/src/routes/pages/featured.tsx +27 -3
- package/src/routes/pages/home.tsx +15 -14
- 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 +800 -230
- package/src/services/__tests__/search.test.ts +67 -10
- package/src/services/__tests__/settings.test.ts +3 -3
- 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 +764 -172
- package/src/services/search.ts +161 -74
- package/src/services/settings.ts +6 -2
- package/src/styles/components.css +293 -62
- package/src/styles/tokens.css +93 -5
- package/src/styles/ui.css +4349 -766
- package/src/types/bindings.ts +8 -0
- package/src/types/config.ts +34 -4
- package/src/types/constants.ts +17 -2
- package/src/types/entities.ts +83 -37
- package/src/types/operations.ts +20 -27
- package/src/types/props.ts +52 -17
- package/src/types/views.ts +48 -24
- package/src/ui/color-themes.ts +133 -23
- package/src/ui/compose/ComposeDialog.tsx +255 -16
- 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 +12 -2
- package/src/ui/dash/appearance/AdvancedContent.tsx +71 -59
- package/src/ui/dash/appearance/ColorThemeContent.tsx +48 -33
- package/src/ui/dash/appearance/FontThemeContent.tsx +65 -73
- package/src/ui/dash/appearance/NavigationContent.tsx +106 -135
- package/src/ui/dash/index.ts +0 -3
- package/src/ui/dash/settings/AccountContent.tsx +87 -146
- 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 +78 -0
- package/src/ui/dash/settings/GeneralContent.tsx +3 -62
- package/src/ui/dash/settings/SessionsContent.tsx +159 -0
- package/src/ui/dash/settings/SettingsRootContent.tsx +266 -0
- 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 +116 -103
- package/src/ui/pages/ArchivePage.tsx +923 -95
- package/src/ui/pages/CollectionPage.tsx +6 -35
- package/src/ui/pages/CollectionsPage.tsx +2 -1
- package/src/ui/pages/ComposePage.tsx +54 -0
- package/src/ui/pages/FeaturedPage.tsx +2 -1
- package/src/ui/pages/HomePage.tsx +1 -1
- package/src/ui/pages/PostPage.tsx +30 -45
- package/src/ui/pages/SearchPage.tsx +182 -38
- package/src/ui/shared/AdminBreadcrumb.tsx +29 -0
- package/src/ui/shared/CollectionsSidebar.tsx +239 -4
- package/src/ui/shared/MediaGallery.tsx +475 -41
- 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/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/meta/0003_snapshot.json +0 -821
- package/src/lib/__tests__/sqid.test.ts +0 -65
- package/src/lib/collections-reorder.ts +0 -28
- package/src/lib/compose-bridge.ts +0 -280
- package/src/lib/media-upload.ts +0 -148
- 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/appearance.tsx +0 -240
- package/src/routes/dash/collections.tsx +0 -211
- package/src/routes/dash/index.tsx +0 -103
- package/src/routes/dash/media.tsx +0 -132
- package/src/routes/dash/pages.tsx +0 -239
- package/src/routes/dash/posts.tsx +0 -334
- package/src/routes/dash/redirects.tsx +0 -257
- 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 -203
- package/src/services/path-registry.ts +0 -160
- package/src/services/redirect.ts +0 -97
- package/src/types/sortablejs.d.ts +0 -29
- package/src/ui/components/__tests__/jant-compose-dialog.test.ts +0 -512
- package/src/ui/components/__tests__/jant-compose-editor.test.ts +0 -272
- package/src/ui/components/compose-types.ts +0 -75
- package/src/ui/components/jant-collection-form.ts +0 -512
- package/src/ui/components/jant-compose-dialog.ts +0 -495
- package/src/ui/components/jant-compose-editor.ts +0 -814
- package/src/ui/dash/PageForm.tsx +0 -185
- package/src/ui/dash/PostList.tsx +0 -95
- package/src/ui/dash/appearance/AppearanceNav.tsx +0 -60
- package/src/ui/dash/collections/CollectionForm.tsx +0 -166
- package/src/ui/dash/collections/CollectionsListContent.tsx +0 -146
- package/src/ui/dash/collections/IconPickerGrid.tsx +0 -50
- package/src/ui/dash/collections/ViewCollectionContent.tsx +0 -103
- package/src/ui/dash/media/MediaListContent.tsx +0 -201
- package/src/ui/dash/media/ViewMediaContent.tsx +0 -208
- package/src/ui/dash/pages/PagesContent.tsx +0 -74
- package/src/ui/dash/posts/PostForm.tsx +0 -248
- package/src/ui/dash/settings/SettingsNav.tsx +0 -52
- package/src/ui/layouts/DashLayout.tsx +0 -165
- package/src/ui/pages/SinglePage.tsx +0 -23
- package/src/ui/shared/ThreadView.tsx +0 -136
- /package/src/{ui → client}/components/settings-types.ts +0 -0
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slash Commands Extension
|
|
3
|
+
*
|
|
4
|
+
* Provides a "/" command menu for block formatting.
|
|
5
|
+
* Built on @tiptap/suggestion for cursor tracking and filtering.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Extension } from "@tiptap/core";
|
|
9
|
+
import Suggestion, {
|
|
10
|
+
type SuggestionOptions,
|
|
11
|
+
type SuggestionProps,
|
|
12
|
+
type SuggestionKeyDownProps,
|
|
13
|
+
} from "@tiptap/suggestion";
|
|
14
|
+
import type { Editor, Range } from "@tiptap/core";
|
|
15
|
+
|
|
16
|
+
// SVG icons (18×18, stroke-based, Lucide style)
|
|
17
|
+
const ICONS = {
|
|
18
|
+
image: `<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="18" x="3" y="3" rx="2" ry="2"/><circle cx="9" cy="9" r="2"/><path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21"/></svg>`,
|
|
19
|
+
divider: `<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 12h18"/></svg>`,
|
|
20
|
+
readMore: `<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 12h18"/><path d="m9 18 3 3 3-3"/><path d="m9 6-3-3-3 3"/><path d="M3 6h18"/></svg>`,
|
|
21
|
+
table: `<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3v18"/><rect width="18" height="18" x="3" y="3" rx="2"/><path d="M3 9h18"/><path d="M3 15h18"/></svg>`,
|
|
22
|
+
code: `<svg width="18" height="18" 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>`,
|
|
23
|
+
blockquote: `<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 21c3 0 7-1 7-8V5c0-1.25-.756-2.017-2-2H4c-1.25 0-2 .75-2 1.972V11c0 1.25.75 2 2 2 1 0 1 0 1 1v1c0 1-1 2-2 2s-1 .008-1 1.031V20c0 1 0 1 1 1z"/><path d="M15 21c3 0 7-1 7-8V5c0-1.25-.757-2.017-2-2h-4c-1.25 0-2 .75-2 1.972V11c0 1.25.75 2 2 2h.75c0 2.25.25 4-2.75 4v3c0 1 0 1 1 1z"/></svg>`,
|
|
24
|
+
bulletList: `<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="8" x2="21" y1="6" y2="6"/><line x1="8" x2="21" y1="12" y2="12"/><line x1="8" x2="21" y1="18" y2="18"/><line x1="3" x2="3.01" y1="6" y2="6"/><line x1="3" x2="3.01" y1="12" y2="12"/><line x1="3" x2="3.01" y1="18" y2="18"/></svg>`,
|
|
25
|
+
orderedList: `<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="10" x2="21" y1="6" y2="6"/><line x1="10" x2="21" y1="12" y2="12"/><line x1="10" x2="21" y1="18" y2="18"/><path d="M4 6h1v4"/><path d="M4 10h2"/><path d="M6 18H4c0-1 2-2 2-3s-1-1.5-2-1"/></svg>`,
|
|
26
|
+
h1: `<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12h8"/><path d="M4 18V6"/><path d="M12 18V6"/><path d="M17 12l3-2v10"/></svg>`,
|
|
27
|
+
h2: `<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12h8"/><path d="M4 18V6"/><path d="M12 18V6"/><path d="M21 18h-4c0-4 4-3 4-6 0-1.5-2-2.5-4-1"/></svg>`,
|
|
28
|
+
h3: `<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12h8"/><path d="M4 18V6"/><path d="M12 18V6"/><path d="M17.5 10.5c1.7-1 3.5 0 3.5 1.5a2 2 0 0 1-2 2"/><path d="M17 17.5c2 1.5 4 .3 4-1.5a2 2 0 0 0-2-2"/></svg>`,
|
|
29
|
+
} as const;
|
|
30
|
+
|
|
31
|
+
interface SlashCommandItem {
|
|
32
|
+
label: string;
|
|
33
|
+
icon: string;
|
|
34
|
+
command: (editor: Editor, range: Range) => void;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const SLASH_COMMANDS: SlashCommandItem[] = [
|
|
38
|
+
{
|
|
39
|
+
label: "Media",
|
|
40
|
+
icon: ICONS.image,
|
|
41
|
+
command: (editor, range) => {
|
|
42
|
+
editor.chain().focus().deleteRange(range).run();
|
|
43
|
+
document.dispatchEvent(
|
|
44
|
+
new CustomEvent("jant:slash-image", { bubbles: true }),
|
|
45
|
+
);
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
label: "Divider",
|
|
50
|
+
icon: ICONS.divider,
|
|
51
|
+
command: (editor, range) => {
|
|
52
|
+
editor.chain().focus().deleteRange(range).setHorizontalRule().run();
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
label: "Read More",
|
|
57
|
+
icon: ICONS.readMore,
|
|
58
|
+
command: (editor, range) => {
|
|
59
|
+
editor
|
|
60
|
+
.chain()
|
|
61
|
+
.focus()
|
|
62
|
+
.deleteRange(range)
|
|
63
|
+
.insertContent({ type: "moreBreak" })
|
|
64
|
+
.run();
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
label: "Table",
|
|
69
|
+
icon: ICONS.table,
|
|
70
|
+
command: (editor, range) => {
|
|
71
|
+
editor
|
|
72
|
+
.chain()
|
|
73
|
+
.focus()
|
|
74
|
+
.deleteRange(range)
|
|
75
|
+
.insertTable({ rows: 3, cols: 3, withHeaderRow: true })
|
|
76
|
+
.run();
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
label: "Code Block",
|
|
81
|
+
icon: ICONS.code,
|
|
82
|
+
command: (editor, range) => {
|
|
83
|
+
editor.chain().focus().deleteRange(range).toggleCodeBlock().run();
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
label: "Blockquote",
|
|
88
|
+
icon: ICONS.blockquote,
|
|
89
|
+
command: (editor, range) => {
|
|
90
|
+
editor.chain().focus().deleteRange(range).toggleBlockquote().run();
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
label: "Bullet List",
|
|
95
|
+
icon: ICONS.bulletList,
|
|
96
|
+
command: (editor, range) => {
|
|
97
|
+
editor.chain().focus().deleteRange(range).toggleBulletList().run();
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
label: "Ordered List",
|
|
102
|
+
icon: ICONS.orderedList,
|
|
103
|
+
command: (editor, range) => {
|
|
104
|
+
editor.chain().focus().deleteRange(range).toggleOrderedList().run();
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
label: "Heading 1",
|
|
109
|
+
icon: ICONS.h1,
|
|
110
|
+
command: (editor, range) => {
|
|
111
|
+
editor
|
|
112
|
+
.chain()
|
|
113
|
+
.focus()
|
|
114
|
+
.deleteRange(range)
|
|
115
|
+
.toggleHeading({ level: 1 })
|
|
116
|
+
.run();
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
label: "Heading 2",
|
|
121
|
+
icon: ICONS.h2,
|
|
122
|
+
command: (editor, range) => {
|
|
123
|
+
editor
|
|
124
|
+
.chain()
|
|
125
|
+
.focus()
|
|
126
|
+
.deleteRange(range)
|
|
127
|
+
.toggleHeading({ level: 2 })
|
|
128
|
+
.run();
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
label: "Heading 3",
|
|
133
|
+
icon: ICONS.h3,
|
|
134
|
+
command: (editor, range) => {
|
|
135
|
+
editor
|
|
136
|
+
.chain()
|
|
137
|
+
.focus()
|
|
138
|
+
.deleteRange(range)
|
|
139
|
+
.toggleHeading({ level: 3 })
|
|
140
|
+
.run();
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
];
|
|
144
|
+
|
|
145
|
+
/** Check whether a document already contains a moreBreak node */
|
|
146
|
+
function hasMoreBreak(editor: Editor): boolean {
|
|
147
|
+
let found = false;
|
|
148
|
+
editor.state.doc.descendants((node) => {
|
|
149
|
+
if (node.type.name === "moreBreak") {
|
|
150
|
+
found = true;
|
|
151
|
+
return false; // stop traversal
|
|
152
|
+
}
|
|
153
|
+
return !found;
|
|
154
|
+
});
|
|
155
|
+
return found;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Returns the slash commands list, used by both the extension and the + menu.
|
|
160
|
+
* Omits "Read More" when the document already contains one.
|
|
161
|
+
*/
|
|
162
|
+
export function getSlashCommands(editor?: Editor): SlashCommandItem[] {
|
|
163
|
+
if (editor && hasMoreBreak(editor)) {
|
|
164
|
+
return SLASH_COMMANDS.filter((item) => item.label !== "Read More");
|
|
165
|
+
}
|
|
166
|
+
return SLASH_COMMANDS;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Popup element management
|
|
170
|
+
let popupEl: HTMLElement | null = null;
|
|
171
|
+
let selectedIndex = 0;
|
|
172
|
+
let filteredItems: SlashCommandItem[] = [];
|
|
173
|
+
let commandFn: ((item: { index: number }) => void) | null = null;
|
|
174
|
+
let editorRef: Editor | null = null;
|
|
175
|
+
let currentRange: Range | null = null;
|
|
176
|
+
let outsideClickHandler: ((e: MouseEvent) => void) | null = null;
|
|
177
|
+
|
|
178
|
+
function createPopup(): HTMLElement {
|
|
179
|
+
const el = document.createElement("div");
|
|
180
|
+
el.className = "tiptap-slash-menu";
|
|
181
|
+
el.style.position = "fixed";
|
|
182
|
+
return el;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** Scroll the selected item into view within the popup only (no ancestor scroll) */
|
|
186
|
+
function scrollSelectedIntoView() {
|
|
187
|
+
if (!popupEl) return;
|
|
188
|
+
const selected = popupEl.querySelector(
|
|
189
|
+
".tiptap-slash-item.is-selected",
|
|
190
|
+
) as HTMLElement | null;
|
|
191
|
+
if (!selected) return;
|
|
192
|
+
const itemTop = selected.offsetTop;
|
|
193
|
+
const itemBottom = itemTop + selected.offsetHeight;
|
|
194
|
+
const scrollTop = popupEl.scrollTop;
|
|
195
|
+
const viewBottom = scrollTop + popupEl.clientHeight;
|
|
196
|
+
if (itemTop < scrollTop) {
|
|
197
|
+
popupEl.scrollTop = itemTop;
|
|
198
|
+
} else if (itemBottom > viewBottom) {
|
|
199
|
+
popupEl.scrollTop = itemBottom - popupEl.clientHeight;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** Update selection highlight and scroll into view */
|
|
204
|
+
function updateSelection() {
|
|
205
|
+
popupEl
|
|
206
|
+
?.querySelectorAll(".tiptap-slash-item")
|
|
207
|
+
.forEach((item, i) =>
|
|
208
|
+
item.classList.toggle("is-selected", i === selectedIndex),
|
|
209
|
+
);
|
|
210
|
+
scrollSelectedIntoView();
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function renderPopup(
|
|
214
|
+
items: SlashCommandItem[],
|
|
215
|
+
onSelect: (index: number) => void,
|
|
216
|
+
) {
|
|
217
|
+
if (!popupEl) return;
|
|
218
|
+
|
|
219
|
+
filteredItems = items;
|
|
220
|
+
if (selectedIndex >= items.length) selectedIndex = 0;
|
|
221
|
+
|
|
222
|
+
popupEl.innerHTML = items
|
|
223
|
+
.map(
|
|
224
|
+
(item, i) =>
|
|
225
|
+
`<div class="tiptap-slash-item${i === selectedIndex ? " is-selected" : ""}" data-index="${i}">
|
|
226
|
+
<span class="tiptap-slash-item-icon">${item.icon}</span>
|
|
227
|
+
<span class="tiptap-slash-item-label">${item.label}</span>
|
|
228
|
+
</div>`,
|
|
229
|
+
)
|
|
230
|
+
.join("");
|
|
231
|
+
|
|
232
|
+
// Click handlers
|
|
233
|
+
popupEl.querySelectorAll<HTMLElement>(".tiptap-slash-item").forEach((el) => {
|
|
234
|
+
el.addEventListener("mousedown", (e) => {
|
|
235
|
+
e.preventDefault();
|
|
236
|
+
const idx = parseInt(el.dataset.index ?? "0", 10);
|
|
237
|
+
onSelect(idx);
|
|
238
|
+
});
|
|
239
|
+
el.addEventListener("mouseenter", () => {
|
|
240
|
+
selectedIndex = parseInt(el.dataset.index ?? "0", 10);
|
|
241
|
+
updateSelection();
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function destroyPopup() {
|
|
247
|
+
if (outsideClickHandler) {
|
|
248
|
+
document.removeEventListener("mousedown", outsideClickHandler, true);
|
|
249
|
+
outsideClickHandler = null;
|
|
250
|
+
}
|
|
251
|
+
popupEl?.remove();
|
|
252
|
+
popupEl = null;
|
|
253
|
+
selectedIndex = 0;
|
|
254
|
+
filteredItems = [];
|
|
255
|
+
commandFn = null;
|
|
256
|
+
editorRef = null;
|
|
257
|
+
currentRange = null;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Position the popup relative to the cursor, accounting for dialog containing block.
|
|
262
|
+
* When a `<dialog>` has CSS animation, it creates a containing block that makes
|
|
263
|
+
* `position: fixed` relative to the dialog instead of the viewport.
|
|
264
|
+
* Flips above the cursor when there isn't enough space below.
|
|
265
|
+
*/
|
|
266
|
+
function positionPopup(
|
|
267
|
+
rect: globalThis.DOMRect,
|
|
268
|
+
container: HTMLElement | null,
|
|
269
|
+
) {
|
|
270
|
+
if (!popupEl) return;
|
|
271
|
+
|
|
272
|
+
// Reset inline max-height so offsetHeight reflects the natural size
|
|
273
|
+
popupEl.style.maxHeight = "";
|
|
274
|
+
|
|
275
|
+
const offsetX = container?.getBoundingClientRect().left ?? 0;
|
|
276
|
+
const offsetY = container?.getBoundingClientRect().top ?? 0;
|
|
277
|
+
const containerHeight = container?.clientHeight ?? window.innerHeight;
|
|
278
|
+
const popupHeight = popupEl.offsetHeight;
|
|
279
|
+
const gap = 4;
|
|
280
|
+
|
|
281
|
+
const left = rect.left - offsetX;
|
|
282
|
+
const belowTop = rect.bottom + gap - offsetY;
|
|
283
|
+
const spaceBelow = containerHeight - belowTop;
|
|
284
|
+
const spaceAbove = rect.top - offsetY - gap;
|
|
285
|
+
|
|
286
|
+
popupEl.style.left = `${left}px`;
|
|
287
|
+
|
|
288
|
+
if (popupHeight > spaceBelow && spaceAbove > spaceBelow) {
|
|
289
|
+
// Not enough space below and more room above — flip
|
|
290
|
+
const maxH = Math.min(popupHeight, spaceAbove);
|
|
291
|
+
if (popupHeight > spaceAbove) {
|
|
292
|
+
popupEl.style.maxHeight = `${spaceAbove}px`;
|
|
293
|
+
}
|
|
294
|
+
popupEl.style.top = `${rect.top - offsetY - maxH - gap}px`;
|
|
295
|
+
} else {
|
|
296
|
+
// Show below (constrain if needed)
|
|
297
|
+
if (popupHeight > spaceBelow) {
|
|
298
|
+
popupEl.style.maxHeight = `${spaceBelow}px`;
|
|
299
|
+
}
|
|
300
|
+
popupEl.style.top = `${belowTop}px`;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/** Install a click-outside handler to dismiss the suggestion on external clicks */
|
|
305
|
+
function installClickOutside() {
|
|
306
|
+
outsideClickHandler = (e: MouseEvent) => {
|
|
307
|
+
if (!popupEl || popupEl.contains(e.target as Node)) return;
|
|
308
|
+
// Click anywhere outside the popup (including inside the editor) — dismiss
|
|
309
|
+
// by deleting the trigger text so the suggestion plugin deactivates via onExit
|
|
310
|
+
if (editorRef && currentRange) {
|
|
311
|
+
const { state, view } = editorRef;
|
|
312
|
+
view.dispatch(state.tr.delete(currentRange.from, currentRange.to));
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
document.addEventListener("mousedown", outsideClickHandler, true);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Slash commands Tiptap extension.
|
|
320
|
+
*/
|
|
321
|
+
export const SlashCommands = Extension.create({
|
|
322
|
+
name: "slashCommands",
|
|
323
|
+
|
|
324
|
+
addOptions() {
|
|
325
|
+
return {
|
|
326
|
+
suggestion: {
|
|
327
|
+
char: "/",
|
|
328
|
+
startOfLine: false,
|
|
329
|
+
items: ({ query, editor }: { query: string; editor: Editor }) => {
|
|
330
|
+
const q = query.toLowerCase();
|
|
331
|
+
return getSlashCommands(editor).filter((item) =>
|
|
332
|
+
item.label.toLowerCase().includes(q),
|
|
333
|
+
);
|
|
334
|
+
},
|
|
335
|
+
render: () => {
|
|
336
|
+
function getEditorElement(editor: Editor): globalThis.Element | null {
|
|
337
|
+
const el = editor.options.element;
|
|
338
|
+
return el instanceof globalThis.Element ? el : null;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return {
|
|
342
|
+
onStart: (
|
|
343
|
+
props: SuggestionProps<SlashCommandItem, { index: number }>,
|
|
344
|
+
) => {
|
|
345
|
+
popupEl = createPopup();
|
|
346
|
+
selectedIndex = 0;
|
|
347
|
+
commandFn = props.command;
|
|
348
|
+
editorRef = props.editor;
|
|
349
|
+
currentRange = props.range;
|
|
350
|
+
renderPopup(props.items, (index) => props.command({ index }));
|
|
351
|
+
|
|
352
|
+
// Append inside the closest dialog (top-layer) or body
|
|
353
|
+
const editorEl = getEditorElement(props.editor);
|
|
354
|
+
const dialog = editorEl?.closest("dialog") ?? null;
|
|
355
|
+
(dialog ?? document.body).appendChild(popupEl);
|
|
356
|
+
|
|
357
|
+
const rect = props.clientRect?.();
|
|
358
|
+
if (rect) {
|
|
359
|
+
positionPopup(rect, dialog);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
installClickOutside();
|
|
363
|
+
},
|
|
364
|
+
onUpdate: (
|
|
365
|
+
props: SuggestionProps<SlashCommandItem, { index: number }>,
|
|
366
|
+
) => {
|
|
367
|
+
commandFn = props.command;
|
|
368
|
+
currentRange = props.range;
|
|
369
|
+
renderPopup(props.items, (index) => props.command({ index }));
|
|
370
|
+
const rect = props.clientRect?.();
|
|
371
|
+
if (rect) {
|
|
372
|
+
const editorEl = getEditorElement(props.editor);
|
|
373
|
+
const dialog = editorEl?.closest("dialog") ?? null;
|
|
374
|
+
positionPopup(rect, dialog);
|
|
375
|
+
}
|
|
376
|
+
},
|
|
377
|
+
onKeyDown: (props: SuggestionKeyDownProps) => {
|
|
378
|
+
const { event } = props;
|
|
379
|
+
if (event.key === "ArrowDown") {
|
|
380
|
+
event.preventDefault();
|
|
381
|
+
selectedIndex = (selectedIndex + 1) % filteredItems.length;
|
|
382
|
+
updateSelection();
|
|
383
|
+
return true;
|
|
384
|
+
}
|
|
385
|
+
if (event.key === "ArrowUp") {
|
|
386
|
+
event.preventDefault();
|
|
387
|
+
selectedIndex =
|
|
388
|
+
(selectedIndex - 1 + filteredItems.length) %
|
|
389
|
+
filteredItems.length;
|
|
390
|
+
updateSelection();
|
|
391
|
+
return true;
|
|
392
|
+
}
|
|
393
|
+
if (event.key === "Enter") {
|
|
394
|
+
event.preventDefault();
|
|
395
|
+
commandFn?.({ index: selectedIndex });
|
|
396
|
+
return true;
|
|
397
|
+
}
|
|
398
|
+
if (event.key === "Escape") {
|
|
399
|
+
// Stop propagation to prevent parent dialog from closing
|
|
400
|
+
event.stopPropagation();
|
|
401
|
+
event.preventDefault();
|
|
402
|
+
destroyPopup();
|
|
403
|
+
return true;
|
|
404
|
+
}
|
|
405
|
+
return false;
|
|
406
|
+
},
|
|
407
|
+
onExit: () => {
|
|
408
|
+
destroyPopup();
|
|
409
|
+
},
|
|
410
|
+
};
|
|
411
|
+
},
|
|
412
|
+
command: ({
|
|
413
|
+
editor,
|
|
414
|
+
range,
|
|
415
|
+
props,
|
|
416
|
+
}: {
|
|
417
|
+
editor: Editor;
|
|
418
|
+
range: Range;
|
|
419
|
+
props: { index: number };
|
|
420
|
+
}) => {
|
|
421
|
+
const item = filteredItems[props.index];
|
|
422
|
+
if (item) {
|
|
423
|
+
item.command(editor, range);
|
|
424
|
+
}
|
|
425
|
+
},
|
|
426
|
+
} satisfies Partial<SuggestionOptions>,
|
|
427
|
+
};
|
|
428
|
+
},
|
|
429
|
+
|
|
430
|
+
addProseMirrorPlugins() {
|
|
431
|
+
return [
|
|
432
|
+
Suggestion({
|
|
433
|
+
editor: this.editor,
|
|
434
|
+
...this.options.suggestion,
|
|
435
|
+
}),
|
|
436
|
+
];
|
|
437
|
+
},
|
|
438
|
+
});
|
|
@@ -5,6 +5,13 @@
|
|
|
5
5
|
* Appends a temporary notification to `#toast-container`.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
/** Ensure the toast container is in the top layer (above <dialog> etc.) */
|
|
9
|
+
function ensureTopLayer(container: HTMLElement): void {
|
|
10
|
+
if (!container.matches(":popover-open")) {
|
|
11
|
+
container.showPopover();
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
8
15
|
const TOAST_ICONS = {
|
|
9
16
|
success:
|
|
10
17
|
'<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><path d="m9 11 3 3L22 4"/></svg>',
|
|
@@ -12,6 +19,26 @@ const TOAST_ICONS = {
|
|
|
12
19
|
'<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>',
|
|
13
20
|
};
|
|
14
21
|
|
|
22
|
+
/** Build toast inner content using safe DOM APIs (icon is trusted, text uses textContent). */
|
|
23
|
+
function setToastContent(
|
|
24
|
+
toast: HTMLElement,
|
|
25
|
+
type: "success" | "error",
|
|
26
|
+
message: string,
|
|
27
|
+
action?: { label: string; href: string },
|
|
28
|
+
): void {
|
|
29
|
+
toast.innerHTML = TOAST_ICONS[type];
|
|
30
|
+
const span = document.createElement("span");
|
|
31
|
+
span.textContent = message;
|
|
32
|
+
toast.appendChild(span);
|
|
33
|
+
if (action) {
|
|
34
|
+
const a = document.createElement("a");
|
|
35
|
+
a.href = action.href;
|
|
36
|
+
a.className = "toast-action";
|
|
37
|
+
a.textContent = action.label;
|
|
38
|
+
toast.appendChild(a);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
15
42
|
/**
|
|
16
43
|
* Show a toast notification.
|
|
17
44
|
*
|
|
@@ -31,9 +58,44 @@ export function showToast(
|
|
|
31
58
|
const container = document.getElementById("toast-container");
|
|
32
59
|
if (!container) return;
|
|
33
60
|
|
|
61
|
+
ensureTopLayer(container);
|
|
62
|
+
|
|
34
63
|
const toast = document.createElement("div");
|
|
35
64
|
toast.className = `toast toast-${type}`;
|
|
36
|
-
toast
|
|
65
|
+
setToastContent(toast, type, message);
|
|
66
|
+
container.appendChild(toast);
|
|
67
|
+
|
|
68
|
+
setTimeout(() => {
|
|
69
|
+
toast.classList.add("toast-out");
|
|
70
|
+
toast.addEventListener("animationend", () => toast.remove());
|
|
71
|
+
}, 3000);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Show a toast with an action link.
|
|
76
|
+
*
|
|
77
|
+
* @param message - Text to display
|
|
78
|
+
* @param action - Action link with label and href
|
|
79
|
+
* @param type - Visual style: "success" (default) or "error"
|
|
80
|
+
*
|
|
81
|
+
* @example
|
|
82
|
+
* showToastWithAction("Post published.", { label: "View", href: "/p/abc" });
|
|
83
|
+
*/
|
|
84
|
+
export function showToastWithAction(
|
|
85
|
+
message: string,
|
|
86
|
+
action: { label: string; href: string },
|
|
87
|
+
type: "success" | "error" = "success",
|
|
88
|
+
): void {
|
|
89
|
+
if (!message) return;
|
|
90
|
+
|
|
91
|
+
const container = document.getElementById("toast-container");
|
|
92
|
+
if (!container) return;
|
|
93
|
+
|
|
94
|
+
ensureTopLayer(container);
|
|
95
|
+
|
|
96
|
+
const toast = document.createElement("div");
|
|
97
|
+
toast.className = `toast toast-${type}`;
|
|
98
|
+
setToastContent(toast, type, message, action);
|
|
37
99
|
container.appendChild(toast);
|
|
38
100
|
|
|
39
101
|
setTimeout(() => {
|
|
@@ -61,10 +123,12 @@ export function showPersistentToast(
|
|
|
61
123
|
const container = document.getElementById("toast-container");
|
|
62
124
|
if (!container) return null;
|
|
63
125
|
|
|
126
|
+
ensureTopLayer(container);
|
|
127
|
+
|
|
64
128
|
const toast = document.createElement("div");
|
|
65
129
|
toast.className = `toast toast-${type}`;
|
|
66
130
|
toast.id = `toast-${id}`;
|
|
67
|
-
toast
|
|
131
|
+
setToastContent(toast, type, message);
|
|
68
132
|
container.appendChild(toast);
|
|
69
133
|
|
|
70
134
|
return toast;
|
|
@@ -125,7 +189,41 @@ export function replaceWithAutoClose(
|
|
|
125
189
|
}
|
|
126
190
|
|
|
127
191
|
toast.className = `toast toast-${type}`;
|
|
128
|
-
toast.
|
|
192
|
+
toast.replaceChildren();
|
|
193
|
+
setToastContent(toast, type, message);
|
|
194
|
+
|
|
195
|
+
setTimeout(() => {
|
|
196
|
+
toast.classList.add("toast-out");
|
|
197
|
+
toast.addEventListener("animationend", () => toast.remove());
|
|
198
|
+
}, 3000);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Replace a persistent toast with an auto-dismissing one that has an action link.
|
|
203
|
+
*
|
|
204
|
+
* @param id - The toast identifier
|
|
205
|
+
* @param message - New message text
|
|
206
|
+
* @param action - Action link with label and href
|
|
207
|
+
* @param type - Visual style: "success" (default) or "error"
|
|
208
|
+
*
|
|
209
|
+
* @example
|
|
210
|
+
* replaceWithAutoCloseAction("upload", "Post published.", { label: "View", href: "/p/abc" });
|
|
211
|
+
*/
|
|
212
|
+
export function replaceWithAutoCloseAction(
|
|
213
|
+
id: string,
|
|
214
|
+
message: string,
|
|
215
|
+
action: { label: string; href: string },
|
|
216
|
+
type: "success" | "error" = "success",
|
|
217
|
+
): void {
|
|
218
|
+
const toast = document.getElementById(`toast-${id}`);
|
|
219
|
+
if (!toast) {
|
|
220
|
+
showToastWithAction(message, action, type);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
toast.className = `toast toast-${type}`;
|
|
225
|
+
toast.replaceChildren();
|
|
226
|
+
setToastContent(toast, type, message, action);
|
|
129
227
|
|
|
130
228
|
setTimeout(() => {
|
|
131
229
|
toast.classList.add("toast-out");
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal type declarations for sortablejs
|
|
3
|
+
*
|
|
4
|
+
* Only covers the API surface used by jant-nav-manager and jant-collection-sidebar.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
declare module "sortablejs" {
|
|
8
|
+
interface SortableEvent {
|
|
9
|
+
oldIndex?: number;
|
|
10
|
+
newIndex?: number;
|
|
11
|
+
item: HTMLElement;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface SortableOptions {
|
|
15
|
+
animation?: number;
|
|
16
|
+
bubbleScroll?: boolean;
|
|
17
|
+
chosenClass?: string;
|
|
18
|
+
direction?: "horizontal" | "vertical";
|
|
19
|
+
dragClass?: string;
|
|
20
|
+
fallbackTolerance?: number;
|
|
21
|
+
filter?: string;
|
|
22
|
+
forceAutoScrollFallback?: boolean;
|
|
23
|
+
ghostClass?: string;
|
|
24
|
+
handle?: string;
|
|
25
|
+
onChoose?: (event: SortableEvent) => void;
|
|
26
|
+
onStart?: (event: SortableEvent) => void;
|
|
27
|
+
onUnchoose?: (event: SortableEvent) => void;
|
|
28
|
+
onEnd?: (event: SortableEvent) => void;
|
|
29
|
+
preventOnFilter?: boolean;
|
|
30
|
+
scroll?: boolean | HTMLElement;
|
|
31
|
+
scrollSensitivity?: number;
|
|
32
|
+
scrollSpeed?: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface SortableInstance {
|
|
36
|
+
destroy(): void;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const Sortable: {
|
|
40
|
+
create(el: HTMLElement, options?: SortableOptions): SortableInstance;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export default Sortable;
|
|
44
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared Upload Helper with Metadata
|
|
3
|
+
*
|
|
4
|
+
* Processes images via ImageProcessor, extracts dimensions + blurhash,
|
|
5
|
+
* and uploads with metadata attached to the FormData.
|
|
6
|
+
* Used by paste-image, image-node replace, and fullscreen compose.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { ImageProcessor } from "./image-processor.js";
|
|
10
|
+
import { extractImageMetadata } from "./media-metadata.js";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Process an image file and upload it with dimension/blurhash metadata.
|
|
14
|
+
*
|
|
15
|
+
* @returns The server response with url and id
|
|
16
|
+
*/
|
|
17
|
+
export async function uploadWithMetadata(
|
|
18
|
+
file: File,
|
|
19
|
+
): Promise<{ url: string; id: string }> {
|
|
20
|
+
// Process image (resize, convert to WebP)
|
|
21
|
+
const {
|
|
22
|
+
file: processed,
|
|
23
|
+
width,
|
|
24
|
+
height,
|
|
25
|
+
} = await ImageProcessor.processToFile(file);
|
|
26
|
+
|
|
27
|
+
// Extract blurhash from the processed file
|
|
28
|
+
let blurhash: string | undefined;
|
|
29
|
+
try {
|
|
30
|
+
const meta = await extractImageMetadata(processed);
|
|
31
|
+
blurhash = meta.blurhash;
|
|
32
|
+
} catch {
|
|
33
|
+
// Blurhash extraction failed — upload without it
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const formData = new FormData();
|
|
37
|
+
formData.append("file", processed);
|
|
38
|
+
formData.append("width", String(width));
|
|
39
|
+
formData.append("height", String(height));
|
|
40
|
+
if (blurhash) {
|
|
41
|
+
formData.append("blurhash", blurhash);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const response = await fetch("/api/upload", {
|
|
45
|
+
method: "POST",
|
|
46
|
+
body: formData,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
if (!response.ok) {
|
|
50
|
+
throw new Error(`Upload failed: ${response.status}`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return (await response.json()) as { url: string; id: string };
|
|
54
|
+
}
|