@jant/core 0.3.27 → 0.3.29

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 (314) hide show
  1. package/bin/reset-password.js +22 -0
  2. package/dist/client/client.css +1 -0
  3. package/dist/client/client.js +31561 -0
  4. package/dist/index.js +15209 -15
  5. package/package.json +25 -15
  6. package/src/__tests__/helpers/app.ts +19 -3
  7. package/src/__tests__/helpers/db.ts +44 -0
  8. package/src/__tests__/helpers/lingui-core-macro-mock.ts +33 -0
  9. package/src/app.tsx +111 -174
  10. package/src/client.ts +13 -0
  11. package/src/db/migrations/0007_post_collections_m2m.sql +94 -0
  12. package/src/db/migrations/0008_add_collection_dividers.sql +8 -0
  13. package/src/db/migrations/0009_drop_collection_show_divider.sql +2 -0
  14. package/src/db/migrations/0010_add_performance_indexes.sql +16 -0
  15. package/src/db/schema.ts +24 -4
  16. package/src/i18n/locales/en.po +810 -385
  17. package/src/i18n/locales/en.ts +1 -1
  18. package/src/i18n/locales/zh-Hans.po +733 -522
  19. package/src/i18n/locales/zh-Hans.ts +1 -1
  20. package/src/i18n/locales/zh-Hant.po +733 -522
  21. package/src/i18n/locales/zh-Hant.ts +1 -1
  22. package/src/i18n/middleware.ts +7 -11
  23. package/src/index.ts +1 -1
  24. package/src/lib/__tests__/icons.test.ts +178 -0
  25. package/src/lib/__tests__/resolve-config.test.ts +184 -0
  26. package/src/lib/__tests__/schemas.test.ts +12 -6
  27. package/src/lib/__tests__/theme.test.ts +62 -0
  28. package/src/lib/__tests__/timezones.test.ts +1 -1
  29. package/src/lib/__tests__/url.test.ts +12 -0
  30. package/src/lib/__tests__/view.test.ts +1 -5
  31. package/src/lib/avatar-upload.ts +18 -10
  32. package/src/lib/collection-form-bridge.ts +52 -0
  33. package/src/lib/collections-reorder.ts +28 -0
  34. package/src/lib/compose-bridge.ts +251 -0
  35. package/src/lib/errors.ts +116 -0
  36. package/src/lib/excerpt.ts +1 -1
  37. package/src/lib/favicon.ts +3 -5
  38. package/src/lib/html.ts +22 -0
  39. package/src/lib/icon-catalog.ts +181 -0
  40. package/src/lib/icons.ts +202 -0
  41. package/src/lib/navigation.ts +18 -33
  42. package/src/lib/pagination.ts +3 -2
  43. package/src/lib/post-form-bridge.ts +136 -0
  44. package/src/lib/render.tsx +11 -4
  45. package/src/lib/resolve-config.ts +157 -0
  46. package/src/lib/schemas.ts +76 -12
  47. package/src/lib/settings-bridge.ts +139 -0
  48. package/src/lib/storage.ts +37 -16
  49. package/src/lib/theme.ts +5 -7
  50. package/src/lib/timeline.ts +4 -8
  51. package/src/lib/toast.ts +134 -0
  52. package/src/lib/upload.ts +71 -0
  53. package/src/lib/url.ts +9 -1
  54. package/src/lib/version.ts +16 -0
  55. package/src/lib/view.ts +9 -10
  56. package/src/middleware/__tests__/auth.test.ts +6 -28
  57. package/src/middleware/__tests__/onboarding.test.ts +1 -1
  58. package/src/middleware/auth.ts +6 -12
  59. package/src/middleware/config.ts +51 -0
  60. package/src/middleware/error-handler.ts +56 -0
  61. package/src/middleware/onboarding.ts +1 -1
  62. package/src/preset.css +6 -0
  63. package/src/routes/__tests__/compose.test.ts +104 -17
  64. package/src/routes/api/__tests__/collections.test.ts +93 -2
  65. package/src/routes/api/__tests__/posts.test.ts +2 -1
  66. package/src/routes/api/__tests__/settings.test.ts +1 -1
  67. package/src/routes/api/collections.ts +64 -68
  68. package/src/routes/api/nav-items.ts +21 -59
  69. package/src/routes/api/pages.ts +18 -46
  70. package/src/routes/api/posts.ts +64 -86
  71. package/src/routes/api/search.ts +6 -4
  72. package/src/routes/api/settings.ts +8 -24
  73. package/src/routes/api/upload.ts +55 -53
  74. package/src/routes/auth/__tests__/setup.test.ts +118 -0
  75. package/src/routes/auth/reset.tsx +17 -66
  76. package/src/routes/auth/setup.tsx +67 -11
  77. package/src/routes/auth/signin.tsx +44 -8
  78. package/src/routes/compose.tsx +194 -0
  79. package/src/routes/dash/__tests__/font-theme.test.ts +110 -0
  80. package/src/routes/dash/__tests__/pages.test.ts +2 -2
  81. package/src/routes/dash/__tests__/settings-avatar.test.ts +23 -12
  82. package/src/routes/dash/appearance.tsx +173 -0
  83. package/src/routes/dash/collections.tsx +80 -14
  84. package/src/routes/dash/index.tsx +12 -14
  85. package/src/routes/dash/media.tsx +46 -49
  86. package/src/routes/dash/pages.tsx +85 -37
  87. package/src/routes/dash/posts.tsx +60 -23
  88. package/src/routes/dash/redirects.tsx +43 -33
  89. package/src/routes/dash/settings.tsx +234 -214
  90. package/src/routes/feed/__tests__/rss.test.ts +7 -3
  91. package/src/routes/feed/rss.ts +11 -16
  92. package/src/routes/feed/sitemap.ts +15 -9
  93. package/src/routes/pages/__tests__/collections.test.ts +9 -8
  94. package/src/routes/pages/archive.tsx +2 -2
  95. package/src/routes/pages/collection.tsx +76 -9
  96. package/src/routes/pages/collections.tsx +3 -1
  97. package/src/routes/pages/featured.tsx +2 -2
  98. package/src/routes/pages/home.tsx +3 -3
  99. package/src/routes/pages/latest.tsx +2 -2
  100. package/src/routes/pages/page.tsx +2 -2
  101. package/src/routes/pages/post.tsx +2 -2
  102. package/src/routes/pages/search.tsx +2 -2
  103. package/src/services/__tests__/collection.test.ts +324 -34
  104. package/src/services/__tests__/media.test.ts +1 -1
  105. package/src/services/__tests__/page.test.ts +116 -1
  106. package/src/services/auth.ts +88 -0
  107. package/src/services/collection.ts +169 -30
  108. package/src/services/index.ts +8 -3
  109. package/src/services/media.ts +39 -12
  110. package/src/services/navigation.ts +17 -5
  111. package/src/services/page.ts +24 -4
  112. package/src/services/post.ts +87 -19
  113. package/src/services/search.ts +0 -1
  114. package/src/services/settings.ts +21 -13
  115. package/src/style.css +3 -0
  116. package/src/styles/components.css +42 -1
  117. package/src/styles/tokens.css +4 -0
  118. package/src/styles/ui.css +902 -73
  119. package/src/types/app-context.ts +25 -0
  120. package/src/types/bindings.ts +1 -0
  121. package/src/types/config.ts +60 -23
  122. package/src/types/entities.ts +12 -2
  123. package/src/types/lingui-react-macro.d.ts +3 -3
  124. package/src/types/operations.ts +2 -4
  125. package/src/types/views.ts +1 -3
  126. package/src/ui/__tests__/font-themes.test.ts +27 -8
  127. package/src/ui/color-themes.ts +1 -1
  128. package/src/ui/components/__tests__/jant-collection-form.test.ts +153 -0
  129. package/src/ui/components/__tests__/jant-compose-dialog.test.ts +512 -0
  130. package/src/ui/components/__tests__/jant-compose-editor.test.ts +272 -0
  131. package/src/ui/components/__tests__/jant-post-form.test.ts +172 -0
  132. package/src/ui/components/__tests__/jant-settings-avatar.test.ts +235 -0
  133. package/src/ui/components/__tests__/jant-settings-general.test.ts +319 -0
  134. package/src/ui/components/collection-types.ts +45 -0
  135. package/src/ui/components/compose-types.ts +75 -0
  136. package/src/ui/components/jant-collection-form.ts +512 -0
  137. package/src/ui/components/jant-compose-dialog.ts +494 -0
  138. package/src/ui/components/jant-compose-editor.ts +799 -0
  139. package/src/ui/components/jant-post-form.ts +290 -0
  140. package/src/ui/components/jant-settings-avatar.ts +231 -0
  141. package/src/ui/components/jant-settings-general.ts +436 -0
  142. package/src/ui/components/post-form-template.ts +260 -0
  143. package/src/ui/components/post-form-types.ts +87 -0
  144. package/src/ui/components/settings-types.ts +62 -0
  145. package/src/ui/compose/ComposeDialog.tsx +141 -385
  146. package/src/ui/compose/ComposePrompt.tsx +3 -3
  147. package/src/ui/dash/PostList.tsx +55 -61
  148. package/src/ui/dash/appearance/AdvancedContent.tsx +80 -0
  149. package/src/ui/dash/appearance/AppearanceNav.tsx +56 -0
  150. package/src/ui/dash/appearance/ColorThemeContent.tsx +129 -0
  151. package/src/ui/dash/appearance/FontThemeContent.tsx +98 -0
  152. package/src/ui/dash/collections/CollectionForm.tsx +130 -117
  153. package/src/ui/dash/collections/CollectionsListContent.tsx +102 -41
  154. package/src/ui/dash/collections/IconPickerGrid.tsx +50 -0
  155. package/src/ui/dash/collections/ViewCollectionContent.tsx +14 -3
  156. package/src/ui/dash/index.ts +1 -1
  157. package/src/ui/dash/posts/PostForm.tsx +248 -0
  158. package/src/ui/dash/settings/AccountContent.tsx +69 -80
  159. package/src/ui/dash/settings/GeneralContent.tsx +159 -478
  160. package/src/ui/dash/settings/SettingsNav.tsx +4 -4
  161. package/src/ui/font-themes.ts +115 -32
  162. package/src/ui/layouts/BaseLayout.tsx +49 -19
  163. package/src/ui/layouts/DashLayout.tsx +14 -9
  164. package/src/ui/layouts/SiteLayout.tsx +38 -23
  165. package/src/ui/pages/CollectionPage.tsx +12 -2
  166. package/src/ui/pages/CollectionsPage.tsx +27 -27
  167. package/src/ui/pages/HomePage.tsx +15 -6
  168. package/src/ui/pages/SearchPage.tsx +1 -2
  169. package/src/ui/shared/CollectionsSidebar.tsx +59 -0
  170. package/src/ui/shared/Pagination.tsx +2 -2
  171. package/dist/app.js +0 -267
  172. package/dist/auth.js +0 -39
  173. package/dist/client.js +0 -13
  174. package/dist/db/index.js +0 -10
  175. package/dist/db/schema.js +0 -224
  176. package/dist/i18n/Trans.js +0 -24
  177. package/dist/i18n/context.js +0 -58
  178. package/dist/i18n/detect.js +0 -26
  179. package/dist/i18n/i18n.js +0 -49
  180. package/dist/i18n/index.js +0 -44
  181. package/dist/i18n/locales/en.js +0 -1
  182. package/dist/i18n/locales/zh-Hans.js +0 -1
  183. package/dist/i18n/locales/zh-Hant.js +0 -1
  184. package/dist/i18n/locales.js +0 -13
  185. package/dist/i18n/middleware.js +0 -30
  186. package/dist/lib/avatar-upload.js +0 -134
  187. package/dist/lib/config.js +0 -143
  188. package/dist/lib/constants.js +0 -50
  189. package/dist/lib/excerpt.js +0 -76
  190. package/dist/lib/favicon.js +0 -102
  191. package/dist/lib/feed.js +0 -123
  192. package/dist/lib/image-processor.js +0 -187
  193. package/dist/lib/image.js +0 -97
  194. package/dist/lib/index.js +0 -7
  195. package/dist/lib/markdown.js +0 -83
  196. package/dist/lib/media-helpers.js +0 -49
  197. package/dist/lib/media-upload.js +0 -104
  198. package/dist/lib/nav-reorder.js +0 -27
  199. package/dist/lib/navigation.js +0 -79
  200. package/dist/lib/pagination.js +0 -44
  201. package/dist/lib/render.js +0 -53
  202. package/dist/lib/schemas.js +0 -174
  203. package/dist/lib/sqid.js +0 -72
  204. package/dist/lib/sse.js +0 -218
  205. package/dist/lib/storage.js +0 -164
  206. package/dist/lib/theme.js +0 -65
  207. package/dist/lib/time.js +0 -159
  208. package/dist/lib/timeline.js +0 -95
  209. package/dist/lib/timezones.js +0 -388
  210. package/dist/lib/url.js +0 -89
  211. package/dist/lib/view.js +0 -217
  212. package/dist/middleware/auth.js +0 -52
  213. package/dist/middleware/onboarding.js +0 -41
  214. package/dist/routes/api/collections.js +0 -124
  215. package/dist/routes/api/nav-items.js +0 -104
  216. package/dist/routes/api/pages.js +0 -91
  217. package/dist/routes/api/posts.js +0 -218
  218. package/dist/routes/api/search.js +0 -48
  219. package/dist/routes/api/settings.js +0 -68
  220. package/dist/routes/api/upload.js +0 -246
  221. package/dist/routes/auth/reset.js +0 -221
  222. package/dist/routes/auth/setup.js +0 -194
  223. package/dist/routes/auth/signin.js +0 -176
  224. package/dist/routes/compose.js +0 -48
  225. package/dist/routes/dash/collections.js +0 -115
  226. package/dist/routes/dash/index.js +0 -118
  227. package/dist/routes/dash/media.js +0 -106
  228. package/dist/routes/dash/pages.js +0 -294
  229. package/dist/routes/dash/posts.js +0 -244
  230. package/dist/routes/dash/redirects.js +0 -257
  231. package/dist/routes/dash/settings.js +0 -379
  232. package/dist/routes/feed/rss.js +0 -62
  233. package/dist/routes/feed/sitemap.js +0 -49
  234. package/dist/routes/pages/archive.js +0 -62
  235. package/dist/routes/pages/collection.js +0 -34
  236. package/dist/routes/pages/collections.js +0 -28
  237. package/dist/routes/pages/featured.js +0 -36
  238. package/dist/routes/pages/home.js +0 -64
  239. package/dist/routes/pages/latest.js +0 -45
  240. package/dist/routes/pages/page.js +0 -68
  241. package/dist/routes/pages/post.js +0 -44
  242. package/dist/routes/pages/search.js +0 -54
  243. package/dist/services/collection.js +0 -109
  244. package/dist/services/index.js +0 -24
  245. package/dist/services/media.js +0 -117
  246. package/dist/services/navigation.js +0 -91
  247. package/dist/services/page.js +0 -84
  248. package/dist/services/post.js +0 -229
  249. package/dist/services/redirect.js +0 -48
  250. package/dist/services/search.js +0 -67
  251. package/dist/services/settings.js +0 -68
  252. package/dist/types/bindings.js +0 -3
  253. package/dist/types/config.js +0 -147
  254. package/dist/types/constants.js +0 -27
  255. package/dist/types/entities.js +0 -3
  256. package/dist/types/lingui-react-macro.d.js +0 -9
  257. package/dist/types/operations.js +0 -3
  258. package/dist/types/props.js +0 -3
  259. package/dist/types/sortablejs.d.js +0 -5
  260. package/dist/types/views.js +0 -5
  261. package/dist/types.js +0 -11
  262. package/dist/ui/color-themes.js +0 -268
  263. package/dist/ui/compose/ComposeDialog.js +0 -467
  264. package/dist/ui/compose/ComposePrompt.js +0 -55
  265. package/dist/ui/dash/ActionButtons.js +0 -46
  266. package/dist/ui/dash/CrudPageHeader.js +0 -22
  267. package/dist/ui/dash/DangerZone.js +0 -36
  268. package/dist/ui/dash/FormatBadge.js +0 -27
  269. package/dist/ui/dash/ListItemRow.js +0 -21
  270. package/dist/ui/dash/PageForm.js +0 -195
  271. package/dist/ui/dash/PostForm.js +0 -395
  272. package/dist/ui/dash/PostList.js +0 -83
  273. package/dist/ui/dash/StatusBadge.js +0 -46
  274. package/dist/ui/dash/collections/CollectionForm.js +0 -152
  275. package/dist/ui/dash/collections/CollectionsListContent.js +0 -68
  276. package/dist/ui/dash/collections/ViewCollectionContent.js +0 -96
  277. package/dist/ui/dash/index.js +0 -10
  278. package/dist/ui/dash/media/MediaListContent.js +0 -166
  279. package/dist/ui/dash/media/ViewMediaContent.js +0 -212
  280. package/dist/ui/dash/pages/LinkFormContent.js +0 -130
  281. package/dist/ui/dash/pages/UnifiedPagesContent.js +0 -193
  282. package/dist/ui/dash/settings/AccountContent.js +0 -209
  283. package/dist/ui/dash/settings/AppearanceContent.js +0 -259
  284. package/dist/ui/dash/settings/GeneralContent.js +0 -536
  285. package/dist/ui/dash/settings/SettingsNav.js +0 -41
  286. package/dist/ui/feed/LinkCard.js +0 -72
  287. package/dist/ui/feed/NoteCard.js +0 -58
  288. package/dist/ui/feed/QuoteCard.js +0 -63
  289. package/dist/ui/feed/ThreadPreview.js +0 -48
  290. package/dist/ui/feed/TimelineFeed.js +0 -41
  291. package/dist/ui/feed/TimelineItem.js +0 -27
  292. package/dist/ui/font-themes.js +0 -36
  293. package/dist/ui/layouts/BaseLayout.js +0 -153
  294. package/dist/ui/layouts/DashLayout.js +0 -141
  295. package/dist/ui/layouts/SiteLayout.js +0 -169
  296. package/dist/ui/pages/ArchivePage.js +0 -143
  297. package/dist/ui/pages/CollectionPage.js +0 -70
  298. package/dist/ui/pages/CollectionsPage.js +0 -76
  299. package/dist/ui/pages/FeaturedPage.js +0 -24
  300. package/dist/ui/pages/HomePage.js +0 -24
  301. package/dist/ui/pages/PostPage.js +0 -55
  302. package/dist/ui/pages/SearchPage.js +0 -122
  303. package/dist/ui/pages/SinglePage.js +0 -23
  304. package/dist/ui/shared/EmptyState.js +0 -27
  305. package/dist/ui/shared/MediaGallery.js +0 -35
  306. package/dist/ui/shared/Pagination.js +0 -195
  307. package/dist/ui/shared/ThreadView.js +0 -108
  308. package/dist/ui/shared/index.js +0 -5
  309. package/dist/vendor/datastar.js +0 -1606
  310. package/src/lib/__tests__/config.test.ts +0 -192
  311. package/src/lib/config.ts +0 -167
  312. package/src/routes/compose.ts +0 -63
  313. package/src/ui/dash/PostForm.tsx +0 -360
  314. package/src/ui/dash/settings/AppearanceContent.tsx +0 -254
