@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.
Files changed (307) hide show
  1. package/bin/commands/export.js +1 -1
  2. package/bin/commands/import-site.js +529 -0
  3. package/bin/commands/reset-password.js +3 -2
  4. package/dist/client/assets/heic-to-DIRPI3VF.js +1 -0
  5. package/dist/client/assets/module-RjUF93sV.js +716 -0
  6. package/dist/client/assets/native-48B9X9Wg.js +1 -0
  7. package/dist/client/assets/url-FWFqPJPb.js +1 -0
  8. package/dist/client/client.css +1 -1
  9. package/dist/client/client.js +4564 -3013
  10. package/dist/index.js +12885 -8161
  11. package/package.json +23 -6
  12. package/src/__tests__/helpers/app.ts +10 -10
  13. package/src/__tests__/helpers/db.ts +91 -87
  14. package/src/app.tsx +157 -31
  15. package/src/auth.ts +20 -2
  16. package/src/client/archive-nav.js +187 -0
  17. package/src/client/audio-player.ts +478 -0
  18. package/src/client/audio-processor.ts +84 -0
  19. package/src/{lib → client}/avatar-upload.ts +4 -3
  20. package/src/{lib → client}/collection-form-bridge.ts +2 -2
  21. package/src/{ui → client}/components/__tests__/jant-collection-form.test.ts +26 -9
  22. package/src/client/components/__tests__/jant-compose-dialog.test.ts +1140 -0
  23. package/src/client/components/__tests__/jant-compose-editor.test.ts +504 -0
  24. package/src/{ui → client}/components/__tests__/jant-post-form.test.ts +37 -17
  25. package/src/{ui → client}/components/__tests__/jant-settings-avatar.test.ts +2 -2
  26. package/src/{ui → client}/components/__tests__/jant-settings-general.test.ts +3 -3
  27. package/src/client/components/collection-sidebar-types.ts +43 -0
  28. package/src/{ui → client}/components/collection-types.ts +3 -4
  29. package/src/client/components/compose-types.ts +174 -0
  30. package/src/client/components/jant-collection-form.ts +667 -0
  31. package/src/client/components/jant-collection-sidebar.ts +805 -0
  32. package/src/client/components/jant-compose-dialog.ts +2161 -0
  33. package/src/client/components/jant-compose-editor.ts +1813 -0
  34. package/src/client/components/jant-compose-fullscreen.ts +283 -0
  35. package/src/client/components/jant-media-lightbox.ts +259 -0
  36. package/src/{ui → client}/components/jant-nav-manager.ts +97 -298
  37. package/src/{ui → client}/components/jant-post-form.ts +141 -12
  38. package/src/client/components/jant-post-menu.ts +1019 -0
  39. package/src/{ui → client}/components/jant-settings-avatar.ts +3 -3
  40. package/src/{ui → client}/components/jant-settings-general.ts +38 -4
  41. package/src/client/components/jant-text-preview.ts +232 -0
  42. package/src/{ui → client}/components/nav-manager-types.ts +6 -18
  43. package/src/{ui → client}/components/post-form-template.ts +137 -38
  44. package/src/{ui → client}/components/post-form-types.ts +15 -4
  45. package/src/client/compose-bridge.ts +583 -0
  46. package/src/{lib → client}/image-processor.ts +26 -8
  47. package/src/client/lazy-slugify.ts +51 -0
  48. package/src/client/media-metadata.ts +247 -0
  49. package/src/client/multipart-upload.ts +160 -0
  50. package/src/{lib → client}/nav-manager-bridge.ts +1 -1
  51. package/src/{lib → client}/post-form-bridge.ts +53 -2
  52. package/src/{lib → client}/settings-bridge.ts +3 -15
  53. package/src/client/thread-context.ts +140 -0
  54. package/src/client/tiptap/bubble-menu.ts +205 -0
  55. package/src/client/tiptap/create-editor.ts +86 -0
  56. package/src/client/tiptap/exitable-marks.ts +73 -0
  57. package/src/client/tiptap/extensions.ts +65 -0
  58. package/src/client/tiptap/image-node.ts +482 -0
  59. package/src/client/tiptap/link-toolbar.ts +371 -0
  60. package/src/client/tiptap/more-break.ts +50 -0
  61. package/src/client/tiptap/paste-image.ts +129 -0
  62. package/src/client/tiptap/slash-commands.ts +438 -0
  63. package/src/{lib → client}/toast.ts +101 -3
  64. package/src/client/types/sortablejs.d.ts +44 -0
  65. package/src/client/upload-with-metadata.ts +54 -0
  66. package/src/client/video-processor.ts +207 -0
  67. package/src/client.ts +27 -17
  68. package/src/db/__tests__/migrations.test.ts +118 -0
  69. package/src/db/index.ts +52 -0
  70. package/src/db/migrations/0000_baseline.sql +269 -0
  71. package/src/db/migrations/0001_fts_setup.sql +31 -0
  72. package/src/db/migrations/meta/0000_snapshot.json +703 -119
  73. package/src/db/migrations/meta/0001_snapshot.json +1337 -0
  74. package/src/db/migrations/meta/_journal.json +4 -39
  75. package/src/db/schema.ts +409 -140
  76. package/src/i18n/__tests__/detect.test.ts +115 -0
  77. package/src/i18n/context.tsx +2 -2
  78. package/src/i18n/detect.ts +85 -1
  79. package/src/i18n/i18n.ts +1 -1
  80. package/src/i18n/index.ts +2 -1
  81. package/src/i18n/locales/en.po +783 -1087
  82. package/src/i18n/locales/en.ts +1 -1
  83. package/src/i18n/locales/zh-Hans.po +867 -812
  84. package/src/i18n/locales/zh-Hans.ts +1 -1
  85. package/src/i18n/locales/zh-Hant.po +878 -823
  86. package/src/i18n/locales/zh-Hant.ts +1 -1
  87. package/src/i18n/middleware.ts +6 -0
  88. package/src/index.ts +5 -7
  89. package/src/lib/__tests__/blurhash-placeholder.test.ts +75 -0
  90. package/src/lib/__tests__/constants.test.ts +0 -1
  91. package/src/lib/__tests__/markdown-to-tiptap.test.ts +358 -0
  92. package/src/lib/__tests__/nanoid.test.ts +26 -0
  93. package/src/lib/__tests__/resolve-config.test.ts +2 -2
  94. package/src/lib/__tests__/schemas.test.ts +186 -65
  95. package/src/lib/__tests__/slug.test.ts +126 -0
  96. package/src/lib/__tests__/sse.test.ts +6 -6
  97. package/src/lib/__tests__/summary.test.ts +264 -0
  98. package/src/lib/__tests__/theme.test.ts +1 -1
  99. package/src/lib/__tests__/timeline.test.ts +33 -30
  100. package/src/lib/__tests__/tiptap-to-markdown.test.ts +346 -0
  101. package/src/lib/__tests__/url.test.ts +2 -2
  102. package/src/lib/__tests__/view.test.ts +140 -65
  103. package/src/lib/blurhash-placeholder.ts +102 -0
  104. package/src/lib/constants.ts +3 -1
  105. package/src/lib/emoji-catalog.ts +963 -0
  106. package/src/lib/errors.ts +11 -8
  107. package/src/lib/feed.ts +77 -31
  108. package/src/lib/html.ts +2 -1
  109. package/src/lib/icon-catalog.ts +5033 -1
  110. package/src/lib/icons.ts +3 -2
  111. package/src/lib/index.ts +0 -1
  112. package/src/lib/markdown-to-tiptap.ts +286 -0
  113. package/src/lib/media-helpers.ts +22 -12
  114. package/src/lib/nanoid.ts +29 -0
  115. package/src/lib/navigation.ts +1 -1
  116. package/src/lib/render.tsx +24 -5
  117. package/src/lib/resolve-config.ts +13 -2
  118. package/src/lib/schemas.ts +226 -58
  119. package/src/lib/search-snippet.ts +34 -0
  120. package/src/lib/slug.ts +96 -0
  121. package/src/lib/sse.ts +6 -6
  122. package/src/lib/storage.ts +115 -7
  123. package/src/lib/summary.ts +158 -0
  124. package/src/lib/theme.ts +11 -8
  125. package/src/lib/timeline.ts +76 -34
  126. package/src/lib/tiptap-render.ts +191 -0
  127. package/src/lib/tiptap-to-markdown.ts +305 -0
  128. package/src/lib/upload.ts +263 -14
  129. package/src/lib/url.ts +37 -22
  130. package/src/lib/view.ts +236 -55
  131. package/src/middleware/__tests__/auth.test.ts +191 -11
  132. package/src/middleware/__tests__/onboarding.test.ts +12 -10
  133. package/src/middleware/auth.ts +63 -9
  134. package/src/middleware/error-handler.ts +3 -3
  135. package/src/middleware/onboarding.ts +1 -1
  136. package/src/middleware/secure-headers.ts +40 -0
  137. package/src/preset.css +83 -2
  138. package/src/routes/__tests__/compose.test.ts +17 -24
  139. package/src/routes/api/__tests__/collections.test.ts +109 -61
  140. package/src/routes/api/__tests__/nav-items.test.ts +46 -29
  141. package/src/routes/api/__tests__/posts.test.ts +132 -68
  142. package/src/routes/api/__tests__/search.test.ts +15 -2
  143. package/src/routes/api/__tests__/upload-multipart.test.ts +534 -0
  144. package/src/routes/api/collections.ts +57 -31
  145. package/src/routes/api/custom-urls.ts +80 -0
  146. package/src/routes/api/export.ts +31 -0
  147. package/src/routes/api/nav-items.ts +23 -19
  148. package/src/routes/api/posts.ts +81 -62
  149. package/src/routes/api/search.ts +3 -4
  150. package/src/routes/api/upload-multipart.ts +245 -0
  151. package/src/routes/api/upload.ts +92 -24
  152. package/src/routes/auth/__tests__/setup.test.ts +20 -60
  153. package/src/routes/auth/reset.tsx +5 -4
  154. package/src/routes/auth/setup.tsx +39 -31
  155. package/src/routes/auth/signin.tsx +13 -14
  156. package/src/routes/compose.tsx +27 -63
  157. package/src/routes/dash/__tests__/settings-avatar.test.ts +44 -9
  158. package/src/routes/dash/custom-urls.tsx +414 -0
  159. package/src/routes/dash/settings.tsx +475 -99
  160. package/src/routes/feed/__tests__/rss.test.ts +22 -23
  161. package/src/routes/feed/rss.ts +6 -2
  162. package/src/routes/feed/sitemap.ts +2 -12
  163. package/src/routes/pages/__tests__/collections.test.ts +5 -6
  164. package/src/routes/pages/__tests__/featured.test.ts +36 -18
  165. package/src/routes/pages/archive.tsx +177 -37
  166. package/src/routes/pages/collection.tsx +43 -14
  167. package/src/routes/pages/collections.tsx +11 -2
  168. package/src/routes/pages/featured.tsx +27 -3
  169. package/src/routes/pages/home.tsx +15 -14
  170. package/src/routes/pages/latest.tsx +1 -11
  171. package/src/routes/pages/new.tsx +39 -0
  172. package/src/routes/pages/page.tsx +95 -49
  173. package/src/routes/pages/search.tsx +1 -1
  174. package/src/services/__tests__/api-token.test.ts +135 -0
  175. package/src/services/__tests__/collection.test.ts +275 -227
  176. package/src/services/__tests__/custom-url.test.ts +213 -0
  177. package/src/services/__tests__/media.test.ts +162 -22
  178. package/src/services/__tests__/navigation.test.ts +109 -68
  179. package/src/services/__tests__/post-timeline.test.ts +205 -32
  180. package/src/services/__tests__/post.test.ts +800 -230
  181. package/src/services/__tests__/search.test.ts +67 -10
  182. package/src/services/__tests__/settings.test.ts +3 -3
  183. package/src/services/api-token.ts +166 -0
  184. package/src/services/auth.ts +17 -2
  185. package/src/services/collection.ts +397 -131
  186. package/src/services/custom-url.ts +188 -0
  187. package/src/services/export.ts +802 -0
  188. package/src/services/index.ts +26 -19
  189. package/src/services/media.ts +100 -22
  190. package/src/services/navigation.ts +158 -47
  191. package/src/services/path.ts +339 -0
  192. package/src/services/post.ts +764 -172
  193. package/src/services/search.ts +161 -74
  194. package/src/services/settings.ts +6 -2
  195. package/src/styles/components.css +293 -62
  196. package/src/styles/tokens.css +93 -5
  197. package/src/styles/ui.css +4349 -766
  198. package/src/types/bindings.ts +8 -0
  199. package/src/types/config.ts +34 -4
  200. package/src/types/constants.ts +17 -2
  201. package/src/types/entities.ts +83 -37
  202. package/src/types/operations.ts +20 -27
  203. package/src/types/props.ts +52 -17
  204. package/src/types/views.ts +48 -24
  205. package/src/ui/color-themes.ts +133 -23
  206. package/src/ui/compose/ComposeDialog.tsx +255 -16
  207. package/src/ui/compose/ComposePrompt.tsx +1 -1
  208. package/src/ui/dash/CrudPageHeader.tsx +1 -1
  209. package/src/ui/dash/ListItemRow.tsx +1 -1
  210. package/src/ui/dash/StatusBadge.tsx +12 -2
  211. package/src/ui/dash/appearance/AdvancedContent.tsx +71 -59
  212. package/src/ui/dash/appearance/ColorThemeContent.tsx +48 -33
  213. package/src/ui/dash/appearance/FontThemeContent.tsx +65 -73
  214. package/src/ui/dash/appearance/NavigationContent.tsx +106 -135
  215. package/src/ui/dash/index.ts +0 -3
  216. package/src/ui/dash/settings/AccountContent.tsx +87 -146
  217. package/src/ui/dash/settings/AccountMenuContent.tsx +147 -0
  218. package/src/ui/dash/settings/ApiTokensContent.tsx +232 -0
  219. package/src/ui/dash/settings/AvatarContent.tsx +78 -0
  220. package/src/ui/dash/settings/GeneralContent.tsx +3 -62
  221. package/src/ui/dash/settings/SessionsContent.tsx +159 -0
  222. package/src/ui/dash/settings/SettingsRootContent.tsx +266 -0
  223. package/src/ui/feed/LinkCard.tsx +89 -40
  224. package/src/ui/feed/NoteCard.tsx +39 -25
  225. package/src/ui/feed/PostStatusBadges.tsx +67 -0
  226. package/src/ui/feed/QuoteCard.tsx +38 -23
  227. package/src/ui/feed/ThreadPreview.tsx +90 -26
  228. package/src/ui/feed/TimelineFeed.tsx +3 -2
  229. package/src/ui/feed/TimelineItem.tsx +15 -6
  230. package/src/ui/feed/__tests__/thread-preview.test.ts +107 -0
  231. package/src/ui/feed/thread-preview-state.ts +61 -0
  232. package/src/ui/font-themes.ts +2 -2
  233. package/src/ui/layouts/BaseLayout.tsx +2 -2
  234. package/src/ui/layouts/SiteLayout.tsx +116 -103
  235. package/src/ui/pages/ArchivePage.tsx +923 -95
  236. package/src/ui/pages/CollectionPage.tsx +6 -35
  237. package/src/ui/pages/CollectionsPage.tsx +2 -1
  238. package/src/ui/pages/ComposePage.tsx +54 -0
  239. package/src/ui/pages/FeaturedPage.tsx +2 -1
  240. package/src/ui/pages/HomePage.tsx +1 -1
  241. package/src/ui/pages/PostPage.tsx +30 -45
  242. package/src/ui/pages/SearchPage.tsx +182 -38
  243. package/src/ui/shared/AdminBreadcrumb.tsx +29 -0
  244. package/src/ui/shared/CollectionsSidebar.tsx +239 -4
  245. package/src/ui/shared/MediaGallery.tsx +475 -41
  246. package/src/ui/shared/PostFooter.tsx +204 -0
  247. package/src/ui/shared/StarRating.tsx +27 -0
  248. package/src/ui/shared/__tests__/format-chars.test.ts +35 -0
  249. package/src/ui/shared/index.ts +0 -1
  250. package/src/db/migrations/0000_square_wallflower.sql +0 -118
  251. package/src/db/migrations/0001_add_search_fts.sql +0 -34
  252. package/src/db/migrations/0002_add_media_attachments.sql +0 -3
  253. package/src/db/migrations/0003_add_navigation_links.sql +0 -8
  254. package/src/db/migrations/0004_add_storage_provider.sql +0 -3
  255. package/src/db/migrations/0005_v2_schema_migration.sql +0 -268
  256. package/src/db/migrations/0006_rename_slug_to_path.sql +0 -5
  257. package/src/db/migrations/0007_post_collections_m2m.sql +0 -94
  258. package/src/db/migrations/0008_add_collection_dividers.sql +0 -8
  259. package/src/db/migrations/0009_drop_collection_show_divider.sql +0 -2
  260. package/src/db/migrations/0010_add_performance_indexes.sql +0 -16
  261. package/src/db/migrations/0011_add_path_registry.sql +0 -23
  262. package/src/db/migrations/meta/0003_snapshot.json +0 -821
  263. package/src/lib/__tests__/sqid.test.ts +0 -65
  264. package/src/lib/collections-reorder.ts +0 -28
  265. package/src/lib/compose-bridge.ts +0 -280
  266. package/src/lib/media-upload.ts +0 -148
  267. package/src/lib/sqid.ts +0 -79
  268. package/src/routes/api/__tests__/pages.test.ts +0 -218
  269. package/src/routes/api/pages.ts +0 -73
  270. package/src/routes/dash/__tests__/pages.test.ts +0 -226
  271. package/src/routes/dash/appearance.tsx +0 -240
  272. package/src/routes/dash/collections.tsx +0 -211
  273. package/src/routes/dash/index.tsx +0 -103
  274. package/src/routes/dash/media.tsx +0 -132
  275. package/src/routes/dash/pages.tsx +0 -239
  276. package/src/routes/dash/posts.tsx +0 -334
  277. package/src/routes/dash/redirects.tsx +0 -257
  278. package/src/routes/pages/post.tsx +0 -59
  279. package/src/services/__tests__/page.test.ts +0 -298
  280. package/src/services/__tests__/path-registry.test.ts +0 -165
  281. package/src/services/__tests__/redirect.test.ts +0 -159
  282. package/src/services/page.ts +0 -203
  283. package/src/services/path-registry.ts +0 -160
  284. package/src/services/redirect.ts +0 -97
  285. package/src/types/sortablejs.d.ts +0 -29
  286. package/src/ui/components/__tests__/jant-compose-dialog.test.ts +0 -512
  287. package/src/ui/components/__tests__/jant-compose-editor.test.ts +0 -272
  288. package/src/ui/components/compose-types.ts +0 -75
  289. package/src/ui/components/jant-collection-form.ts +0 -512
  290. package/src/ui/components/jant-compose-dialog.ts +0 -495
  291. package/src/ui/components/jant-compose-editor.ts +0 -814
  292. package/src/ui/dash/PageForm.tsx +0 -185
  293. package/src/ui/dash/PostList.tsx +0 -95
  294. package/src/ui/dash/appearance/AppearanceNav.tsx +0 -60
  295. package/src/ui/dash/collections/CollectionForm.tsx +0 -166
  296. package/src/ui/dash/collections/CollectionsListContent.tsx +0 -146
  297. package/src/ui/dash/collections/IconPickerGrid.tsx +0 -50
  298. package/src/ui/dash/collections/ViewCollectionContent.tsx +0 -103
  299. package/src/ui/dash/media/MediaListContent.tsx +0 -201
  300. package/src/ui/dash/media/ViewMediaContent.tsx +0 -208
  301. package/src/ui/dash/pages/PagesContent.tsx +0 -74
  302. package/src/ui/dash/posts/PostForm.tsx +0 -248
  303. package/src/ui/dash/settings/SettingsNav.tsx +0 -52
  304. package/src/ui/layouts/DashLayout.tsx +0 -165
  305. package/src/ui/pages/SinglePage.tsx +0 -23
  306. package/src/ui/shared/ThreadView.tsx +0 -136
  307. /package/src/{ui → client}/components/settings-types.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);