@jant/core 0.3.27 → 0.3.28

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 (313) hide show
  1. package/dist/client/client.css +1 -0
  2. package/dist/client/client.js +31561 -0
  3. package/dist/index.js +15209 -15
  4. package/package.json +21 -15
  5. package/src/__tests__/helpers/app.ts +19 -3
  6. package/src/__tests__/helpers/db.ts +44 -0
  7. package/src/__tests__/helpers/lingui-core-macro-mock.ts +33 -0
  8. package/src/app.tsx +111 -174
  9. package/src/client.ts +13 -0
  10. package/src/db/migrations/0007_post_collections_m2m.sql +94 -0
  11. package/src/db/migrations/0008_add_collection_dividers.sql +8 -0
  12. package/src/db/migrations/0009_drop_collection_show_divider.sql +2 -0
  13. package/src/db/migrations/0010_add_performance_indexes.sql +16 -0
  14. package/src/db/schema.ts +24 -4
  15. package/src/i18n/locales/en.po +810 -385
  16. package/src/i18n/locales/en.ts +1 -1
  17. package/src/i18n/locales/zh-Hans.po +733 -522
  18. package/src/i18n/locales/zh-Hans.ts +1 -1
  19. package/src/i18n/locales/zh-Hant.po +733 -522
  20. package/src/i18n/locales/zh-Hant.ts +1 -1
  21. package/src/i18n/middleware.ts +7 -11
  22. package/src/index.ts +1 -1
  23. package/src/lib/__tests__/icons.test.ts +178 -0
  24. package/src/lib/__tests__/resolve-config.test.ts +184 -0
  25. package/src/lib/__tests__/schemas.test.ts +12 -6
  26. package/src/lib/__tests__/theme.test.ts +62 -0
  27. package/src/lib/__tests__/timezones.test.ts +1 -1
  28. package/src/lib/__tests__/url.test.ts +12 -0
  29. package/src/lib/__tests__/view.test.ts +1 -5
  30. package/src/lib/avatar-upload.ts +18 -10
  31. package/src/lib/collection-form-bridge.ts +52 -0
  32. package/src/lib/collections-reorder.ts +28 -0
  33. package/src/lib/compose-bridge.ts +251 -0
  34. package/src/lib/errors.ts +116 -0
  35. package/src/lib/excerpt.ts +1 -1
  36. package/src/lib/favicon.ts +3 -5
  37. package/src/lib/html.ts +22 -0
  38. package/src/lib/icon-catalog.ts +181 -0
  39. package/src/lib/icons.ts +202 -0
  40. package/src/lib/navigation.ts +18 -33
  41. package/src/lib/pagination.ts +3 -2
  42. package/src/lib/post-form-bridge.ts +136 -0
  43. package/src/lib/render.tsx +11 -4
  44. package/src/lib/resolve-config.ts +157 -0
  45. package/src/lib/schemas.ts +76 -12
  46. package/src/lib/settings-bridge.ts +139 -0
  47. package/src/lib/storage.ts +37 -16
  48. package/src/lib/theme.ts +5 -7
  49. package/src/lib/timeline.ts +4 -8
  50. package/src/lib/toast.ts +134 -0
  51. package/src/lib/upload.ts +71 -0
  52. package/src/lib/url.ts +9 -1
  53. package/src/lib/version.ts +16 -0
  54. package/src/lib/view.ts +9 -10
  55. package/src/middleware/__tests__/auth.test.ts +6 -28
  56. package/src/middleware/__tests__/onboarding.test.ts +1 -1
  57. package/src/middleware/auth.ts +6 -12
  58. package/src/middleware/config.ts +51 -0
  59. package/src/middleware/error-handler.ts +56 -0
  60. package/src/middleware/onboarding.ts +1 -1
  61. package/src/preset.css +6 -0
  62. package/src/routes/__tests__/compose.test.ts +104 -17
  63. package/src/routes/api/__tests__/collections.test.ts +93 -2
  64. package/src/routes/api/__tests__/posts.test.ts +2 -1
  65. package/src/routes/api/__tests__/settings.test.ts +1 -1
  66. package/src/routes/api/collections.ts +64 -68
  67. package/src/routes/api/nav-items.ts +21 -59
  68. package/src/routes/api/pages.ts +18 -46
  69. package/src/routes/api/posts.ts +64 -86
  70. package/src/routes/api/search.ts +6 -4
  71. package/src/routes/api/settings.ts +8 -24
  72. package/src/routes/api/upload.ts +55 -53
  73. package/src/routes/auth/__tests__/setup.test.ts +118 -0
  74. package/src/routes/auth/reset.tsx +17 -66
  75. package/src/routes/auth/setup.tsx +67 -11
  76. package/src/routes/auth/signin.tsx +44 -8
  77. package/src/routes/compose.tsx +194 -0
  78. package/src/routes/dash/__tests__/font-theme.test.ts +110 -0
  79. package/src/routes/dash/__tests__/pages.test.ts +2 -2
  80. package/src/routes/dash/__tests__/settings-avatar.test.ts +23 -12
  81. package/src/routes/dash/appearance.tsx +173 -0
  82. package/src/routes/dash/collections.tsx +80 -14
  83. package/src/routes/dash/index.tsx +12 -14
  84. package/src/routes/dash/media.tsx +46 -49
  85. package/src/routes/dash/pages.tsx +85 -37
  86. package/src/routes/dash/posts.tsx +60 -23
  87. package/src/routes/dash/redirects.tsx +43 -33
  88. package/src/routes/dash/settings.tsx +234 -214
  89. package/src/routes/feed/__tests__/rss.test.ts +7 -3
  90. package/src/routes/feed/rss.ts +11 -16
  91. package/src/routes/feed/sitemap.ts +15 -9
  92. package/src/routes/pages/__tests__/collections.test.ts +9 -8
  93. package/src/routes/pages/archive.tsx +2 -2
  94. package/src/routes/pages/collection.tsx +76 -9
  95. package/src/routes/pages/collections.tsx +3 -1
  96. package/src/routes/pages/featured.tsx +2 -2
  97. package/src/routes/pages/home.tsx +3 -3
  98. package/src/routes/pages/latest.tsx +2 -2
  99. package/src/routes/pages/page.tsx +2 -2
  100. package/src/routes/pages/post.tsx +2 -2
  101. package/src/routes/pages/search.tsx +2 -2
  102. package/src/services/__tests__/collection.test.ts +324 -34
  103. package/src/services/__tests__/media.test.ts +1 -1
  104. package/src/services/__tests__/page.test.ts +116 -1
  105. package/src/services/auth.ts +88 -0
  106. package/src/services/collection.ts +169 -30
  107. package/src/services/index.ts +8 -3
  108. package/src/services/media.ts +39 -12
  109. package/src/services/navigation.ts +17 -5
  110. package/src/services/page.ts +24 -4
  111. package/src/services/post.ts +87 -19
  112. package/src/services/search.ts +0 -1
  113. package/src/services/settings.ts +21 -13
  114. package/src/style.css +3 -0
  115. package/src/styles/components.css +42 -1
  116. package/src/styles/tokens.css +4 -0
  117. package/src/styles/ui.css +902 -73
  118. package/src/types/app-context.ts +25 -0
  119. package/src/types/bindings.ts +1 -0
  120. package/src/types/config.ts +60 -23
  121. package/src/types/entities.ts +12 -2
  122. package/src/types/lingui-react-macro.d.ts +3 -3
  123. package/src/types/operations.ts +2 -4
  124. package/src/types/views.ts +1 -3
  125. package/src/ui/__tests__/font-themes.test.ts +27 -8
  126. package/src/ui/color-themes.ts +1 -1
  127. package/src/ui/components/__tests__/jant-collection-form.test.ts +153 -0
  128. package/src/ui/components/__tests__/jant-compose-dialog.test.ts +512 -0
  129. package/src/ui/components/__tests__/jant-compose-editor.test.ts +272 -0
  130. package/src/ui/components/__tests__/jant-post-form.test.ts +172 -0
  131. package/src/ui/components/__tests__/jant-settings-avatar.test.ts +235 -0
  132. package/src/ui/components/__tests__/jant-settings-general.test.ts +319 -0
  133. package/src/ui/components/collection-types.ts +45 -0
  134. package/src/ui/components/compose-types.ts +75 -0
  135. package/src/ui/components/jant-collection-form.ts +512 -0
  136. package/src/ui/components/jant-compose-dialog.ts +494 -0
  137. package/src/ui/components/jant-compose-editor.ts +799 -0
  138. package/src/ui/components/jant-post-form.ts +290 -0
  139. package/src/ui/components/jant-settings-avatar.ts +231 -0
  140. package/src/ui/components/jant-settings-general.ts +436 -0
  141. package/src/ui/components/post-form-template.ts +260 -0
  142. package/src/ui/components/post-form-types.ts +87 -0
  143. package/src/ui/components/settings-types.ts +62 -0
  144. package/src/ui/compose/ComposeDialog.tsx +141 -385
  145. package/src/ui/compose/ComposePrompt.tsx +3 -3
  146. package/src/ui/dash/PostList.tsx +55 -61
  147. package/src/ui/dash/appearance/AdvancedContent.tsx +80 -0
  148. package/src/ui/dash/appearance/AppearanceNav.tsx +56 -0
  149. package/src/ui/dash/appearance/ColorThemeContent.tsx +129 -0
  150. package/src/ui/dash/appearance/FontThemeContent.tsx +98 -0
  151. package/src/ui/dash/collections/CollectionForm.tsx +130 -117
  152. package/src/ui/dash/collections/CollectionsListContent.tsx +102 -41
  153. package/src/ui/dash/collections/IconPickerGrid.tsx +50 -0
  154. package/src/ui/dash/collections/ViewCollectionContent.tsx +14 -3
  155. package/src/ui/dash/index.ts +1 -1
  156. package/src/ui/dash/posts/PostForm.tsx +248 -0
  157. package/src/ui/dash/settings/AccountContent.tsx +69 -80
  158. package/src/ui/dash/settings/GeneralContent.tsx +159 -478
  159. package/src/ui/dash/settings/SettingsNav.tsx +4 -4
  160. package/src/ui/font-themes.ts +115 -32
  161. package/src/ui/layouts/BaseLayout.tsx +49 -19
  162. package/src/ui/layouts/DashLayout.tsx +14 -9
  163. package/src/ui/layouts/SiteLayout.tsx +38 -23
  164. package/src/ui/pages/CollectionPage.tsx +12 -2
  165. package/src/ui/pages/CollectionsPage.tsx +27 -27
  166. package/src/ui/pages/HomePage.tsx +15 -6
  167. package/src/ui/pages/SearchPage.tsx +1 -2
  168. package/src/ui/shared/CollectionsSidebar.tsx +59 -0
  169. package/src/ui/shared/Pagination.tsx +2 -2
  170. package/dist/app.js +0 -267
  171. package/dist/auth.js +0 -39
  172. package/dist/client.js +0 -13
  173. package/dist/db/index.js +0 -10
  174. package/dist/db/schema.js +0 -224
  175. package/dist/i18n/Trans.js +0 -24
  176. package/dist/i18n/context.js +0 -58
  177. package/dist/i18n/detect.js +0 -26
  178. package/dist/i18n/i18n.js +0 -49
  179. package/dist/i18n/index.js +0 -44
  180. package/dist/i18n/locales/en.js +0 -1
  181. package/dist/i18n/locales/zh-Hans.js +0 -1
  182. package/dist/i18n/locales/zh-Hant.js +0 -1
  183. package/dist/i18n/locales.js +0 -13
  184. package/dist/i18n/middleware.js +0 -30
  185. package/dist/lib/avatar-upload.js +0 -134
  186. package/dist/lib/config.js +0 -143
  187. package/dist/lib/constants.js +0 -50
  188. package/dist/lib/excerpt.js +0 -76
  189. package/dist/lib/favicon.js +0 -102
  190. package/dist/lib/feed.js +0 -123
  191. package/dist/lib/image-processor.js +0 -187
  192. package/dist/lib/image.js +0 -97
  193. package/dist/lib/index.js +0 -7
  194. package/dist/lib/markdown.js +0 -83
  195. package/dist/lib/media-helpers.js +0 -49
  196. package/dist/lib/media-upload.js +0 -104
  197. package/dist/lib/nav-reorder.js +0 -27
  198. package/dist/lib/navigation.js +0 -79
  199. package/dist/lib/pagination.js +0 -44
  200. package/dist/lib/render.js +0 -53
  201. package/dist/lib/schemas.js +0 -174
  202. package/dist/lib/sqid.js +0 -72
  203. package/dist/lib/sse.js +0 -218
  204. package/dist/lib/storage.js +0 -164
  205. package/dist/lib/theme.js +0 -65
  206. package/dist/lib/time.js +0 -159
  207. package/dist/lib/timeline.js +0 -95
  208. package/dist/lib/timezones.js +0 -388
  209. package/dist/lib/url.js +0 -89
  210. package/dist/lib/view.js +0 -217
  211. package/dist/middleware/auth.js +0 -52
  212. package/dist/middleware/onboarding.js +0 -41
  213. package/dist/routes/api/collections.js +0 -124
  214. package/dist/routes/api/nav-items.js +0 -104
  215. package/dist/routes/api/pages.js +0 -91
  216. package/dist/routes/api/posts.js +0 -218
  217. package/dist/routes/api/search.js +0 -48
  218. package/dist/routes/api/settings.js +0 -68
  219. package/dist/routes/api/upload.js +0 -246
  220. package/dist/routes/auth/reset.js +0 -221
  221. package/dist/routes/auth/setup.js +0 -194
  222. package/dist/routes/auth/signin.js +0 -176
  223. package/dist/routes/compose.js +0 -48
  224. package/dist/routes/dash/collections.js +0 -115
  225. package/dist/routes/dash/index.js +0 -118
  226. package/dist/routes/dash/media.js +0 -106
  227. package/dist/routes/dash/pages.js +0 -294
  228. package/dist/routes/dash/posts.js +0 -244
  229. package/dist/routes/dash/redirects.js +0 -257
  230. package/dist/routes/dash/settings.js +0 -379
  231. package/dist/routes/feed/rss.js +0 -62
  232. package/dist/routes/feed/sitemap.js +0 -49
  233. package/dist/routes/pages/archive.js +0 -62
  234. package/dist/routes/pages/collection.js +0 -34
  235. package/dist/routes/pages/collections.js +0 -28
  236. package/dist/routes/pages/featured.js +0 -36
  237. package/dist/routes/pages/home.js +0 -64
  238. package/dist/routes/pages/latest.js +0 -45
  239. package/dist/routes/pages/page.js +0 -68
  240. package/dist/routes/pages/post.js +0 -44
  241. package/dist/routes/pages/search.js +0 -54
  242. package/dist/services/collection.js +0 -109
  243. package/dist/services/index.js +0 -24
  244. package/dist/services/media.js +0 -117
  245. package/dist/services/navigation.js +0 -91
  246. package/dist/services/page.js +0 -84
  247. package/dist/services/post.js +0 -229
  248. package/dist/services/redirect.js +0 -48
  249. package/dist/services/search.js +0 -67
  250. package/dist/services/settings.js +0 -68
  251. package/dist/types/bindings.js +0 -3
  252. package/dist/types/config.js +0 -147
  253. package/dist/types/constants.js +0 -27
  254. package/dist/types/entities.js +0 -3
  255. package/dist/types/lingui-react-macro.d.js +0 -9
  256. package/dist/types/operations.js +0 -3
  257. package/dist/types/props.js +0 -3
  258. package/dist/types/sortablejs.d.js +0 -5
  259. package/dist/types/views.js +0 -5
  260. package/dist/types.js +0 -11
  261. package/dist/ui/color-themes.js +0 -268
  262. package/dist/ui/compose/ComposeDialog.js +0 -467
  263. package/dist/ui/compose/ComposePrompt.js +0 -55
  264. package/dist/ui/dash/ActionButtons.js +0 -46
  265. package/dist/ui/dash/CrudPageHeader.js +0 -22
  266. package/dist/ui/dash/DangerZone.js +0 -36
  267. package/dist/ui/dash/FormatBadge.js +0 -27
  268. package/dist/ui/dash/ListItemRow.js +0 -21
  269. package/dist/ui/dash/PageForm.js +0 -195
  270. package/dist/ui/dash/PostForm.js +0 -395
  271. package/dist/ui/dash/PostList.js +0 -83
  272. package/dist/ui/dash/StatusBadge.js +0 -46
  273. package/dist/ui/dash/collections/CollectionForm.js +0 -152
  274. package/dist/ui/dash/collections/CollectionsListContent.js +0 -68
  275. package/dist/ui/dash/collections/ViewCollectionContent.js +0 -96
  276. package/dist/ui/dash/index.js +0 -10
  277. package/dist/ui/dash/media/MediaListContent.js +0 -166
  278. package/dist/ui/dash/media/ViewMediaContent.js +0 -212
  279. package/dist/ui/dash/pages/LinkFormContent.js +0 -130
  280. package/dist/ui/dash/pages/UnifiedPagesContent.js +0 -193
  281. package/dist/ui/dash/settings/AccountContent.js +0 -209
  282. package/dist/ui/dash/settings/AppearanceContent.js +0 -259
  283. package/dist/ui/dash/settings/GeneralContent.js +0 -536
  284. package/dist/ui/dash/settings/SettingsNav.js +0 -41
  285. package/dist/ui/feed/LinkCard.js +0 -72
  286. package/dist/ui/feed/NoteCard.js +0 -58
  287. package/dist/ui/feed/QuoteCard.js +0 -63
  288. package/dist/ui/feed/ThreadPreview.js +0 -48
  289. package/dist/ui/feed/TimelineFeed.js +0 -41
  290. package/dist/ui/feed/TimelineItem.js +0 -27
  291. package/dist/ui/font-themes.js +0 -36
  292. package/dist/ui/layouts/BaseLayout.js +0 -153
  293. package/dist/ui/layouts/DashLayout.js +0 -141
  294. package/dist/ui/layouts/SiteLayout.js +0 -169
  295. package/dist/ui/pages/ArchivePage.js +0 -143
  296. package/dist/ui/pages/CollectionPage.js +0 -70
  297. package/dist/ui/pages/CollectionsPage.js +0 -76
  298. package/dist/ui/pages/FeaturedPage.js +0 -24
  299. package/dist/ui/pages/HomePage.js +0 -24
  300. package/dist/ui/pages/PostPage.js +0 -55
  301. package/dist/ui/pages/SearchPage.js +0 -122
  302. package/dist/ui/pages/SinglePage.js +0 -23
  303. package/dist/ui/shared/EmptyState.js +0 -27
  304. package/dist/ui/shared/MediaGallery.js +0 -35
  305. package/dist/ui/shared/Pagination.js +0 -195
  306. package/dist/ui/shared/ThreadView.js +0 -108
  307. package/dist/ui/shared/index.js +0 -5
  308. package/dist/vendor/datastar.js +0 -1606
  309. package/src/lib/__tests__/config.test.ts +0 -192
  310. package/src/lib/config.ts +0 -167
  311. package/src/routes/compose.ts +0 -63
  312. package/src/ui/dash/PostForm.tsx +0 -360
  313. package/src/ui/dash/settings/AppearanceContent.tsx +0 -254
