@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.
Files changed (156) hide show
  1. package/dist/client/assets/module-RjUF93sV.js +716 -0
  2. package/dist/client/assets/native-48B9X9Wg.js +1 -0
  3. package/dist/client/assets/url-8Dj-5CLW.js +1 -0
  4. package/dist/client/client.css +1 -1
  5. package/dist/client/client.js +3109 -2294
  6. package/dist/index.js +3026 -2778
  7. package/package.json +13 -4
  8. package/src/__tests__/helpers/app.ts +1 -1
  9. package/src/__tests__/helpers/db.ts +6 -0
  10. package/src/app.tsx +1 -5
  11. package/src/{lib → client}/avatar-upload.ts +1 -1
  12. package/src/{lib → client}/collection-form-bridge.ts +2 -2
  13. package/src/{ui → client}/components/__tests__/jant-collection-form.test.ts +26 -9
  14. package/src/{ui → client}/components/__tests__/jant-compose-dialog.test.ts +46 -14
  15. package/src/{ui → client}/components/__tests__/jant-compose-editor.test.ts +64 -24
  16. package/src/{ui → client}/components/__tests__/jant-post-form.test.ts +24 -14
  17. package/src/{ui → client}/components/__tests__/jant-settings-general.test.ts +3 -3
  18. package/src/client/components/collection-sidebar-types.ts +45 -0
  19. package/src/{ui → client}/components/collection-types.ts +3 -4
  20. package/src/{ui → client}/components/compose-types.ts +3 -1
  21. package/src/{ui → client}/components/jant-collection-form.ts +301 -182
  22. package/src/client/components/jant-collection-sidebar.ts +801 -0
  23. package/src/{ui → client}/components/jant-compose-dialog.ts +231 -1
  24. package/src/client/components/jant-compose-editor.ts +1249 -0
  25. package/src/client/components/jant-compose-fullscreen.ts +338 -0
  26. package/src/client/components/jant-media-lightbox.ts +257 -0
  27. package/src/{ui → client}/components/jant-nav-manager.ts +143 -84
  28. package/src/{ui → client}/components/jant-post-form.ts +57 -8
  29. package/src/{ui → client}/components/jant-settings-general.ts +2 -2
  30. package/src/{ui → client}/components/nav-manager-types.ts +3 -0
  31. package/src/{ui → client}/components/post-form-template.ts +35 -31
  32. package/src/{ui → client}/components/post-form-types.ts +7 -3
  33. package/src/{lib → client}/compose-bridge.ts +9 -7
  34. package/src/client/lazy-slugify.ts +51 -0
  35. package/src/{lib → client}/media-upload.ts +16 -3
  36. package/src/{lib → client}/nav-manager-bridge.ts +1 -1
  37. package/src/client/page-slug-bridge.ts +42 -0
  38. package/src/{lib → client}/post-form-bridge.ts +2 -2
  39. package/src/{lib → client}/settings-bridge.ts +3 -3
  40. package/src/client/tiptap/bubble-menu.ts +205 -0
  41. package/src/client/tiptap/create-editor.ts +40 -0
  42. package/src/client/tiptap/exitable-marks.ts +73 -0
  43. package/src/client/tiptap/extensions.ts +60 -0
  44. package/src/client/tiptap/image-node.ts +488 -0
  45. package/src/client/tiptap/link-toolbar.ts +371 -0
  46. package/src/client/tiptap/more-break.ts +50 -0
  47. package/src/client/tiptap/paste-image.ts +140 -0
  48. package/src/client/tiptap/slash-commands.ts +328 -0
  49. package/src/{types → client/types}/sortablejs.d.ts +1 -1
  50. package/src/client.ts +24 -17
  51. package/src/db/migrations/0012_add_tiptap_columns.sql +2 -0
  52. package/src/db/migrations/0013_replace_featured_with_visibility.sql +8 -0
  53. package/src/db/schema.ts +6 -1
  54. package/src/i18n/locales/en.po +641 -215
  55. package/src/i18n/locales/en.ts +1 -1
  56. package/src/i18n/locales/zh-Hans.po +642 -204
  57. package/src/i18n/locales/zh-Hans.ts +1 -1
  58. package/src/i18n/locales/zh-Hant.po +642 -204
  59. package/src/i18n/locales/zh-Hant.ts +1 -1
  60. package/src/lib/__tests__/resolve-config.test.ts +2 -2
  61. package/src/lib/__tests__/schemas.test.ts +9 -6
  62. package/src/lib/__tests__/url.test.ts +2 -2
  63. package/src/lib/__tests__/view.test.ts +9 -9
  64. package/src/lib/emoji-catalog.ts +146 -0
  65. package/src/lib/feed.ts +1 -1
  66. package/src/lib/media-helpers.ts +10 -9
  67. package/src/lib/render.tsx +4 -3
  68. package/src/lib/resolve-config.ts +8 -1
  69. package/src/lib/schemas.ts +2 -3
  70. package/src/lib/summary.ts +92 -0
  71. package/src/lib/timeline.ts +2 -0
  72. package/src/lib/tiptap-render.ts +196 -0
  73. package/src/lib/upload.ts +97 -9
  74. package/src/lib/url.ts +7 -23
  75. package/src/lib/view.ts +33 -19
  76. package/src/middleware/error-handler.ts +3 -3
  77. package/src/preset.css +38 -0
  78. package/src/routes/api/collections.ts +20 -3
  79. package/src/routes/api/posts.ts +48 -33
  80. package/src/routes/api/upload.ts +7 -5
  81. package/src/routes/auth/reset.tsx +5 -4
  82. package/src/routes/auth/setup.tsx +26 -11
  83. package/src/routes/auth/signin.tsx +10 -7
  84. package/src/routes/compose.tsx +20 -11
  85. package/src/routes/dash/__tests__/settings-avatar.test.ts +43 -8
  86. package/src/routes/dash/index.tsx +7 -1
  87. package/src/routes/dash/media.tsx +3 -0
  88. package/src/routes/dash/pages.tsx +8 -2
  89. package/src/routes/dash/posts.tsx +6 -2
  90. package/src/routes/dash/redirects.tsx +15 -9
  91. package/src/routes/dash/settings.tsx +336 -32
  92. package/src/routes/feed/__tests__/rss.test.ts +7 -7
  93. package/src/routes/feed/rss.ts +8 -6
  94. package/src/routes/pages/__tests__/featured.test.ts +6 -7
  95. package/src/routes/pages/archive.tsx +11 -7
  96. package/src/routes/pages/collection.tsx +32 -15
  97. package/src/routes/pages/collections.tsx +11 -2
  98. package/src/routes/pages/featured.tsx +1 -1
  99. package/src/routes/pages/home.tsx +1 -1
  100. package/src/services/__tests__/post.test.ts +124 -33
  101. package/src/services/__tests__/settings.test.ts +3 -3
  102. package/src/services/page.ts +16 -3
  103. package/src/services/post.ts +96 -37
  104. package/src/services/search.ts +4 -2
  105. package/src/services/settings.ts +6 -2
  106. package/src/styles/components.css +240 -60
  107. package/src/styles/tokens.css +10 -0
  108. package/src/styles/ui.css +1157 -81
  109. package/src/types/bindings.ts +5 -0
  110. package/src/types/config.ts +23 -1
  111. package/src/types/constants.ts +3 -0
  112. package/src/types/entities.ts +9 -2
  113. package/src/types/operations.ts +9 -3
  114. package/src/types/props.ts +3 -3
  115. package/src/types/views.ts +3 -2
  116. package/src/ui/compose/ComposeDialog.tsx +24 -7
  117. package/src/ui/dash/PageForm.tsx +2 -0
  118. package/src/ui/dash/PostList.tsx +5 -5
  119. package/src/ui/dash/StatusBadge.tsx +13 -5
  120. package/src/ui/dash/appearance/AdvancedContent.tsx +52 -61
  121. package/src/ui/dash/appearance/ColorThemeContent.tsx +30 -35
  122. package/src/ui/dash/appearance/FontThemeContent.tsx +65 -73
  123. package/src/ui/dash/appearance/NavigationContent.tsx +107 -96
  124. package/src/ui/dash/media/MediaListContent.tsx +9 -4
  125. package/src/ui/dash/media/ViewMediaContent.tsx +2 -2
  126. package/src/ui/dash/pages/PagesContent.tsx +2 -1
  127. package/src/ui/dash/posts/PostForm.tsx +19 -7
  128. package/src/ui/dash/settings/AccountContent.tsx +133 -138
  129. package/src/ui/dash/settings/AvatarContent.tsx +70 -0
  130. package/src/ui/dash/settings/GeneralContent.tsx +3 -62
  131. package/src/ui/dash/settings/SettingsRootContent.tsx +236 -0
  132. package/src/ui/layouts/DashLayout.tsx +157 -75
  133. package/src/ui/layouts/SiteLayout.tsx +13 -13
  134. package/src/ui/pages/ArchivePage.tsx +10 -7
  135. package/src/ui/pages/CollectionPage.tsx +6 -35
  136. package/src/ui/pages/CollectionsPage.tsx +2 -1
  137. package/src/ui/pages/FeaturedPage.tsx +2 -1
  138. package/src/ui/pages/HomePage.tsx +1 -1
  139. package/src/ui/pages/SearchPage.tsx +1 -1
  140. package/src/ui/shared/CollectionsSidebar.tsx +228 -3
  141. package/src/ui/shared/MediaGallery.tsx +179 -41
  142. package/src/lib/collections-reorder.ts +0 -28
  143. package/src/routes/dash/appearance.tsx +0 -240
  144. package/src/routes/dash/collections.tsx +0 -211
  145. package/src/ui/components/jant-compose-editor.ts +0 -814
  146. package/src/ui/dash/appearance/AppearanceNav.tsx +0 -60
  147. package/src/ui/dash/collections/CollectionForm.tsx +0 -166
  148. package/src/ui/dash/collections/CollectionsListContent.tsx +0 -146
  149. package/src/ui/dash/collections/IconPickerGrid.tsx +0 -50
  150. package/src/ui/dash/collections/ViewCollectionContent.tsx +0 -103
  151. package/src/ui/dash/settings/SettingsNav.tsx +0 -52
  152. /package/src/{ui → client}/components/__tests__/jant-settings-avatar.test.ts +0 -0
  153. /package/src/{ui → client}/components/jant-settings-avatar.ts +0 -0
  154. /package/src/{ui → client}/components/settings-types.ts +0 -0
  155. /package/src/{lib → client}/image-processor.ts +0 -0
  156. /package/src/{lib → client}/toast.ts +0 -0
