@jant/core 0.3.35 → 0.3.36
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/assets/module-RjUF93sV.js +716 -0
- package/dist/client/assets/native-48B9X9Wg.js +1 -0
- package/dist/client/assets/url-8Dj-5CLW.js +1 -0
- package/dist/client/client.css +1 -1
- package/dist/client/client.js +3109 -2294
- package/dist/index.js +3026 -2778
- package/package.json +13 -4
- package/src/__tests__/helpers/app.ts +1 -1
- package/src/__tests__/helpers/db.ts +6 -0
- package/src/app.tsx +1 -5
- package/src/{lib → client}/avatar-upload.ts +1 -1
- 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/{ui → client}/components/__tests__/jant-compose-dialog.test.ts +46 -14
- package/src/{ui → client}/components/__tests__/jant-compose-editor.test.ts +64 -24
- package/src/{ui → client}/components/__tests__/jant-post-form.test.ts +24 -14
- package/src/{ui → client}/components/__tests__/jant-settings-general.test.ts +3 -3
- package/src/client/components/collection-sidebar-types.ts +45 -0
- package/src/{ui → client}/components/collection-types.ts +3 -4
- package/src/{ui → client}/components/compose-types.ts +3 -1
- package/src/{ui → client}/components/jant-collection-form.ts +301 -182
- package/src/client/components/jant-collection-sidebar.ts +801 -0
- package/src/{ui → client}/components/jant-compose-dialog.ts +231 -1
- package/src/client/components/jant-compose-editor.ts +1249 -0
- package/src/client/components/jant-compose-fullscreen.ts +338 -0
- package/src/client/components/jant-media-lightbox.ts +257 -0
- package/src/{ui → client}/components/jant-nav-manager.ts +143 -84
- package/src/{ui → client}/components/jant-post-form.ts +57 -8
- package/src/{ui → client}/components/jant-settings-general.ts +2 -2
- package/src/{ui → client}/components/nav-manager-types.ts +3 -0
- package/src/{ui → client}/components/post-form-template.ts +35 -31
- package/src/{ui → client}/components/post-form-types.ts +7 -3
- package/src/{lib → client}/compose-bridge.ts +9 -7
- package/src/client/lazy-slugify.ts +51 -0
- package/src/{lib → client}/media-upload.ts +16 -3
- package/src/{lib → client}/nav-manager-bridge.ts +1 -1
- package/src/client/page-slug-bridge.ts +42 -0
- package/src/{lib → client}/post-form-bridge.ts +2 -2
- package/src/{lib → client}/settings-bridge.ts +3 -3
- package/src/client/tiptap/bubble-menu.ts +205 -0
- package/src/client/tiptap/create-editor.ts +40 -0
- package/src/client/tiptap/exitable-marks.ts +73 -0
- package/src/client/tiptap/extensions.ts +60 -0
- package/src/client/tiptap/image-node.ts +488 -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 +140 -0
- package/src/client/tiptap/slash-commands.ts +328 -0
- package/src/{types → client/types}/sortablejs.d.ts +1 -1
- package/src/client.ts +24 -17
- package/src/db/migrations/0012_add_tiptap_columns.sql +2 -0
- package/src/db/migrations/0013_replace_featured_with_visibility.sql +8 -0
- package/src/db/schema.ts +6 -1
- package/src/i18n/locales/en.po +641 -215
- package/src/i18n/locales/en.ts +1 -1
- package/src/i18n/locales/zh-Hans.po +642 -204
- package/src/i18n/locales/zh-Hans.ts +1 -1
- package/src/i18n/locales/zh-Hant.po +642 -204
- package/src/i18n/locales/zh-Hant.ts +1 -1
- package/src/lib/__tests__/resolve-config.test.ts +2 -2
- package/src/lib/__tests__/schemas.test.ts +9 -6
- package/src/lib/__tests__/url.test.ts +2 -2
- package/src/lib/__tests__/view.test.ts +9 -9
- package/src/lib/emoji-catalog.ts +146 -0
- package/src/lib/feed.ts +1 -1
- package/src/lib/media-helpers.ts +10 -9
- package/src/lib/render.tsx +4 -3
- package/src/lib/resolve-config.ts +8 -1
- package/src/lib/schemas.ts +2 -3
- package/src/lib/summary.ts +92 -0
- package/src/lib/timeline.ts +2 -0
- package/src/lib/tiptap-render.ts +196 -0
- package/src/lib/upload.ts +97 -9
- package/src/lib/url.ts +7 -23
- package/src/lib/view.ts +33 -19
- package/src/middleware/error-handler.ts +3 -3
- package/src/preset.css +38 -0
- package/src/routes/api/collections.ts +20 -3
- package/src/routes/api/posts.ts +48 -33
- package/src/routes/api/upload.ts +7 -5
- package/src/routes/auth/reset.tsx +5 -4
- package/src/routes/auth/setup.tsx +26 -11
- package/src/routes/auth/signin.tsx +10 -7
- package/src/routes/compose.tsx +20 -11
- package/src/routes/dash/__tests__/settings-avatar.test.ts +43 -8
- package/src/routes/dash/index.tsx +7 -1
- package/src/routes/dash/media.tsx +3 -0
- package/src/routes/dash/pages.tsx +8 -2
- package/src/routes/dash/posts.tsx +6 -2
- package/src/routes/dash/redirects.tsx +15 -9
- package/src/routes/dash/settings.tsx +336 -32
- package/src/routes/feed/__tests__/rss.test.ts +7 -7
- package/src/routes/feed/rss.ts +8 -6
- package/src/routes/pages/__tests__/featured.test.ts +6 -7
- package/src/routes/pages/archive.tsx +11 -7
- package/src/routes/pages/collection.tsx +32 -15
- package/src/routes/pages/collections.tsx +11 -2
- package/src/routes/pages/featured.tsx +1 -1
- package/src/routes/pages/home.tsx +1 -1
- package/src/services/__tests__/post.test.ts +124 -33
- package/src/services/__tests__/settings.test.ts +3 -3
- package/src/services/page.ts +16 -3
- package/src/services/post.ts +96 -37
- package/src/services/search.ts +4 -2
- package/src/services/settings.ts +6 -2
- package/src/styles/components.css +240 -60
- package/src/styles/tokens.css +10 -0
- package/src/styles/ui.css +1157 -81
- package/src/types/bindings.ts +5 -0
- package/src/types/config.ts +23 -1
- package/src/types/constants.ts +3 -0
- package/src/types/entities.ts +9 -2
- package/src/types/operations.ts +9 -3
- package/src/types/props.ts +3 -3
- package/src/types/views.ts +3 -2
- package/src/ui/compose/ComposeDialog.tsx +24 -7
- package/src/ui/dash/PageForm.tsx +2 -0
- package/src/ui/dash/PostList.tsx +5 -5
- package/src/ui/dash/StatusBadge.tsx +13 -5
- package/src/ui/dash/appearance/AdvancedContent.tsx +52 -61
- package/src/ui/dash/appearance/ColorThemeContent.tsx +30 -35
- package/src/ui/dash/appearance/FontThemeContent.tsx +65 -73
- package/src/ui/dash/appearance/NavigationContent.tsx +107 -96
- package/src/ui/dash/media/MediaListContent.tsx +9 -4
- package/src/ui/dash/media/ViewMediaContent.tsx +2 -2
- package/src/ui/dash/pages/PagesContent.tsx +2 -1
- package/src/ui/dash/posts/PostForm.tsx +19 -7
- package/src/ui/dash/settings/AccountContent.tsx +133 -138
- package/src/ui/dash/settings/AvatarContent.tsx +70 -0
- package/src/ui/dash/settings/GeneralContent.tsx +3 -62
- package/src/ui/dash/settings/SettingsRootContent.tsx +236 -0
- package/src/ui/layouts/DashLayout.tsx +157 -75
- package/src/ui/layouts/SiteLayout.tsx +13 -13
- package/src/ui/pages/ArchivePage.tsx +10 -7
- package/src/ui/pages/CollectionPage.tsx +6 -35
- package/src/ui/pages/CollectionsPage.tsx +2 -1
- package/src/ui/pages/FeaturedPage.tsx +2 -1
- package/src/ui/pages/HomePage.tsx +1 -1
- package/src/ui/pages/SearchPage.tsx +1 -1
- package/src/ui/shared/CollectionsSidebar.tsx +228 -3
- package/src/ui/shared/MediaGallery.tsx +179 -41
- package/src/lib/collections-reorder.ts +0 -28
- package/src/routes/dash/appearance.tsx +0 -240
- package/src/routes/dash/collections.tsx +0 -211
- package/src/ui/components/jant-compose-editor.ts +0 -814
- 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/settings/SettingsNav.tsx +0 -52
- /package/src/{ui → client}/components/__tests__/jant-settings-avatar.test.ts +0 -0
- /package/src/{ui → client}/components/jant-settings-avatar.ts +0 -0
- /package/src/{ui → client}/components/settings-types.ts +0 -0
- /package/src/{lib → client}/image-processor.ts +0 -0
- /package/src/{lib → client}/toast.ts +0 -0
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compose Fullscreen (Zen Mode)
|
|
3
|
+
*
|
|
4
|
+
* Full-screen overlay editor with its own Tiptap instance.
|
|
5
|
+
* Opens from compose editor via jant:fullscreen-open event,
|
|
6
|
+
* returns content via jant:fullscreen-close event.
|
|
7
|
+
*
|
|
8
|
+
* Light DOM only — BaseCoat and Tailwind classes apply directly.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { LitElement, html, nothing } from "lit";
|
|
12
|
+
import type { Editor, JSONContent } from "@tiptap/core";
|
|
13
|
+
import type { ComposeLabels } from "./compose-types.js";
|
|
14
|
+
import { createTiptapEditor } from "../tiptap/create-editor.js";
|
|
15
|
+
import { getSlashCommands } from "../tiptap/slash-commands.js";
|
|
16
|
+
|
|
17
|
+
export class JantComposeFullscreen extends LitElement {
|
|
18
|
+
static properties = {
|
|
19
|
+
labels: { type: Object },
|
|
20
|
+
_open: { state: true },
|
|
21
|
+
_title: { state: true },
|
|
22
|
+
_showTitle: { state: true },
|
|
23
|
+
_actionsOpen: { state: true },
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
declare labels: ComposeLabels;
|
|
27
|
+
declare _open: boolean;
|
|
28
|
+
declare _title: string;
|
|
29
|
+
declare _showTitle: boolean;
|
|
30
|
+
declare _actionsOpen: boolean;
|
|
31
|
+
|
|
32
|
+
private _editor: Editor | null = null;
|
|
33
|
+
private _content: JSONContent | null = null;
|
|
34
|
+
private _fileInput: HTMLInputElement | null = null;
|
|
35
|
+
|
|
36
|
+
createRenderRoot() {
|
|
37
|
+
return this;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
constructor() {
|
|
41
|
+
super();
|
|
42
|
+
this.labels = {} as ComposeLabels;
|
|
43
|
+
this._open = false;
|
|
44
|
+
this._title = "";
|
|
45
|
+
this._showTitle = false;
|
|
46
|
+
this._actionsOpen = false;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
connectedCallback() {
|
|
50
|
+
super.connectedCallback();
|
|
51
|
+
document.addEventListener(
|
|
52
|
+
"jant:fullscreen-open",
|
|
53
|
+
this._onOpen as EventListener,
|
|
54
|
+
);
|
|
55
|
+
document.addEventListener("jant:slash-image", this._onSlashImage);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
disconnectedCallback() {
|
|
59
|
+
super.disconnectedCallback();
|
|
60
|
+
document.removeEventListener(
|
|
61
|
+
"jant:fullscreen-open",
|
|
62
|
+
this._onOpen as EventListener,
|
|
63
|
+
);
|
|
64
|
+
document.removeEventListener("jant:slash-image", this._onSlashImage);
|
|
65
|
+
this._fileInput?.remove();
|
|
66
|
+
this._destroyEditor();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
private _onSlashImage = () => {
|
|
70
|
+
if (!this._open || !this._editor) return;
|
|
71
|
+
this._triggerImagePicker();
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
private _triggerImagePicker() {
|
|
75
|
+
if (!this._fileInput) {
|
|
76
|
+
this._fileInput = document.createElement("input");
|
|
77
|
+
this._fileInput.type = "file";
|
|
78
|
+
this._fileInput.accept = "image/*";
|
|
79
|
+
this._fileInput.style.display = "none";
|
|
80
|
+
this._fileInput.addEventListener("change", () => {
|
|
81
|
+
const file = this._fileInput?.files?.[0];
|
|
82
|
+
if (file && this._editor) {
|
|
83
|
+
this._uploadAndInsertImage(file);
|
|
84
|
+
}
|
|
85
|
+
if (this._fileInput) this._fileInput.value = "";
|
|
86
|
+
});
|
|
87
|
+
document.body.appendChild(this._fileInput);
|
|
88
|
+
}
|
|
89
|
+
this._fileInput.click();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
private async _uploadAndInsertImage(file: File) {
|
|
93
|
+
if (!this._editor) return;
|
|
94
|
+
|
|
95
|
+
const placeholderUrl = URL.createObjectURL(file);
|
|
96
|
+
this._editor.chain().focus().setImage({ src: placeholderUrl }).run();
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
const formData = new FormData();
|
|
100
|
+
formData.append("file", file);
|
|
101
|
+
const response = await fetch("/api/upload", {
|
|
102
|
+
method: "POST",
|
|
103
|
+
body: formData,
|
|
104
|
+
});
|
|
105
|
+
if (!response.ok) throw new Error(`Upload failed: ${response.status}`);
|
|
106
|
+
const data = (await response.json()) as { url: string };
|
|
107
|
+
|
|
108
|
+
// Replace placeholder with real URL
|
|
109
|
+
const { doc } = this._editor.state;
|
|
110
|
+
let replaced = false;
|
|
111
|
+
doc.descendants((node, pos) => {
|
|
112
|
+
if (
|
|
113
|
+
replaced ||
|
|
114
|
+
node.type.name !== "image" ||
|
|
115
|
+
node.attrs.src !== placeholderUrl
|
|
116
|
+
)
|
|
117
|
+
return;
|
|
118
|
+
this._editor
|
|
119
|
+
?.chain()
|
|
120
|
+
.focus()
|
|
121
|
+
.command(({ tr }) => {
|
|
122
|
+
tr.setNodeMarkup(pos, undefined, { ...node.attrs, src: data.url });
|
|
123
|
+
return true;
|
|
124
|
+
})
|
|
125
|
+
.run();
|
|
126
|
+
replaced = true;
|
|
127
|
+
});
|
|
128
|
+
} catch {
|
|
129
|
+
// Remove placeholder on failure
|
|
130
|
+
const { doc } = this._editor.state;
|
|
131
|
+
doc.descendants((node, pos) => {
|
|
132
|
+
if (node.type.name === "image" && node.attrs.src === placeholderUrl) {
|
|
133
|
+
this._editor
|
|
134
|
+
?.chain()
|
|
135
|
+
.command(({ tr }) => {
|
|
136
|
+
tr.delete(pos, pos + node.nodeSize);
|
|
137
|
+
return true;
|
|
138
|
+
})
|
|
139
|
+
.run();
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
} finally {
|
|
143
|
+
URL.revokeObjectURL(placeholderUrl);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
private _onOpen = (
|
|
148
|
+
e: CustomEvent<{
|
|
149
|
+
json: JSONContent | null;
|
|
150
|
+
title: string;
|
|
151
|
+
showTitle: boolean;
|
|
152
|
+
format?: string;
|
|
153
|
+
labels?: ComposeLabels;
|
|
154
|
+
}>,
|
|
155
|
+
) => {
|
|
156
|
+
this._content = e.detail.json;
|
|
157
|
+
this._title = e.detail.title;
|
|
158
|
+
if (e.detail.labels) {
|
|
159
|
+
this.labels = e.detail.labels;
|
|
160
|
+
}
|
|
161
|
+
// Always show title in fullscreen — it's the primary editing surface
|
|
162
|
+
this._showTitle = true;
|
|
163
|
+
this._open = true;
|
|
164
|
+
this._actionsOpen = false;
|
|
165
|
+
// Show as modal (top layer) and init editor after render
|
|
166
|
+
this.updateComplete.then(() => {
|
|
167
|
+
const dialog = this.querySelector<HTMLDialogElement>(
|
|
168
|
+
".compose-fullscreen-dialog",
|
|
169
|
+
);
|
|
170
|
+
if (dialog && !dialog.open) {
|
|
171
|
+
dialog.showModal();
|
|
172
|
+
}
|
|
173
|
+
this._initEditor();
|
|
174
|
+
});
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
private _initEditor() {
|
|
178
|
+
const container = this.querySelector<HTMLElement>(
|
|
179
|
+
".compose-fullscreen .compose-tiptap-body",
|
|
180
|
+
);
|
|
181
|
+
if (!container || this._editor) return;
|
|
182
|
+
|
|
183
|
+
this._editor = createTiptapEditor({
|
|
184
|
+
element: container,
|
|
185
|
+
placeholder: this.labels.bodyPlaceholder ?? "Write something…",
|
|
186
|
+
content: this._content,
|
|
187
|
+
onUpdate: (json) => {
|
|
188
|
+
this._content = json;
|
|
189
|
+
},
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
private _destroyEditor() {
|
|
194
|
+
this._editor?.destroy();
|
|
195
|
+
this._editor = null;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
private _onDialogCancel = (e: Event) => {
|
|
199
|
+
// Intercept Escape key to save content back instead of just closing
|
|
200
|
+
e.preventDefault();
|
|
201
|
+
this._close();
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
private _close() {
|
|
205
|
+
const json = this._editor?.getJSON() ?? this._content;
|
|
206
|
+
this._destroyEditor();
|
|
207
|
+
|
|
208
|
+
// Close the modal dialog before Lit removes it from DOM
|
|
209
|
+
const dialog = this.querySelector<HTMLDialogElement>(
|
|
210
|
+
".compose-fullscreen-dialog",
|
|
211
|
+
);
|
|
212
|
+
dialog?.close();
|
|
213
|
+
this._open = false;
|
|
214
|
+
|
|
215
|
+
// Dispatch on document so the compose dialog (a separate subtree) receives it
|
|
216
|
+
document.dispatchEvent(
|
|
217
|
+
new CustomEvent("jant:fullscreen-close", {
|
|
218
|
+
bubbles: true,
|
|
219
|
+
detail: { json, title: this._title },
|
|
220
|
+
}),
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
private _toggleActions() {
|
|
225
|
+
this._actionsOpen = !this._actionsOpen;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
private _executeCommand(index: number) {
|
|
229
|
+
const commands = getSlashCommands();
|
|
230
|
+
const item = commands[index];
|
|
231
|
+
if (!item || !this._editor) {
|
|
232
|
+
this._actionsOpen = false;
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Image command: trigger file picker directly
|
|
237
|
+
if (item.label === "Image") {
|
|
238
|
+
this._actionsOpen = false;
|
|
239
|
+
this._triggerImagePicker();
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const { from, to } = this._editor.state.selection;
|
|
244
|
+
item.command(this._editor, { from, to });
|
|
245
|
+
this._actionsOpen = false;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
private _renderActionsMenu() {
|
|
249
|
+
if (!this._actionsOpen) return nothing;
|
|
250
|
+
const commands = getSlashCommands();
|
|
251
|
+
return html`
|
|
252
|
+
<div class="tiptap-slash-menu compose-fullscreen-plus-dropdown">
|
|
253
|
+
${commands.map(
|
|
254
|
+
(item, i) => html`
|
|
255
|
+
<div
|
|
256
|
+
class="tiptap-slash-item"
|
|
257
|
+
@mousedown=${(e: Event) => {
|
|
258
|
+
e.preventDefault();
|
|
259
|
+
this._executeCommand(i);
|
|
260
|
+
}}
|
|
261
|
+
>
|
|
262
|
+
<span class="tiptap-slash-item-icon">${item.icon}</span>
|
|
263
|
+
<span class="tiptap-slash-item-label">${item.label}</span>
|
|
264
|
+
</div>
|
|
265
|
+
`,
|
|
266
|
+
)}
|
|
267
|
+
</div>
|
|
268
|
+
`;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
render() {
|
|
272
|
+
if (!this._open) return nothing;
|
|
273
|
+
|
|
274
|
+
return html`
|
|
275
|
+
<dialog class="compose-fullscreen-dialog" @cancel=${this._onDialogCancel}>
|
|
276
|
+
<div class="compose-fullscreen">
|
|
277
|
+
<div class="compose-fullscreen-toolbar">
|
|
278
|
+
<div class="compose-fullscreen-plus-menu">
|
|
279
|
+
<button
|
|
280
|
+
type="button"
|
|
281
|
+
class="compose-tool-btn"
|
|
282
|
+
@click=${() => this._toggleActions()}
|
|
283
|
+
>
|
|
284
|
+
<svg
|
|
285
|
+
width="18"
|
|
286
|
+
height="18"
|
|
287
|
+
viewBox="0 0 18 18"
|
|
288
|
+
fill="none"
|
|
289
|
+
stroke="currentColor"
|
|
290
|
+
stroke-width="2"
|
|
291
|
+
stroke-linecap="round"
|
|
292
|
+
>
|
|
293
|
+
<line x1="9" y1="3" x2="9" y2="15" />
|
|
294
|
+
<line x1="3" y1="9" x2="15" y2="9" />
|
|
295
|
+
</svg>
|
|
296
|
+
</button>
|
|
297
|
+
${this._renderActionsMenu()}
|
|
298
|
+
</div>
|
|
299
|
+
<div class="flex-1"></div>
|
|
300
|
+
<button
|
|
301
|
+
type="button"
|
|
302
|
+
class="compose-tool-btn"
|
|
303
|
+
@click=${() => this._close()}
|
|
304
|
+
>
|
|
305
|
+
${this.labels.done || "Done"}
|
|
306
|
+
</button>
|
|
307
|
+
</div>
|
|
308
|
+
<div class="compose-fullscreen-content">
|
|
309
|
+
<div class="compose-fullscreen-inner">
|
|
310
|
+
${this._showTitle
|
|
311
|
+
? html`
|
|
312
|
+
<input
|
|
313
|
+
type="text"
|
|
314
|
+
.value=${this._title}
|
|
315
|
+
@input=${(e: Event) => {
|
|
316
|
+
this._title = (e.target as HTMLInputElement).value;
|
|
317
|
+
}}
|
|
318
|
+
@keydown=${(e: globalThis.KeyboardEvent) => {
|
|
319
|
+
if (e.key === "Enter") {
|
|
320
|
+
e.preventDefault();
|
|
321
|
+
this._editor?.commands.focus("start");
|
|
322
|
+
}
|
|
323
|
+
}}
|
|
324
|
+
class="compose-fullscreen-title"
|
|
325
|
+
placeholder=${this.labels.titlePlaceholder ?? "Title"}
|
|
326
|
+
/>
|
|
327
|
+
`
|
|
328
|
+
: nothing}
|
|
329
|
+
<div class="compose-tiptap-body"></div>
|
|
330
|
+
</div>
|
|
331
|
+
</div>
|
|
332
|
+
</div>
|
|
333
|
+
</dialog>
|
|
334
|
+
`;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
customElements.define("jant-compose-fullscreen", JantComposeFullscreen);
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Media Lightbox
|
|
3
|
+
*
|
|
4
|
+
* Fullscreen overlay carousel for post media galleries.
|
|
5
|
+
* Intercepts clicks on [data-post-media] a[data-lightbox-index] via
|
|
6
|
+
* delegated listener, reads image data from [data-lightbox-group],
|
|
7
|
+
* and displays images in a native <dialog>.
|
|
8
|
+
*
|
|
9
|
+
* Light DOM only — BaseCoat and Tailwind classes apply directly.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { LitElement, html, nothing } from "lit";
|
|
13
|
+
|
|
14
|
+
interface LightboxImage {
|
|
15
|
+
url: string;
|
|
16
|
+
alt: string;
|
|
17
|
+
width?: number;
|
|
18
|
+
height?: number;
|
|
19
|
+
mimeType?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export class JantMediaLightbox extends LitElement {
|
|
23
|
+
static properties = {
|
|
24
|
+
_images: { state: true },
|
|
25
|
+
_currentIndex: { state: true },
|
|
26
|
+
_open: { state: true },
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
declare _images: LightboxImage[];
|
|
30
|
+
declare _currentIndex: number;
|
|
31
|
+
declare _open: boolean;
|
|
32
|
+
|
|
33
|
+
createRenderRoot() {
|
|
34
|
+
this.innerHTML = "";
|
|
35
|
+
return this;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
constructor() {
|
|
39
|
+
super();
|
|
40
|
+
this._images = [];
|
|
41
|
+
this._currentIndex = 0;
|
|
42
|
+
this._open = false;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
connectedCallback() {
|
|
46
|
+
super.connectedCallback();
|
|
47
|
+
document.addEventListener("click", this.#handleDocumentClick);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
disconnectedCallback() {
|
|
51
|
+
super.disconnectedCallback();
|
|
52
|
+
document.removeEventListener("click", this.#handleDocumentClick);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
open(images: LightboxImage[], index: number) {
|
|
56
|
+
this._images = images;
|
|
57
|
+
this._currentIndex = Math.max(0, Math.min(index, images.length - 1));
|
|
58
|
+
this._open = true;
|
|
59
|
+
this.updateComplete.then(() => {
|
|
60
|
+
const dialog = this.querySelector<HTMLDialogElement>(".media-lightbox");
|
|
61
|
+
dialog?.showModal();
|
|
62
|
+
// Focus the content wrapper instead of letting the browser auto-focus
|
|
63
|
+
// the close button, which would show a focus ring on arrow-key nav.
|
|
64
|
+
this.querySelector<HTMLElement>(".media-lightbox-content")?.focus();
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
close() {
|
|
69
|
+
this.querySelector<HTMLDialogElement>(".media-lightbox")?.close();
|
|
70
|
+
this._open = false;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
#handleDocumentClick = (e: Event) => {
|
|
74
|
+
const target = e.target as HTMLElement;
|
|
75
|
+
|
|
76
|
+
// Find the closest anchor with data-lightbox-index inside [data-post-media]
|
|
77
|
+
// Media gallery lightbox (existing)
|
|
78
|
+
const anchor = target.closest<HTMLAnchorElement>(
|
|
79
|
+
"[data-post-media] a[data-lightbox-index]",
|
|
80
|
+
);
|
|
81
|
+
if (anchor) {
|
|
82
|
+
const group = anchor.closest<HTMLElement>("[data-lightbox-group]");
|
|
83
|
+
if (!group) return;
|
|
84
|
+
|
|
85
|
+
e.preventDefault();
|
|
86
|
+
|
|
87
|
+
const index = parseInt(anchor.dataset.lightboxIndex ?? "0", 10);
|
|
88
|
+
try {
|
|
89
|
+
const images: LightboxImage[] = JSON.parse(
|
|
90
|
+
group.dataset.lightboxGroup ?? "[]",
|
|
91
|
+
);
|
|
92
|
+
if (images.length > 0) {
|
|
93
|
+
this.open(images, index);
|
|
94
|
+
}
|
|
95
|
+
} catch {
|
|
96
|
+
// JSON parse failed — fall through to default link behavior
|
|
97
|
+
}
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Inline body images — collect all <img> within the same [data-post-body]
|
|
102
|
+
const img = target.closest<HTMLImageElement>("[data-post-body] img");
|
|
103
|
+
if (img) {
|
|
104
|
+
e.preventDefault();
|
|
105
|
+
const container = img.closest<HTMLElement>("[data-post-body]");
|
|
106
|
+
if (!container) return;
|
|
107
|
+
const allImages = Array.from(
|
|
108
|
+
container.querySelectorAll<HTMLImageElement>("img"),
|
|
109
|
+
);
|
|
110
|
+
const images: LightboxImage[] = allImages.map((i) => ({
|
|
111
|
+
url: i.src,
|
|
112
|
+
alt: i.alt || "",
|
|
113
|
+
}));
|
|
114
|
+
const index = allImages.indexOf(img);
|
|
115
|
+
if (images.length > 0) this.open(images, Math.max(0, index));
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
#prev() {
|
|
120
|
+
if (this._images.length <= 1) return;
|
|
121
|
+
this._currentIndex =
|
|
122
|
+
(this._currentIndex - 1 + this._images.length) % this._images.length;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
#next() {
|
|
126
|
+
if (this._images.length <= 1) return;
|
|
127
|
+
this._currentIndex = (this._currentIndex + 1) % this._images.length;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
#handleKeydown = (e: Event) => {
|
|
131
|
+
const ke = e as globalThis.KeyboardEvent;
|
|
132
|
+
if (ke.key === "ArrowLeft") {
|
|
133
|
+
e.preventDefault();
|
|
134
|
+
this.#prev();
|
|
135
|
+
} else if (ke.key === "ArrowRight") {
|
|
136
|
+
e.preventDefault();
|
|
137
|
+
this.#next();
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
#handleDialogClick = (e: Event) => {
|
|
142
|
+
const target = e.target as HTMLElement;
|
|
143
|
+
// Close on backdrop click (dialog itself or the content wrapper, not media/buttons)
|
|
144
|
+
if (
|
|
145
|
+
target === e.currentTarget ||
|
|
146
|
+
target.classList.contains("media-lightbox-content")
|
|
147
|
+
) {
|
|
148
|
+
this.close();
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
#handleClose = () => {
|
|
153
|
+
this._open = false;
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
render() {
|
|
157
|
+
if (!this._open) return nothing;
|
|
158
|
+
|
|
159
|
+
const img = this._images[this._currentIndex];
|
|
160
|
+
const multiple = this._images.length > 1;
|
|
161
|
+
|
|
162
|
+
return html`
|
|
163
|
+
<dialog
|
|
164
|
+
class="media-lightbox"
|
|
165
|
+
@keydown=${this.#handleKeydown}
|
|
166
|
+
@click=${this.#handleDialogClick}
|
|
167
|
+
@close=${this.#handleClose}
|
|
168
|
+
>
|
|
169
|
+
<div class="media-lightbox-content" tabindex="-1">
|
|
170
|
+
<button
|
|
171
|
+
type="button"
|
|
172
|
+
class="media-lightbox-close"
|
|
173
|
+
@click=${() => this.close()}
|
|
174
|
+
aria-label="Close"
|
|
175
|
+
>
|
|
176
|
+
<svg
|
|
177
|
+
width="20"
|
|
178
|
+
height="20"
|
|
179
|
+
viewBox="0 0 24 24"
|
|
180
|
+
fill="none"
|
|
181
|
+
stroke="currentColor"
|
|
182
|
+
stroke-width="2"
|
|
183
|
+
stroke-linecap="round"
|
|
184
|
+
stroke-linejoin="round"
|
|
185
|
+
>
|
|
186
|
+
<path d="M18 6 6 18" />
|
|
187
|
+
<path d="m6 6 12 12" />
|
|
188
|
+
</svg>
|
|
189
|
+
</button>
|
|
190
|
+
|
|
191
|
+
${multiple
|
|
192
|
+
? html`<div class="media-lightbox-counter">
|
|
193
|
+
${this._currentIndex + 1} / ${this._images.length}
|
|
194
|
+
</div>`
|
|
195
|
+
: nothing}
|
|
196
|
+
${img?.mimeType?.startsWith("video/")
|
|
197
|
+
? html`<video
|
|
198
|
+
class="media-lightbox-video"
|
|
199
|
+
src=${img.url}
|
|
200
|
+
controls
|
|
201
|
+
autoplay
|
|
202
|
+
playsinline
|
|
203
|
+
></video>`
|
|
204
|
+
: html`<img
|
|
205
|
+
class="media-lightbox-img"
|
|
206
|
+
src=${img?.url ?? ""}
|
|
207
|
+
alt=${img?.alt ?? ""}
|
|
208
|
+
/>`}
|
|
209
|
+
${multiple
|
|
210
|
+
? html`
|
|
211
|
+
<button
|
|
212
|
+
type="button"
|
|
213
|
+
class="media-lightbox-nav media-lightbox-nav-prev"
|
|
214
|
+
@click=${() => this.#prev()}
|
|
215
|
+
aria-label="Previous"
|
|
216
|
+
>
|
|
217
|
+
<svg
|
|
218
|
+
width="24"
|
|
219
|
+
height="24"
|
|
220
|
+
viewBox="0 0 24 24"
|
|
221
|
+
fill="none"
|
|
222
|
+
stroke="currentColor"
|
|
223
|
+
stroke-width="2"
|
|
224
|
+
stroke-linecap="round"
|
|
225
|
+
stroke-linejoin="round"
|
|
226
|
+
>
|
|
227
|
+
<path d="m15 18-6-6 6-6" />
|
|
228
|
+
</svg>
|
|
229
|
+
</button>
|
|
230
|
+
<button
|
|
231
|
+
type="button"
|
|
232
|
+
class="media-lightbox-nav media-lightbox-nav-next"
|
|
233
|
+
@click=${() => this.#next()}
|
|
234
|
+
aria-label="Next"
|
|
235
|
+
>
|
|
236
|
+
<svg
|
|
237
|
+
width="24"
|
|
238
|
+
height="24"
|
|
239
|
+
viewBox="0 0 24 24"
|
|
240
|
+
fill="none"
|
|
241
|
+
stroke="currentColor"
|
|
242
|
+
stroke-width="2"
|
|
243
|
+
stroke-linecap="round"
|
|
244
|
+
stroke-linejoin="round"
|
|
245
|
+
>
|
|
246
|
+
<path d="m9 18 6-6-6-6" />
|
|
247
|
+
</svg>
|
|
248
|
+
</button>
|
|
249
|
+
`
|
|
250
|
+
: nothing}
|
|
251
|
+
</div>
|
|
252
|
+
</dialog>
|
|
253
|
+
`;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
customElements.define("jant-media-lightbox", JantMediaLightbox);
|