@nuraly/lumenjs 0.1.3 → 0.1.4

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 (333) hide show
  1. package/README.md +62 -282
  2. package/dist/auth/config.d.ts +23 -0
  3. package/dist/auth/config.js +115 -0
  4. package/dist/auth/guard.d.ts +12 -0
  5. package/dist/auth/guard.js +28 -0
  6. package/dist/auth/index.d.ts +3 -0
  7. package/dist/auth/index.js +1 -0
  8. package/dist/auth/middleware.d.ts +23 -0
  9. package/dist/auth/middleware.js +89 -0
  10. package/dist/auth/native-auth.d.ts +73 -0
  11. package/dist/auth/native-auth.js +293 -0
  12. package/dist/auth/oidc-client.d.ts +17 -0
  13. package/dist/auth/oidc-client.js +123 -0
  14. package/dist/auth/providers/google.d.ts +23 -0
  15. package/dist/auth/providers/google.js +25 -0
  16. package/dist/auth/providers/index.d.ts +2 -0
  17. package/dist/auth/providers/index.js +1 -0
  18. package/dist/auth/routes/login.d.ts +8 -0
  19. package/dist/auth/routes/login.js +98 -0
  20. package/dist/auth/routes/logout.d.ts +4 -0
  21. package/dist/auth/routes/logout.js +79 -0
  22. package/dist/auth/routes/oidc-callback.d.ts +3 -0
  23. package/dist/auth/routes/oidc-callback.js +70 -0
  24. package/dist/auth/routes/password.d.ts +5 -0
  25. package/dist/auth/routes/password.js +149 -0
  26. package/dist/auth/routes/signup.d.ts +3 -0
  27. package/dist/auth/routes/signup.js +81 -0
  28. package/dist/auth/routes/token.d.ts +4 -0
  29. package/dist/auth/routes/token.js +70 -0
  30. package/dist/auth/routes/utils.d.ts +7 -0
  31. package/dist/auth/routes/utils.js +35 -0
  32. package/dist/auth/routes/verify.d.ts +3 -0
  33. package/dist/auth/routes/verify.js +26 -0
  34. package/dist/auth/routes.d.ts +8 -0
  35. package/dist/auth/routes.js +110 -0
  36. package/dist/auth/session.d.ts +8 -0
  37. package/dist/auth/session.js +54 -0
  38. package/dist/auth/token.d.ts +33 -0
  39. package/dist/auth/token.js +90 -0
  40. package/dist/auth/types.d.ts +156 -0
  41. package/dist/auth/types.js +2 -0
  42. package/dist/build/build-client.d.ts +15 -0
  43. package/dist/build/build-client.js +45 -0
  44. package/dist/build/build-prerender.d.ts +11 -0
  45. package/dist/build/build-prerender.js +159 -0
  46. package/dist/build/build-server.d.ts +17 -0
  47. package/dist/build/build-server.js +98 -0
  48. package/dist/build/build.js +48 -120
  49. package/dist/build/scan.d.ts +17 -0
  50. package/dist/build/scan.js +76 -6
  51. package/dist/build/serve-api.js +8 -2
  52. package/dist/build/serve-loaders.d.ts +4 -4
  53. package/dist/build/serve-loaders.js +26 -18
  54. package/dist/build/serve-ssr.js +38 -11
  55. package/dist/build/serve-static.js +3 -3
  56. package/dist/build/serve.js +218 -15
  57. package/dist/cli.js +37 -6
  58. package/dist/communication/encryption.d.ts +35 -0
  59. package/dist/communication/encryption.js +90 -0
  60. package/dist/communication/handlers/context.d.ts +27 -0
  61. package/dist/communication/handlers/context.js +1 -0
  62. package/dist/communication/handlers/conversation.d.ts +24 -0
  63. package/dist/communication/handlers/conversation.js +113 -0
  64. package/dist/communication/handlers/file-upload.d.ts +17 -0
  65. package/dist/communication/handlers/file-upload.js +62 -0
  66. package/dist/communication/handlers/messaging.d.ts +30 -0
  67. package/dist/communication/handlers/messaging.js +237 -0
  68. package/dist/communication/handlers/presence.d.ts +15 -0
  69. package/dist/communication/handlers/presence.js +76 -0
  70. package/dist/communication/handlers.d.ts +5 -0
  71. package/dist/communication/handlers.js +5 -0
  72. package/dist/communication/index.d.ts +9 -0
  73. package/dist/communication/index.js +7 -0
  74. package/dist/communication/link-preview.d.ts +18 -0
  75. package/dist/communication/link-preview.js +115 -0
  76. package/dist/communication/schema.d.ts +10 -0
  77. package/dist/communication/schema.js +101 -0
  78. package/dist/communication/server.d.ts +86 -0
  79. package/dist/communication/server.js +212 -0
  80. package/dist/communication/signaling.d.ts +43 -0
  81. package/dist/communication/signaling.js +271 -0
  82. package/dist/communication/store.d.ts +71 -0
  83. package/dist/communication/store.js +289 -0
  84. package/dist/communication/types.d.ts +454 -0
  85. package/dist/communication/types.js +1 -0
  86. package/dist/create.d.ts +1 -0
  87. package/dist/create.js +55 -0
  88. package/dist/db/auto-migrate.d.ts +3 -0
  89. package/dist/db/auto-migrate.js +100 -0
  90. package/dist/db/client.d.ts +3 -0
  91. package/dist/db/client.js +18 -0
  92. package/dist/db/index.d.ts +17 -13
  93. package/dist/db/index.js +205 -26
  94. package/dist/db/seed.d.ts +12 -0
  95. package/dist/db/seed.js +88 -0
  96. package/dist/db/table.d.ts +10 -0
  97. package/dist/db/table.js +12 -0
  98. package/dist/dev-server/config.d.ts +11 -0
  99. package/dist/dev-server/config.js +23 -20
  100. package/dist/dev-server/index-html.d.ts +3 -0
  101. package/dist/dev-server/index-html.js +18 -6
  102. package/dist/dev-server/nuralyui-aliases.d.ts +0 -4
  103. package/dist/dev-server/nuralyui-aliases.js +115 -94
  104. package/dist/dev-server/plugins/vite-plugin-api-routes.js +29 -5
  105. package/dist/dev-server/plugins/vite-plugin-auth.d.ts +6 -0
  106. package/dist/dev-server/plugins/vite-plugin-auth.js +223 -0
  107. package/dist/dev-server/plugins/vite-plugin-auto-define.d.ts +16 -0
  108. package/dist/dev-server/plugins/vite-plugin-auto-define.js +111 -0
  109. package/dist/dev-server/plugins/vite-plugin-communication.d.ts +6 -0
  110. package/dist/dev-server/plugins/vite-plugin-communication.js +205 -0
  111. package/dist/dev-server/plugins/vite-plugin-editor-api.d.ts +6 -0
  112. package/dist/dev-server/plugins/vite-plugin-editor-api.js +318 -0
  113. package/dist/dev-server/plugins/vite-plugin-i18n.js +69 -2
  114. package/dist/dev-server/plugins/vite-plugin-lit-dedup.d.ts +6 -0
  115. package/dist/dev-server/plugins/vite-plugin-lit-dedup.js +78 -34
  116. package/dist/dev-server/plugins/vite-plugin-lit-hmr.js +44 -2
  117. package/dist/dev-server/plugins/vite-plugin-llms.d.ts +2 -0
  118. package/dist/dev-server/plugins/vite-plugin-llms.js +92 -0
  119. package/dist/dev-server/plugins/vite-plugin-loaders.js +146 -13
  120. package/dist/dev-server/plugins/vite-plugin-routes.js +15 -5
  121. package/dist/dev-server/plugins/vite-plugin-socketio.d.ts +2 -0
  122. package/dist/dev-server/plugins/vite-plugin-socketio.js +51 -0
  123. package/dist/dev-server/plugins/vite-plugin-source-annotator.d.ts +2 -0
  124. package/dist/dev-server/plugins/vite-plugin-source-annotator.js +26 -3
  125. package/dist/dev-server/plugins/vite-plugin-storage.d.ts +10 -0
  126. package/dist/dev-server/plugins/vite-plugin-storage.js +126 -0
  127. package/dist/dev-server/plugins/vite-plugin-virtual-modules.js +111 -2
  128. package/dist/dev-server/server.js +127 -13
  129. package/dist/dev-server/ssr-render.d.ts +2 -1
  130. package/dist/dev-server/ssr-render.js +107 -48
  131. package/dist/editor/ai/backend.d.ts +20 -0
  132. package/dist/editor/ai/backend.js +104 -0
  133. package/dist/editor/ai/claude-code-client.d.ts +20 -0
  134. package/dist/editor/ai/claude-code-client.js +145 -0
  135. package/dist/editor/ai/opencode-client.d.ts +14 -0
  136. package/dist/editor/ai/opencode-client.js +125 -0
  137. package/dist/editor/ai/snapshot-store.d.ts +22 -0
  138. package/dist/editor/ai/snapshot-store.js +35 -0
  139. package/dist/editor/ai/types.d.ts +30 -0
  140. package/dist/editor/ai/types.js +136 -0
  141. package/dist/editor/ai-chat-panel.d.ts +13 -0
  142. package/dist/editor/ai-chat-panel.js +587 -0
  143. package/dist/editor/ai-markdown.d.ts +10 -0
  144. package/dist/editor/ai-markdown.js +70 -0
  145. package/dist/editor/ai-project-panel.d.ts +11 -0
  146. package/dist/editor/ai-project-panel.js +332 -0
  147. package/dist/editor/ast-modification.d.ts +11 -0
  148. package/dist/editor/ast-modification.js +1 -0
  149. package/dist/editor/ast-service.d.ts +30 -0
  150. package/dist/editor/ast-service.js +180 -0
  151. package/dist/editor/css-rules.d.ts +54 -0
  152. package/dist/editor/css-rules.js +423 -0
  153. package/dist/editor/editor-api-client.d.ts +51 -0
  154. package/dist/editor/editor-api-client.js +162 -0
  155. package/dist/editor/editor-bridge.d.ts +1 -0
  156. package/dist/editor/editor-bridge.js +17 -8
  157. package/dist/editor/editor-toolbar.d.ts +14 -0
  158. package/dist/editor/editor-toolbar.js +115 -0
  159. package/dist/editor/file-editor.d.ts +9 -0
  160. package/dist/editor/file-editor.js +236 -0
  161. package/dist/editor/file-service.d.ts +16 -0
  162. package/dist/editor/file-service.js +52 -0
  163. package/dist/editor/i18n-key-gen.d.ts +1 -0
  164. package/dist/editor/i18n-key-gen.js +7 -0
  165. package/dist/editor/inline-text-edit.d.ts +5 -0
  166. package/dist/editor/inline-text-edit.js +173 -92
  167. package/dist/editor/overlay-events.d.ts +5 -0
  168. package/dist/editor/overlay-events.js +364 -0
  169. package/dist/editor/overlay-hmr.d.ts +2 -0
  170. package/dist/editor/overlay-hmr.js +75 -0
  171. package/dist/editor/overlay-selection.d.ts +29 -0
  172. package/dist/editor/overlay-selection.js +148 -0
  173. package/dist/editor/overlay-utils.d.ts +12 -0
  174. package/dist/editor/overlay-utils.js +59 -0
  175. package/dist/editor/properties-panel-persist.d.ts +14 -0
  176. package/dist/editor/properties-panel-persist.js +70 -0
  177. package/dist/editor/properties-panel-rows.d.ts +10 -0
  178. package/dist/editor/properties-panel-rows.js +349 -0
  179. package/dist/editor/properties-panel-styles.d.ts +4 -0
  180. package/dist/editor/properties-panel-styles.js +174 -0
  181. package/dist/editor/properties-panel.d.ts +4 -0
  182. package/dist/editor/properties-panel.js +148 -0
  183. package/dist/editor/property-registry.d.ts +16 -0
  184. package/dist/editor/property-registry.js +303 -0
  185. package/dist/editor/standalone-file-panel.d.ts +0 -0
  186. package/dist/editor/standalone-file-panel.js +1 -0
  187. package/dist/editor/standalone-overlay-dom.d.ts +0 -0
  188. package/dist/editor/standalone-overlay-dom.js +1 -0
  189. package/dist/editor/standalone-overlay-styles.d.ts +0 -0
  190. package/dist/editor/standalone-overlay-styles.js +1 -0
  191. package/dist/editor/standalone-overlay.d.ts +1 -0
  192. package/dist/editor/standalone-overlay.js +76 -0
  193. package/dist/editor/syntax-highlighter.d.ts +4 -0
  194. package/dist/editor/syntax-highlighter.js +81 -0
  195. package/dist/editor/text-toolbar.d.ts +11 -0
  196. package/dist/editor/text-toolbar.js +327 -0
  197. package/dist/editor/toolbar-styles.d.ts +4 -0
  198. package/dist/editor/toolbar-styles.js +198 -0
  199. package/dist/email/index.d.ts +32 -0
  200. package/dist/email/index.js +154 -0
  201. package/dist/email/providers/resend.d.ts +2 -0
  202. package/dist/email/providers/resend.js +24 -0
  203. package/dist/email/providers/sendgrid.d.ts +2 -0
  204. package/dist/email/providers/sendgrid.js +31 -0
  205. package/dist/email/providers/smtp.d.ts +13 -0
  206. package/dist/email/providers/smtp.js +125 -0
  207. package/dist/email/template-engine.d.ts +18 -0
  208. package/dist/email/template-engine.js +116 -0
  209. package/dist/email/templates/base.d.ts +9 -0
  210. package/dist/email/templates/base.js +65 -0
  211. package/dist/email/templates/password-reset.d.ts +5 -0
  212. package/dist/email/templates/password-reset.js +15 -0
  213. package/dist/email/templates/verify-email.d.ts +5 -0
  214. package/dist/email/templates/verify-email.js +15 -0
  215. package/dist/email/templates/welcome.d.ts +5 -0
  216. package/dist/email/templates/welcome.js +13 -0
  217. package/dist/email/types.d.ts +49 -0
  218. package/dist/email/types.js +1 -0
  219. package/dist/llms/generate.d.ts +46 -0
  220. package/dist/llms/generate.js +185 -0
  221. package/dist/permissions/guard.d.ts +28 -0
  222. package/dist/permissions/guard.js +30 -0
  223. package/dist/permissions/index.d.ts +6 -0
  224. package/dist/permissions/index.js +3 -0
  225. package/dist/permissions/service.d.ts +80 -0
  226. package/dist/permissions/service.js +210 -0
  227. package/dist/permissions/tables.d.ts +5 -0
  228. package/dist/permissions/tables.js +68 -0
  229. package/dist/permissions/types.d.ts +33 -0
  230. package/dist/permissions/types.js +1 -0
  231. package/dist/runtime/app-shell.js +163 -0
  232. package/dist/runtime/auth.d.ts +10 -0
  233. package/dist/runtime/auth.js +30 -0
  234. package/dist/runtime/communication.d.ts +137 -0
  235. package/dist/runtime/communication.js +228 -0
  236. package/dist/runtime/error-boundary.d.ts +23 -0
  237. package/dist/runtime/error-boundary.js +120 -0
  238. package/dist/runtime/i18n.d.ts +6 -1
  239. package/dist/runtime/i18n.js +42 -21
  240. package/dist/runtime/router-data.d.ts +3 -0
  241. package/dist/runtime/router-data.js +102 -17
  242. package/dist/runtime/router-hydration.js +25 -0
  243. package/dist/runtime/router.d.ts +16 -1
  244. package/dist/runtime/router.js +188 -42
  245. package/dist/runtime/socket-client.d.ts +2 -0
  246. package/dist/runtime/socket-client.js +30 -0
  247. package/dist/runtime/webrtc.d.ts +47 -0
  248. package/dist/runtime/webrtc.js +178 -0
  249. package/dist/shared/graceful-shutdown.d.ts +8 -0
  250. package/dist/shared/graceful-shutdown.js +36 -0
  251. package/dist/shared/health.d.ts +8 -0
  252. package/dist/shared/health.js +25 -0
  253. package/dist/shared/llms-txt.d.ts +31 -0
  254. package/dist/shared/llms-txt.js +85 -0
  255. package/dist/shared/logger.d.ts +32 -0
  256. package/dist/shared/logger.js +93 -0
  257. package/dist/shared/meta.d.ts +27 -0
  258. package/dist/shared/meta.js +71 -0
  259. package/dist/shared/middleware-runner.d.ts +9 -0
  260. package/dist/shared/middleware-runner.js +29 -0
  261. package/dist/shared/rate-limit.d.ts +18 -0
  262. package/dist/shared/rate-limit.js +71 -0
  263. package/dist/shared/request-id.d.ts +5 -0
  264. package/dist/shared/request-id.js +18 -0
  265. package/dist/shared/route-matching.js +16 -1
  266. package/dist/shared/security-headers.d.ts +18 -0
  267. package/dist/shared/security-headers.js +38 -0
  268. package/dist/shared/socket-io-setup.d.ts +11 -0
  269. package/dist/shared/socket-io-setup.js +51 -0
  270. package/dist/shared/types.d.ts +14 -0
  271. package/dist/shared/utils.d.ts +33 -7
  272. package/dist/shared/utils.js +164 -27
  273. package/dist/storage/adapters/local.d.ts +44 -0
  274. package/dist/storage/adapters/local.js +85 -0
  275. package/dist/storage/adapters/s3.d.ts +32 -0
  276. package/dist/storage/adapters/s3.js +116 -0
  277. package/dist/storage/adapters/types.d.ts +53 -0
  278. package/dist/storage/adapters/types.js +1 -0
  279. package/dist/storage/index.d.ts +76 -0
  280. package/dist/storage/index.js +83 -0
  281. package/package.json +19 -7
  282. package/templates/blog/api/posts.ts +4 -18
  283. package/templates/blog/data/migrations/001_init.sql +6 -5
  284. package/templates/blog/lumenjs.config.ts +3 -0
  285. package/templates/blog/package.json +14 -0
  286. package/templates/blog/pages/_layout.ts +25 -0
  287. package/templates/blog/pages/index.ts +48 -22
  288. package/templates/blog/pages/posts/[slug].ts +45 -20
  289. package/templates/blog/pages/tag/[tag].ts +44 -0
  290. package/templates/dashboard/api/stats.ts +8 -5
  291. package/templates/dashboard/lumenjs.config.ts +3 -0
  292. package/templates/dashboard/package.json +14 -0
  293. package/templates/dashboard/pages/_layout.ts +25 -0
  294. package/templates/dashboard/pages/index.ts +54 -23
  295. package/templates/dashboard/pages/settings/index.ts +29 -0
  296. package/templates/default/lumenjs.config.ts +3 -0
  297. package/templates/default/package.json +14 -0
  298. package/templates/default/pages/index.ts +24 -0
  299. package/templates/social/api/posts/[id].ts +14 -0
  300. package/templates/social/api/posts.ts +11 -0
  301. package/templates/social/api/profile/[username].ts +10 -0
  302. package/templates/social/api/upload.ts +19 -0
  303. package/templates/social/data/migrations/001_init.sql +78 -0
  304. package/templates/social/data/migrations/002_add_image_url.sql +1 -0
  305. package/templates/social/data/migrations/003_auth.sql +7 -0
  306. package/templates/social/docs/architecture.md +76 -0
  307. package/templates/social/docs/components.md +100 -0
  308. package/templates/social/docs/data.md +89 -0
  309. package/templates/social/docs/pages.md +96 -0
  310. package/templates/social/docs/theming.md +52 -0
  311. package/templates/social/lib/media.ts +130 -0
  312. package/templates/social/lumenjs.auth.ts +21 -0
  313. package/templates/social/lumenjs.config.ts +3 -0
  314. package/templates/social/package.json +5 -0
  315. package/templates/social/pages/_layout.ts +239 -0
  316. package/templates/social/pages/apps/[id].ts +173 -0
  317. package/templates/social/pages/apps/index.ts +116 -0
  318. package/templates/social/pages/auth/login.ts +92 -0
  319. package/templates/social/pages/bookmarks.ts +57 -0
  320. package/templates/social/pages/explore.ts +73 -0
  321. package/templates/social/pages/index.ts +351 -0
  322. package/templates/social/pages/messages.ts +298 -0
  323. package/templates/social/pages/new.ts +77 -0
  324. package/templates/social/pages/notifications.ts +73 -0
  325. package/templates/social/pages/post/[id].ts +124 -0
  326. package/templates/social/pages/profile/[username].ts +100 -0
  327. package/templates/social/pages/settings/accessibility.ts +153 -0
  328. package/templates/social/pages/settings/account.ts +260 -0
  329. package/templates/social/pages/settings/help.ts +141 -0
  330. package/templates/social/pages/settings/language.ts +103 -0
  331. package/templates/social/pages/settings/privacy.ts +183 -0
  332. package/templates/social/pages/settings/security.ts +133 -0
  333. package/templates/social/pages/settings.ts +185 -0