@@ -1,814 +0,0 @@
1
- /**
2
- * Compose Editor
3
- *
4
- * Format-specific content editing sub-component for the compose dialog.
5
- * Handles note/link/quote fields, star rating, attached text panel,
6
- * file attachments with thumbnail strip, and alt text editing.
7
- *
8
- * Light DOM only — BaseCoat and Tailwind classes apply directly.
9
- */
10
-
11
- import { LitElement, html, nothing } from "lit";
12
- import { classMap } from "lit/directives/class-map.js";
13
- import type {
14
- ComposeFormat,
15
- ComposeLabels,
16
- ComposeAttachment,
17
- } from "./compose-types.js";
18
-
19
- export class JantComposeEditor extends LitElement {
20
- static properties = {
21
- format: { type: String },
22
- labels: { type: Object },
23
- _title: { state: true },
24
- _body: { state: true },
25
- _url: { state: true },
26
- _quoteText: { state: true },
27
- _quoteAuthor: { state: true },
28
- _rating: { state: true },
29
- _showTitle: { state: true },
30
- _showRating: { state: true },
31
- _attachedText: { state: true },
32
- _showAttachedText: { state: true },
33
- _attachments: { state: true },
34
- _showAltPanel: { state: true },
35
- _altPanelIndex: { state: true },
36
- };
37
-
38
- declare format: ComposeFormat;
39
- declare labels: ComposeLabels;
40
- declare _title: string;
41
- declare _body: string;
42
- declare _url: string;
43
- declare _quoteText: string;
44
- declare _quoteAuthor: string;
45
- declare _rating: number;
46
- declare _showTitle: boolean;
47
- declare _showRating: boolean;
48
- declare _attachedText: string;
49
- declare _showAttachedText: boolean;
50
- declare _attachments: ComposeAttachment[];
51
- declare _showAltPanel: boolean;
52
- declare _altPanelIndex: number;
53
-
54
- private _fileInput: HTMLInputElement | null = null;
55
-
56
- createRenderRoot() {
57
- return this;
58
- }
59
-
60
- constructor() {
61
- super();
62
- this.format = "note";
63
- this.labels = {} as ComposeLabels;
64
- this._title = "";
65
- this._body = "";
66
- this._url = "";
67
- this._quoteText = "";
68
- this._quoteAuthor = "";
69
- this._rating = 0;
70
- this._showTitle = false;
71
- this._showRating = false;
72
- this._attachedText = "";
73
- this._showAttachedText = false;
74
- this._attachments = [];
75
- this._showAltPanel = false;
76
- this._altPanelIndex = 0;
77
- }
78
-
79
- getData() {
80
- const shared = {
81
- rating: this._rating,
82
- attachedText: this._attachedText,
83
- attachments: this._attachments,
84
- };
85
-
86
- switch (this.format) {
87
- case "link":
88
- return {
89
- ...shared,
90
- title: this._title,
91
- body: this._body,
92
- url: this._url,
93
- quoteText: "",
94
- quoteAuthor: "",
95
- };
96
- case "quote":
97
- return {
98
- ...shared,
99
- title: "",
100
- body: this._body,
101
- url: this._url,
102
- quoteText: this._quoteText,
103
- quoteAuthor: this._quoteAuthor,
104
- };
105
- default:
106
- return {
107
- ...shared,
108
- title: this._title,
109
- body: this._body,
110
- url: "",
111
- quoteText: "",
112
- quoteAuthor: "",
113
- };
114
- }
115
- }
116
-
117
- reset() {
118
- this._title = "";
119
- this._body = "";
120
- this._url = "";
121
- this._quoteText = "";
122
- this._quoteAuthor = "";
123
- this._rating = 0;
124
- this._showTitle = false;
125
- this._showRating = false;
126
- this._attachedText = "";
127
- this._showAttachedText = false;
128
- // Revoke preview URLs before clearing
129
- for (const a of this._attachments) {
130
- URL.revokeObjectURL(a.previewUrl);
131
- }
132
- this._attachments = [];
133
- this._showAltPanel = false;
134
- this._altPanelIndex = 0;
135
- }
136
-
137
- updateAttachmentStatus(
138
- clientId: string,
139
- status: ComposeAttachment["status"],
140
- mediaId: string | null,
141
- error: string | null,
142
- ) {
143
- this._attachments = this._attachments.map((a) =>
144
- a.clientId === clientId ? { ...a, status, mediaId, error } : a,
145
- );
146
- }
147
-
148
- focusInput() {
149
- const selector =
150
- this.format === "link"
151
- ? '.compose-input[type="url"]'
152
- : this.format === "quote"
153
- ? ".compose-quote-text"
154
- : ".compose-body-input";
155
- this.querySelector<HTMLElement>(selector)?.focus();
156
- }
157
-
158
- private _openAttachedText() {
159
- this._showAttachedText = true;
160
- this.updateComplete.then(() => {
161
- this.querySelector<HTMLTextAreaElement>(
162
- ".compose-attached-textarea",
163
- )?.focus();
164
- });
165
- }
166
-
167
- private _onInput(field: string, e: Event) {
168
- const target = e.target as HTMLInputElement | HTMLTextAreaElement;
169
- (this as Record<string, unknown>)[field] = target.value;
170
- if (
171
- target.tagName === "TEXTAREA" &&
172
- !target.classList.contains("compose-attached-textarea")
173
- ) {
174
- this._autoResize(target as HTMLElement);
175
- }
176
- }
177
-
178
- private _autoResize(el: HTMLElement) {
179
- el.style.height = "auto";
180
- el.style.height = `${el.scrollHeight}px`;
181
- }
182
-
183
- private _setRating(star: number) {
184
- this._rating = this._rating === star ? 0 : star;
185
- }
186
-
187
- private _openFilePicker() {
188
- if (!this._fileInput) {
189
- this._fileInput = document.createElement("input");
190
- this._fileInput.type = "file";
191
- this._fileInput.accept = "image/*";
192
- this._fileInput.multiple = true;
193
- this._fileInput.style.display = "none";
194
- this._fileInput.addEventListener("change", () =>
195
- this._handleFilesSelected(),
196
- );
197
- this.appendChild(this._fileInput);
198
- }
199
- this._fileInput.value = "";
200
- this._fileInput.click();
201
- }
202
-
203
- private _handleFilesSelected() {
204
- if (!this._fileInput?.files?.length) return;
205
-
206
- const newAttachments: ComposeAttachment[] = [];
207
- const files: { file: File; clientId: string }[] = [];
208
-
209
- for (const file of Array.from(this._fileInput.files)) {
210
- const clientId = crypto.randomUUID();
211
- const previewUrl = URL.createObjectURL(file);
212
- newAttachments.push({
213
- clientId,
214
- file,
215
- previewUrl,
216
- status: "pending",
217
- mediaId: null,
218
- alt: "",
219
- error: null,
220
- });
221
- files.push({ file, clientId });
222
- }
223
-
224
- this._attachments = [...this._attachments, ...newAttachments];
225
-
226
- this.dispatchEvent(
227
- new CustomEvent("jant:files-selected", {
228
- bubbles: true,
229
- detail: { files },
230
- }),
231
- );
232
- }
233
-
234
- private _removeAttachment(index: number) {
235
- const attachment = this._attachments[index];
236
- if (attachment) {
237
- URL.revokeObjectURL(attachment.previewUrl);
238
- this.dispatchEvent(
239
- new CustomEvent("jant:attachment-removed", {
240
- bubbles: true,
241
- detail: {
242
- clientId: attachment.clientId,
243
- mediaId: attachment.mediaId,
244
- },
245
- }),
246
- );
247
- }
248
- this._attachments = this._attachments.filter((_, i) => i !== index);
249
- // Close alt panel if it was showing the removed item
250
- if (this._showAltPanel && this._altPanelIndex === index) {
251
- this._showAltPanel = false;
252
- } else if (this._showAltPanel && this._altPanelIndex > index) {
253
- this._altPanelIndex = this._altPanelIndex - 1;
254
- }
255
- }
256
-
257
- private _openAltPanel(index: number) {
258
- this._altPanelIndex = index;
259
- this._showAltPanel = true;
260
- this.updateComplete.then(() => {
261
- this.querySelector<HTMLTextAreaElement>(".compose-alt-textarea")?.focus();
262
- });
263
- }
264
-
265
- private _closeAltPanel() {
266
- this._showAltPanel = false;
267
- }
268
-
269
- private _onAltInput(e: Event) {
270
- const value = (e.target as HTMLTextAreaElement).value;
271
- this._attachments = this._attachments.map((a, i) =>
272
- i === this._altPanelIndex ? { ...a, alt: value } : a,
273
- );
274
- }
275
-
276
- // ── Render helpers ────────────────────────────────────────────────
277
-
278
- private _renderNoteFields() {
279
- return html`
280
- <div class="compose-field-enter">
281
- ${this._showTitle
282
- ? html`
283
- <div class="compose-note-title-row">
284
- <input
285
- type="text"
286
- .value=${this._title}
287
- @input=${(e: Event) => this._onInput("_title", e)}
288
- class="compose-input compose-note-title"
289
- placeholder=${this.labels.titlePlaceholder}
290
- />
291
- <button
292
- type="button"
293
- class="compose-note-title-dismiss"
294
- @click=${() => {
295
- this._showTitle = false;
296
- }}
297
- >
298
-
299
- </button>
300
- </div>
301
- `
302
- : nothing}
303
- <textarea
304
- .value=${this._body}
305
- @input=${(e: Event) => this._onInput("_body", e)}
306
- class="compose-input compose-body-input"
307
- placeholder=${this.labels.bodyPlaceholder}
308
- rows="4"
309
- ></textarea>
310
- </div>
311
- `;
312
- }
313
-
314
- private _renderLinkFields() {
315
- return html`
316
- <div class="compose-field-enter">
317
- <div class="compose-link-url-wrap">
318
- <span class="text-base opacity-50 shrink-0">🔗</span>
319
- <input
320
- type="url"
321
- .value=${this._url}
322
- @input=${(e: Event) => this._onInput("_url", e)}
323
- class="compose-input text-[0.9rem]"
324
- placeholder=${this.labels.urlPlaceholder}
325
- />
326
- </div>
327
- <input
328
- type="text"
329
- .value=${this._title}
330
- @input=${(e: Event) => this._onInput("_title", e)}
331
- class="compose-input compose-link-title"
332
- placeholder=${this.labels.linkTitlePlaceholder}
333
- />
334
- <div class="compose-divider"></div>
335
- <textarea
336
- .value=${this._body}
337
- @input=${(e: Event) => this._onInput("_body", e)}
338
- class="compose-input compose-thoughts"
339
- placeholder=${this.labels.thoughtsPlaceholder}
340
- rows="3"
341
- ></textarea>
342
- </div>
343
- `;
344
- }
345
-
346
- private _renderQuoteFields() {
347
- return html`
348
- <div class="compose-field-enter">
349
- <div class="compose-quote-wrap">
350
- <span class="compose-quote-mark">"</span>
351
- <textarea
352
- .value=${this._quoteText}
353
- @input=${(e: Event) => this._onInput("_quoteText", e)}
354
- class="compose-input compose-quote-text"
355
- placeholder=${this.labels.quotePlaceholder}
356
- rows="3"
357
- ></textarea>
358
- </div>
359
- <div class="compose-quote-author-row">
360
- <span class="compose-quote-dash">—</span>
361
- <input
362
- type="text"
363
- .value=${this._quoteAuthor}
364
- @input=${(e: Event) => this._onInput("_quoteAuthor", e)}
365
- class="compose-input compose-quote-author"
366
- placeholder=${this.labels.authorPlaceholder}
367
- />
368
- </div>
369
- <div class="compose-quote-source">
370
- <input
371
- type="url"
372
- .value=${this._url}
373
- @input=${(e: Event) => this._onInput("_url", e)}
374
- class="compose-input text-[0.78rem]"
375
- placeholder=${this.labels.sourcePlaceholder}
376
- />
377
- </div>
378
- <div class="compose-divider"></div>
379
- <textarea
380
- .value=${this._body}
381
- @input=${(e: Event) => this._onInput("_body", e)}
382
- class="compose-input compose-thoughts"
383
- placeholder=${this.labels.thoughtsPlaceholder}
384
- rows="2"
385
- ></textarea>
386
- </div>
387
- `;
388
- }
389
-
390
- private _renderStarRating() {
391
- if (!this._showRating) return nothing;
392
- const stars = [1, 2, 3, 4, 5];
393
- return html`
394
- <div class="compose-star-rating">
395
- ${stars.map(
396
- (n) => html`
397
- <button
398
- type="button"
399
- class=${classMap({
400
- "compose-star": true,
401
- "compose-star-filled": this._rating >= n,
402
- })}
403
- @click=${() => this._setRating(n)}
404
- >
405
-
406
- </button>
407
- `,
408
- )}
409
- ${this._rating > 0
410
- ? html`<span class="compose-star-label">${this._rating}/5</span>`
411
- : nothing}
412
- </div>
413
- `;
414
- }
415
-
416
- private _renderAttachedBadge() {
417
- if (this._attachedText.trim().length === 0 || this._showAttachedText)
418
- return nothing;
419
- return html`
420
- <div
421
- class="compose-attached-badge"
422
- @click=${() => this._openAttachedText()}
423
- >
424
- <svg
425
- width="14"
426
- height="14"
427
- viewBox="0 0 18 18"
428
- fill="none"
429
- stroke="currentColor"
430
- stroke-width="1.3"
431
- stroke-linecap="round"
432
- class="text-muted-foreground icon-fine"
433
- >
434
- <rect x="3" y="2" width="12" height="14" rx="2" />
435
- <line x1="6" y1="6" x2="12" y2="6" />
436
- <line x1="6" y1="9" x2="12" y2="9" />
437
- <line x1="6" y1="12" x2="9.5" y2="12" />
438
- </svg>
439
- <span class="text-xs font-medium">${this.labels.attachedText}</span>
440
- <span class="text-xs text-muted-foreground"
441
- >· ${this._attachedText.length.toLocaleString()} chars</span
442
- >
443
- <div class="flex-1"></div>
444
- <button
445
- type="button"
446
- class="compose-attached-badge-dismiss"
447
- @click=${(e: Event) => {
448
- e.stopPropagation();
449
- this._attachedText = "";
450
- }}
451
- >
452
-
453
- </button>
454
- </div>
455
- `;
456
- }
457
-
458
- private _renderAttachedPanel() {
459
- if (!this._showAttachedText) return nothing;
460
- return html`
461
- <div class="compose-attached-panel">
462
- <div
463
- class="flex items-center gap-2.5 px-3 py-2.5 border-b border-border"
464
- >
465
- <button
466
- type="button"
467
- class="compose-attached-panel-back"
468
- @click=${() => {
469
- this._showAttachedText = false;
470
- }}
471
- >
472
- <svg
473
- class="icon-fine"
474
- width="16"
475
- height="16"
476
- viewBox="0 0 16 16"
477
- fill="none"
478
- stroke="currentColor"
479
- stroke-width="1.5"
480
- stroke-linecap="round"
481
- stroke-linejoin="round"
482
- >
483
- <path d="M11 3L6 8l5 5" />
484
- </svg>
485
- </button>
486
- <span class="text-sm font-medium tracking-tight"
487
- >${this.labels.attachedText}</span
488
- >
489
- <div class="flex-1"></div>
490
- ${this._attachedText.length > 0
491
- ? html`<span class="text-xs text-muted-foreground tracking-wide"
492
- >${this._attachedText.length.toLocaleString()} chars</span
493
- >`
494
- : nothing}
495
- </div>
496
- <div class="flex-1 p-4 overflow-hidden flex flex-col">
497
- <textarea
498
- .value=${this._attachedText}
499
- @input=${(e: Event) => this._onInput("_attachedText", e)}
500
- class="compose-input compose-attached-textarea"
501
- placeholder=${this.labels.attachedTextPlaceholder}
502
- ></textarea>
503
- </div>
504
- <div
505
- class="flex items-center justify-between px-3 py-2 border-t border-border"
506
- >
507
- <span class="text-xs text-muted-foreground"
508
- >${this.labels.attachedTextHint}</span
509
- >
510
- <button
511
- type="button"
512
- class="compose-post-btn"
513
- @click=${() => {
514
- this._showAttachedText = false;
515
- }}
516
- >
517
- ${this.labels.done}
518
- </button>
519
- </div>
520
- </div>
521
- `;
522
- }
523
-
524
- private _renderAltPanel() {
525
- if (!this._showAltPanel) return nothing;
526
- const attachment = this._attachments[this._altPanelIndex];
527
- if (!attachment) return nothing;
528
-
529
- return html`
530
- <div class="compose-alt-panel">
531
- <div
532
- class="flex items-center gap-2.5 px-3 py-2.5 border-b border-border"
533
- >
534
- <button
535
- type="button"
536
- class="compose-attached-panel-back"
537
- @click=${() => this._closeAltPanel()}
538
- >
539
- <svg
540
- class="icon-fine"
541
- width="16"
542
- height="16"
543
- viewBox="0 0 16 16"
544
- fill="none"
545
- stroke="currentColor"
546
- stroke-width="1.5"
547
- stroke-linecap="round"
548
- stroke-linejoin="round"
549
- >
550
- <path d="M11 3L6 8l5 5" />
551
- </svg>
552
- </button>
553
- <span class="text-sm font-medium tracking-tight"
554
- >${this.labels.addAltTitle}</span
555
- >
556
- </div>
557
- <div class="compose-alt-preview">
558
- <img
559
- src=${attachment.previewUrl}
560
- alt=""
561
- class="compose-alt-preview-img"
562
- />
563
- </div>
564
- <div class="flex-1 p-4 overflow-hidden flex flex-col">
565
- <textarea
566
- .value=${attachment.alt}
567
- @input=${(e: Event) => this._onAltInput(e)}
568
- class="compose-input compose-alt-textarea"
569
- placeholder=${this.labels.altPlaceholder}
570
- rows="3"
571
- ></textarea>
572
- </div>
573
- <div
574
- class="flex items-center justify-between px-3 py-2 border-t border-border"
575
- >
576
- <span class="text-xs text-muted-foreground"
577
- >${this.labels.altHint}</span
578
- >
579
- <button
580
- type="button"
581
- class="compose-post-btn"
582
- @click=${() => this._closeAltPanel()}
583
- >
584
- ${this.labels.done}
585
- </button>
586
- </div>
587
- </div>
588
- `;
589
- }
590
-
591
- private _renderAttachments() {
592
- if (this._attachments.length === 0) return nothing;
593
-
594
- return html`
595
- <div class="compose-attachments">
596
- ${this._attachments.map(
597
- (a, i) => html`
598
- <div class="compose-attachment">
599
- <div class="compose-attachment-thumb">
600
- <img
601
- src=${a.previewUrl}
602
- alt=""
603
- class="compose-attachment-img"
604
- />
605
- ${a.status === "pending" || a.status === "uploading"
606
- ? html`
607
- <div class="compose-attachment-overlay">
608
- <svg
609
- class="animate-spin size-4"
610
- viewBox="0 0 24 24"
611
- fill="none"
612
- stroke="currentColor"
613
- style="stroke-width: 2.5"
614
- stroke-linecap="round"
615
- >
616
- <path d="M21 12a9 9 0 1 1-6.219-8.56" />
617
- </svg>
618
- </div>
619
- `
620
- : nothing}
621
- ${a.status === "error"
622
- ? html`
623
- <div
624
- class="compose-attachment-overlay compose-attachment-error"
625
- >
626
- <svg
627
- class="icon-fine"
628
- width="16"
629
- height="16"
630
- viewBox="0 0 16 16"
631
- fill="none"
632
- stroke="currentColor"
633
- stroke-width="1.5"
634
- stroke-linecap="round"
635
- >
636
- <circle cx="8" cy="8" r="6" />
637
- <path d="M10 6L6 10M6 6l4 4" />
638
- </svg>
639
- </div>
640
- `
641
- : nothing}
642
- <button
643
- type="button"
644
- class="compose-attachment-remove"
645
- @click=${() => this._removeAttachment(i)}
646
- >
647
-
648
- </button>
649
- </div>
650
- <button
651
- type="button"
652
- class=${classMap({
653
- "compose-attachment-alt": true,
654
- "compose-attachment-alt-set": a.alt.length > 0,
655
- })}
656
- @click=${() => this._openAltPanel(i)}
657
- >
658
- ${a.alt.length > 0 ? "ALT" : "+ ALT"}
659
- </button>
660
- </div>
661
- `,
662
- )}
663
- </div>
664
- `;
665
- }
666
-
667
- private _renderToolsRow() {
668
- const hasAttached = this._attachedText.trim().length > 0;
669
- return html`
670
- <div class="compose-tools-row">
671
- <!-- Media / Add -->
672
- <button
673
- type="button"
674
- class=${classMap({
675
- "compose-tool-btn": true,
676
- "compose-tool-btn-active": this._attachments.length > 0,
677
- })}
678
- @click=${() => this._openFilePicker()}
679
- >
680
- <svg
681
- class="icon-fine"
682
- width="18"
683
- height="18"
684
- viewBox="0 0 18 18"
685
- fill="none"
686
- stroke="currentColor"
687
- stroke-width="1.4"
688
- stroke-linecap="round"
689
- stroke-linejoin="round"
690
- >
691
- <rect x="2" y="3" width="14" height="12" rx="2.5" />
692
- <circle cx="6.5" cy="7.5" r="1.5" />
693
- <path d="M2 13l4-4c.6-.6 1.4-.6 2 0l4 4" />
694
- <path d="M11 11l1.5-1.5c.6-.6 1.4-.6 2 0L16 11" />
695
- </svg>
696
- <span class="compose-tool-tip"
697
- >${this._attachments.length > 0
698
- ? this.labels.addMore
699
- : this.labels.media}</span
700
- >
701
- </button>
702
-
703
- <!-- Attached Text -->
704
- <button
705
- type="button"
706
- class=${classMap({
707
- "compose-tool-btn": true,
708
- "compose-tool-btn-active": hasAttached,
709
- })}
710
- @click=${() => this._openAttachedText()}
711
- >
712
- <svg
713
- class="icon-fine"
714
- width="18"
715
- height="18"
716
- viewBox="0 0 18 18"
717
- fill="none"
718
- stroke="currentColor"
719
- stroke-width="1.3"
720
- stroke-linecap="round"
721
- >
722
- <rect x="3" y="2" width="12" height="14" rx="2" />
723
- <line x1="6" y1="6" x2="12" y2="6" />
724
- <line x1="6" y1="9" x2="12" y2="9" />
725
- <line x1="6" y1="12" x2="9.5" y2="12" />
726
- </svg>
727
- <span class="compose-tool-tip">${this.labels.attachedText}</span>
728
- </button>
729
-
730
- <!-- Score -->
731
- <button
732
- type="button"
733
- class=${classMap({
734
- "compose-tool-btn": true,
735
- "compose-tool-btn-active": this._showRating,
736
- })}
737
- @click=${() => {
738
- this._showRating = !this._showRating;
739
- }}
740
- >
741
- <svg
742
- class="icon-fine"
743
- width="18"
744
- height="18"
745
- viewBox="0 0 18 18"
746
- fill="none"
747
- stroke="currentColor"
748
- stroke-width="1.4"
749
- stroke-linecap="round"
750
- stroke-linejoin="round"
751
- >
752
- <rect x="3" y="12" width="2.8" height="3" rx="0.7" />
753
- <rect x="7.6" y="8.5" width="2.8" height="6.5" rx="0.7" />
754
- <rect x="12.2" y="5" width="2.8" height="10" rx="0.7" />
755
- </svg>
756
- <span class="compose-tool-tip">${this.labels.score}</span>
757
- </button>
758
-
759
- <!-- Title toggle (Note only) -->
760
- ${this.format === "note"
761
- ? html`
762
- <div class="flex items-center gap-0.5">
763
- <div class="compose-tool-sep"></div>
764
- <button
765
- type="button"
766
- class=${classMap({
767
- "compose-tool-btn": true,
768
- "compose-tool-btn-active": this._showTitle,
769
- })}
770
- @click=${() => {
771
- this._showTitle = !this._showTitle;
772
- }}
773
- >
774
- <svg width="18" height="18" viewBox="0 0 18 18" fill="none">
775
- <text
776
- x="3.5"
777
- y="14"
778
- font-family="serif"
779
- font-size="14"
780
- font-weight="400"
781
- fill="currentColor"
782
- >
783
- T
784
- </text>
785
- </svg>
786
- <span class="compose-tool-tip">${this.labels.title}</span>
787
- </button>
788
- </div>
789
- `
790
- : nothing}
791
-
792
- <div class="flex-1"></div>
793
- </div>
794
- `;
795
- }
796
-
797
- render() {
798
- return html`
799
- ${this._renderAttachedPanel()} ${this._renderAltPanel()}
800
- <section class="compose-body">
801
- ${this.format === "note"
802
- ? this._renderNoteFields()
803
- : this.format === "link"
804
- ? this._renderLinkFields()
805
- : this._renderQuoteFields()}
806
- ${this._renderStarRating()} ${this._renderAttachedBadge()}
807
- ${this._renderAttachments()}
808
- </section>
809
- ${this._renderToolsRow()}
810
- `;
811
- }
812
- }
813
-
814
- customElements.define("jant-compose-editor", JantComposeEditor);