@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,512 +0,0 @@
1
- /**
2
- * Collection Form Component
3
- *
4
- * Handles create/edit collection form interactions:
5
- * - Maintains form state for title, slug, description, sort order, and icon
6
- * - Opens the icon picker dialog with search and color presets
7
- * - Dispatches `jant:collection-submit` for the bridge to POST to the server
8
- *
9
- * Light DOM only — BaseCoat and Tailwind classes apply directly.
10
- */
11
-
12
- import { LitElement, html, nothing } from "lit";
13
- import type { PropertyValueMap } from "lit";
14
- import { classMap } from "lit/directives/class-map.js";
15
- import { unsafeHTML } from "lit/directives/unsafe-html.js";
16
- import {
17
- DEFAULT_ICON_COLOR,
18
- ICON_COLOR_PRESETS,
19
- createIconValue,
20
- parseCollectionIcon,
21
- renderCollectionIcon,
22
- getIconSvg,
23
- } from "../../lib/icons.js";
24
- import { ICON_CATALOG } from "../../lib/icon-catalog.js";
25
- import type {
26
- CollectionFormInitial,
27
- CollectionFormLabels,
28
- CollectionSubmitDetail,
29
- } from "./collection-types.js";
30
-
31
- type CatalogCategory = {
32
- name: string;
33
- icons: Array<{ name: string; svg: string }>;
34
- };
35
-
36
- function slugifyTitle(title: string): string {
37
- return title
38
- .toLowerCase()
39
- .trim()
40
- .replace(/[^\w\s-]/g, "")
41
- .replace(/[\s_-]+/g, "-")
42
- .replace(/^-+|-+$/g, "");
43
- }
44
-
45
- export class JantCollectionForm extends LitElement {
46
- static properties = {
47
- labels: { type: Object },
48
- initial: { type: Object },
49
- action: { type: String },
50
- cancelHref: { type: String, attribute: "cancel-href" },
51
- isEdit: { type: Boolean, attribute: "is-edit" },
52
-
53
- _title: { state: true },
54
- _slug: { state: true },
55
- _description: { state: true },
56
- _sortOrder: { state: true },
57
- _iconName: { state: true },
58
- _iconSvg: { state: true },
59
- _iconColor: { state: true },
60
- _iconSearch: { state: true },
61
- _loading: { state: true },
62
- };
63
-
64
- declare labels: CollectionFormLabels;
65
- declare initial: CollectionFormInitial;
66
- declare action: string;
67
- declare cancelHref: string;
68
- declare isEdit: boolean;
69
-
70
- declare _title: string;
71
- declare _slug: string;
72
- declare _description: string;
73
- declare _sortOrder: string;
74
- declare _iconName: string;
75
- declare _iconSvg: string;
76
- declare _iconColor: string;
77
- declare _iconSearch: string;
78
- declare _loading: boolean;
79
-
80
- #initialized = false;
81
-
82
- createRenderRoot() {
83
- this.innerHTML = "";
84
- return this;
85
- }
86
-
87
- constructor() {
88
- super();
89
- this.labels = {} as CollectionFormLabels;
90
- this.initial = {
91
- title: "",
92
- slug: "",
93
- description: "",
94
- sortOrder: "newest",
95
- icon: "",
96
- };
97
- this.action = "";
98
- this.cancelHref = "/";
99
- this.isEdit = false;
100
-
101
- this._title = "";
102
- this._slug = "";
103
- this._description = "";
104
- this._sortOrder = "newest";
105
- this._iconName = "";
106
- this._iconSvg = "";
107
- this._iconColor = DEFAULT_ICON_COLOR;
108
- this._iconSearch = "";
109
- this._loading = false;
110
- }
111
-
112
- protected update(
113
- changedProperties: PropertyValueMap<JantCollectionForm>,
114
- ): void {
115
- if (!this.#initialized || changedProperties.has("initial")) {
116
- this.#applyInitialData();
117
- }
118
- super.update(changedProperties);
119
- }
120
-
121
- set loading(value: boolean) {
122
- this._loading = value;
123
- }
124
-
125
- get loading(): boolean {
126
- return this._loading;
127
- }
128
-
129
- #applyInitialData() {
130
- if (!this.initial) return;
131
- this.#initialized = true;
132
- this._title = this.initial.title ?? "";
133
- this._slug = this.initial.slug ?? "";
134
- this._description = this.initial.description ?? "";
135
- this._sortOrder = this.initial.sortOrder ?? "newest";
136
-
137
- const parsed = parseCollectionIcon(this.initial.icon ?? "");
138
- if (parsed) {
139
- this._iconName = parsed.name;
140
- this._iconSvg = parsed.svg;
141
- this._iconColor = parsed.color || DEFAULT_ICON_COLOR;
142
- } else {
143
- this._iconName = "";
144
- this._iconSvg = "";
145
- this._iconColor = DEFAULT_ICON_COLOR;
146
- }
147
- }
148
-
149
- disconnectedCallback() {
150
- super.disconnectedCallback();
151
- const dialog = this.#iconDialog;
152
- if (dialog?.open) dialog.close();
153
- }
154
-
155
- get #iconDialog(): HTMLDialogElement | null {
156
- return this.querySelector<HTMLDialogElement>("#collection-icon-dialog");
157
- }
158
-
159
- get #iconValue(): string {
160
- if (this._iconName && this._iconSvg) {
161
- return createIconValue(
162
- this._iconName,
163
- this._iconSvg,
164
- this._iconColor || DEFAULT_ICON_COLOR,
165
- );
166
- }
167
- return "";
168
- }
169
-
170
- #filteredCatalog(): CatalogCategory[] {
171
- const q = this._iconSearch.trim().toLowerCase();
172
- const result: CatalogCategory[] = [];
173
- for (const [category, names] of Object.entries(ICON_CATALOG)) {
174
- const icons = names
175
- .filter((name) => (q ? name.includes(q) : true))
176
- .map((name) => {
177
- const svg = getIconSvg(name);
178
- return svg ? { name, svg } : null;
179
- })
180
- .filter((icon): icon is { name: string; svg: string } => Boolean(icon));
181
- if (icons.length > 0) {
182
- result.push({ name: category, icons });
183
- }
184
- }
185
- return result;
186
- }
187
-
188
- #openDialog() {
189
- this.#iconDialog?.showModal();
190
- }
191
-
192
- #closeDialog() {
193
- this.#iconDialog?.close();
194
- }
195
-
196
- #handleSubmit(e: Event) {
197
- e.preventDefault();
198
- const title = this._title.trim();
199
- const slug = this._slug.trim();
200
- if (!title || !slug) {
201
- return;
202
- }
203
-
204
- const detail: CollectionSubmitDetail = {
205
- endpoint: this.action,
206
- isEdit: this.isEdit,
207
- data: {
208
- title,
209
- slug,
210
- description: this._description.trim() || undefined,
211
- icon: this.#iconValue || undefined,
212
- sortOrder: this._sortOrder || undefined,
213
- },
214
- };
215
-
216
- this.dispatchEvent(
217
- new CustomEvent<CollectionSubmitDetail>("jant:collection-submit", {
218
- bubbles: true,
219
- detail,
220
- }),
221
- );
222
- }
223
-
224
- #renderIconPreview() {
225
- if (this._iconSvg) {
226
- const htmlString = renderCollectionIcon(this.#iconValue, {
227
- size: 24,
228
- fallback: false,
229
- });
230
- return html`<span
231
- class="w-6 h-6 flex items-center justify-center"
232
- style=${`color:${this._iconColor}`}
233
- >
234
- ${unsafeHTML(htmlString)}
235
- </span>`;
236
- }
237
- if (this.initial.icon && !this.initial.icon.startsWith("{")) {
238
- const htmlString = renderCollectionIcon(this.initial.icon, {
239
- size: 24,
240
- fallback: false,
241
- });
242
- if (htmlString) {
243
- return html`<span class="w-6 h-6 flex items-center justify-center">
244
- ${unsafeHTML(htmlString)}
245
- </span>`;
246
- }
247
- }
248
- return html`<span class="text-muted-foreground text-lg">?</span>`;
249
- }
250
-
251
- #renderColorPresets() {
252
- if (!this._iconSvg) return nothing;
253
- return html`
254
- <div class="flex items-center gap-2 mt-2">
255
- ${ICON_COLOR_PRESETS.map((preset) => {
256
- const isActive = this._iconColor === preset.value;
257
- return html`
258
- <button
259
- type="button"
260
- class=${classMap({
261
- "w-6 h-6 rounded-full border-2 transition-transform hover:scale-110": true,
262
- "ring-2": isActive,
263
- "ring-offset-1": isActive,
264
- "ring-primary": isActive,
265
- })}
266
- style=${`background-color:${preset.value}; border-color: transparent`}
267
- title=${preset.name}
268
- @click=${() => {
269
- this._iconColor = preset.value;
270
- }}
271
- ></button>
272
- `;
273
- })}
274
- </div>
275
- `;
276
- }
277
-
278
- #renderIconDialog() {
279
- const categories = this.#filteredCatalog();
280
- return html`
281
- <dialog
282
- id="collection-icon-dialog"
283
- class="m-auto rounded-lg border border-border bg-background text-foreground p-0 w-full max-w-md max-h-[80vh] shadow-lg backdrop:bg-black/50"
284
- @cancel=${() => this.#closeDialog()}
285
- >
286
- <div class="flex flex-col max-h-[80vh]">
287
- <div class="flex flex-col gap-3 p-4 border-b border-border">
288
- <div class="flex items-center justify-between">
289
- <h2 class="font-semibold">${this.labels.dialogTitle}</h2>
290
- <button
291
- type="button"
292
- class="btn-ghost text-sm"
293
- @click=${() => this.#closeDialog()}
294
- >
295
- ${this.labels.dialogClose}
296
- </button>
297
- </div>
298
- <input
299
- type="search"
300
- class="input text-sm"
301
- placeholder=${this.labels.searchIconsPlaceholder}
302
- .value=${this._iconSearch}
303
- @input=${(event: Event) => {
304
- const target = event.target as HTMLInputElement;
305
- this._iconSearch = target.value;
306
- }}
307
- />
308
- </div>
309
- <div class="overflow-y-auto p-4 flex-1">
310
- ${categories.length === 0
311
- ? html`<p class="text-sm text-muted-foreground">
312
- ${this.labels.searchIconsPlaceholder}
313
- </p>`
314
- : categories.map(
315
- (category) => html`
316
- <div
317
- class="flex flex-col gap-2 mb-4"
318
- data-category=${category.name}
319
- >
320
- <h3
321
- class="text-xs font-medium text-muted-foreground uppercase tracking-wider"
322
- >
323
- ${category.name}
324
- </h3>
325
- <div class="grid grid-cols-8 gap-1">
326
- ${category.icons.map(
327
- (icon) => html`
328
- <button
329
- type="button"
330
- class=${classMap({
331
- "flex items-center justify-center w-9 h-9 rounded-md hover:bg-accent transition-colors": true,
332
- "ring-2":
333
- this._iconName === icon.name &&
334
- this._iconSvg === icon.svg,
335
- "ring-primary":
336
- this._iconName === icon.name &&
337
- this._iconSvg === icon.svg,
338
- })}
339
- data-icon-name=${icon.name}
340
- @click=${() => {
341
- this._iconName = icon.name;
342
- this._iconSvg = icon.svg;
343
- if (!this._iconColor) {
344
- this._iconColor = DEFAULT_ICON_COLOR;
345
- }
346
- this._iconSearch = "";
347
- this.#closeDialog();
348
- }}
349
- >
350
- <span
351
- class="w-5 h-5 flex items-center justify-center"
352
- >
353
- ${unsafeHTML(
354
- icon.svg
355
- .replace(/width="24"/, 'width="20"')
356
- .replace(/height="24"/, 'height="20"'),
357
- )}
358
- </span>
359
- </button>
360
- `,
361
- )}
362
- </div>
363
- </div>
364
- `,
365
- )}
366
- </div>
367
- </div>
368
- </dialog>
369
- `;
370
- }
371
-
372
- render() {
373
- return html`
374
- <form
375
- class="flex flex-col gap-4 max-w-lg"
376
- @submit=${(event: Event) => this.#handleSubmit(event)}
377
- >
378
- <div class="field">
379
- <label class="label">${this.labels.titleLabel}</label>
380
- <input
381
- type="text"
382
- class="input"
383
- required
384
- .value=${this._title}
385
- placeholder=${this.isEdit ? nothing : this.labels.titlePlaceholder}
386
- @input=${(event: Event) => {
387
- const target = event.target as HTMLInputElement;
388
- this._title = target.value;
389
- if (!this.isEdit) {
390
- this._slug = slugifyTitle(target.value);
391
- }
392
- }}
393
- />
394
- </div>
395
-
396
- <div class="field">
397
- <label class="label">${this.labels.slugLabel}</label>
398
- <input
399
- type="text"
400
- class="input"
401
- required
402
- pattern="[a-z0-9\\-]+"
403
- .value=${this._slug}
404
- placeholder=${this.isEdit ? nothing : "my-collection"}
405
- @input=${(event: Event) => {
406
- const target = event.target as HTMLInputElement;
407
- this._slug = target.value.toLowerCase();
408
- }}
409
- />
410
- ${this.isEdit
411
- ? nothing
412
- : html`<p class="text-xs text-muted-foreground mt-1">
413
- ${this.labels.slugHelp}
414
- </p>`}
415
- </div>
416
-
417
- <div class="field">
418
- <label class="label">${this.labels.descriptionLabel}</label>
419
- <textarea
420
- class="textarea"
421
- rows="3"
422
- .value=${this._description}
423
- placeholder=${this.isEdit
424
- ? nothing
425
- : this.labels.descriptionPlaceholder}
426
- @input=${(event: Event) => {
427
- const target = event.target as HTMLTextAreaElement;
428
- this._description = target.value;
429
- }}
430
- ></textarea>
431
- </div>
432
-
433
- <div class="field">
434
- <label class="label">${this.labels.iconLabel}</label>
435
- <div class="flex items-center gap-3">
436
- <div
437
- class="flex items-center justify-center w-10 h-10 rounded-md border border-border"
438
- >
439
- ${this.#renderIconPreview()}
440
- </div>
441
- <button
442
- type="button"
443
- class="btn-outline text-sm"
444
- @click=${() => this.#openDialog()}
445
- >
446
- ${this.labels.chooseIcon}
447
- </button>
448
- ${this._iconSvg
449
- ? html`<button
450
- type="button"
451
- class="btn-ghost text-sm"
452
- @click=${() => {
453
- this._iconName = "";
454
- this._iconSvg = "";
455
- this._iconColor = DEFAULT_ICON_COLOR;
456
- }}
457
- >
458
- ${this.labels.removeIcon}
459
- </button>`
460
- : nothing}
461
- </div>
462
- ${this.#renderColorPresets()}
463
- </div>
464
-
465
- <div class="field">
466
- <label class="label">${this.labels.sortOrderLabel}</label>
467
- <select
468
- class="select"
469
- .value=${this._sortOrder}
470
- @change=${(event: Event) => {
471
- const target = event.target as HTMLSelectElement;
472
- this._sortOrder = target.value;
473
- }}
474
- >
475
- <option value="newest">${this.labels.sortNewest}</option>
476
- <option value="oldest">${this.labels.sortOldest}</option>
477
- <option value="rating_desc">${this.labels.sortRatingDesc}</option>
478
- <option value="rating_asc">${this.labels.sortRatingAsc}</option>
479
- </select>
480
- </div>
481
-
482
- <div class="flex gap-2">
483
- <button type="submit" class="btn" ?disabled=${this._loading}>
484
- ${this._loading
485
- ? html`<svg
486
- class="animate-spin size-4"
487
- xmlns="http://www.w3.org/2000/svg"
488
- viewBox="0 0 24 24"
489
- fill="none"
490
- stroke="currentColor"
491
- stroke-width="2"
492
- stroke-linecap="round"
493
- stroke-linejoin="round"
494
- role="status"
495
- >
496
- <path d="M21 12a9 9 0 1 1-6.219-8.56" />
497
- </svg>`
498
- : nothing}
499
- ${this.labels.submitLabel}
500
- </button>
501
- <a href=${this.cancelHref} class="btn-outline">
502
- ${this.labels.cancelLabel}
503
- </a>
504
- </div>
505
- </form>
506
-
507
- ${this.#renderIconDialog()}
508
- `;
509
- }
510
- }
511
-
512
- customElements.define("jant-collection-form", JantCollectionForm);