@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,838 @@
1
+ <script setup lang="ts">
2
+ definePageMeta({ middleware: 'auth' });
3
+ useSeoMeta({ title: `Edit Profile — ${useSiteName()}` });
4
+
5
+ const { user } = useAuth();
6
+ const toast = useToast();
7
+ const { extract: extractError } = useApiError();
8
+ const saving = ref(false);
9
+ const isDirty = ref(false);
10
+
11
+ // Track changes
12
+ function markDirty(): void { isDirty.value = true; }
13
+
14
+ onBeforeRouteLeave((_to, _from, next) => {
15
+ if (isDirty.value && !confirm('You have unsaved changes. Leave anyway?')) {
16
+ next(false);
17
+ } else {
18
+ next();
19
+ }
20
+ });
21
+
22
+ const form = ref({
23
+ displayName: '',
24
+ username: '',
25
+ bio: '',
26
+ location: '',
27
+ website: '',
28
+ headline: '',
29
+ avatarUrl: '',
30
+ bannerUrl: '',
31
+ });
32
+
33
+ const skills = ref<Array<{ name: string; proficiency: number }>>([]);
34
+ const socialLinks = ref({
35
+ github: '',
36
+ twitter: '',
37
+ linkedin: '',
38
+ website: '',
39
+ });
40
+ const experience = ref<Array<{ id: string; title: string; company: string; startDate: string; endDate: string; description: string }>>([]);
41
+
42
+ const avatarInput = ref<HTMLInputElement | null>(null);
43
+ const bannerInput = ref<HTMLInputElement | null>(null);
44
+
45
+ // Load current profile
46
+ import type { Serialized, UserProfile } from '@commonpub/server';
47
+
48
+ const { data: profile } = await useFetch<Serialized<UserProfile>>('/api/profile');
49
+
50
+ if (profile.value) {
51
+ const p = profile.value;
52
+ form.value.displayName = p.displayName || '';
53
+ form.value.username = p.username || '';
54
+ form.value.bio = p.bio || '';
55
+ form.value.location = p.location || '';
56
+ form.value.website = p.website || '';
57
+ form.value.headline = p.headline || '';
58
+ form.value.avatarUrl = p.avatarUrl || '';
59
+ form.value.bannerUrl = p.bannerUrl || '';
60
+
61
+ if (Array.isArray(p.skills)) {
62
+ skills.value = p.skills.map((s) =>
63
+ typeof s === 'string' ? { name: s, proficiency: 3 } : s,
64
+ );
65
+ }
66
+ if (p.socialLinks) {
67
+ socialLinks.value.github = p.socialLinks.github || '';
68
+ socialLinks.value.twitter = p.socialLinks.twitter || '';
69
+ socialLinks.value.linkedin = p.socialLinks.linkedin || '';
70
+ socialLinks.value.website = (p.socialLinks as Record<string, string | undefined>).website || '';
71
+ }
72
+ const profileRecord = p as Record<string, unknown>;
73
+ if (Array.isArray(profileRecord.experience)) {
74
+ experience.value = (profileRecord.experience as Array<Record<string, unknown>>).map((e) => ({ ...e }) as typeof experience.value[number]);
75
+ }
76
+ }
77
+
78
+ // Watch for form changes AFTER initial data is loaded (nextTick avoids false positive)
79
+ onMounted(() => {
80
+ nextTick(() => {
81
+ watch([form, skills, socialLinks, experience], () => { isDirty.value = true; }, { deep: true });
82
+ });
83
+ });
84
+
85
+ function addSkill(): void {
86
+ skills.value.push({ name: '', proficiency: 50 });
87
+ }
88
+
89
+ function removeSkill(index: number): void {
90
+ skills.value.splice(index, 1);
91
+ }
92
+
93
+ function generateId(): string {
94
+ return `exp-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
95
+ }
96
+
97
+ function addExperience(): void {
98
+ experience.value.push({
99
+ id: generateId(),
100
+ title: '',
101
+ company: '',
102
+ startDate: '',
103
+ endDate: '',
104
+ description: '',
105
+ });
106
+ }
107
+
108
+ function removeExperience(index: number): void {
109
+ experience.value.splice(index, 1);
110
+ }
111
+
112
+ async function handleAvatarUpload(event: Event): Promise<void> {
113
+ const file = (event.target as HTMLInputElement).files?.[0];
114
+ if (!file) return;
115
+ const formData = new FormData();
116
+ formData.append('file', file);
117
+ formData.append('purpose', 'avatar');
118
+ try {
119
+ const result = await $fetch<{ url: string }>('/api/files/upload', { method: 'POST', body: formData });
120
+ form.value.avatarUrl = result.url;
121
+ } catch (err: unknown) {
122
+ toast.error(extractError(err));
123
+ }
124
+ }
125
+
126
+ async function handleBannerUpload(event: Event): Promise<void> {
127
+ const file = (event.target as HTMLInputElement).files?.[0];
128
+ if (!file) return;
129
+ const formData = new FormData();
130
+ formData.append('file', file);
131
+ formData.append('purpose', 'banner');
132
+ try {
133
+ const result = await $fetch<{ url: string }>('/api/files/upload', { method: 'POST', body: formData });
134
+ form.value.bannerUrl = result.url;
135
+ } catch (err: unknown) {
136
+ toast.error(extractError(err));
137
+ }
138
+ }
139
+
140
+ async function handleSave(): Promise<void> {
141
+ saving.value = true;
142
+ try {
143
+ await $fetch('/api/profile', {
144
+ method: 'PUT',
145
+ body: {
146
+ ...form.value,
147
+ skills: skills.value.filter((s) => s.name.trim()),
148
+ socialLinks: socialLinks.value,
149
+ experience: experience.value.filter((e) => e.title.trim()),
150
+ },
151
+ });
152
+ toast.success('Profile updated');
153
+ isDirty.value = false;
154
+ } catch (err: unknown) {
155
+ toast.error(extractError(err));
156
+ } finally {
157
+ saving.value = false;
158
+ }
159
+ }
160
+ </script>
161
+
162
+ <template>
163
+ <div class="cpub-settings">
164
+ <h1 class="cpub-page-title">Edit Profile</h1>
165
+
166
+ <form class="cpub-settings-form" @submit.prevent="handleSave">
167
+ <!-- Avatar & Banner -->
168
+ <div class="cpub-form-section">
169
+ <span class="cpub-form-section-label">Images</span>
170
+
171
+ <!-- Banner upload -->
172
+ <div class="cpub-form-group">
173
+ <label class="cpub-form-label">Banner Image</label>
174
+ <button
175
+ type="button"
176
+ class="cpub-banner-upload"
177
+ aria-label="Upload banner image"
178
+ @click="bannerInput?.click()"
179
+ >
180
+ <img
181
+ v-if="form.bannerUrl"
182
+ :src="form.bannerUrl"
183
+ alt="Banner preview"
184
+ class="cpub-banner-preview"
185
+ />
186
+ <div v-else class="cpub-banner-placeholder">
187
+ <i class="fa-solid fa-image" aria-hidden="true"></i>
188
+ <span>Click to upload banner</span>
189
+ </div>
190
+ </button>
191
+ <input
192
+ ref="bannerInput"
193
+ type="file"
194
+ accept="image/*"
195
+ class="cpub-file-hidden"
196
+ aria-label="Banner file input"
197
+ @change="handleBannerUpload"
198
+ />
199
+ </div>
200
+
201
+ <!-- Avatar upload -->
202
+ <div class="cpub-form-group">
203
+ <label class="cpub-form-label">Avatar</label>
204
+ <button
205
+ type="button"
206
+ class="cpub-avatar-upload"
207
+ aria-label="Upload avatar image"
208
+ @click="avatarInput?.click()"
209
+ >
210
+ <img
211
+ v-if="form.avatarUrl"
212
+ :src="form.avatarUrl"
213
+ alt="Avatar preview"
214
+ class="cpub-avatar-preview"
215
+ />
216
+ <div v-else class="cpub-avatar-placeholder">
217
+ <i class="fa-solid fa-camera" aria-hidden="true"></i>
218
+ </div>
219
+ <div class="cpub-avatar-overlay" aria-hidden="true">
220
+ <i class="fa-solid fa-camera"></i>
221
+ </div>
222
+ </button>
223
+ <input
224
+ ref="avatarInput"
225
+ type="file"
226
+ accept="image/*"
227
+ class="cpub-file-hidden"
228
+ aria-label="Avatar file input"
229
+ @change="handleAvatarUpload"
230
+ />
231
+ </div>
232
+ </div>
233
+
234
+ <!-- Basic Info -->
235
+ <div class="cpub-form-section">
236
+ <span class="cpub-form-section-label">Profile</span>
237
+
238
+ <div class="cpub-form-group">
239
+ <label for="displayName" class="cpub-form-label">Display Name</label>
240
+ <input
241
+ id="displayName"
242
+ v-model="form.displayName"
243
+ type="text"
244
+ class="cpub-input"
245
+ />
246
+ </div>
247
+
248
+ <div class="cpub-form-group">
249
+ <label for="username" class="cpub-form-label">Username</label>
250
+ <input
251
+ id="username"
252
+ :value="form.username"
253
+ type="text"
254
+ class="cpub-input cpub-input-readonly"
255
+ readonly
256
+ aria-readonly="true"
257
+ />
258
+ <span class="cpub-form-hint">Username cannot be changed</span>
259
+ </div>
260
+
261
+ <div class="cpub-form-group">
262
+ <label for="headline" class="cpub-form-label">Headline</label>
263
+ <input
264
+ id="headline"
265
+ v-model="form.headline"
266
+ type="text"
267
+ class="cpub-input"
268
+ placeholder="e.g., Full-stack maker"
269
+ />
270
+ </div>
271
+
272
+ <div class="cpub-form-group">
273
+ <label for="bio" class="cpub-form-label">Bio</label>
274
+ <textarea
275
+ id="bio"
276
+ v-model="form.bio"
277
+ class="cpub-textarea"
278
+ rows="4"
279
+ placeholder="Tell people about yourself..."
280
+ ></textarea>
281
+ </div>
282
+
283
+ <div class="cpub-form-group">
284
+ <label for="location" class="cpub-form-label">Location</label>
285
+ <input
286
+ id="location"
287
+ v-model="form.location"
288
+ type="text"
289
+ class="cpub-input"
290
+ placeholder="City, Country"
291
+ />
292
+ </div>
293
+
294
+ <div class="cpub-form-group">
295
+ <label for="website" class="cpub-form-label">Website</label>
296
+ <input
297
+ id="website"
298
+ v-model="form.website"
299
+ type="url"
300
+ class="cpub-input"
301
+ placeholder="https://..."
302
+ />
303
+ </div>
304
+ </div>
305
+
306
+ <!-- Skills -->
307
+ <div class="cpub-form-section">
308
+ <span class="cpub-form-section-label">Skills</span>
309
+
310
+ <div
311
+ v-for="(skill, index) in skills"
312
+ :key="index"
313
+ class="cpub-skill-row"
314
+ >
315
+ <div class="cpub-skill-name">
316
+ <input
317
+ v-model="skill.name"
318
+ type="text"
319
+ class="cpub-input"
320
+ placeholder="Skill name"
321
+ :aria-label="`Skill ${index + 1} name`"
322
+ />
323
+ </div>
324
+ <div class="cpub-skill-slider">
325
+ <input
326
+ v-model.number="skill.proficiency"
327
+ type="range"
328
+ min="0"
329
+ max="100"
330
+ class="cpub-range"
331
+ :aria-label="`Skill ${index + 1} proficiency`"
332
+ />
333
+ <span class="cpub-skill-value">{{ skill.proficiency }}%</span>
334
+ </div>
335
+ <button
336
+ type="button"
337
+ class="cpub-btn-icon cpub-btn-danger"
338
+ :aria-label="`Remove skill ${skill.name || index + 1}`"
339
+ @click="removeSkill(index)"
340
+ >
341
+ <i class="fa-solid fa-xmark" aria-hidden="true"></i>
342
+ </button>
343
+ </div>
344
+
345
+ <button
346
+ type="button"
347
+ class="cpub-btn-add"
348
+ @click="addSkill"
349
+ >
350
+ <i class="fa-solid fa-plus" aria-hidden="true"></i>
351
+ Add Skill
352
+ </button>
353
+ </div>
354
+
355
+ <!-- Social Links -->
356
+ <div class="cpub-form-section">
357
+ <span class="cpub-form-section-label">Social Links</span>
358
+
359
+ <div class="cpub-form-group">
360
+ <label for="social-github" class="cpub-form-label">GitHub</label>
361
+ <input
362
+ id="social-github"
363
+ v-model="socialLinks.github"
364
+ type="url"
365
+ class="cpub-input"
366
+ placeholder="https://github.com/username"
367
+ />
368
+ </div>
369
+
370
+ <div class="cpub-form-group">
371
+ <label for="social-twitter" class="cpub-form-label">Twitter / X</label>
372
+ <input
373
+ id="social-twitter"
374
+ v-model="socialLinks.twitter"
375
+ type="url"
376
+ class="cpub-input"
377
+ placeholder="https://x.com/username"
378
+ />
379
+ </div>
380
+
381
+ <div class="cpub-form-group">
382
+ <label for="social-linkedin" class="cpub-form-label">LinkedIn</label>
383
+ <input
384
+ id="social-linkedin"
385
+ v-model="socialLinks.linkedin"
386
+ type="url"
387
+ class="cpub-input"
388
+ placeholder="https://linkedin.com/in/username"
389
+ />
390
+ </div>
391
+
392
+ <div class="cpub-form-group">
393
+ <label for="social-website" class="cpub-form-label">Website URL</label>
394
+ <input
395
+ id="social-website"
396
+ v-model="socialLinks.website"
397
+ type="url"
398
+ class="cpub-input"
399
+ placeholder="https://..."
400
+ />
401
+ </div>
402
+ </div>
403
+
404
+ <!-- Experience -->
405
+ <div class="cpub-form-section">
406
+ <span class="cpub-form-section-label">Experience</span>
407
+
408
+ <div
409
+ v-for="(entry, index) in experience"
410
+ :key="entry.id"
411
+ class="cpub-experience-card"
412
+ >
413
+ <div class="cpub-experience-header">
414
+ <span class="cpub-experience-number">{{ index + 1 }}</span>
415
+ <button
416
+ type="button"
417
+ class="cpub-btn-icon cpub-btn-danger"
418
+ :aria-label="`Remove experience entry ${index + 1}`"
419
+ @click="removeExperience(index)"
420
+ >
421
+ <i class="fa-solid fa-trash" aria-hidden="true"></i>
422
+ </button>
423
+ </div>
424
+
425
+ <div class="cpub-experience-fields">
426
+ <div class="cpub-form-group">
427
+ <label :for="`exp-title-${entry.id}`" class="cpub-form-label">Title</label>
428
+ <input
429
+ :id="`exp-title-${entry.id}`"
430
+ v-model="entry.title"
431
+ type="text"
432
+ class="cpub-input"
433
+ placeholder="e.g., Senior Developer"
434
+ />
435
+ </div>
436
+
437
+ <div class="cpub-form-group">
438
+ <label :for="`exp-company-${entry.id}`" class="cpub-form-label">Company</label>
439
+ <input
440
+ :id="`exp-company-${entry.id}`"
441
+ v-model="entry.company"
442
+ type="text"
443
+ class="cpub-input"
444
+ placeholder="Company name"
445
+ />
446
+ </div>
447
+
448
+ <div class="cpub-experience-dates">
449
+ <div class="cpub-form-group">
450
+ <label :for="`exp-start-${entry.id}`" class="cpub-form-label">Start Date</label>
451
+ <input
452
+ :id="`exp-start-${entry.id}`"
453
+ v-model="entry.startDate"
454
+ type="month"
455
+ class="cpub-input"
456
+ />
457
+ </div>
458
+ <div class="cpub-form-group">
459
+ <label :for="`exp-end-${entry.id}`" class="cpub-form-label">End Date</label>
460
+ <input
461
+ :id="`exp-end-${entry.id}`"
462
+ v-model="entry.endDate"
463
+ type="month"
464
+ class="cpub-input"
465
+ placeholder="Present"
466
+ />
467
+ </div>
468
+ </div>
469
+
470
+ <div class="cpub-form-group">
471
+ <label :for="`exp-desc-${entry.id}`" class="cpub-form-label">Description</label>
472
+ <textarea
473
+ :id="`exp-desc-${entry.id}`"
474
+ v-model="entry.description"
475
+ class="cpub-textarea"
476
+ rows="3"
477
+ placeholder="What did you do?"
478
+ ></textarea>
479
+ </div>
480
+ </div>
481
+ </div>
482
+
483
+ <button
484
+ type="button"
485
+ class="cpub-btn-add"
486
+ @click="addExperience"
487
+ >
488
+ <i class="fa-solid fa-plus" aria-hidden="true"></i>
489
+ Add Experience
490
+ </button>
491
+ </div>
492
+
493
+ <!-- Actions -->
494
+ <div class="cpub-form-actions">
495
+ <button type="submit" class="cpub-save-btn" :disabled="saving">
496
+ {{ saving ? 'Saving...' : 'Save Changes' }}
497
+ </button>
498
+ </div>
499
+ </form>
500
+ </div>
501
+ </template>
502
+
503
+ <style scoped>
504
+ .cpub-settings {
505
+ max-width: 640px;
506
+ padding: var(--space-6);
507
+ }
508
+
509
+ .cpub-page-title {
510
+ font-size: var(--text-xl);
511
+ font-weight: var(--font-weight-bold);
512
+ margin-bottom: var(--space-6);
513
+ }
514
+
515
+ .cpub-settings-form {
516
+ display: flex;
517
+ flex-direction: column;
518
+ gap: var(--space-6);
519
+ }
520
+
521
+ .cpub-form-section {
522
+ padding-bottom: var(--space-6);
523
+ border-bottom: var(--border-width-default) solid var(--border);
524
+ }
525
+
526
+ .cpub-form-section-label {
527
+ display: block;
528
+ font-family: var(--font-mono);
529
+ font-size: var(--text-label);
530
+ font-weight: var(--font-weight-semibold);
531
+ text-transform: uppercase;
532
+ letter-spacing: var(--tracking-widest);
533
+ color: var(--text-faint);
534
+ margin-bottom: var(--space-4);
535
+ }
536
+
537
+ /* ─── Banner upload ─── */
538
+ .cpub-banner-upload {
539
+ display: block;
540
+ width: 100%;
541
+ height: 140px;
542
+ border: 2px dashed var(--border2);
543
+ background: var(--surface);
544
+ cursor: pointer;
545
+ overflow: hidden;
546
+ position: relative;
547
+ padding: 0;
548
+ }
549
+
550
+ .cpub-banner-upload:hover {
551
+ border-color: var(--accent);
552
+ }
553
+
554
+ .cpub-banner-upload:focus-visible {
555
+ outline: 2px solid var(--accent);
556
+ outline-offset: 2px;
557
+ }
558
+
559
+ .cpub-banner-preview {
560
+ width: 100%;
561
+ height: 100%;
562
+ object-fit: cover;
563
+ }
564
+
565
+ .cpub-banner-placeholder {
566
+ display: flex;
567
+ flex-direction: column;
568
+ align-items: center;
569
+ justify-content: center;
570
+ height: 100%;
571
+ gap: var(--space-2);
572
+ color: var(--text-faint);
573
+ font-size: var(--text-sm);
574
+ }
575
+
576
+ .cpub-banner-placeholder i {
577
+ font-size: var(--text-xl);
578
+ }
579
+
580
+ /* ─── Avatar upload ─── */
581
+ .cpub-avatar-upload {
582
+ width: 96px;
583
+ height: 96px;
584
+ border-radius: 50%;
585
+ border: var(--border-width-default) solid var(--border2);
586
+ background: var(--surface);
587
+ cursor: pointer;
588
+ overflow: hidden;
589
+ position: relative;
590
+ padding: 0;
591
+ }
592
+
593
+ .cpub-avatar-upload:hover {
594
+ border-color: var(--accent);
595
+ }
596
+
597
+ .cpub-avatar-upload:focus-visible {
598
+ outline: 2px solid var(--accent);
599
+ outline-offset: 2px;
600
+ }
601
+
602
+ .cpub-avatar-preview {
603
+ width: 100%;
604
+ height: 100%;
605
+ object-fit: cover;
606
+ }
607
+
608
+ .cpub-avatar-placeholder {
609
+ display: flex;
610
+ align-items: center;
611
+ justify-content: center;
612
+ width: 100%;
613
+ height: 100%;
614
+ color: var(--text-faint);
615
+ font-size: var(--text-xl);
616
+ }
617
+
618
+ .cpub-avatar-overlay {
619
+ position: absolute;
620
+ inset: 0;
621
+ display: flex;
622
+ align-items: center;
623
+ justify-content: center;
624
+ background: var(--color-surface-overlay);
625
+ color: var(--color-text-inverse);
626
+ font-size: var(--text-md);
627
+ opacity: 0;
628
+ transition: opacity var(--transition-fast);
629
+ border-radius: 50%;
630
+ }
631
+
632
+ .cpub-avatar-upload:hover .cpub-avatar-overlay {
633
+ opacity: 1;
634
+ }
635
+
636
+ .cpub-file-hidden {
637
+ display: none;
638
+ }
639
+
640
+ /* ─── Read-only input ─── */
641
+ .cpub-input-readonly {
642
+ opacity: 0.6;
643
+ cursor: not-allowed;
644
+ background: var(--surface2);
645
+ }
646
+
647
+ /* ─── Skills ─── */
648
+ .cpub-skill-row {
649
+ display: flex;
650
+ align-items: center;
651
+ gap: var(--space-3);
652
+ margin-bottom: var(--space-3);
653
+ }
654
+
655
+ .cpub-skill-name {
656
+ flex: 1;
657
+ min-width: 0;
658
+ }
659
+
660
+ .cpub-skill-slider {
661
+ display: flex;
662
+ align-items: center;
663
+ gap: var(--space-2);
664
+ width: 180px;
665
+ flex-shrink: 0;
666
+ }
667
+
668
+ .cpub-range {
669
+ flex: 1;
670
+ appearance: none;
671
+ height: 4px;
672
+ background: var(--border2);
673
+ outline: none;
674
+ cursor: pointer;
675
+ }
676
+
677
+ .cpub-range::-webkit-slider-thumb {
678
+ appearance: none;
679
+ width: 14px;
680
+ height: 14px;
681
+ background: var(--accent);
682
+ border: var(--border-width-default) solid var(--accent);
683
+ cursor: pointer;
684
+ }
685
+
686
+ .cpub-range::-moz-range-thumb {
687
+ width: 14px;
688
+ height: 14px;
689
+ background: var(--accent);
690
+ border: var(--border-width-default) solid var(--accent);
691
+ cursor: pointer;
692
+ }
693
+
694
+ .cpub-range:focus-visible::-webkit-slider-thumb {
695
+ outline: 2px solid var(--accent);
696
+ outline-offset: 2px;
697
+ }
698
+
699
+ .cpub-skill-value {
700
+ font-family: var(--font-mono);
701
+ font-size: var(--text-xs);
702
+ color: var(--text-dim);
703
+ min-width: 36px;
704
+ text-align: right;
705
+ }
706
+
707
+ /* ─── Buttons ─── */
708
+ .cpub-btn-icon {
709
+ width: 32px;
710
+ height: 32px;
711
+ display: flex;
712
+ align-items: center;
713
+ justify-content: center;
714
+ border: var(--border-width-default) solid var(--border2);
715
+ background: var(--surface);
716
+ color: var(--text-dim);
717
+ cursor: pointer;
718
+ flex-shrink: 0;
719
+ }
720
+
721
+ .cpub-btn-icon:hover {
722
+ border-color: var(--border);
723
+ color: var(--text);
724
+ }
725
+
726
+ .cpub-btn-icon:focus-visible {
727
+ outline: 2px solid var(--accent);
728
+ outline-offset: 1px;
729
+ }
730
+
731
+ .cpub-btn-danger:hover {
732
+ color: var(--red);
733
+ border-color: var(--red);
734
+ }
735
+
736
+ .cpub-btn-add {
737
+ display: inline-flex;
738
+ align-items: center;
739
+ gap: var(--space-2);
740
+ padding: var(--space-2) var(--space-4);
741
+ border: 2px dashed var(--border2);
742
+ background: none;
743
+ color: var(--text-dim);
744
+ font-size: var(--text-sm);
745
+ font-family: var(--font-sans);
746
+ cursor: pointer;
747
+ margin-top: var(--space-2);
748
+ }
749
+
750
+ .cpub-btn-add:hover {
751
+ border-color: var(--accent);
752
+ color: var(--accent);
753
+ }
754
+
755
+ .cpub-btn-add:focus-visible {
756
+ outline: 2px solid var(--accent);
757
+ outline-offset: 2px;
758
+ }
759
+
760
+ /* ─── Experience ─── */
761
+ .cpub-experience-card {
762
+ border: var(--border-width-default) solid var(--border);
763
+ background: var(--surface);
764
+ padding: var(--space-4);
765
+ margin-bottom: var(--space-4);
766
+ }
767
+
768
+ .cpub-experience-header {
769
+ display: flex;
770
+ align-items: center;
771
+ justify-content: space-between;
772
+ margin-bottom: var(--space-3);
773
+ }
774
+
775
+ .cpub-experience-number {
776
+ font-family: var(--font-mono);
777
+ font-size: var(--text-xs);
778
+ font-weight: var(--font-weight-bold);
779
+ color: var(--text-faint);
780
+ text-transform: uppercase;
781
+ letter-spacing: var(--tracking-wide);
782
+ }
783
+
784
+ .cpub-experience-fields {
785
+ display: flex;
786
+ flex-direction: column;
787
+ }
788
+
789
+ .cpub-experience-dates {
790
+ display: grid;
791
+ grid-template-columns: 1fr 1fr;
792
+ gap: var(--space-4);
793
+ }
794
+
795
+ /* ─── Form actions ─── */
796
+ .cpub-form-actions {
797
+ display: flex;
798
+ align-items: center;
799
+ gap: var(--space-3);
800
+ padding-top: var(--space-4);
801
+ }
802
+
803
+ .cpub-save-btn {
804
+ padding: var(--space-2) var(--space-5);
805
+ background: var(--accent);
806
+ color: var(--color-text-inverse);
807
+ border: var(--border-width-default) solid var(--border);
808
+ font-size: var(--text-sm);
809
+ cursor: pointer;
810
+ font-family: var(--font-sans);
811
+ box-shadow: var(--shadow-sm);
812
+ }
813
+
814
+ .cpub-save-btn:hover {
815
+ opacity: 0.85;
816
+ }
817
+
818
+ .cpub-save-btn:disabled {
819
+ opacity: 0.5;
820
+ cursor: not-allowed;
821
+ }
822
+
823
+ .cpub-save-btn:focus-visible {
824
+ outline: 2px solid var(--accent);
825
+ outline-offset: 2px;
826
+ }
827
+
828
+ @media (max-width: 768px) {
829
+ .cpub-settings-form { padding: 0 var(--space-1); }
830
+ .cpub-skill-slider { width: 120px; }
831
+ .cpub-experience-dates { grid-template-columns: 1fr; }
832
+ .cpub-banner-upload { height: 100px; }
833
+ }
834
+
835
+ @media (max-width: 480px) {
836
+ .cpub-skill-slider { width: 80px; }
837
+ }
838
+ </style>