@@ -0,0 +1,290 @@
1
+ /**
2
+ * Dashboard Post Form
3
+ *
4
+ * Light DOM Lit component that manages post create/edit form state.
5
+ * Dispatches `jant:post-submit` for the bridge to handle networking.
6
+ */
7
+
8
+ import { LitElement } from "lit";
9
+ import type {
10
+ PostFormInitial,
11
+ PostFormLabels,
12
+ PostCollectionOption,
13
+ PostMediaItem,
14
+ PostSubmitDetail,
15
+ PostFormat,
16
+ PostStatus,
17
+ } from "./post-form-types.js";
18
+ import { renderPostForm } from "./post-form-template.js";
19
+
20
+ const DEFAULT_INITIAL: PostFormInitial = {
21
+ format: "note",
22
+ title: "",
23
+ body: "",
24
+ url: "",
25
+ quoteText: "",
26
+ status: "published",
27
+ featured: false,
28
+ pinned: false,
29
+ rating: 0,
30
+ collectionIds: [],
31
+ mediaIds: [],
32
+ };
33
+
34
+ const EMPTY_LABELS: PostFormLabels = {
35
+ formatLabel: "",
36
+ noteOption: "",
37
+ linkOption: "",
38
+ quoteOption: "",
39
+ titleLabel: "",
40
+ titlePlaceholder: "",
41
+ bodyLabel: "",
42
+ bodyPlaceholder: "",
43
+ urlLabel: "",
44
+ urlPlaceholder: "",
45
+ quoteTextLabel: "",
46
+ quoteTextPlaceholder: "",
47
+ mediaLabel: "",
48
+ mediaAddButton: "",
49
+ mediaRemoveButton: "",
50
+ mediaEmptyLabel: "",
51
+ statusLabel: "",
52
+ statusPublished: "",
53
+ statusDraft: "",
54
+ featuredLabel: "",
55
+ pinnedLabel: "",
56
+ collectionsLabel: "",
57
+ submitLabel: "",
58
+ cancelLabel: "",
59
+ mediaDialogTitle: "",
60
+ mediaDialogDone: "",
61
+ mediaDialogLoading: "",
62
+ submitSuccessMessage: "",
63
+ submitErrorMessage: "",
64
+ };
65
+
66
+ function parseJson<T>(value: unknown, fallback: T): T {
67
+ if (typeof value === "string") {
68
+ try {
69
+ return JSON.parse(value) as T;
70
+ } catch {
71
+ return fallback;
72
+ }
73
+ }
74
+ if (value && typeof value === "object") {
75
+ return value as T;
76
+ }
77
+ return fallback;
78
+ }
79
+
80
+ export class JantPostForm extends LitElement {
81
+ static properties = {
82
+ labels: { type: Object },
83
+ initial: { type: Object },
84
+ collections: { type: Array },
85
+ media: { type: Array },
86
+ action: { type: String },
87
+ cancelHref: { type: String, attribute: "cancel-href" },
88
+ mediaPickerUrl: { type: String, attribute: "media-picker-url" },
89
+ isEdit: { type: Boolean, attribute: "is-edit" },
90
+ _format: { state: true },
91
+ _title: { state: true },
92
+ _body: { state: true },
93
+ _url: { state: true },
94
+ _quoteText: { state: true },
95
+ _status: { state: true },
96
+ _featured: { state: true },
97
+ _pinned: { state: true },
98
+ _rating: { state: true },
99
+ _collectionIds: { state: true },
100
+ _mediaIds: { state: true },
101
+ _loading: { state: true },
102
+ };
103
+
104
+ declare labels: PostFormLabels;
105
+ declare initial: PostFormInitial;
106
+ declare collections: PostCollectionOption[];
107
+ declare media: PostMediaItem[];
108
+ declare action: string;
109
+ declare cancelHref: string;
110
+ declare mediaPickerUrl: string;
111
+ declare isEdit: boolean;
112
+ declare _format: PostFormat;
113
+ declare _title: string;
114
+ declare _body: string;
115
+ declare _url: string;
116
+ declare _quoteText: string;
117
+ declare _status: PostStatus;
118
+ declare _featured: boolean;
119
+ declare _pinned: boolean;
120
+ declare _rating: number;
121
+ declare _collectionIds: number[];
122
+ declare _mediaIds: string[];
123
+ declare _loading: boolean;
124
+
125
+ #initialized = false;
126
+
127
+ createRenderRoot() {
128
+ this.innerHTML = "";
129
+ return this;
130
+ }
131
+
132
+ constructor() {
133
+ super();
134
+ this.labels = { ...EMPTY_LABELS };
135
+ this.initial = { ...DEFAULT_INITIAL };
136
+ this.collections = [];
137
+ this.media = [];
138
+ this.action = "";
139
+ this.cancelHref = "/dash/posts";
140
+ this.mediaPickerUrl = "/dash/media/picker";
141
+ this.isEdit = false;
142
+ this._format = "note";
143
+ this._title = "";
144
+ this._body = "";
145
+ this._url = "";
146
+ this._quoteText = "";
147
+ this._status = "published";
148
+ this._featured = false;
149
+ this._pinned = false;
150
+ this._rating = 0;
151
+ this._collectionIds = [];
152
+ this._mediaIds = [];
153
+ this._loading = false;
154
+ }
155
+
156
+ protected willUpdate(changed: Map<string, unknown>): void {
157
+ if (typeof this.labels === "string") {
158
+ this.labels = parseJson<PostFormLabels>(this.labels, { ...EMPTY_LABELS });
159
+ }
160
+ if (typeof this.initial === "string") {
161
+ this.initial = parseJson<PostFormInitial>(this.initial, {
162
+ ...DEFAULT_INITIAL,
163
+ });
164
+ }
165
+ if (typeof this.collections === "string") {
166
+ this.collections = parseJson<PostCollectionOption[]>(
167
+ this.collections,
168
+ [],
169
+ );
170
+ }
171
+ if (typeof this.media === "string") {
172
+ this.media = parseJson<PostMediaItem[]>(this.media, []);
173
+ }
174
+
175
+ if (!this.#initialized || changed.has("initial")) this.#applyInitial();
176
+ }
177
+
178
+ set loading(value: boolean) {
179
+ this._loading = value;
180
+ }
181
+
182
+ get loading(): boolean {
183
+ return this._loading;
184
+ }
185
+
186
+ set mediaIds(ids: string[]) {
187
+ this._mediaIds = [...ids];
188
+ }
189
+
190
+ get mediaIds(): string[] {
191
+ return [...this._mediaIds];
192
+ }
193
+
194
+ get #mediaDialog(): HTMLDialogElement | null {
195
+ return this.querySelector("#post-media-picker");
196
+ }
197
+
198
+ disconnectedCallback() {
199
+ super.disconnectedCallback();
200
+ this.closeMediaPicker();
201
+ }
202
+
203
+ #applyInitial() {
204
+ const init = this.initial ?? DEFAULT_INITIAL;
205
+ this._format = init.format ?? "note";
206
+ this._title = init.title ?? "";
207
+ this._body = init.body ?? "";
208
+ this._url = init.url ?? "";
209
+ this._quoteText = init.quoteText ?? "";
210
+ this._status = init.status ?? "published";
211
+ this._featured = !!init.featured;
212
+ this._pinned = !!init.pinned;
213
+ this._rating = init.rating ?? 0;
214
+ this._collectionIds = [...(init.collectionIds ?? [])];
215
+ this._mediaIds = [...(init.mediaIds ?? [])];
216
+ this.#initialized = true;
217
+ }
218
+
219
+ handleInput(field: "_title" | "_body" | "_url" | "_quoteText", e: Event) {
220
+ const target = e.target as HTMLInputElement | HTMLTextAreaElement;
221
+ (this as unknown as Record<string, string>)[field] = target.value;
222
+ }
223
+
224
+ toggleCollection(id: number) {
225
+ this._collectionIds = this._collectionIds.includes(id)
226
+ ? this._collectionIds.filter((cid) => cid !== id)
227
+ : [...this._collectionIds, id];
228
+ }
229
+
230
+ removeMedia(id: string) {
231
+ this._mediaIds = this._mediaIds.filter((mid) => mid !== id);
232
+ }
233
+
234
+ openMediaPicker() {
235
+ const dialog = this.#mediaDialog;
236
+ if (!dialog) return;
237
+ dialog.showModal();
238
+ this.dispatchEvent(
239
+ new CustomEvent("jant:post-load-media", {
240
+ bubbles: true,
241
+ detail: {
242
+ endpoint: this.mediaPickerUrl,
243
+ selectedIds: [...this._mediaIds],
244
+ },
245
+ }),
246
+ );
247
+ }
248
+
249
+ closeMediaPicker() {
250
+ this.#mediaDialog?.close();
251
+ }
252
+
253
+ handleSubmit(e: Event) {
254
+ e.preventDefault();
255
+ if (this._loading || !this.action) return;
256
+ const detail: PostSubmitDetail = {
257
+ endpoint: this.action,
258
+ isEdit: this.isEdit,
259
+ data: {
260
+ format: this._format,
261
+ title: this._title.trim(),
262
+ body: this._body,
263
+ status: this._status,
264
+ featured: this._featured,
265
+ pinned: this._pinned,
266
+ url: this._url.trim(),
267
+ quoteText: this._quoteText.trim(),
268
+ rating: this._rating,
269
+ collectionIds: [...this._collectionIds],
270
+ mediaIds: [...this._mediaIds],
271
+ },
272
+ messages: {
273
+ success: this.labels.submitSuccessMessage,
274
+ error: this.labels.submitErrorMessage,
275
+ },
276
+ };
277
+ this.dispatchEvent(
278
+ new CustomEvent<PostSubmitDetail>("jant:post-submit", {
279
+ bubbles: true,
280
+ detail,
281
+ }),
282
+ );
283
+ }
284
+
285
+ render() {
286
+ return renderPostForm(this);
287
+ }
288
+ }
289
+
290
+ customElements.define("jant-post-form", JantPostForm);
@@ -0,0 +1,231 @@
1
+ /**
2
+ * Settings Avatar Section
3
+ *
4
+ * Handles avatar preview, upload button, remove button,
5
+ * and "display in header" toggle with dirty tracking.
6
+ *
7
+ * Upload is handled by the existing avatar-upload.ts script
8
+ * via `[data-avatar-upload]` event delegation (Light DOM).
9
+ * Remove dispatches `jant:avatar-remove` for the bridge.
10
+ *
11
+ * Light DOM only — BaseCoat and Tailwind classes apply directly.
12
+ */
13
+
14
+ import { LitElement, html, nothing } from "lit";
15
+ import type { SettingsLabels } from "./settings-types.js";
16
+
17
+ export class JantSettingsAvatar extends LitElement {
18
+ static properties = {
19
+ avatarUrl: { type: String, attribute: "avatar-url" },
20
+ showInHeader: { type: Boolean, attribute: "show-in-header" },
21
+ labels: { type: Object },
22
+ _showInHeader: { state: true },
23
+ _origShowInHeader: { state: true },
24
+ _dirty: { state: true },
25
+ _loading: { state: true },
26
+ _removeLoading: { state: true },
27
+ };
28
+
29
+ declare avatarUrl: string;
30
+ declare showInHeader: boolean;
31
+ declare labels: SettingsLabels;
32
+ declare _showInHeader: boolean;
33
+ declare _origShowInHeader: boolean;
34
+ declare _dirty: boolean;
35
+ declare _loading: boolean;
36
+ declare _removeLoading: boolean;
37
+
38
+ createRenderRoot() {
39
+ this.innerHTML = "";
40
+ return this;
41
+ }
42
+
43
+ constructor() {
44
+ super();
45
+ this.avatarUrl = "";
46
+ this.showInHeader = false;
47
+ this.labels = {} as SettingsLabels;
48
+ this._showInHeader = false;
49
+ this._origShowInHeader = false;
50
+ this._dirty = false;
51
+ this._loading = false;
52
+ this._removeLoading = false;
53
+ }
54
+
55
+ connectedCallback() {
56
+ super.connectedCallback();
57
+ this._showInHeader = this.showInHeader;
58
+ this._origShowInHeader = this.showInHeader;
59
+ }
60
+
61
+ /** Called by bridge after successful display save */
62
+ saved() {
63
+ this._origShowInHeader = this._showInHeader;
64
+ this._dirty = false;
65
+ this._loading = false;
66
+ }
67
+
68
+ /** Called by bridge on save error */
69
+ saveError() {
70
+ this._loading = false;
71
+ }
72
+
73
+ private _toggleDisplay() {
74
+ this._showInHeader = !this._showInHeader;
75
+ this._dirty = this._showInHeader !== this._origShowInHeader;
76
+ }
77
+
78
+ private _cancelDisplay() {
79
+ this._showInHeader = this._origShowInHeader;
80
+ this._dirty = false;
81
+ }
82
+
83
+ private _saveDisplay() {
84
+ if (this._loading || !this._dirty) return;
85
+ this._loading = true;
86
+ this.dispatchEvent(
87
+ new CustomEvent("jant:settings-save", {
88
+ bubbles: true,
89
+ detail: {
90
+ endpoint: "/dash/settings/avatar/display",
91
+ data: { showHeaderAvatar: this._showInHeader ? "true" : "" },
92
+ section: "avatar-display",
93
+ },
94
+ }),
95
+ );
96
+ }
97
+
98
+ private _removeAvatar() {
99
+ if (this._removeLoading) return;
100
+ this._removeLoading = true;
101
+ this.dispatchEvent(
102
+ new CustomEvent("jant:avatar-remove", {
103
+ bubbles: true,
104
+ detail: { endpoint: "/dash/settings/avatar/remove" },
105
+ }),
106
+ );
107
+ }
108
+
109
+ private _renderPreview() {
110
+ if (this.avatarUrl) {
111
+ return html`<img
112
+ src=${this.avatarUrl}
113
+ alt=""
114
+ class="rounded-full object-cover size-16"
115
+ />`;
116
+ }
117
+ return html`
118
+ <div
119
+ class="rounded-full bg-muted flex items-center justify-center text-muted-foreground size-16"
120
+ >
121
+ <svg
122
+ xmlns="http://www.w3.org/2000/svg"
123
+ width="24"
124
+ height="24"
125
+ viewBox="0 0 24 24"
126
+ fill="none"
127
+ stroke="currentColor"
128
+ stroke-width="2"
129
+ stroke-linecap="round"
130
+ stroke-linejoin="round"
131
+ >
132
+ <rect width="18" height="18" x="3" y="3" rx="2" ry="2" />
133
+ <circle cx="9" cy="9" r="2" />
134
+ <path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21" />
135
+ </svg>
136
+ </div>
137
+ `;
138
+ }
139
+
140
+ render() {
141
+ return html`
142
+ <div>
143
+ <h2 class="text-lg font-semibold mb-4">${this.labels.blogAvatar}</h2>
144
+ <div class="flex flex-col gap-4">
145
+ <div class="flex items-center gap-4">
146
+ ${this._renderPreview()}
147
+ <div class="flex flex-col gap-2">
148
+ <form
149
+ action="/dash/settings/avatar"
150
+ method="post"
151
+ enctype="multipart/form-data"
152
+ class="inline"
153
+ >
154
+ <label class="btn text-sm cursor-pointer">
155
+ ${this.labels.uploadAvatar}
156
+ <input
157
+ type="file"
158
+ name="file"
159
+ accept="image/jpeg,image/png,image/gif,image/webp,image/svg+xml"
160
+ class="hidden"
161
+ data-avatar-upload
162
+ data-text-processing=${this.labels.processing}
163
+ data-text-uploading=${this.labels.uploading}
164
+ data-text-error=${this.labels.uploadError}
165
+ />
166
+ </label>
167
+ </form>
168
+ ${this.avatarUrl
169
+ ? html`
170
+ <button
171
+ type="button"
172
+ class="btn-outline text-sm"
173
+ ?disabled=${this._removeLoading}
174
+ @click=${this._removeAvatar}
175
+ >
176
+ ${this.labels.remove}
177
+ </button>
178
+ `
179
+ : nothing}
180
+ </div>
181
+ </div>
182
+ <p class="text-sm text-muted-foreground">${this.labels.avatarHelp}</p>
183
+ <label class="flex items-center gap-2 cursor-pointer">
184
+ <input
185
+ type="checkbox"
186
+ class="checkbox"
187
+ .checked=${this._showInHeader}
188
+ @change=${this._toggleDisplay}
189
+ />
190
+ <span>${this.labels.displayInHeader}</span>
191
+ </label>
192
+ <div class="flex gap-2 mt-4">
193
+ <button
194
+ type="button"
195
+ class="btn"
196
+ ?disabled=${this._loading || !this._dirty}
197
+ @click=${this._saveDisplay}
198
+ >
199
+ ${this._loading
200
+ ? html`<svg
201
+ class="animate-spin size-4"
202
+ xmlns="http://www.w3.org/2000/svg"
203
+ viewBox="0 0 24 24"
204
+ fill="none"
205
+ stroke="currentColor"
206
+ stroke-width="2"
207
+ stroke-linecap="round"
208
+ stroke-linejoin="round"
209
+ role="status"
210
+ >
211
+ <path d="M21 12a9 9 0 1 1-6.219-8.56" />
212
+ </svg>`
213
+ : nothing}
214
+ ${this.labels.save}
215
+ </button>
216
+ <button
217
+ type="button"
218
+ class="btn-outline"
219
+ ?disabled=${this._loading || !this._dirty}
220
+ @click=${this._cancelDisplay}
221
+ >
222
+ ${this.labels.cancel}
223
+ </button>
224
+ </div>
225
+ </div>
226
+ </div>
227
+ `;
228
+ }
229
+ }
230
+
231
+ customElements.define("jant-settings-avatar", JantSettingsAvatar);