@commonpub/layer 0.1.0

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 (436) hide show
  1. package/LICENSE +661 -0
  2. package/app.vue +7 -0
  3. package/components/AnnouncementBand.vue +117 -0
  4. package/components/AppToast.vue +108 -0
  5. package/components/AuthorCard.vue +119 -0
  6. package/components/AuthorRow.vue +81 -0
  7. package/components/CommentSection.vue +330 -0
  8. package/components/ContentCard.vue +340 -0
  9. package/components/ContentPicker.vue +240 -0
  10. package/components/ContentStarterForm.vue +214 -0
  11. package/components/ContentTypeBadge.vue +46 -0
  12. package/components/CountdownTimer.vue +68 -0
  13. package/components/CpubEditor.vue +87 -0
  14. package/components/DiscussionItem.vue +191 -0
  15. package/components/EditorPropertiesPanel.vue +393 -0
  16. package/components/EngagementBar.vue +131 -0
  17. package/components/FederatedContentCard.vue +291 -0
  18. package/components/FeedItem.vue +283 -0
  19. package/components/FilterChip.vue +21 -0
  20. package/components/HeatmapGrid.vue +92 -0
  21. package/components/ImageUpload.vue +219 -0
  22. package/components/MemberCard.vue +163 -0
  23. package/components/MessageThread.vue +120 -0
  24. package/components/NotificationItem.vue +103 -0
  25. package/components/ProgressTracker.vue +41 -0
  26. package/components/PublishErrorsModal.vue +116 -0
  27. package/components/RemoteActorCard.vue +206 -0
  28. package/components/RemoteUserSearch.vue +117 -0
  29. package/components/SearchFilters.vue +188 -0
  30. package/components/SearchSidebar.vue +181 -0
  31. package/components/SectionHeader.vue +17 -0
  32. package/components/ShareToHubModal.vue +189 -0
  33. package/components/SiteLogo.vue +21 -0
  34. package/components/SkillBar.vue +57 -0
  35. package/components/SortSelect.vue +30 -0
  36. package/components/StatBar.vue +14 -0
  37. package/components/TOCNav.vue +69 -0
  38. package/components/TimelineItem.vue +82 -0
  39. package/components/VideoCard.vue +106 -0
  40. package/components/blocks/BlockBuildStepView.vue +92 -0
  41. package/components/blocks/BlockCalloutView.vue +82 -0
  42. package/components/blocks/BlockCheckpointView.vue +50 -0
  43. package/components/blocks/BlockCodeView.vue +212 -0
  44. package/components/blocks/BlockContentRenderer.vue +143 -0
  45. package/components/blocks/BlockDividerView.vue +11 -0
  46. package/components/blocks/BlockDownloadsView.vue +126 -0
  47. package/components/blocks/BlockEmbedView.vue +61 -0
  48. package/components/blocks/BlockGalleryView.vue +57 -0
  49. package/components/blocks/BlockHeadingView.vue +29 -0
  50. package/components/blocks/BlockImageView.vue +34 -0
  51. package/components/blocks/BlockMarkdownView.vue +118 -0
  52. package/components/blocks/BlockMathView.vue +45 -0
  53. package/components/blocks/BlockPartsListView.vue +104 -0
  54. package/components/blocks/BlockQuizView.vue +239 -0
  55. package/components/blocks/BlockQuoteView.vue +41 -0
  56. package/components/blocks/BlockSectionHeaderView.vue +58 -0
  57. package/components/blocks/BlockSliderView.vue +236 -0
  58. package/components/blocks/BlockTextView.vue +41 -0
  59. package/components/blocks/BlockToolListView.vue +87 -0
  60. package/components/blocks/BlockVideoView.vue +89 -0
  61. package/components/editors/ArticleEditor.vue +545 -0
  62. package/components/editors/BlockCanvas.vue +487 -0
  63. package/components/editors/BlockInsertZone.vue +84 -0
  64. package/components/editors/BlockPicker.vue +285 -0
  65. package/components/editors/BlockWrapper.vue +192 -0
  66. package/components/editors/BlogEditor.vue +567 -0
  67. package/components/editors/EditorBlocks.vue +248 -0
  68. package/components/editors/EditorSection.vue +81 -0
  69. package/components/editors/EditorShell.vue +168 -0
  70. package/components/editors/EditorTagInput.vue +114 -0
  71. package/components/editors/EditorVisibility.vue +110 -0
  72. package/components/editors/ExplainerEditor.vue +503 -0
  73. package/components/editors/MarkdownImportDialog.vue +249 -0
  74. package/components/editors/ProjectEditor.vue +446 -0
  75. package/components/editors/blocks/BuildStepBlock.vue +102 -0
  76. package/components/editors/blocks/CalloutBlock.vue +122 -0
  77. package/components/editors/blocks/CheckpointBlock.vue +27 -0
  78. package/components/editors/blocks/CodeBlock.vue +177 -0
  79. package/components/editors/blocks/DividerBlock.vue +22 -0
  80. package/components/editors/blocks/DownloadsBlock.vue +41 -0
  81. package/components/editors/blocks/EmbedBlock.vue +20 -0
  82. package/components/editors/blocks/GalleryBlock.vue +236 -0
  83. package/components/editors/blocks/HeadingBlock.vue +96 -0
  84. package/components/editors/blocks/ImageBlock.vue +271 -0
  85. package/components/editors/blocks/MarkdownBlock.vue +258 -0
  86. package/components/editors/blocks/MathBlock.vue +37 -0
  87. package/components/editors/blocks/PartsListBlock.vue +358 -0
  88. package/components/editors/blocks/QuizBlock.vue +47 -0
  89. package/components/editors/blocks/QuoteBlock.vue +101 -0
  90. package/components/editors/blocks/SectionHeaderBlock.vue +130 -0
  91. package/components/editors/blocks/SliderBlock.vue +318 -0
  92. package/components/editors/blocks/TextBlock.vue +201 -0
  93. package/components/editors/blocks/ToolListBlock.vue +70 -0
  94. package/components/editors/blocks/VideoBlock.vue +22 -0
  95. package/components/hub/HubDiscussions.vue +47 -0
  96. package/components/hub/HubFeed.vue +199 -0
  97. package/components/hub/HubHero.vue +185 -0
  98. package/components/hub/HubLayout.vue +103 -0
  99. package/components/hub/HubMembers.vue +40 -0
  100. package/components/hub/HubProducts.vue +93 -0
  101. package/components/hub/HubProjects.vue +207 -0
  102. package/components/hub/HubSidebar.vue +12 -0
  103. package/components/hub/HubSidebarCard.vue +35 -0
  104. package/components/views/ArticleView.vue +771 -0
  105. package/components/views/BlogView.vue +667 -0
  106. package/components/views/ExplainerView.vue +688 -0
  107. package/components/views/ProjectView.vue +1500 -0
  108. package/composables/useApiError.ts +39 -0
  109. package/composables/useAuth.ts +87 -0
  110. package/composables/useBlockEditor.ts +187 -0
  111. package/composables/useContentSave.ts +253 -0
  112. package/composables/useContentTypes.ts +37 -0
  113. package/composables/useEngagement.ts +196 -0
  114. package/composables/useFeatures.ts +33 -0
  115. package/composables/useFederation.ts +72 -0
  116. package/composables/useJsonLd.ts +183 -0
  117. package/composables/useMarkdownImport.ts +77 -0
  118. package/composables/useMirrorContent.ts +105 -0
  119. package/composables/useNotifications.ts +73 -0
  120. package/composables/usePublishValidation.ts +65 -0
  121. package/composables/useSanitize.ts +34 -0
  122. package/composables/useSiteName.ts +4 -0
  123. package/composables/useTheme.ts +34 -0
  124. package/composables/useToast.ts +35 -0
  125. package/error.vue +129 -0
  126. package/layouts/admin.vue +213 -0
  127. package/layouts/auth.vue +63 -0
  128. package/layouts/default.vue +269 -0
  129. package/layouts/editor.vue +129 -0
  130. package/middleware/auth.ts +6 -0
  131. package/nuxt.config.ts +83 -0
  132. package/package.json +59 -0
  133. package/pages/[type]/[slug]/edit.vue +676 -0
  134. package/pages/[type]/[slug]/index.vue +313 -0
  135. package/pages/[type]/index.vue +118 -0
  136. package/pages/about.vue +100 -0
  137. package/pages/admin/audit.vue +66 -0
  138. package/pages/admin/content.vue +116 -0
  139. package/pages/admin/federation.vue +446 -0
  140. package/pages/admin/index.vue +62 -0
  141. package/pages/admin/reports.vue +88 -0
  142. package/pages/admin/settings.vue +167 -0
  143. package/pages/admin/users.vue +145 -0
  144. package/pages/auth/forgot-password.vue +103 -0
  145. package/pages/auth/login.vue +216 -0
  146. package/pages/auth/oauth/authorize.vue +178 -0
  147. package/pages/auth/register.vue +246 -0
  148. package/pages/auth/reset-password.vue +124 -0
  149. package/pages/auth/verify-email.vue +80 -0
  150. package/pages/cert/[code].vue +154 -0
  151. package/pages/contests/[slug]/edit.vue +153 -0
  152. package/pages/contests/[slug]/index.vue +556 -0
  153. package/pages/contests/[slug]/judge.vue +160 -0
  154. package/pages/contests/create.vue +192 -0
  155. package/pages/contests/index.vue +69 -0
  156. package/pages/create.vue +219 -0
  157. package/pages/dashboard.vue +521 -0
  158. package/pages/docs/[siteSlug]/[...pagePath].vue +704 -0
  159. package/pages/docs/[siteSlug]/edit.vue +480 -0
  160. package/pages/docs/[siteSlug]/index.vue +380 -0
  161. package/pages/docs/create.vue +60 -0
  162. package/pages/docs/index.vue +181 -0
  163. package/pages/explore.vue +422 -0
  164. package/pages/federated-hubs/[id]/index.vue +385 -0
  165. package/pages/federated-hubs/[id]/posts/[postId].vue +309 -0
  166. package/pages/federation/index.vue +157 -0
  167. package/pages/federation/search.vue +43 -0
  168. package/pages/federation/users/[handle].vue +221 -0
  169. package/pages/feed.vue +135 -0
  170. package/pages/hubs/[slug]/index.vue +513 -0
  171. package/pages/hubs/[slug]/members.vue +134 -0
  172. package/pages/hubs/[slug]/posts/[postId].vue +352 -0
  173. package/pages/hubs/[slug]/settings.vue +254 -0
  174. package/pages/hubs/create.vue +201 -0
  175. package/pages/hubs/index.vue +207 -0
  176. package/pages/index.vue +1005 -0
  177. package/pages/learn/[slug]/[lessonSlug]/edit.vue +413 -0
  178. package/pages/learn/[slug]/[lessonSlug]/index.vue +438 -0
  179. package/pages/learn/[slug]/edit.vue +414 -0
  180. package/pages/learn/[slug]/index.vue +341 -0
  181. package/pages/learn/create.vue +71 -0
  182. package/pages/learn/index.vue +360 -0
  183. package/pages/messages/[conversationId].vue +113 -0
  184. package/pages/messages/index.vue +303 -0
  185. package/pages/mirror/[id].vue +115 -0
  186. package/pages/notifications.vue +91 -0
  187. package/pages/products/[slug].vue +128 -0
  188. package/pages/products/index.vue +122 -0
  189. package/pages/search.vue +692 -0
  190. package/pages/settings/account.vue +170 -0
  191. package/pages/settings/appearance.vue +80 -0
  192. package/pages/settings/index.vue +81 -0
  193. package/pages/settings/notifications.vue +68 -0
  194. package/pages/settings/profile.vue +838 -0
  195. package/pages/tags/[slug].vue +111 -0
  196. package/pages/tags/index.vue +73 -0
  197. package/pages/u/[username]/followers.vue +86 -0
  198. package/pages/u/[username]/following.vue +94 -0
  199. package/pages/u/[username]/index.vue +837 -0
  200. package/pages/videos/[id].vue +212 -0
  201. package/pages/videos/index.vue +327 -0
  202. package/pages/videos/submit.vue +112 -0
  203. package/plugins/auth.ts +23 -0
  204. package/server/api/admin/audit.get.ts +17 -0
  205. package/server/api/admin/content/[id].delete.ts +15 -0
  206. package/server/api/admin/content/[id].patch.ts +37 -0
  207. package/server/api/admin/federation/activity.get.ts +31 -0
  208. package/server/api/admin/federation/clients.get.ts +9 -0
  209. package/server/api/admin/federation/clients.post.ts +16 -0
  210. package/server/api/admin/federation/hub-mirrors/index.get.ts +10 -0
  211. package/server/api/admin/federation/hub-mirrors/index.post.ts +42 -0
  212. package/server/api/admin/federation/mirrors/[id]/backfill.post.ts +39 -0
  213. package/server/api/admin/federation/mirrors/[id].delete.ts +11 -0
  214. package/server/api/admin/federation/mirrors/[id].get.ts +15 -0
  215. package/server/api/admin/federation/mirrors/[id].put.ts +22 -0
  216. package/server/api/admin/federation/mirrors/index.get.ts +9 -0
  217. package/server/api/admin/federation/mirrors/index.post.ts +30 -0
  218. package/server/api/admin/federation/pending.get.ts +22 -0
  219. package/server/api/admin/federation/refederate.post.ts +92 -0
  220. package/server/api/admin/federation/repair-types.post.ts +57 -0
  221. package/server/api/admin/federation/retry.post.ts +41 -0
  222. package/server/api/admin/federation/stats.get.ts +26 -0
  223. package/server/api/admin/reports/[id]/resolve.post.ts +12 -0
  224. package/server/api/admin/reports.get.ts +17 -0
  225. package/server/api/admin/settings.get.ts +22 -0
  226. package/server/api/admin/settings.put.ts +11 -0
  227. package/server/api/admin/stats.get.ts +9 -0
  228. package/server/api/admin/users/[id]/role.put.ts +12 -0
  229. package/server/api/admin/users/[id]/status.put.ts +12 -0
  230. package/server/api/admin/users/[id].delete.ts +10 -0
  231. package/server/api/admin/users.get.ts +18 -0
  232. package/server/api/auth/federated/callback.get.ts +67 -0
  233. package/server/api/auth/federated/login.post.ts +60 -0
  234. package/server/api/auth/oauth2/authorize.get.ts +30 -0
  235. package/server/api/auth/oauth2/authorize.post.ts +51 -0
  236. package/server/api/auth/oauth2/register.post.ts +41 -0
  237. package/server/api/auth/oauth2/token.post.ts +48 -0
  238. package/server/api/cert/[code].get.ts +13 -0
  239. package/server/api/content/[id]/build.post.ts +9 -0
  240. package/server/api/content/[id]/fork.post.ts +10 -0
  241. package/server/api/content/[id]/index.delete.ts +18 -0
  242. package/server/api/content/[id]/index.get.ts +15 -0
  243. package/server/api/content/[id]/index.put.ts +23 -0
  244. package/server/api/content/[id]/products/[productId].delete.ts +28 -0
  245. package/server/api/content/[id]/products-sync.post.ts +29 -0
  246. package/server/api/content/[id]/products.get.ts +9 -0
  247. package/server/api/content/[id]/products.post.ts +31 -0
  248. package/server/api/content/[id]/publish.post.ts +17 -0
  249. package/server/api/content/[id]/report.post.ts +17 -0
  250. package/server/api/content/[id]/versions.get.ts +9 -0
  251. package/server/api/content/[id]/view.post.ts +34 -0
  252. package/server/api/content/index.get.ts +23 -0
  253. package/server/api/content/index.post.ts +11 -0
  254. package/server/api/contests/[slug]/entries.get.ts +18 -0
  255. package/server/api/contests/[slug]/entries.post.ts +23 -0
  256. package/server/api/contests/[slug]/index.delete.ts +21 -0
  257. package/server/api/contests/[slug]/index.get.ts +11 -0
  258. package/server/api/contests/[slug]/index.put.ts +15 -0
  259. package/server/api/contests/[slug]/judge.post.ts +12 -0
  260. package/server/api/contests/[slug]/transition.post.ts +24 -0
  261. package/server/api/contests/index.get.ts +10 -0
  262. package/server/api/contests/index.post.ts +28 -0
  263. package/server/api/docs/[siteSlug]/index.delete.ts +14 -0
  264. package/server/api/docs/[siteSlug]/index.get.ts +17 -0
  265. package/server/api/docs/[siteSlug]/index.put.ts +20 -0
  266. package/server/api/docs/[siteSlug]/nav.get.ts +26 -0
  267. package/server/api/docs/[siteSlug]/pages/[pageId].delete.ts +14 -0
  268. package/server/api/docs/[siteSlug]/pages/[pageId].get.ts +31 -0
  269. package/server/api/docs/[siteSlug]/pages/[pageId].put.ts +15 -0
  270. package/server/api/docs/[siteSlug]/pages/index.get.ts +34 -0
  271. package/server/api/docs/[siteSlug]/pages/index.post.ts +28 -0
  272. package/server/api/docs/[siteSlug]/pages/reorder.post.ts +26 -0
  273. package/server/api/docs/[siteSlug]/search.get.ts +20 -0
  274. package/server/api/docs/[siteSlug]/versions.post.ts +11 -0
  275. package/server/api/docs/index.get.ts +6 -0
  276. package/server/api/docs/index.post.ts +10 -0
  277. package/server/api/federated-hubs/[id]/posts/[postId].get.ts +16 -0
  278. package/server/api/federated-hubs/[id]/posts.get.ts +15 -0
  279. package/server/api/federated-hubs/[id].get.ts +16 -0
  280. package/server/api/federation/boost.post.ts +21 -0
  281. package/server/api/federation/content/[id].get.ts +15 -0
  282. package/server/api/federation/follow.post.ts +16 -0
  283. package/server/api/federation/health.get.ts +56 -0
  284. package/server/api/federation/hub-follow.post.ts +27 -0
  285. package/server/api/federation/hub-post-like.post.ts +115 -0
  286. package/server/api/federation/hub-post-likes.get.ts +24 -0
  287. package/server/api/federation/hub-post-reply.post.ts +42 -0
  288. package/server/api/federation/hub-post.post.ts +33 -0
  289. package/server/api/federation/like.post.ts +21 -0
  290. package/server/api/federation/remote-actor.get.ts +22 -0
  291. package/server/api/federation/reply.post.ts +22 -0
  292. package/server/api/federation/search.post.ts +17 -0
  293. package/server/api/federation/timeline.get.ts +22 -0
  294. package/server/api/federation/unfollow.post.ts +17 -0
  295. package/server/api/files/[id].delete.ts +35 -0
  296. package/server/api/files/mine.get.ts +31 -0
  297. package/server/api/files/upload-from-url.post.ts +68 -0
  298. package/server/api/files/upload.post.ts +105 -0
  299. package/server/api/health.get.ts +4 -0
  300. package/server/api/hubs/[slug]/bans/[userId].delete.ts +13 -0
  301. package/server/api/hubs/[slug]/bans.get.ts +20 -0
  302. package/server/api/hubs/[slug]/bans.post.ts +23 -0
  303. package/server/api/hubs/[slug]/feed.xml.get.ts +60 -0
  304. package/server/api/hubs/[slug]/gallery.get.ts +21 -0
  305. package/server/api/hubs/[slug]/index.delete.ts +18 -0
  306. package/server/api/hubs/[slug]/index.get.ts +14 -0
  307. package/server/api/hubs/[slug]/index.put.ts +22 -0
  308. package/server/api/hubs/[slug]/invites.get.ts +20 -0
  309. package/server/api/hubs/[slug]/invites.post.ts +27 -0
  310. package/server/api/hubs/[slug]/join.post.ts +17 -0
  311. package/server/api/hubs/[slug]/leave.post.ts +13 -0
  312. package/server/api/hubs/[slug]/members/[userId].delete.ts +13 -0
  313. package/server/api/hubs/[slug]/members/[userId].put.ts +16 -0
  314. package/server/api/hubs/[slug]/members.get.ts +20 -0
  315. package/server/api/hubs/[slug]/posts/[postId]/like.post.ts +21 -0
  316. package/server/api/hubs/[slug]/posts/[postId]/lock.post.ts +17 -0
  317. package/server/api/hubs/[slug]/posts/[postId]/pin.post.ts +17 -0
  318. package/server/api/hubs/[slug]/posts/[postId]/replies.get.ts +16 -0
  319. package/server/api/hubs/[slug]/posts/[postId]/replies.post.ts +21 -0
  320. package/server/api/hubs/[slug]/posts/[postId].delete.ts +23 -0
  321. package/server/api/hubs/[slug]/posts/[postId].get.ts +16 -0
  322. package/server/api/hubs/[slug]/posts/index.get.ts +16 -0
  323. package/server/api/hubs/[slug]/posts/index.post.ts +36 -0
  324. package/server/api/hubs/[slug]/products.get.ts +26 -0
  325. package/server/api/hubs/[slug]/products.post.ts +18 -0
  326. package/server/api/hubs/[slug]/share.post.ts +39 -0
  327. package/server/api/hubs/index.get.ts +15 -0
  328. package/server/api/hubs/index.post.ts +11 -0
  329. package/server/api/image-proxy.get.ts +91 -0
  330. package/server/api/learn/[slug]/[lessonSlug]/complete.post.ts +13 -0
  331. package/server/api/learn/[slug]/[lessonSlug]/index.get.ts +68 -0
  332. package/server/api/learn/[slug]/enroll.post.ts +12 -0
  333. package/server/api/learn/[slug]/index.delete.ts +12 -0
  334. package/server/api/learn/[slug]/index.get.ts +14 -0
  335. package/server/api/learn/[slug]/index.put.ts +19 -0
  336. package/server/api/learn/[slug]/lessons/[lessonId].delete.ts +14 -0
  337. package/server/api/learn/[slug]/lessons/[lessonId].put.ts +24 -0
  338. package/server/api/learn/[slug]/lessons.post.ts +10 -0
  339. package/server/api/learn/[slug]/modules/[moduleId].delete.ts +14 -0
  340. package/server/api/learn/[slug]/modules/[moduleId].put.ts +15 -0
  341. package/server/api/learn/[slug]/modules.post.ts +14 -0
  342. package/server/api/learn/[slug]/publish.post.ts +13 -0
  343. package/server/api/learn/[slug]/unenroll.post.ts +12 -0
  344. package/server/api/learn/certificates.get.ts +9 -0
  345. package/server/api/learn/enrollments.get.ts +9 -0
  346. package/server/api/learn/index.get.ts +17 -0
  347. package/server/api/learn/index.post.ts +11 -0
  348. package/server/api/me.get.ts +13 -0
  349. package/server/api/messages/[conversationId]/info.get.ts +43 -0
  350. package/server/api/messages/[conversationId]/stream.get.ts +73 -0
  351. package/server/api/messages/[conversationId].get.ts +13 -0
  352. package/server/api/messages/[conversationId].post.ts +12 -0
  353. package/server/api/messages/index.get.ts +39 -0
  354. package/server/api/messages/index.post.ts +58 -0
  355. package/server/api/notifications/[id].delete.ts +11 -0
  356. package/server/api/notifications/count.get.ts +10 -0
  357. package/server/api/notifications/index.get.ts +24 -0
  358. package/server/api/notifications/read.post.ts +15 -0
  359. package/server/api/notifications/stream.get.ts +59 -0
  360. package/server/api/openapi.get.ts +5 -0
  361. package/server/api/products/[id].delete.ts +15 -0
  362. package/server/api/products/[id].put.ts +18 -0
  363. package/server/api/products/[slug]/content.get.ts +21 -0
  364. package/server/api/products/[slug].get.ts +16 -0
  365. package/server/api/products/index.get.ts +28 -0
  366. package/server/api/profile.get.ts +15 -0
  367. package/server/api/profile.put.ts +24 -0
  368. package/server/api/resolve-identity.post.ts +34 -0
  369. package/server/api/search/index.get.ts +24 -0
  370. package/server/api/search/trending.get.ts +22 -0
  371. package/server/api/social/bookmark.get.ts +16 -0
  372. package/server/api/social/bookmark.post.ts +15 -0
  373. package/server/api/social/bookmarks.get.ts +16 -0
  374. package/server/api/social/comments/[id].delete.ts +13 -0
  375. package/server/api/social/comments.get.ts +18 -0
  376. package/server/api/social/comments.post.ts +11 -0
  377. package/server/api/social/like.get.ts +17 -0
  378. package/server/api/social/like.post.ts +35 -0
  379. package/server/api/stats.get.ts +8 -0
  380. package/server/api/user/hubs.get.ts +22 -0
  381. package/server/api/users/[username]/content.get.ts +21 -0
  382. package/server/api/users/[username]/feed.xml.get.ts +65 -0
  383. package/server/api/users/[username]/follow.delete.ts +15 -0
  384. package/server/api/users/[username]/follow.post.ts +15 -0
  385. package/server/api/users/[username]/followers.get.ts +22 -0
  386. package/server/api/users/[username]/following.get.ts +22 -0
  387. package/server/api/users/[username]/learning.get.ts +18 -0
  388. package/server/api/users/[username].get.ts +25 -0
  389. package/server/api/users/index.get.ts +78 -0
  390. package/server/api/videos/[id].get.ts +18 -0
  391. package/server/api/videos/categories/[id].delete.ts +15 -0
  392. package/server/api/videos/categories/[id].put.ts +17 -0
  393. package/server/api/videos/categories.get.ts +7 -0
  394. package/server/api/videos/categories.post.ts +11 -0
  395. package/server/api/videos/index.get.ts +20 -0
  396. package/server/api/videos/index.post.ts +11 -0
  397. package/server/middleware/auth.ts +180 -0
  398. package/server/middleware/features.ts +31 -0
  399. package/server/middleware/security.ts +54 -0
  400. package/server/plugins/auto-admin.ts +69 -0
  401. package/server/plugins/federation-delivery.ts +74 -0
  402. package/server/routes/.well-known/nodeinfo.ts +15 -0
  403. package/server/routes/.well-known/webfinger.ts +86 -0
  404. package/server/routes/actor/followers.ts +34 -0
  405. package/server/routes/actor/following.ts +34 -0
  406. package/server/routes/actor/outbox.ts +31 -0
  407. package/server/routes/actor.ts +30 -0
  408. package/server/routes/feed.xml.ts +59 -0
  409. package/server/routes/hubs/[slug]/followers.ts +39 -0
  410. package/server/routes/hubs/[slug]/inbox.ts +35 -0
  411. package/server/routes/hubs/[slug]/outbox.ts +43 -0
  412. package/server/routes/hubs/[slug]/posts/[postId].ts +63 -0
  413. package/server/routes/hubs/[slug].ts +29 -0
  414. package/server/routes/inbox.ts +34 -0
  415. package/server/routes/nodeinfo/2.1.ts +27 -0
  416. package/server/routes/robots.txt.ts +19 -0
  417. package/server/routes/sitemap.xml.ts +105 -0
  418. package/server/routes/users/[username]/followers.ts +33 -0
  419. package/server/routes/users/[username]/following.ts +33 -0
  420. package/server/routes/users/[username]/inbox.ts +34 -0
  421. package/server/routes/users/[username]/outbox.ts +35 -0
  422. package/server/routes/users/[username].ts +62 -0
  423. package/server/utils/auth.ts +36 -0
  424. package/server/utils/db.ts +34 -0
  425. package/server/utils/errors.ts +24 -0
  426. package/server/utils/inbox.ts +122 -0
  427. package/server/utils/validate.ts +82 -0
  428. package/theme/base.css +283 -0
  429. package/theme/components.css +322 -0
  430. package/theme/dark.css +87 -0
  431. package/theme/editor-panels.css +63 -0
  432. package/theme/forms.css +216 -0
  433. package/theme/generics.css +68 -0
  434. package/theme/layouts.css +415 -0
  435. package/theme/prose.css +342 -0
  436. package/types/hub.ts +61 -0
