@jant/core 0.3.26 → 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 (314) 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 +112 -173
  9. package/src/auth.ts +4 -1
  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 -265
  172. package/dist/auth.js +0 -36
  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,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);