@@ -0,0 +1,96 @@
1
+ # Pages Reference
2
+
3
+ ## `_layout.ts` — App Shell
4
+
5
+ Root layout wrapping all pages.
6
+
7
+ **Desktop**: icon sidebar | main `<slot>` | right sidebar (weather, calendar, quick apps, trending)
8
+ **Mobile**: top bar (avatar + "Home" + icons) | content | bottom nav (home, explore, +post, messages, more)
9
+
10
+ Features:
11
+ - Sidebar nav: Home, Explore, Notifications, Messages, Bookmarks, Apps, Profile, Settings
12
+ - Right sidebar widgets: weather (Tunisia 24°), calendar (3 events), quick apps (4 icons), trending tags
13
+ - Mobile "more" bottom sheet: Profile, Bookmarks, Apps, Notifications, Settings
14
+ - Dark mode theming via CSS custom properties
15
+ - Viewport meta override (no zoom on mobile)
16
+
17
+ ## `index.ts` — Feed (`/`)
18
+
19
+ **Stories bar**: horizontal scrollable thumbnails (56x80px, rounded rect), "You" has + icon, images from Unsplash. Click opens full-screen story viewer with progress bars, auto-advance (5s), tap left/right to navigate.
20
+
21
+ **Widget cards** mixed between posts:
22
+ - **Poll** (position 2): question + 4 options with percentage bars + vote count
23
+ - **Event** (position 4): title, date, location, attendees, "Interested" button
24
+ - **Job** (position 6): company, role, location, salary, "Apply" button
25
+
26
+ **Post cards**: avatar (image for aymen, initials for others) + name + @handle + time + content (3-line clamp, "See more" on first post) + media (image/video, 16:9 aspect ratio) + action bar (comment, repost, heart, share with SVG icons)
27
+
28
+ Loader returns: `{ stories[], widgets[], posts[] }`
29
+
30
+ ## `post/[id].ts` — Post Detail (`/post/:id`)
31
+
32
+ Full article view with larger text. Shows media (full-width, no padding). Tags, author meta, date + read time.
33
+
34
+ Action bar: heart, comment, bookmark (SVG). Comment input below. Replies rendered as cards with avatar + name + content.
35
+
36
+ 6 mock posts with IDs 1-6. Posts 1, 3, 4, 6 have media.
37
+
38
+ ## `profile/[username].ts` — Profile (`/profile/:username`)
39
+
40
+ No banner. Avatar + name + bio on same row, follow button (SVG user-plus icon) top-right.
41
+
42
+ Meta: location (pin icon), work (briefcase icon), joined date (calendar icon).
43
+
44
+ Posts list below. 4 mock users: aymen, alex_design, mike_ops, emma_data.
45
+
46
+ Aymen uses GitHub avatar image, others use letter initials with brand colors.
47
+
48
+ ## `explore.ts` — Explore (`/explore`)
49
+
50
+ Tag cards grid (colored top bar per tag), 8 tags. Top posts section with title links.
51
+
52
+ ## `notifications.ts` — Notifications (`/notifications`)
53
+
54
+ List with avatar + type icon badge (heart=red, comment=blue, follow=green). 5 mock notifications.
55
+
56
+ ## `messages.ts` — Messages (`/messages`)
57
+
58
+ Split view: conversation list (340px) + chat panel. Click conversation to switch. 5 conversations with full message histories.
59
+
60
+ Chat features: message bubbles (purple=me, gray=them), small avatar on their messages, date separators, read receipts (single check=sent, double purple check=read), online indicators (green dot).
61
+
62
+ Input: attach, image, emoji, mic buttons + text input + send button.
63
+
64
+ Mobile: click conversation → full-screen chat with back arrow.
65
+
66
+ ## `bookmarks.ts` — Bookmarks (`/bookmarks`)
67
+
68
+ Simple list with bookmark icon + title + author + saved time. 3 mock bookmarks.
69
+
70
+ ## `settings.ts` — Settings (`/settings`)
71
+
72
+ Profile card (avatar + name + email + edit button).
73
+
74
+ Sections:
75
+ - Account: account info, password, privacy (menu items with chevron)
76
+ - Preferences: dark mode (wired toggle), push notifications, email notifications, sound effects
77
+ - More: accessibility, language, help center, log out (red)
78
+
79
+ ## `new.ts` — New Post (`/new`)
80
+
81
+ Compose screen: close button + post button in header. Avatar + textarea. Visibility line ("Everyone can reply"). Tool bar: image, video, emoji, location + char counter (0/500).
82
+
83
+ ## `apps/index.ts` — App Marketplace (`/apps`)
84
+
85
+ Search bar, 3 featured cards, category filter buttons (All + 6 categories, active state), app list (icon + name + desc + category + "Open" button). 12 mock apps.
86
+
87
+ ## `apps/[id].ts` — App Detail (`/apps/:id`)
88
+
89
+ Header: large icon + name + description + "Open" + "Add to sidebar" buttons.
90
+
91
+ Body: 3 apps have real UI:
92
+ - **poll**: question input + option inputs + add option + settings
93
+ - **calendar**: March 2025 grid with today highlighted + nav arrows
94
+ - **notes**: note list with title/preview/date + new note button
95
+
96
+ Other apps show placeholder.
@@ -0,0 +1,52 @@
1
+ # Theming & Dark Mode
2
+
3
+ ## How It Works
4
+
5
+ 1. `_layout.ts` defines two theme objects: `THEME_LIGHT` and `THEME_DARK`
6
+ 2. `applyTheme(dark)` sets CSS custom properties on `document.documentElement`
7
+ 3. On page load, reads `localStorage.getItem('theme')` and applies immediately
8
+ 4. `window.__nk_toggle_theme(dark)` exposed for the settings page to call
9
+ 5. All pages use `var(--xxx)` instead of hardcoded colors
10
+
11
+ ## CSS Custom Properties
12
+
13
+ | Variable | Light | Dark | Usage |
14
+ |----------|-------|------|-------|
15
+ | `--bg` | `#fff` | `#15202b` | Page/card backgrounds |
16
+ | `--bg-secondary` | `#f7f9f9` | `#1e2d3d` | Sidebar cards, secondary panels |
17
+ | `--bg-hover` | `#fafafa` | `#1c2b3a` | Hover states |
18
+ | `--border` | `#eff3f4` | `#38444d` | Primary borders |
19
+ | `--border-light` | `#f0f0f0` | `#2f3b44` | Subtle dividers |
20
+ | `--text` | `#0f1419` | `#e7e9ea` | Primary text |
21
+ | `--text-secondary` | `#536471` | `#8b98a5` | Secondary text, handles |
22
+ | `--text-tertiary` | `#a3a3a3` | `#6e767d` | Timestamps, counts |
23
+ | `--accent` | `#7c3aed` | `#8b5cf6` | Purple accent (buttons, links) |
24
+ | `--accent-hover` | `#6d28d9` | `#7c3aed` | Accent hover state |
25
+ | `--input-bg` | `#eff3f4` | `#253341` | Input/search backgrounds |
26
+ | `--card-bg` | `#f7f9f9` | `#1e2d3d` | Card backgrounds |
27
+ | `--overlay` | `rgba(0,0,0,0.4)` | `rgba(0,0,0,0.7)` | Modal overlays |
28
+ | `--backdrop` | `rgba(255,255,255,0.85)` | `rgba(21,32,43,0.85)` | Glassmorphism headers |
29
+
30
+ ## Persistence
31
+
32
+ - Key: `localStorage.getItem('theme')`
33
+ - Values: `'light'` (default) or `'dark'`
34
+ - Applied before first render (runs at module scope in `_layout.ts`)
35
+
36
+ ## Settings Toggle
37
+
38
+ In `pages/settings.ts`, the dark mode checkbox:
39
+ ```typescript
40
+ <input type="checkbox"
41
+ .checked=${this._dark}
42
+ @change=${(e) => {
43
+ this._dark = e.target.checked;
44
+ window.__nk_toggle_theme?.(this._dark);
45
+ }}>
46
+ ```
47
+
48
+ ## Colors That Stay Fixed (Not Themed)
49
+
50
+ Avatar brand colors are per-user and don't change with theme:
51
+ - `#e44d26` (Alex), `#3572a5` (Emma), `#22c55e` (Mike), `#a855f7` (Lisa), `#ef4444` (Tom), `#f59e0b` (Nina)
52
+ - White text on colored backgrounds (`color: #fff`) stays white in both themes
@@ -0,0 +1,130 @@
1
+ import os from 'os';
2
+ import path from 'path';
3
+ import fs from 'fs';
4
+ import crypto from 'crypto';
5
+
6
+ export interface CompressOptions {
7
+ /** Max width in pixels for images. Default: 1920 */
8
+ maxWidth?: number;
9
+ /** JPEG quality 1–100. Default: 82 */
10
+ quality?: number;
11
+ /** Audio bitrate. Default: '128k' */
12
+ audioBitrate?: string;
13
+ }
14
+
15
+ export interface CompressResult {
16
+ data: Buffer;
17
+ mimeType: string;
18
+ /** File extension including dot, e.g. '.jpg' or '.aac' */
19
+ ext: string;
20
+ }
21
+
22
+ // ── Image ────────────────────────────────────────────────────────────────────
23
+
24
+ async function compressImage(data: Buffer, mimeType: string, opts: CompressOptions): Promise<CompressResult> {
25
+ // GIF: skip — compressing would break animation
26
+ if (mimeType === 'image/gif') return { data, mimeType, ext: '.gif' };
27
+
28
+ let sharp: any;
29
+ try {
30
+ const mod = await import('sharp' as string);
31
+ sharp = mod.default ?? mod;
32
+ } catch {
33
+ throw new Error(
34
+ '[LumenJS:Media] sharp is required for image compression. ' +
35
+ 'Install with: npm install sharp',
36
+ );
37
+ }
38
+
39
+ const compressed = await sharp(data)
40
+ .resize({ width: opts.maxWidth ?? 1920, withoutEnlargement: true })
41
+ .jpeg({ quality: opts.quality ?? 82 })
42
+ .toBuffer();
43
+
44
+ return { data: compressed, mimeType: 'image/jpeg', ext: '.jpg' };
45
+ }
46
+
47
+ // ── Audio ────────────────────────────────────────────────────────────────────
48
+
49
+ const AUDIO_INPUT_EXT: Record<string, string> = {
50
+ 'audio/mpeg': '.mp3',
51
+ 'audio/mp3': '.mp3',
52
+ 'audio/mp4': '.m4a',
53
+ 'audio/aac': '.aac',
54
+ 'audio/ogg': '.ogg',
55
+ 'audio/wav': '.wav',
56
+ 'audio/webm': '.webm',
57
+ 'audio/flac': '.flac',
58
+ 'audio/x-flac': '.flac',
59
+ };
60
+
61
+ async function compressAudio(data: Buffer, mimeType: string, opts: CompressOptions): Promise<CompressResult> {
62
+ // Already AAC at target bitrate — skip re-encoding
63
+ if (mimeType === 'audio/aac') return { data, mimeType, ext: '.aac' };
64
+
65
+ let ffmpeg: any;
66
+ try {
67
+ const [ffmpegMod, installerMod] = await Promise.all([
68
+ import('fluent-ffmpeg' as string),
69
+ import('@ffmpeg-installer/ffmpeg' as string),
70
+ ]);
71
+ ffmpeg = ffmpegMod.default ?? ffmpegMod;
72
+ ffmpeg.setFfmpegPath((installerMod.default ?? installerMod).path);
73
+ } catch {
74
+ throw new Error(
75
+ '[LumenJS:Media] fluent-ffmpeg and @ffmpeg-installer/ffmpeg are required for audio compression. ' +
76
+ 'Install with: npm install fluent-ffmpeg @ffmpeg-installer/ffmpeg',
77
+ );
78
+ }
79
+
80
+ const inExt = AUDIO_INPUT_EXT[mimeType] ?? '.bin';
81
+ const tmpIn = path.join(os.tmpdir(), `nk-audio-in-${crypto.randomUUID()}${inExt}`);
82
+ const tmpOut = path.join(os.tmpdir(), `nk-audio-out-${crypto.randomUUID()}.aac`);
83
+
84
+ try {
85
+ fs.writeFileSync(tmpIn, data);
86
+ await new Promise<void>((resolve, reject) => {
87
+ ffmpeg(tmpIn)
88
+ .noVideo()
89
+ .audioCodec('aac')
90
+ .audioBitrate(opts.audioBitrate ?? '128k')
91
+ .on('error', reject)
92
+ .on('end', resolve)
93
+ .save(tmpOut);
94
+ });
95
+ return { data: fs.readFileSync(tmpOut), mimeType: 'audio/aac', ext: '.aac' };
96
+ } finally {
97
+ fs.rmSync(tmpIn, { force: true });
98
+ fs.rmSync(tmpOut, { force: true });
99
+ }
100
+ }
101
+
102
+ // ── Public API ───────────────────────────────────────────────────────────────
103
+
104
+ /**
105
+ * Compress a media buffer before storing.
106
+ *
107
+ * - Images (JPEG, PNG, WebP) → JPEG, max 1920px wide, quality 82
108
+ * - GIF → pass-through (preserves animation)
109
+ * - Audio → AAC 128k via ffmpeg
110
+ * - Everything else (video, PDF, binary) → pass-through
111
+ *
112
+ * Uses lazy dynamic imports so sharp/ffmpeg are only required if you
113
+ * actually upload that media type.
114
+ *
115
+ * @example
116
+ * const { data, mimeType, ext } = await compress(file.data, file.contentType);
117
+ * const key = `uploads/${crypto.randomUUID()}${ext}`;
118
+ * const stored = await req.storage.put(data, { key, mimeType });
119
+ */
120
+ export async function compress(
121
+ data: Buffer,
122
+ mimeType: string,
123
+ options: CompressOptions = {},
124
+ ): Promise<CompressResult> {
125
+ if (mimeType.startsWith('image/')) return compressImage(data, mimeType, options);
126
+ if (mimeType.startsWith('audio/')) return compressAudio(data, mimeType, options);
127
+ // video, PDF, binary — store as-is
128
+ const ext = path.extname(mimeType.split('/')[1] ?? '') || '';
129
+ return { data, mimeType, ext: ext ? `.${ext}` : '' };
130
+ }
@@ -0,0 +1,21 @@
1
+ import { googleProvider } from '@nuraly/lumenjs/dist/auth/index.js';
2
+
3
+ export default {
4
+ providers: [
5
+ googleProvider({
6
+ clientId: process.env.GOOGLE_CLIENT_ID!,
7
+ clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
8
+ }),
9
+ ],
10
+ session: {
11
+ secret: process.env.SESSION_SECRET ?? 'dev-secret-change-in-production',
12
+ cookieName: 'social-session',
13
+ maxAge: 60 * 60 * 24 * 7, // 7 days
14
+ secure: process.env.NODE_ENV === 'production',
15
+ },
16
+ routes: {
17
+ loginPage: '/auth/login',
18
+ postLogin: '/',
19
+ postLogout: '/auth/login',
20
+ },
21
+ };
@@ -0,0 +1,3 @@
1
+ export default {
2
+ title: '{{PROJECT_NAME}}',
3
+ };
@@ -0,0 +1,5 @@
1
+ {
2
+ "name": "{{PROJECT_NAME}}",
3
+ "private": true,
4
+ "type": "module"
5
+ }
@@ -0,0 +1,239 @@
1
+ import { LitElement, html, css } from 'lit';
2
+
3
+ const svg = {
4
+ home: html`<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M3 9l9-7 9 7v11a2 2 0 01-2 2H5a2 2 0 01-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>`,
5
+ explore: html`<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>`,
6
+ bell: html`<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M18 8A6 6 0 006 8c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 01-3.46 0"/></svg>`,
7
+ message: html`<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/><polyline points="22,6 12,13 2,6"/></svg>`,
8
+ user: html`<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M20 21v-2a4 4 0 00-4-4H8a4 4 0 00-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>`,
9
+ search: html`<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>`,
10
+ post: html`<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 013 3L7 19l-4 1 1-4L16.5 3.5z"/></svg>`,
11
+ bookmark: html`<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M19 21l-7-5-7 5V5a2 2 0 012-2h10a2 2 0 012 2z"/></svg>`,
12
+ plus: html`<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>`,
13
+ settings: html`<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 010 2.83 2 2 0 01-2.83 0l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 01-4 0v-.09A1.65 1.65 0 009 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 01-2.83-2.83l.06-.06A1.65 1.65 0 004.68 15a1.65 1.65 0 00-1.51-1H3a2 2 0 010-4h.09A1.65 1.65 0 004.6 9a1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 012.83-2.83l.06.06A1.65 1.65 0 009 4.68a1.65 1.65 0 001-1.51V3a2 2 0 014 0v.09a1.65 1.65 0 001 1.51 1.65 1.65 0 001.82-.33l.06-.06a2 2 0 012.83 2.83l-.06.06A1.65 1.65 0 0019.4 9a1.65 1.65 0 001.51 1H21a2 2 0 010 4h-.09a1.65 1.65 0 00-1.51 1z"/></svg>`,
14
+ };
15
+
16
+ const THEME_LIGHT = {
17
+ '--bg': '#fff', '--bg-secondary': '#f7f9f9', '--bg-hover': '#fafafa',
18
+ '--border': '#eff3f4', '--border-light': '#f0f0f0',
19
+ '--text': '#0f1419', '--text-secondary': '#536471', '--text-tertiary': '#a3a3a3',
20
+ '--accent': '#7c3aed', '--accent-hover': '#6d28d9',
21
+ '--input-bg': '#eff3f4', '--card-bg': '#f7f9f9',
22
+ '--overlay': 'rgba(0,0,0,0.4)', '--backdrop': 'rgba(255,255,255,0.85)',
23
+ };
24
+
25
+ const THEME_DARK = {
26
+ '--bg': '#15202b', '--bg-secondary': '#1e2d3d', '--bg-hover': '#1c2b3a',
27
+ '--border': '#38444d', '--border-light': '#2f3b44',
28
+ '--text': '#e7e9ea', '--text-secondary': '#8b98a5', '--text-tertiary': '#6e767d',
29
+ '--accent': '#8b5cf6', '--accent-hover': '#7c3aed',
30
+ '--input-bg': '#253341', '--card-bg': '#1e2d3d',
31
+ '--overlay': 'rgba(0,0,0,0.7)', '--backdrop': 'rgba(21,32,43,0.85)',
32
+ };
33
+
34
+ function applyTheme(dark: boolean) {
35
+ const theme = dark ? THEME_DARK : THEME_LIGHT;
36
+ const root = document.documentElement;
37
+ for (const [k, v] of Object.entries(theme)) root.style.setProperty(k, v);
38
+ root.style.setProperty('color-scheme', dark ? 'dark' : 'light');
39
+ document.body.style.background = theme['--bg'];
40
+ }
41
+
42
+ // Apply saved theme immediately (before render)
43
+ applyTheme(typeof localStorage !== 'undefined' && localStorage.getItem('theme') === 'dark');
44
+
45
+ // Expose toggle for settings page
46
+ (window as any).__nk_toggle_theme = (dark: boolean) => {
47
+ localStorage.setItem('theme', dark ? 'dark' : 'light');
48
+ applyTheme(dark);
49
+ };
50
+
51
+ export class LayoutRoot extends LitElement {
52
+
53
+ static styles = css`
54
+ :host { display: block; min-height: 100vh; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; color: var(--text); background: var(--bg); font-size: 15px; touch-action: pan-x pan-y; -webkit-text-size-adjust: 100%; }
55
+
56
+ .shell { display: flex; max-width: 1000px; margin: 0 auto; min-height: 100vh; }
57
+
58
+ .sidebar { width: 68px; flex-shrink: 0; position: sticky; top: 0; height: 100vh; border-right: 1px solid var(--border); display: flex; flex-direction: column; align-items: center; padding: 12px 0; }
59
+ .sidebar .logo { font-weight: 900; font-size: 22px; color: var(--text); text-decoration: none; margin-bottom: 20px; display: flex; align-items: center; justify-content: center; width: 42px; height: 42px; }
60
+ .sidebar a, .sidebar button { width: 42px; height: 42px; border-radius: 50%; display: flex; align-items: center; justify-content: center; color: var(--text-secondary); text-decoration: none; border: none; background: none; cursor: pointer; margin: 2px 0; position: relative; }
61
+ .sidebar a:hover, .sidebar button:hover { background: var(--input-bg); color: var(--text); }
62
+ .sidebar a[title]:hover::after { content: attr(title); position: absolute; left: 54px; top: 50%; transform: translateY(-50%); background: var(--text); color: var(--bg); font-size: 12px; font-weight: 600; padding: 4px 10px; border-radius: 6px; white-space: nowrap; pointer-events: none; z-index: 100; }
63
+ .sidebar .avatar { width: 34px; height: 34px; border-radius: 50%; background: #0f1419; color: #fff; display: flex; align-items: center; justify-content: center; font-size: 13px; font-weight: 600; margin-top: auto; margin-bottom: 8px; }
64
+ .sidebar .post-btn { width: 42px; height: 42px; border-radius: 50%; background: var(--accent); color: #fff; border: none; cursor: pointer; display: flex; align-items: center; justify-content: center; margin-top: 12px; }
65
+ .sidebar .post-btn:hover { background: var(--accent-hover); }
66
+
67
+ .main { flex: 1; min-width: 0; border-right: 1px solid var(--border); }
68
+
69
+ .right { width: 220px; flex-shrink: 0; padding: 10px 12px; position: sticky; top: 0; height: 100vh; overflow-y: auto; font-size: 13px; display: none; }
70
+ .shell.show-right .right { display: block; }
71
+ .search-wrap { position: relative; margin-bottom: 16px; }
72
+ .search-icon { position: absolute; left: 12px; top: 50%; transform: translateY(-50%); color: var(--text-secondary); display: flex; }
73
+ .search { width: 100%; padding: 10px 12px 10px 36px; border: 1px solid var(--border); border-radius: 9999px; font-size: 14px; outline: none; background: var(--input-bg); }
74
+ .search:focus { border-color: var(--accent); background: var(--bg); box-shadow: 0 0 0 1px #7c3aed; }
75
+ .right-card { background: var(--bg-secondary); border-radius: 16px; margin-bottom: 16px; overflow: hidden; }
76
+ .right-card h3 { font-size: 15px; font-weight: 800; padding: 10px 12px; margin: 0; }
77
+ .trend { padding: 8px 12px; cursor: pointer; }
78
+ .trend:hover { background: var(--input-bg); }
79
+ .trend-label { font-size: 12px; color: var(--text-secondary); }
80
+ .trend-name { font-size: 15px; font-weight: 700; margin-top: 1px; }
81
+ .trend-count { font-size: 12px; color: var(--text-secondary); margin-top: 1px; }
82
+ .who-item { display: flex; align-items: center; gap: 8px; padding: 8px 12px; }
83
+ .who-item:hover { background: var(--input-bg); }
84
+ .who-avatar { width: 36px; height: 36px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 13px; font-weight: 600; color: #fff; flex-shrink: 0; }
85
+ .who-info { flex: 1; min-width: 0; }
86
+ .who-name { font-size: 14px; font-weight: 700; }
87
+ .who-handle { font-size: 13px; color: var(--text-secondary); }
88
+ .follow-btn { padding: 5px 14px; border-radius: 9999px; background: #0f1419; color: #fff; border: none; font-size: 13px; font-weight: 700; cursor: pointer; }
89
+ .follow-btn:hover { opacity: 0.85; }
90
+ .show-more { display: block; padding: 10px 12px; color: var(--accent); font-size: 13px; text-decoration: none; }
91
+ .show-more:hover { background: var(--input-bg); }
92
+
93
+ /* Weather widget */
94
+ .widget-weather { padding: 16px; }
95
+ .w-header { display: flex; justify-content: space-between; align-items: baseline; }
96
+ .w-city { font-size: 15px; font-weight: 700; }
97
+ .w-temp { font-size: 28px; font-weight: 800; color: var(--text); }
98
+ .w-desc { font-size: 13px; color: var(--text-secondary); margin-top: 2px; }
99
+ .w-forecast { display: flex; gap: 0; margin-top: 12px; }
100
+ .w-day { flex: 1; text-align: center; padding: 6px 0; }
101
+ .w-label { display: block; font-size: 12px; color: var(--text-secondary); }
102
+ .w-val { display: block; font-size: 14px; font-weight: 600; margin-top: 2px; }
103
+
104
+ /* Calendar widget */
105
+ .widget-calendar h3 { font-size: 15px; font-weight: 700; }
106
+ .cal-event { display: flex; gap: 10px; padding: 8px 16px; font-size: 13px; }
107
+ .cal-time { color: var(--text-secondary); font-weight: 600; width: 40px; flex-shrink: 0; }
108
+ .cal-name { color: var(--text); }
109
+
110
+ /* Quick apps widget */
111
+ .widget-apps h3 { font-size: 15px; font-weight: 700; }
112
+ .qapps-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 4px; padding: 4px 16px 8px; }
113
+ .qapp { display: flex; flex-direction: column; align-items: center; gap: 4px; padding: 10px 4px; border-radius: 8px; color: var(--text-secondary); text-decoration: none; font-size: 11px; }
114
+ .qapp:hover { background: var(--input-bg); color: var(--text); }
115
+ .show-more:hover { background: var(--input-bg); }
116
+
117
+ /* Mobile top bar + bottom nav */
118
+ .mobile-top { display: none; }
119
+ .mobile-bar { display: none; }
120
+ @media (max-width: 860px) { .right { display: none; } }
121
+ @media (max-width: 640px) {
122
+ .sidebar { display: none; }
123
+ .mobile-top { display: flex; position: sticky; top: 0; z-index: 50; background: var(--bg); border-bottom: 1px solid var(--border); padding: 8px 16px; align-items: center; gap: 12px; }
124
+ .mobile-top .m-avatar { width: 28px; height: 28px; border-radius: 50%; object-fit: cover; }
125
+ .mobile-top .m-title { font-weight: 800; font-size: 18px; color: var(--text); }
126
+ .mobile-top .m-spacer { flex: 1; }
127
+ .mobile-top .m-icon { color: var(--text-secondary); display: flex; align-items: center; justify-content: center; width: 36px; height: 36px; border-radius: 50%; text-decoration: none; }
128
+ .mobile-top .m-icon:hover { background: var(--input-bg); color: var(--text); }
129
+ .mobile-bar { display: flex; position: fixed; bottom: 0; left: 0; right: 0; z-index: 50; background: var(--bg); border-top: 1px solid var(--border); height: 50px; align-items: center; justify-content: space-around; }
130
+ .mobile-bar a { color: var(--text-secondary); display: flex; align-items: center; justify-content: center; width: 44px; height: 44px; border-radius: 50%; text-decoration: none; }
131
+ .mobile-bar a:hover { background: var(--input-bg); color: var(--text); }
132
+ .mobile-bar .post-fab { width: 42px; height: 42px; background: var(--accent); color: #fff; border-radius: 50%; }
133
+ .mobile-bar .post-fab:hover { background: var(--accent-hover); }
134
+ .mobile-bar .more-btn { width: 44px; height: 44px; border: none; background: none; color: var(--text-secondary); cursor: pointer; display: flex; align-items: center; justify-content: center; border-radius: 50%; }
135
+ .mobile-bar .more-btn:hover { background: var(--input-bg); color: var(--text); }
136
+
137
+ /* More menu overlay */
138
+ .more-menu { display: none; }
139
+ .more-menu.open { display: flex; position: fixed; inset: 0; z-index: 100; background: var(--overlay); align-items: flex-end; justify-content: center; }
140
+ .more-sheet { background: var(--bg); border-radius: 16px 16px 0 0; width: 100%; max-width: 480px; padding: 8px 0 20px; }
141
+ .more-handle { width: 36px; height: 4px; border-radius: 2px; background: #d0d0d0; margin: 0 auto 12px; }
142
+ .more-item { display: flex; align-items: center; gap: 14px; padding: 14px 24px; color: var(--text); text-decoration: none; font-size: 16px; font-weight: 500; }
143
+ .more-item:hover { background: var(--bg-secondary); }
144
+ .more-item svg { color: var(--text-secondary); }
145
+ .main { padding-bottom: 56px; }
146
+ }
147
+ `;
148
+
149
+ connectedCallback() {
150
+ super.connectedCallback();
151
+ const vp = document.querySelector('meta[name="viewport"]');
152
+ if (vp) vp.setAttribute('content', 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no');
153
+ }
154
+
155
+ render() {
156
+ return html`
157
+ <div class="mobile-top">
158
+ <a href="/" style="display:flex;align-items:center;gap:8px;text-decoration:none;color:inherit">
159
+ <img class="m-avatar" src="https://avatars.githubusercontent.com/u/3775924?v=4" alt="A">
160
+ <span class="m-title">Home</span>
161
+ </a>
162
+ <div class="m-spacer"></div>
163
+ <a class="m-icon" href="/settings">${svg.settings}</a>
164
+ <a class="m-icon" href="/notifications">${svg.bell}</a>
165
+ <a class="m-icon" href="/messages">${svg.message}</a>
166
+ </div>
167
+ <div class="shell">
168
+ <nav class="sidebar">
169
+ <a class="logo" href="/">N</a>
170
+ <a href="/" title="Home">${svg.home}</a>
171
+ <a href="/explore" title="Explore">${svg.explore}</a>
172
+ <a href="/notifications" title="Notifications">${svg.bell}</a>
173
+ <a href="/messages" title="Messages">${svg.message}</a>
174
+ <a href="/bookmarks" title="Bookmarks">${svg.bookmark}</a>
175
+ <a href="/apps" title="Apps">${svg.explore}</a>
176
+ <a href="/profile/aymen" title="Profile">${svg.user}</a>
177
+ <a href="/settings" title="Settings">${svg.settings}</a>
178
+ <img class="avatar" src="https://avatars.githubusercontent.com/u/3775924?v=4" alt="A">
179
+ </nav>
180
+ <main class="main"><slot></slot></main>
181
+ <aside class="right">
182
+ <div class="search-wrap">
183
+ <span class="search-icon">${svg.search}</span>
184
+ <input class="search" type="text" placeholder="Search">
185
+ </div>
186
+ <div class="right-card widget-weather">
187
+ <div class="w-header"><span class="w-city">Tunisia</span><span class="w-temp">24°</span></div>
188
+ <div class="w-desc">Sunny</div>
189
+ <div class="w-forecast">
190
+ <div class="w-day"><span class="w-label">Mon</span><span class="w-val">22°</span></div>
191
+ <div class="w-day"><span class="w-label">Tue</span><span class="w-val">25°</span></div>
192
+ <div class="w-day"><span class="w-label">Wed</span><span class="w-val">23°</span></div>
193
+ <div class="w-day"><span class="w-label">Thu</span><span class="w-val">26°</span></div>
194
+ </div>
195
+ </div>
196
+ <div class="right-card widget-calendar">
197
+ <h3>Today, Mar 22</h3>
198
+ <div class="cal-event"><span class="cal-time">10:00</span><span class="cal-name">Team standup</span></div>
199
+ <div class="cal-event"><span class="cal-time">14:00</span><span class="cal-name">Design review</span></div>
200
+ <div class="cal-event"><span class="cal-time">16:30</span><span class="cal-name">Deploy v2.1</span></div>
201
+ </div>
202
+ <div class="right-card widget-apps">
203
+ <h3>Quick Apps</h3>
204
+ <div class="qapps-grid">
205
+ <a class="qapp" href="/apps/notes"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg><span>Notes</span></a>
206
+ <a class="qapp" href="/apps/poll"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><line x1="18" y1="20" x2="18" y2="10"/><line x1="12" y1="20" x2="12" y2="4"/><line x1="6" y1="20" x2="6" y2="14"/></svg><span>Polls</span></a>
207
+ <a class="qapp" href="/apps/calendar"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg><span>Calendar</span></a>
208
+ <a class="qapp" href="/apps/timer"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg><span>Timer</span></a>
209
+ </div>
210
+ <a class="show-more" href="/apps">All apps</a>
211
+ </div>
212
+ <div class="right-card">
213
+ <h3>Trending</h3>
214
+ <div class="trend"><div class="trend-name">#WebComponents</div><div class="trend-count">1,250 posts</div></div>
215
+ <div class="trend"><div class="trend-name">#TypeScript</div><div class="trend-count">3,400 posts</div></div>
216
+ <div class="trend"><div class="trend-name">#DevOps</div><div class="trend-count">890 posts</div></div>
217
+ </div>
218
+ </aside>
219
+ </div>
220
+ <nav class="mobile-bar">
221
+ <a href="/">${svg.home}</a>
222
+ <a href="/explore">${svg.explore}</a>
223
+ <a class="post-fab" href="/new">${svg.plus}</a>
224
+ <a href="/messages">${svg.message}</a>
225
+ <button class="more-btn" @click=${() => { const m = this.shadowRoot?.querySelector('.more-menu') as HTMLElement; if (m) m.classList.toggle('open'); }}><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="12" cy="12" r="1"/><circle cx="12" cy="5" r="1"/><circle cx="12" cy="19" r="1"/></svg></button>
226
+ </nav>
227
+ <div class="more-menu" @click=${(e: Event) => { (e.currentTarget as HTMLElement).classList.remove('open'); }}>
228
+ <div class="more-sheet" @click=${(e: Event) => e.stopPropagation()}>
229
+ <div class="more-handle"></div>
230
+ <a class="more-item" href="/profile/aymen">${svg.user} <span>Profile</span></a>
231
+ <a class="more-item" href="/bookmarks">${svg.bookmark} <span>Bookmarks</span></a>
232
+ <a class="more-item" href="/apps">${svg.explore} <span>Apps</span></a>
233
+ <a class="more-item" href="/notifications">${svg.bell} <span>Notifications</span></a>
234
+ <a class="more-item" href="/settings">${svg.settings} <span>Settings</span></a>
235
+ </div>
236
+ </div>
237
+ `;
238
+ }
239
+ }