@@ -0,0 +1,494 @@
1
+ /**
2
+ * Compose Dialog
3
+ *
4
+ * Outer shell for the compose dialog: header with format switcher,
5
+ * collection selector, action row, and attachment upload coordination.
6
+ *
7
+ * Light DOM only — BaseCoat and Tailwind classes apply directly.
8
+ */
9
+
10
+ import { LitElement, html, nothing } from "lit";
11
+ import { classMap } from "lit/directives/class-map.js";
12
+ import { unsafeHTML } from "lit/directives/unsafe-html.js";
13
+ import type {
14
+ ComposeFormat,
15
+ ComposeLabels,
16
+ ComposeCollection,
17
+ ComposeSubmitDetail,
18
+ } from "./compose-types.js";
19
+ import type { JantComposeEditor } from "./jant-compose-editor.js";
20
+
21
+ export class JantComposeDialog extends LitElement {
22
+ static properties = {
23
+ collections: { type: Array },
24
+ labels: { type: Object },
25
+ _format: { state: true },
26
+ _status: { state: true },
27
+ _loading: { state: true },
28
+ _collectionIds: { state: true },
29
+ _showCollection: { state: true },
30
+ _showMoreMenu: { state: true },
31
+ _collectionSearch: { state: true },
32
+ };
33
+
34
+ declare collections: ComposeCollection[];
35
+ declare labels: ComposeLabels;
36
+ declare _format: ComposeFormat;
37
+ declare _status: "published" | "draft";
38
+ declare _loading: boolean;
39
+ declare _collectionIds: number[];
40
+ declare _showCollection: boolean;
41
+ declare _showMoreMenu: boolean;
42
+ declare _collectionSearch: string;
43
+
44
+ createRenderRoot() {
45
+ this.innerHTML = "";
46
+ return this;
47
+ }
48
+
49
+ constructor() {
50
+ super();
51
+ this.collections = [];
52
+ this.labels = {} as ComposeLabels;
53
+ this._format = "note";
54
+ this._status = "published";
55
+ this._loading = false;
56
+ this._collectionIds = [];
57
+ this._showCollection = false;
58
+ this._showMoreMenu = false;
59
+ this._collectionSearch = "";
60
+ }
61
+
62
+ private get _editor(): JantComposeEditor | null {
63
+ return this.querySelector("jant-compose-editor");
64
+ }
65
+
66
+ reset() {
67
+ this._format = "note";
68
+ this._status = "published";
69
+ this._loading = false;
70
+ this._collectionIds = [];
71
+ this._showCollection = false;
72
+ this._showMoreMenu = false;
73
+ this._collectionSearch = "";
74
+ this._editor?.reset();
75
+ }
76
+
77
+ set loading(v: boolean) {
78
+ this._loading = v;
79
+ }
80
+
81
+ private _closeDialog() {
82
+ this.closest("dialog")?.close();
83
+ }
84
+
85
+ private _buildSubmitDetail(
86
+ status: "published" | "draft",
87
+ ): ComposeSubmitDetail | null {
88
+ const editor = this._editor;
89
+ if (!editor) return null;
90
+
91
+ const editorData = editor.getData();
92
+ const attachments = editorData.attachments ?? [];
93
+
94
+ // Collect mediaIds from completed uploads
95
+ const mediaIds = attachments
96
+ .filter((a) => a.status === "done" && a.mediaId)
97
+ .map((a) => a.mediaId as string);
98
+
99
+ // Collect alt text keyed by mediaId
100
+ const mediaAlts: Record<string, string> = {};
101
+ for (const a of attachments) {
102
+ if (a.mediaId && a.alt) {
103
+ mediaAlts[a.mediaId] = a.alt;
104
+ }
105
+ }
106
+
107
+ return {
108
+ format: this._format,
109
+ title: editorData.title,
110
+ body: editorData.body,
111
+ url: editorData.url,
112
+ quoteText: editorData.quoteText,
113
+ quoteAuthor: editorData.quoteAuthor,
114
+ status,
115
+ rating: editorData.rating,
116
+ collectionIds: [...this._collectionIds],
117
+ mediaIds,
118
+ mediaAlts,
119
+ attachedText: editorData.attachedText,
120
+ };
121
+ }
122
+
123
+ private _submit(status: "published" | "draft") {
124
+ if (this._loading) return;
125
+ const editor = this._editor;
126
+ if (!editor) return;
127
+
128
+ const attachments = editor._attachments ?? [];
129
+ const hasPending = attachments.some(
130
+ (a) => a.status === "pending" || a.status === "uploading",
131
+ );
132
+
133
+ const detail = this._buildSubmitDetail(status);
134
+ if (!detail) return;
135
+
136
+ if (hasPending) {
137
+ // Deferred submit: close dialog, let bridge finish uploads and post
138
+ this.dispatchEvent(
139
+ new CustomEvent("jant:compose-submit-deferred", {
140
+ bubbles: true,
141
+ detail: {
142
+ ...detail,
143
+ pendingAttachments: attachments.filter(
144
+ (a) => a.status === "pending" || a.status === "uploading",
145
+ ),
146
+ },
147
+ }),
148
+ );
149
+ this._closeDialog();
150
+ // Prevent browser from restoring focus to the trigger button
151
+ (document.activeElement as HTMLElement)?.blur();
152
+ this.reset();
153
+ } else {
154
+ this.dispatchEvent(
155
+ new CustomEvent("jant:compose-submit", {
156
+ bubbles: true,
157
+ detail,
158
+ }),
159
+ );
160
+ }
161
+ }
162
+
163
+ private _toggleCollection(id: number) {
164
+ if (this._collectionIds.includes(id)) {
165
+ this._collectionIds = this._collectionIds.filter((cid) => cid !== id);
166
+ } else {
167
+ this._collectionIds = [...this._collectionIds, id];
168
+ }
169
+ }
170
+
171
+ connectedCallback() {
172
+ super.connectedCallback();
173
+ this.addEventListener("keydown", this._handleKeydown);
174
+ }
175
+
176
+ disconnectedCallback() {
177
+ super.disconnectedCallback();
178
+ this.removeEventListener("keydown", this._handleKeydown);
179
+ }
180
+
181
+ private _handleKeydown = (e: Event) => {
182
+ const ke = e as globalThis.KeyboardEvent;
183
+ if ((ke.metaKey || ke.ctrlKey) && ke.key === "Enter") {
184
+ e.preventDefault();
185
+ this._submit("published");
186
+ }
187
+ };
188
+
189
+ // ── Render helpers ────────────────────────────────────────────────
190
+
191
+ private _renderHeader() {
192
+ const formats: ComposeFormat[] = ["note", "link", "quote"];
193
+ const formatLabels: Record<ComposeFormat, string> = {
194
+ note: this.labels.note,
195
+ link: this.labels.link,
196
+ quote: this.labels.quote,
197
+ };
198
+
199
+ return html`
200
+ <header class="compose-dialog-header">
201
+ <button
202
+ type="button"
203
+ class="compose-dialog-cancel"
204
+ @click=${() => this._closeDialog()}
205
+ >
206
+ ${this.labels.cancel}
207
+ </button>
208
+
209
+ <div class="compose-dialog-header-center">
210
+ <div class="compose-segmented">
211
+ <div
212
+ class=${classMap({
213
+ "compose-format-pill": true,
214
+ "compose-format-pill-link": this._format === "link",
215
+ "compose-format-pill-quote": this._format === "quote",
216
+ })}
217
+ ></div>
218
+ ${formats.map(
219
+ (f) => html`
220
+ <button
221
+ type="button"
222
+ class=${classMap({
223
+ "compose-segmented-item": true,
224
+ "compose-segmented-item-active": this._format === f,
225
+ })}
226
+ @click=${() => {
227
+ this._format = f;
228
+ globalThis.requestAnimationFrame(() =>
229
+ this._editor?.focusInput(),
230
+ );
231
+ }}
232
+ >
233
+ ${formatLabels[f]}
234
+ </button>
235
+ `,
236
+ )}
237
+ </div>
238
+ </div>
239
+
240
+ <div class="flex items-center gap-0.5 shrink-0">
241
+ <button
242
+ type="button"
243
+ class="compose-dialog-header-btn"
244
+ title=${this.labels.saveDraft}
245
+ ?disabled=${this._loading}
246
+ @click=${() => this._submit("draft")}
247
+ >
248
+ <svg
249
+ width="18"
250
+ height="18"
251
+ viewBox="0 0 18 18"
252
+ fill="none"
253
+ stroke="currentColor"
254
+ stroke-width="1.3"
255
+ stroke-linecap="round"
256
+ stroke-linejoin="round"
257
+ >
258
+ <path d="M14 2.5L15.5 4 7 12.5l-3 .5.5-3L14 2.5z" />
259
+ <path d="M4 15h10" />
260
+ </svg>
261
+ </button>
262
+
263
+ ${this._renderMoreMenu()}
264
+ </div>
265
+ </header>
266
+ `;
267
+ }
268
+
269
+ private _renderMoreMenu() {
270
+ return html`
271
+ <div class="relative">
272
+ ${this._showMoreMenu
273
+ ? html`<div
274
+ class="compose-dropdown-backdrop"
275
+ @click=${() => {
276
+ this._showMoreMenu = false;
277
+ }}
278
+ ></div>`
279
+ : nothing}
280
+ <button
281
+ type="button"
282
+ class="compose-dialog-header-btn"
283
+ @click=${() => {
284
+ this._showMoreMenu = !this._showMoreMenu;
285
+ }}
286
+ >
287
+ <svg width="18" height="18" viewBox="0 0 18 18" fill="currentColor">
288
+ <circle cx="4.5" cy="9" r="1.3" />
289
+ <circle cx="9" cy="9" r="1.3" />
290
+ <circle cx="13.5" cy="9" r="1.3" />
291
+ </svg>
292
+ </button>
293
+ ${this._showMoreMenu
294
+ ? html`
295
+ <div class="compose-dropdown compose-dropdown-right">
296
+ <button
297
+ type="button"
298
+ class="compose-dropdown-item"
299
+ @click=${() => {
300
+ this._submit("draft");
301
+ this._showMoreMenu = false;
302
+ }}
303
+ >
304
+ ${this.labels.saveAsDraft}
305
+ </button>
306
+ <div class="compose-dropdown-divider"></div>
307
+ <button
308
+ type="button"
309
+ class="compose-dropdown-item compose-dropdown-item-danger"
310
+ @click=${() => {
311
+ this._closeDialog();
312
+ this._showMoreMenu = false;
313
+ }}
314
+ >
315
+ ${this.labels.discard}
316
+ </button>
317
+ </div>
318
+ `
319
+ : nothing}
320
+ </div>
321
+ `;
322
+ }
323
+
324
+ private _renderCollectionSelector() {
325
+ if (!this.collections || this.collections.length === 0) {
326
+ return html`<div class="flex-1"></div>`;
327
+ }
328
+
329
+ const search = this._collectionSearch.toLowerCase();
330
+ const filtered = search
331
+ ? this.collections.filter((c) => c.title.toLowerCase().includes(search))
332
+ : this.collections;
333
+ const selectedCount = this._collectionIds.length;
334
+
335
+ return html`
336
+ <div class="flex-1 min-w-0">
337
+ ${this._showCollection
338
+ ? html`<div
339
+ class="compose-dropdown-backdrop"
340
+ @click=${() => {
341
+ this._showCollection = false;
342
+ this._collectionSearch = "";
343
+ }}
344
+ ></div>`
345
+ : nothing}
346
+ <div class="select compose-collection-select">
347
+ <button
348
+ type="button"
349
+ class="compose-collection-trigger"
350
+ @click=${() => {
351
+ this._showCollection = !this._showCollection;
352
+ if (!this._showCollection) {
353
+ this._collectionSearch = "";
354
+ }
355
+ }}
356
+ >
357
+ <svg
358
+ width="14"
359
+ height="14"
360
+ viewBox="0 0 18 18"
361
+ fill="none"
362
+ stroke="currentColor"
363
+ stroke-width="1.4"
364
+ stroke-linecap="round"
365
+ stroke-linejoin="round"
366
+ class="shrink-0"
367
+ >
368
+ <rect x="3" y="5" width="12" height="10" rx="2" />
369
+ <path d="M6 5V4a1 1 0 011-1h4a1 1 0 011 1v1" />
370
+ </svg>
371
+ ${selectedCount > 0
372
+ ? html`<span class="badge compose-collection-badge"
373
+ >${selectedCount}</span
374
+ >`
375
+ : html`<span>${this.labels.collection}</span>`}
376
+ <svg
377
+ width="10"
378
+ height="10"
379
+ viewBox="0 0 10 10"
380
+ fill="none"
381
+ stroke="currentColor"
382
+ stroke-width="1.4"
383
+ stroke-linecap="round"
384
+ stroke-linejoin="round"
385
+ class="shrink-0 opacity-50"
386
+ >
387
+ <path d="M3 4l2 2 2-2" />
388
+ </svg>
389
+ </button>
390
+ <div
391
+ data-popover
392
+ data-side="top"
393
+ aria-hidden=${this._showCollection ? "false" : "true"}
394
+ >
395
+ <header>
396
+ <svg
397
+ width="16"
398
+ height="16"
399
+ viewBox="0 0 24 24"
400
+ fill="none"
401
+ stroke="currentColor"
402
+ stroke-width="2"
403
+ stroke-linecap="round"
404
+ stroke-linejoin="round"
405
+ >
406
+ <circle cx="11" cy="11" r="8" />
407
+ <path d="m21 21-4.3-4.3" />
408
+ </svg>
409
+ <input
410
+ type="text"
411
+ role="combobox"
412
+ placeholder=${this.labels.searchCollections}
413
+ autocomplete="off"
414
+ autocorrect="off"
415
+ spellcheck="false"
416
+ .value=${this._collectionSearch}
417
+ @input=${(e: Event) => {
418
+ this._collectionSearch = (e.target as HTMLInputElement).value;
419
+ }}
420
+ />
421
+ </header>
422
+ <div
423
+ role="listbox"
424
+ aria-multiselectable="true"
425
+ data-empty=${this.labels.noCollections}
426
+ >
427
+ ${filtered.map(
428
+ (col) => html`
429
+ <div
430
+ role="option"
431
+ data-value=${col.id}
432
+ aria-selected=${this._collectionIds.includes(col.id)
433
+ ? "true"
434
+ : nothing}
435
+ @click=${() => this._toggleCollection(col.id)}
436
+ >
437
+ ${col.iconHtml
438
+ ? html`<span
439
+ class="inline-flex items-center justify-center w-4 h-4 shrink-0"
440
+ >${unsafeHTML(col.iconHtml)}</span
441
+ >`
442
+ : nothing}
443
+ ${col.title}
444
+ </div>
445
+ `,
446
+ )}
447
+ </div>
448
+ </div>
449
+ </div>
450
+ </div>
451
+ `;
452
+ }
453
+
454
+ render() {
455
+ return html`
456
+ <div class="compose-dialog-inner">
457
+ ${this._renderHeader()}
458
+ <jant-compose-editor
459
+ .format=${this._format}
460
+ .labels=${this.labels}
461
+ ></jant-compose-editor>
462
+
463
+ <div class="compose-action-row">
464
+ ${this._renderCollectionSelector()}
465
+ <button
466
+ type="button"
467
+ class="compose-post-btn"
468
+ ?disabled=${this._loading}
469
+ @click=${() => this._submit("published")}
470
+ >
471
+ ${this._loading
472
+ ? html`<svg
473
+ class="animate-spin size-4"
474
+ xmlns="http://www.w3.org/2000/svg"
475
+ viewBox="0 0 24 24"
476
+ fill="none"
477
+ stroke="currentColor"
478
+ stroke-width="2"
479
+ stroke-linecap="round"
480
+ stroke-linejoin="round"
481
+ role="status"
482
+ >
483
+ <path d="M21 12a9 9 0 1 1-6.219-8.56" />
484
+ </svg>`
485
+ : nothing}
486
+ ${this.labels.post}
487
+ </button>
488
+ </div>
489
+ </div>
490
+ `;
491
+ }
492
+ }
493
+
494
+ customElements.define("jant-compose-dialog", JantComposeDialog);