@@ -0,0 +1,446 @@
1
+ <script setup lang="ts">
2
+ import type { BlockEditor } from '../../composables/useBlockEditor';
3
+ import type { BlockTypeGroup } from './BlockPicker.vue';
4
+
5
+ const props = defineProps<{
6
+ blockEditor: BlockEditor;
7
+ metadata: Record<string, unknown>;
8
+ }>();
9
+
10
+ const emit = defineEmits<{
11
+ 'update:metadata': [metadata: Record<string, unknown>];
12
+ }>();
13
+
14
+ function updateMeta(key: string, value: unknown): void {
15
+ emit('update:metadata', { ...props.metadata, [key]: value });
16
+ }
17
+
18
+ const blockTypes: BlockTypeGroup[] = [
19
+ {
20
+ name: 'Basic',
21
+ blocks: [
22
+ { type: 'paragraph', label: 'Text', icon: 'fa-align-left', description: 'Body text' },
23
+ { type: 'heading', label: 'Heading', icon: 'fa-heading', description: 'Section header' },
24
+ { type: 'image', label: 'Image', icon: 'fa-image', description: 'Upload or embed' },
25
+ { type: 'code_block', label: 'Code Block', icon: 'fa-code', description: 'Syntax highlighted code' },
26
+ ],
27
+ },
28
+ {
29
+ name: 'Project',
30
+ blocks: [
31
+ { type: 'partsList', label: 'Parts List', icon: 'fa-list-check', description: 'BOM table' },
32
+ { type: 'buildStep', label: 'Build Step', icon: 'fa-hammer', description: 'Numbered step' },
33
+ { type: 'toolList', label: 'Tool List', icon: 'fa-wrench', description: 'Required tools' },
34
+ { type: 'downloads', label: 'Downloads', icon: 'fa-download', description: 'File attachments' },
35
+ ],
36
+ },
37
+ {
38
+ name: 'Rich',
39
+ blocks: [
40
+ { type: 'callout', label: 'Tip', icon: 'fa-lightbulb', description: 'Tip callout', attrs: { variant: 'tip' } },
41
+ { type: 'callout', label: 'Warning', icon: 'fa-triangle-exclamation', description: 'Warning callout', attrs: { variant: 'warning' } },
42
+ { type: 'blockquote', label: 'Quote', icon: 'fa-quote-left', description: 'Blockquote' },
43
+ { type: 'horizontal_rule', label: 'Divider', icon: 'fa-minus', description: 'Visual separator' },
44
+ { type: 'markdown', label: 'Markdown', icon: 'fa-brands fa-markdown', description: 'Raw markdown block' },
45
+ ],
46
+ },
47
+ ];
48
+
49
+ const openSections = ref<Record<string, boolean>>({
50
+ meta: true, tags: true, visibility: true, cover: false, checklist: true,
51
+ });
52
+ function toggleSection(key: string): void {
53
+ openSections.value[key] = !openSections.value[key];
54
+ }
55
+
56
+ const difficulties = ['beginner', 'intermediate', 'advanced'] as const;
57
+
58
+ // --- Cover image ---
59
+ const coverImageUrl = computed(() => (props.metadata.coverImageUrl as string) || '');
60
+
61
+ function onCoverUpload(event: Event): void {
62
+ const input = event.target as HTMLInputElement;
63
+ if (!input.files?.length) return;
64
+ const file = input.files[0];
65
+ if (!file) return;
66
+ const formData = new FormData();
67
+ formData.append('file', file);
68
+ formData.append('purpose', 'cover');
69
+ $fetch<{ url: string }>('/api/files/upload', { method: 'POST', body: formData })
70
+ .then((res) => { updateMeta('coverImageUrl', res.url); })
71
+ .catch(() => { /* silent fallback */ });
72
+ }
73
+
74
+ function onCoverUrl(): void {
75
+ const url = window.prompt('Enter image URL:');
76
+ if (url) updateMeta('coverImageUrl', url);
77
+ }
78
+
79
+ function removeCover(): void {
80
+ updateMeta('coverImageUrl', '');
81
+ }
82
+
83
+ const tags = computed(() => (props.metadata.tags as string[]) || []);
84
+ function onTagsUpdate(newTags: string[]): void { updateMeta('tags', newTags); }
85
+ const visibility = computed(() => (props.metadata.visibility as string) || 'public');
86
+ function onVisibilityUpdate(val: string): void { updateMeta('visibility', val); }
87
+
88
+ const checklist = computed(() => [
89
+ { label: 'Has cover image', pass: !!(props.metadata.coverImageUrl) },
90
+ { label: 'Has description', pass: !!((props.metadata.description as string)?.length) },
91
+ { label: 'Has tags', pass: !!(tags.value.length) },
92
+ { label: 'Has difficulty set', pass: !!(props.metadata.difficulty) },
93
+ { label: 'Has build time', pass: !!(props.metadata.buildTime) },
94
+ { label: 'Has cost estimate', pass: !!(props.metadata.estimatedCost) },
95
+ ]);
96
+ const checklistDone = computed(() => checklist.value.filter((c) => c.pass).length);
97
+
98
+ // --- Mobile sidebar toggles ---
99
+ const mobileLeftOpen = ref(false);
100
+ const mobileRightOpen = ref(false);
101
+ function toggleMobileLeft(): void {
102
+ mobileLeftOpen.value = !mobileLeftOpen.value;
103
+ if (mobileLeftOpen.value) mobileRightOpen.value = false;
104
+ }
105
+ function toggleMobileRight(): void {
106
+ mobileRightOpen.value = !mobileRightOpen.value;
107
+ if (mobileRightOpen.value) mobileLeftOpen.value = false;
108
+ }
109
+ function closeMobileSidebars(): void {
110
+ mobileLeftOpen.value = false;
111
+ mobileRightOpen.value = false;
112
+ }
113
+
114
+ // --- Inline title (local ref avoids textarea re-render clobbering) ---
115
+ const titleRef = ref((props.metadata.title as string) || '');
116
+ watch(() => props.metadata.title, (v) => { if (v !== titleRef.value) titleRef.value = (v as string) || ''; });
117
+ watch(titleRef, (v) => updateMeta('title', v));
118
+
119
+ // --- Canvas toolbar ---
120
+ const viewportMode = ref<'desktop' | 'tablet' | 'mobile'>('desktop');
121
+ const canvasMaxWidth = computed(() => {
122
+ if (viewportMode.value === 'mobile') return '375px';
123
+ if (viewportMode.value === 'tablet') return '768px';
124
+ return '820px';
125
+ });
126
+
127
+ // --- Status bar ---
128
+ const wordCount = computed(() => {
129
+ let count = 0;
130
+ for (const block of props.blockEditor.blocks.value) {
131
+ const html = (block.content.html as string) || '';
132
+ const text = (block.content.text as string) || '';
133
+ const code = (block.content.code as string) || '';
134
+ const instructions = (block.content.instructions as string) || '';
135
+ const combined = html.replace(/<[^>]*>/g, ' ') + ' ' + text + ' ' + code + ' ' + instructions;
136
+ count += combined.split(/\s+/).filter((w) => w.length > 0).length;
137
+ }
138
+ return count;
139
+ });
140
+ const blockCount = computed(() => props.blockEditor.blocks.value.length);
141
+ </script>
142
+
143
+ <template>
144
+ <div class="cpub-pe-shell">
145
+ <!-- Mobile sidebar toggles -->
146
+ <div class="cpub-pe-mobile-toggles">
147
+ <button class="cpub-pe-mobile-btn" aria-label="Toggle blocks panel" @click="toggleMobileLeft"><i class="fa-solid fa-layer-group"></i></button>
148
+ <button class="cpub-pe-mobile-btn" aria-label="Toggle settings panel" @click="toggleMobileRight"><i class="fa-solid fa-sliders"></i></button>
149
+ </div>
150
+ <div v-if="mobileLeftOpen || mobileRightOpen" class="cpub-pe-mobile-overlay" @click="closeMobileSidebars" />
151
+
152
+ <!-- LEFT: Block Library -->
153
+ <aside class="cpub-pe-library" :class="{ 'cpub-pe-sidebar-open': mobileLeftOpen }" aria-label="Block library">
154
+ <EditorsEditorBlocks :groups="blockTypes" :block-editor="blockEditor" />
155
+ </aside>
156
+
157
+ <!-- CENTER: Canvas with toolbar -->
158
+ <div class="cpub-pe-center">
159
+ <!-- Canvas toolbar -->
160
+ <div class="cpub-pe-canvas-toolbar">
161
+ <div class="cpub-pe-viewport-tabs">
162
+ <button class="cpub-pe-viewport-tab" :class="{ active: viewportMode === 'desktop' }" title="Desktop" @click="viewportMode = 'desktop'"><i class="fa-solid fa-desktop"></i></button>
163
+ <button class="cpub-pe-viewport-tab" :class="{ active: viewportMode === 'tablet' }" title="Tablet" @click="viewportMode = 'tablet'"><i class="fa-solid fa-tablet-screen-button"></i></button>
164
+ <button class="cpub-pe-viewport-tab" :class="{ active: viewportMode === 'mobile' }" title="Mobile" @click="viewportMode = 'mobile'"><i class="fa-solid fa-mobile-screen"></i></button>
165
+ </div>
166
+ </div>
167
+
168
+ <div class="cpub-pe-canvas">
169
+ <div class="cpub-pe-canvas-inner" :style="{ maxWidth: canvasMaxWidth }">
170
+ <!-- Cover image area (inline, like blog editor) -->
171
+ <div class="cpub-pe-cover-inline" :class="{ 'has-image': !!coverImageUrl }">
172
+ <template v-if="coverImageUrl">
173
+ <img :src="coverImageUrl" alt="Cover image" class="cpub-pe-cover-inline-img" />
174
+ <div class="cpub-pe-cover-inline-actions">
175
+ <button class="cpub-pe-cover-btn" @click="removeCover"><i class="fa-solid fa-trash"></i> Remove</button>
176
+ <label class="cpub-pe-cover-btn">
177
+ <i class="fa-solid fa-arrow-up-from-bracket"></i> Replace
178
+ <input type="file" accept="image/*" class="cpub-sr-only" @change="onCoverUpload">
179
+ </label>
180
+ </div>
181
+ </template>
182
+ <template v-else>
183
+ <div class="cpub-pe-cover-inline-placeholder">
184
+ <div class="cpub-pe-cover-inline-icon"><i class="fa-regular fa-image"></i></div>
185
+ <span class="cpub-pe-cover-inline-text">Cover image</span>
186
+ </div>
187
+ <div class="cpub-pe-cover-inline-overlay">
188
+ <label class="cpub-pe-cover-btn primary">
189
+ <i class="fa-solid fa-arrow-up-from-bracket"></i> Upload
190
+ <input type="file" accept="image/*" class="cpub-sr-only" @change="onCoverUpload">
191
+ </label>
192
+ <button class="cpub-pe-cover-btn" @click="onCoverUrl"><i class="fa-solid fa-link"></i> From URL</button>
193
+ </div>
194
+ </template>
195
+ </div>
196
+
197
+ <!-- Title -->
198
+ <textarea
199
+ v-model="titleRef"
200
+ class="cpub-pe-title-inline"
201
+ rows="2"
202
+ placeholder="Project title..."
203
+ />
204
+
205
+ <EditorsBlockCanvas :block-editor="blockEditor" :block-types="blockTypes" />
206
+ </div>
207
+ </div>
208
+
209
+ <!-- Status bar -->
210
+ <div class="cpub-pe-statusbar">
211
+ <div class="cpub-pe-status-item"><i class="fa-solid fa-layer-group"></i> <span>{{ blockCount }} blocks</span></div>
212
+ <div class="cpub-pe-status-sep" />
213
+ <div class="cpub-pe-status-item"><i class="fa-solid fa-align-justify"></i> <span>{{ wordCount.toLocaleString() }} words</span></div>
214
+ </div>
215
+ </div>
216
+
217
+ <!-- RIGHT: Settings Panel -->
218
+ <aside class="cpub-pe-settings" :class="{ 'cpub-pe-sidebar-open': mobileRightOpen }" aria-label="Project settings">
219
+ <div class="cpub-pe-settings-body">
220
+ <EditorsEditorSection title="Project Meta" icon="fa-sliders" :open="openSections.meta" @toggle="toggleSection('meta')">
221
+ <div class="cpub-ep-field">
222
+ <label class="cpub-ep-flabel">Slug</label>
223
+ <input class="cpub-ep-input" type="text" :value="metadata.slug" placeholder="project-url-slug" @input="updateMeta('slug', ($event.target as HTMLInputElement).value)">
224
+ </div>
225
+ <div class="cpub-ep-field">
226
+ <label class="cpub-ep-flabel">Difficulty</label>
227
+ <div class="cpub-pe-toggle-group">
228
+ <button
229
+ v-for="d in difficulties"
230
+ :key="d"
231
+ class="cpub-pe-toggle-opt"
232
+ :class="{ active: (metadata.difficulty || 'intermediate') === d }"
233
+ @click="updateMeta('difficulty', d)"
234
+ >{{ d }}</button>
235
+ </div>
236
+ </div>
237
+ <div class="cpub-ep-field">
238
+ <label class="cpub-ep-flabel">Build Time</label>
239
+ <input class="cpub-ep-input" type="text" :value="metadata.buildTime" placeholder="e.g. 2–4 hours" @input="updateMeta('buildTime', ($event.target as HTMLInputElement).value)">
240
+ </div>
241
+ <div class="cpub-ep-field">
242
+ <label class="cpub-ep-flabel">Estimated Cost</label>
243
+ <input class="cpub-ep-input" type="text" :value="metadata.estimatedCost" placeholder="e.g. $45-60" @input="updateMeta('estimatedCost', ($event.target as HTMLInputElement).value)">
244
+ </div>
245
+ <div class="cpub-ep-field">
246
+ <label class="cpub-ep-flabel">Description</label>
247
+ <textarea class="cpub-ep-textarea" rows="3" :value="metadata.description as string" placeholder="Brief project description..." @input="updateMeta('description', ($event.target as HTMLTextAreaElement).value)" />
248
+ </div>
249
+ </EditorsEditorSection>
250
+
251
+ <EditorsEditorSection title="Tags" icon="fa-tag" :open="openSections.tags" @toggle="toggleSection('tags')">
252
+ <EditorsEditorTagInput :tags="tags" @update:tags="onTagsUpdate" />
253
+ </EditorsEditorSection>
254
+
255
+ <EditorsEditorSection title="Visibility" icon="fa-eye" :open="openSections.visibility" @toggle="toggleSection('visibility')">
256
+ <EditorsEditorVisibility :model-value="visibility" @update:model-value="onVisibilityUpdate" />
257
+ </EditorsEditorSection>
258
+
259
+ <EditorsEditorSection title="Cover Image" icon="fa-image" :open="openSections.cover" @toggle="toggleSection('cover')">
260
+ <div class="cpub-pe-cover" :class="{ 'has-image': !!coverImageUrl }">
261
+ <template v-if="coverImageUrl">
262
+ <img :src="coverImageUrl" alt="Cover image" class="cpub-pe-cover-img" />
263
+ <div class="cpub-pe-cover-actions">
264
+ <button class="cpub-pe-cover-btn" @click="removeCover"><i class="fa-solid fa-trash"></i> Remove</button>
265
+ <label class="cpub-pe-cover-btn">
266
+ <i class="fa-solid fa-arrow-up-from-bracket"></i> Replace
267
+ <input type="file" accept="image/*" class="cpub-sr-only" @change="onCoverUpload">
268
+ </label>
269
+ </div>
270
+ </template>
271
+ <template v-else>
272
+ <div class="cpub-pe-cover-placeholder">
273
+ <div class="cpub-pe-cover-icon"><i class="fa-regular fa-image"></i></div>
274
+ <span class="cpub-pe-cover-text">Cover image</span>
275
+ </div>
276
+ <div class="cpub-pe-cover-overlay">
277
+ <label class="cpub-pe-cover-btn primary">
278
+ <i class="fa-solid fa-arrow-up-from-bracket"></i> Upload
279
+ <input type="file" accept="image/*" class="cpub-sr-only" @change="onCoverUpload">
280
+ </label>
281
+ <button class="cpub-pe-cover-btn" @click="onCoverUrl"><i class="fa-solid fa-link"></i> From URL</button>
282
+ </div>
283
+ </template>
284
+ </div>
285
+ </EditorsEditorSection>
286
+
287
+ <EditorsEditorSection title="Checklist" icon="fa-circle-check" :open="openSections.checklist" @toggle="toggleSection('checklist')">
288
+ <div class="cpub-pe-checklist">
289
+ <div v-for="item in checklist" :key="item.label" class="cpub-pe-check-item" :class="{ pass: item.pass }">
290
+ <i :class="item.pass ? 'fa-regular fa-square-check' : 'fa-regular fa-square'" :style="{ color: item.pass ? 'var(--green)' : 'var(--text-faint)' }"></i>
291
+ <span>{{ item.label }}</span>
292
+ </div>
293
+ </div>
294
+ <div class="cpub-pe-checklist-summary">
295
+ {{ checklistDone }}/{{ checklist.length }} complete
296
+ </div>
297
+ </EditorsEditorSection>
298
+ </div>
299
+ </aside>
300
+ </div>
301
+ </template>
302
+
303
+ <style scoped>
304
+ .cpub-pe-shell { display: flex; flex: 1; overflow: hidden; }
305
+ .cpub-pe-library { width: 220px; flex-shrink: 0; background: var(--surface); border-right: var(--border-width-default) solid var(--border); display: flex; flex-direction: column; overflow: hidden; }
306
+ .cpub-pe-center { flex: 1; display: flex; flex-direction: column; overflow: hidden; }
307
+ .cpub-pe-canvas { flex: 1; overflow-y: auto; background: var(--bg); }
308
+ .cpub-pe-canvas-inner { margin: 0 auto; transition: max-width 0.2s; }
309
+
310
+ /* Inline cover image (in canvas body) */
311
+ .cpub-pe-cover-inline {
312
+ position: relative; width: 100%; aspect-ratio: 16/7; background: var(--surface2);
313
+ border-bottom: var(--border-width-default) solid var(--border); display: flex; align-items: center; justify-content: center; overflow: hidden;
314
+ }
315
+ .cpub-pe-cover-inline-img { width: 100%; height: 100%; object-fit: cover; }
316
+ .cpub-pe-cover-inline-placeholder { display: flex; flex-direction: column; align-items: center; gap: 6px; }
317
+ .cpub-pe-cover-inline-icon { font-size: 28px; color: var(--text-faint); }
318
+ .cpub-pe-cover-inline-text { font-size: 11px; color: var(--text-faint); font-family: var(--font-mono); }
319
+ .cpub-pe-cover-inline-overlay, .cpub-pe-cover-inline-actions {
320
+ position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; gap: 8px;
321
+ background: var(--color-surface-scrim); opacity: 0; transition: opacity 0.15s;
322
+ }
323
+ .cpub-pe-cover-inline:hover .cpub-pe-cover-inline-overlay,
324
+ .cpub-pe-cover-inline:hover .cpub-pe-cover-inline-actions,
325
+ .cpub-pe-cover-inline:focus-within .cpub-pe-cover-inline-overlay,
326
+ .cpub-pe-cover-inline:focus-within .cpub-pe-cover-inline-actions { opacity: 1; }
327
+ @media (hover: none) { .cpub-pe-cover-inline-overlay, .cpub-pe-cover-inline-actions { opacity: 1; } }
328
+
329
+ /* Inline title (in canvas body) */
330
+ .cpub-pe-title-inline {
331
+ width: 100%; border: none; outline: none; resize: none; background: transparent;
332
+ font-size: 28px; font-weight: 700; line-height: 1.25; color: var(--text);
333
+ padding: 24px 48px 8px; font-family: var(--font-sans, system-ui);
334
+ }
335
+ .cpub-pe-title-inline::placeholder { color: var(--text-faint); }
336
+
337
+ /* Canvas toolbar */
338
+ .cpub-pe-canvas-toolbar {
339
+ display: flex; align-items: center; gap: 2px; padding: 4px 12px;
340
+ background: var(--surface); border-bottom: var(--border-width-default) solid var(--border); flex-shrink: 0; min-height: 32px;
341
+ justify-content: flex-end;
342
+ }
343
+ .cpub-pe-viewport-tabs { display: flex; gap: 0; }
344
+ .cpub-pe-viewport-tab {
345
+ width: 28px; height: 24px; display: flex; align-items: center; justify-content: center;
346
+ background: none; border: var(--border-width-default) solid var(--border); border-left-width: 0; color: var(--text-faint);
347
+ font-size: 10px; cursor: pointer;
348
+ }
349
+ .cpub-pe-viewport-tab:first-child { border-left-width: 2px; }
350
+ .cpub-pe-viewport-tab.active { background: var(--border); color: var(--color-text-inverse); }
351
+ .cpub-pe-viewport-tab:hover:not(.active) { background: var(--surface2); color: var(--text-dim); }
352
+
353
+ /* Status bar */
354
+ .cpub-pe-statusbar {
355
+ height: 26px; background: var(--surface); border-top: var(--border-width-default) solid var(--border);
356
+ display: flex; align-items: center; padding: 0 14px; gap: 18px; flex-shrink: 0;
357
+ }
358
+ .cpub-pe-status-item {
359
+ display: flex; align-items: center; gap: 5px; font-family: var(--font-mono);
360
+ font-size: 9px; color: var(--text-faint); white-space: nowrap;
361
+ }
362
+ .cpub-pe-status-item i { font-size: 8px; }
363
+ .cpub-pe-status-sep { width: 2px; height: 12px; background: var(--border); }
364
+ .cpub-pe-settings { width: 280px; flex-shrink: 0; background: var(--surface); border-left: var(--border-width-default) solid var(--border); display: flex; flex-direction: column; overflow: hidden; }
365
+ .cpub-pe-settings-body { flex: 1; overflow-y: auto; }
366
+
367
+ .cpub-pe-toggle-group { display: flex; border: var(--border-width-default) solid var(--border); overflow: hidden; }
368
+ .cpub-pe-toggle-opt { flex: 1; padding: 5px 4px; text-align: center; font-size: 10px; font-family: var(--font-mono); cursor: pointer; color: var(--text-faint); background: transparent; border: none; border-right: var(--border-width-default) solid var(--border); text-transform: capitalize; }
369
+ .cpub-pe-toggle-opt:last-child { border-right: none; }
370
+ .cpub-pe-toggle-opt:hover { background: var(--surface2); color: var(--text-dim); }
371
+ .cpub-pe-toggle-opt.active { background: var(--accent-bg); color: var(--accent); }
372
+
373
+ .cpub-pe-checklist { display: flex; flex-direction: column; gap: 5px; }
374
+ .cpub-pe-check-item { display: flex; align-items: center; gap: 8px; font-size: 11px; color: var(--text-faint); }
375
+ .cpub-pe-check-item.pass { color: var(--text); }
376
+ .cpub-pe-checklist-summary { margin-top: 8px; font-family: var(--font-mono); font-size: 10px; color: var(--green); }
377
+
378
+ /* Cover image */
379
+ .cpub-pe-cover {
380
+ position: relative; width: 100%; aspect-ratio: 16/9; background: var(--surface2);
381
+ border: var(--border-width-default) solid var(--border); display: flex; align-items: center; justify-content: center; overflow: hidden;
382
+ }
383
+ .cpub-pe-cover-img { width: 100%; height: 100%; object-fit: cover; }
384
+ .cpub-pe-cover-placeholder { display: flex; flex-direction: column; align-items: center; gap: 6px; }
385
+ .cpub-pe-cover-icon { font-size: 24px; color: var(--text-faint); }
386
+ .cpub-pe-cover-text { font-size: 10px; color: var(--text-faint); font-family: var(--font-mono); }
387
+ .cpub-pe-cover-overlay, .cpub-pe-cover-actions {
388
+ position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; gap: 6px;
389
+ background: var(--color-surface-scrim); opacity: 0; transition: opacity 0.15s;
390
+ }
391
+ .cpub-pe-cover:hover .cpub-pe-cover-overlay,
392
+ .cpub-pe-cover:hover .cpub-pe-cover-actions,
393
+ .cpub-pe-cover:focus-within .cpub-pe-cover-overlay,
394
+ .cpub-pe-cover:focus-within .cpub-pe-cover-actions { opacity: 1; }
395
+ .cpub-pe-cover-btn {
396
+ font-size: 10px; padding: 5px 10px; background: var(--surface); border: var(--border-width-default) solid var(--border);
397
+ color: var(--text-dim); cursor: pointer; display: inline-flex; align-items: center; gap: 4px;
398
+ font-family: var(--font-mono); box-shadow: var(--shadow-sm);
399
+ }
400
+ .cpub-pe-cover-btn.primary { background: var(--accent); color: var(--color-text-inverse); border-color: var(--accent); }
401
+ .cpub-pe-cover-btn:hover { background: var(--surface2); }
402
+ .cpub-pe-cover-btn.primary:hover { opacity: 0.9; background: var(--accent); }
403
+ .cpub-sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0,0,0,0); border: 0; }
404
+
405
+ /* Mobile sidebar toggles */
406
+ .cpub-pe-mobile-toggles { display: none; }
407
+ .cpub-pe-mobile-overlay { display: none; }
408
+
409
+ @media (max-width: 1200px) {
410
+ .cpub-pe-library {
411
+ position: fixed; top: 0; bottom: 0; left: 0; z-index: 200;
412
+ transform: translateX(-100%); transition: transform 0.2s ease;
413
+ }
414
+ .cpub-pe-library.cpub-pe-sidebar-open { transform: translateX(0); }
415
+ }
416
+
417
+ @media (max-width: 1024px) {
418
+ .cpub-pe-settings {
419
+ position: fixed; top: 0; bottom: 0; right: 0; z-index: 200;
420
+ transform: translateX(100%); transition: transform 0.2s ease;
421
+ }
422
+ .cpub-pe-settings.cpub-pe-sidebar-open { transform: translateX(0); }
423
+
424
+ .cpub-pe-mobile-toggles {
425
+ display: flex; position: fixed; bottom: 16px; right: 16px;
426
+ gap: 8px; z-index: 100;
427
+ }
428
+ .cpub-pe-mobile-btn {
429
+ width: 44px; height: 44px; border: var(--border-width-default) solid var(--border); background: var(--surface);
430
+ color: var(--text-dim); font-size: 16px; cursor: pointer;
431
+ display: flex; align-items: center; justify-content: center;
432
+ box-shadow: var(--shadow-md);
433
+ }
434
+ .cpub-pe-mobile-btn:hover { background: var(--surface2); color: var(--text); }
435
+ .cpub-pe-mobile-overlay {
436
+ display: block; position: fixed; inset: 0;
437
+ background: var(--color-surface-overlay-light); z-index: 199;
438
+ }
439
+ }
440
+
441
+ /* Touch devices: always show cover overlays */
442
+ @media (hover: none) {
443
+ .cpub-pe-cover-overlay,
444
+ .cpub-pe-cover-actions { opacity: 1; }
445
+ }
446
+ </style>
@@ -0,0 +1,102 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * Build step block — numbered step with title and instruction text.
4
+ */
5
+ const props = defineProps<{
6
+ content: Record<string, unknown>;
7
+ }>();
8
+
9
+ const emit = defineEmits<{
10
+ update: [content: Record<string, unknown>];
11
+ }>();
12
+
13
+ const stepNumber = computed(() => (props.content.stepNumber as number) ?? 1);
14
+ const title = computed(() => (props.content.title as string) ?? '');
15
+ const instructions = computed(() => (props.content.instructions as string) ?? '');
16
+ const time = computed(() => (props.content.time as string) ?? '');
17
+
18
+ function updateField(field: string, value: unknown): void {
19
+ emit('update', { ...props.content, [field]: value });
20
+ }
21
+ </script>
22
+
23
+ <template>
24
+ <div class="cpub-step-block">
25
+ <div class="cpub-step-header">
26
+ <div class="cpub-step-num">{{ stepNumber }}</div>
27
+ <input
28
+ class="cpub-step-title"
29
+ type="text"
30
+ :value="title"
31
+ placeholder="Step title..."
32
+ @input="updateField('title', ($event.target as HTMLInputElement).value)"
33
+ />
34
+ <input
35
+ class="cpub-step-time"
36
+ type="text"
37
+ :value="time"
38
+ placeholder="Time"
39
+ @input="updateField('time', ($event.target as HTMLInputElement).value)"
40
+ />
41
+ </div>
42
+ <div class="cpub-step-body">
43
+ <textarea
44
+ class="cpub-step-instructions"
45
+ :value="instructions"
46
+ placeholder="Describe this step..."
47
+ rows="4"
48
+ @input="updateField('instructions', ($event.target as HTMLTextAreaElement).value)"
49
+ />
50
+ </div>
51
+ </div>
52
+ </template>
53
+
54
+ <style scoped>
55
+ .cpub-step-block {
56
+ border: var(--border-width-default) solid var(--accent-border);
57
+ border-left: 4px solid var(--accent);
58
+ background: var(--surface);
59
+ }
60
+
61
+ .cpub-step-header {
62
+ display: flex; align-items: center; gap: 12px;
63
+ padding: 12px 16px;
64
+ border-bottom: var(--border-width-default) solid var(--border2);
65
+ background: var(--accent-bg);
66
+ }
67
+
68
+ .cpub-step-num {
69
+ width: 32px; height: 32px;
70
+ background: var(--accent); color: var(--color-text-inverse);
71
+ font-family: var(--font-mono); font-size: 14px; font-weight: 700;
72
+ border-radius: 50%;
73
+ display: flex; align-items: center; justify-content: center;
74
+ flex-shrink: 0;
75
+ }
76
+
77
+ .cpub-step-title {
78
+ flex: 1; font-size: 14px; font-weight: 600;
79
+ background: transparent; border: none; outline: none;
80
+ color: var(--text);
81
+ }
82
+ .cpub-step-title::placeholder { color: var(--text-faint); }
83
+
84
+ .cpub-step-time {
85
+ width: 80px; font-family: var(--font-mono); font-size: 10px;
86
+ background: transparent; border: var(--border-width-default) solid var(--border2);
87
+ padding: 3px 6px; color: var(--text-dim); outline: none;
88
+ text-align: center;
89
+ }
90
+ .cpub-step-time:focus { border-color: var(--accent); }
91
+ .cpub-step-time::placeholder { color: var(--text-faint); }
92
+
93
+ .cpub-step-body { padding: 14px 16px; }
94
+
95
+ .cpub-step-instructions {
96
+ width: 100%; font-size: 13px; line-height: 1.65;
97
+ background: transparent; border: none; outline: none;
98
+ color: var(--text); resize: vertical;
99
+ font-family: var(--font-sans, system-ui);
100
+ }
101
+ .cpub-step-instructions::placeholder { color: var(--text-faint); }
102
+ </style>
@@ -0,0 +1,122 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * Callout block — variant picker (info/tip/warning/danger) + editable body.
4
+ */
5
+ import { sanitizeBlockHtml } from '../../../composables/useSanitize';
6
+
7
+ const props = defineProps<{
8
+ content: Record<string, unknown>;
9
+ }>();
10
+
11
+ const emit = defineEmits<{
12
+ update: [content: Record<string, unknown>];
13
+ }>();
14
+
15
+ const html = computed(() => (props.content.html as string) ?? '');
16
+ const variant = computed(() => (props.content.variant as string) ?? 'info');
17
+
18
+ const variants = [
19
+ { value: 'info', icon: 'fa-circle-info', label: 'Info', color: 'var(--accent)' },
20
+ { value: 'tip', icon: 'fa-lightbulb', label: 'Tip', color: 'var(--green)' },
21
+ { value: 'warning', icon: 'fa-triangle-exclamation', label: 'Warning', color: 'var(--yellow)' },
22
+ { value: 'danger', icon: 'fa-circle-exclamation', label: 'Danger', color: 'var(--red)' },
23
+ ] as const;
24
+
25
+ const currentVariant = computed(() => variants.find((v) => v.value === variant.value) ?? variants[0]);
26
+
27
+ function cycleVariant(): void {
28
+ const idx = variants.findIndex((v) => v.value === variant.value);
29
+ const next = variants[(idx + 1) % variants.length]!;
30
+ emit('update', { html: html.value, variant: next.value });
31
+ }
32
+
33
+ function onBodyInput(event: Event): void {
34
+ const el = event.target as HTMLElement;
35
+ emit('update', { html: sanitizeBlockHtml(el.innerHTML), variant: variant.value });
36
+ }
37
+ </script>
38
+
39
+ <template>
40
+ <div class="cpub-callout-block" :class="`cpub-callout--${variant}`">
41
+ <button
42
+ class="cpub-callout-icon-btn"
43
+ :title="`${currentVariant.label} — click to change`"
44
+ :style="{ color: currentVariant.color }"
45
+ @click="cycleVariant"
46
+ >
47
+ <i :class="['fa-solid', currentVariant.icon]"></i>
48
+ </button>
49
+ <div class="cpub-callout-body">
50
+ <div class="cpub-callout-label" :style="{ color: currentVariant.color }">{{ currentVariant.label }}</div>
51
+ <div
52
+ class="cpub-callout-text"
53
+ contenteditable="true"
54
+ data-placeholder="Callout text..."
55
+ @input="onBodyInput"
56
+ v-html="html"
57
+ />
58
+ </div>
59
+ </div>
60
+ </template>
61
+
62
+ <style scoped>
63
+ .cpub-callout-block {
64
+ display: flex;
65
+ gap: 12px;
66
+ padding: 14px 16px;
67
+ border: var(--border-width-default) solid var(--border2);
68
+ border-left-width: 5px;
69
+ }
70
+
71
+ .cpub-callout--info { border-left-color: var(--accent); background: var(--accent-bg); }
72
+ .cpub-callout--tip { border-left-color: var(--green); background: var(--green-bg); }
73
+ .cpub-callout--warning { border-left-color: var(--yellow); background: var(--yellow-bg); }
74
+ .cpub-callout--danger { border-left-color: var(--red); background: var(--red-bg); }
75
+
76
+ .cpub-callout-icon-btn {
77
+ width: 28px;
78
+ height: 28px;
79
+ background: transparent;
80
+ border: none;
81
+ font-size: 14px;
82
+ cursor: pointer;
83
+ flex-shrink: 0;
84
+ display: flex;
85
+ align-items: center;
86
+ justify-content: center;
87
+ padding: 0;
88
+ transition: transform 0.15s;
89
+ }
90
+
91
+ .cpub-callout-icon-btn:hover {
92
+ transform: scale(1.15);
93
+ }
94
+
95
+ .cpub-callout-body {
96
+ flex: 1;
97
+ min-width: 0;
98
+ }
99
+
100
+ .cpub-callout-label {
101
+ font-family: var(--font-mono);
102
+ font-size: 10px;
103
+ font-weight: 700;
104
+ text-transform: uppercase;
105
+ letter-spacing: 0.1em;
106
+ margin-bottom: 4px;
107
+ }
108
+
109
+ .cpub-callout-text {
110
+ font-size: 13px;
111
+ line-height: 1.65;
112
+ color: var(--text);
113
+ outline: none;
114
+ min-height: 1.65em;
115
+ }
116
+
117
+ .cpub-callout-text:empty::before {
118
+ content: attr(data-placeholder);
119
+ color: var(--text-faint);
120
+ pointer-events: none;
121
+ }
122
+ </style>