@acms-subir/core 0.1.0 → 0.3.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 (237) hide show
  1. package/CHANGELOG.md +106 -0
  2. package/cli/common.mjs +35 -0
  3. package/cli/legacy.mjs +317 -0
  4. package/cli/main.mjs +66 -0
  5. package/cli/palette.mjs +4 -0
  6. package/cli/preview.mjs +50 -0
  7. package/cli/remote.mjs +55 -0
  8. package/cli/themes.mjs +127 -0
  9. package/cli.mjs +2 -315
  10. package/deploy/Caddyfile +8 -0
  11. package/deploy/Dockerfile +34 -0
  12. package/deploy/compose.https.yml +21 -0
  13. package/deploy/compose.yml +46 -0
  14. package/docs/theme-authoring.md +103 -0
  15. package/email-runner.mjs +32 -0
  16. package/integration.mjs +193 -2
  17. package/ops-check.mjs +27 -0
  18. package/package.json +35 -12
  19. package/src/actions/index.ts +744 -38
  20. package/src/components/AiCleaner.tsx +291 -0
  21. package/src/components/AiSettings.tsx +575 -0
  22. package/src/components/CommandPalette.tsx +200 -0
  23. package/src/components/ContentList.astro +246 -0
  24. package/src/components/DesignAccess.tsx +182 -0
  25. package/src/components/DesignManager.tsx +455 -47
  26. package/src/components/EmailSettings.tsx +521 -0
  27. package/src/components/EventPage.astro +178 -0
  28. package/src/components/LegalPage.astro +27 -0
  29. package/src/components/LinksManager.tsx +216 -97
  30. package/src/components/NavManager.tsx +83 -38
  31. package/src/components/ThemeScript.astro +50 -0
  32. package/src/components/ThemeSections.astro +57 -0
  33. package/src/components/ThemeSectionsEditor.tsx +513 -0
  34. package/src/components/booking/BookingFlow.astro +150 -0
  35. package/src/components/booking/HoursEditor.astro +63 -0
  36. package/src/components/builder/PageBuilder.tsx +10 -2
  37. package/src/components/editor/EventPanel.tsx +401 -0
  38. package/src/components/editor/FormatDock.tsx +81 -0
  39. package/src/components/editor/LivePane.tsx +122 -0
  40. package/src/components/editor/MobileToolbar.tsx +93 -0
  41. package/src/components/editor/PostEditor.tsx +826 -271
  42. package/src/components/editor/PreviewOverlay.tsx +242 -43
  43. package/src/components/editor/extensions/calendar.tsx +51 -7
  44. package/src/components/editor/extensions/cards.tsx +17 -1
  45. package/src/components/editor/extensions/codeBlock.tsx +1 -1
  46. package/src/components/editor/extensions/events.tsx +153 -0
  47. package/src/components/editor/extensions/infoCards.tsx +16 -1
  48. package/src/components/editor/extensions/mediaCards.tsx +36 -2
  49. package/src/components/editor/extensions/slash.tsx +77 -9
  50. package/src/components/editor/extensions/toc.tsx +89 -0
  51. package/src/components/editor/extensions/toggle.tsx +70 -0
  52. package/src/components/site/Pager.astro +24 -0
  53. package/src/components/site/PostCard.astro +64 -0
  54. package/src/components/site/TagLabel.astro +11 -0
  55. package/src/layouts/Admin.astro +289 -49
  56. package/src/layouts/Site.astro +331 -57
  57. package/src/lib/agent-api.ts +13 -0
  58. package/src/lib/agent-discovery.ts +40 -0
  59. package/src/lib/agent.ts +114 -0
  60. package/src/lib/ai-config.ts +58 -0
  61. package/src/lib/ai-schema.ts +22 -0
  62. package/src/lib/ai.ts +90 -0
  63. package/src/lib/analytics.ts +121 -0
  64. package/src/lib/api-keys.ts +65 -0
  65. package/src/lib/api.ts +52 -0
  66. package/src/lib/auth.ts +25 -5
  67. package/src/lib/backup/content.ts +36 -0
  68. package/src/lib/backup/unzip.ts +12 -1
  69. package/src/lib/backup/zip.ts +1 -1
  70. package/src/lib/booking/agent.ts +108 -0
  71. package/src/lib/booking/availability.ts +221 -0
  72. package/src/lib/booking/emails.ts +115 -0
  73. package/src/lib/booking/forms.ts +77 -0
  74. package/src/lib/booking/service.ts +277 -0
  75. package/src/lib/booking/view.ts +54 -0
  76. package/src/lib/calendar/agent-schema.ts +48 -0
  77. package/src/lib/calendar/agent.ts +136 -0
  78. package/src/lib/calendar/api.ts +81 -0
  79. package/src/lib/calendar/cards.ts +102 -0
  80. package/src/lib/calendar/config.ts +52 -0
  81. package/src/lib/calendar/data.ts +130 -0
  82. package/src/lib/calendar/events.ts +261 -0
  83. package/src/lib/calendar/feeds.ts +43 -0
  84. package/src/lib/calendar/hooks.ts +38 -0
  85. package/src/lib/calendar/http.ts +30 -0
  86. package/src/lib/calendar/ics-write.ts +120 -0
  87. package/src/lib/calendar/ics.ts +15 -5
  88. package/src/lib/calendar/import.ts +203 -0
  89. package/src/lib/calendar/render.ts +120 -37
  90. package/src/lib/calendar/rrule.ts +149 -0
  91. package/src/lib/calendar/seo.ts +48 -0
  92. package/src/lib/design-service.ts +58 -0
  93. package/src/lib/designDiff.ts +145 -0
  94. package/src/lib/designPreview.ts +112 -0
  95. package/src/lib/documents/cleanup.ts +31 -0
  96. package/src/lib/documents/extract.ts +154 -0
  97. package/src/lib/documents/vendor.d.ts +4 -0
  98. package/src/lib/drafts.ts +131 -0
  99. package/src/lib/dynamicBlocks.ts +79 -7
  100. package/src/lib/email/accounts.ts +55 -0
  101. package/src/lib/email/config.ts +86 -0
  102. package/src/lib/email/migration.ts +24 -0
  103. package/src/lib/email/providers.ts +89 -0
  104. package/src/lib/email/service.ts +78 -0
  105. package/src/lib/email/store.ts +167 -0
  106. package/src/lib/email/templates.ts +18 -0
  107. package/src/lib/email/types.ts +32 -0
  108. package/src/lib/email/webhooks.ts +87 -0
  109. package/src/lib/email.ts +15 -0
  110. package/src/lib/env.ts +1 -1
  111. package/src/lib/legal/index.ts +25 -0
  112. package/src/lib/mobile/bookings.ts +73 -0
  113. package/src/lib/mobile/calendar.ts +85 -0
  114. package/src/lib/mobile/content.ts +85 -0
  115. package/src/lib/mobile/http.ts +63 -0
  116. package/src/lib/mobile/route.ts +20 -0
  117. package/src/lib/ops.ts +8 -0
  118. package/src/lib/preview.ts +21 -1
  119. package/src/lib/repo/migrations.ts +254 -0
  120. package/src/lib/repo/sql.ts +956 -46
  121. package/src/lib/repo/types.ts +140 -3
  122. package/src/lib/scheduler.ts +25 -0
  123. package/src/lib/schema.ts +362 -0
  124. package/src/lib/search-i18n.ts +3 -0
  125. package/src/lib/socialIcons.ts +32 -0
  126. package/src/lib/tagColor.ts +11 -0
  127. package/src/lib/theme.ts +59 -40
  128. package/src/lib/themeTokens.ts +174 -0
  129. package/src/lib/umami.ts +108 -0
  130. package/src/lib/webhooks.ts +139 -0
  131. package/src/middleware.ts +36 -3
  132. package/src/pages/404.astro +23 -0
  133. package/src/pages/[...slug].astro +10 -5
  134. package/src/pages/account/[action].astro +53 -0
  135. package/src/pages/admin/ai.astro +12 -0
  136. package/src/pages/admin/c/[collection]/[id].astro +14 -4
  137. package/src/pages/admin/c/[collection]/index.astro +127 -91
  138. package/src/pages/admin/calendar/bookings.astro +400 -0
  139. package/src/pages/admin/calendar/calendars.astro +265 -0
  140. package/src/pages/admin/calendar/events/[id].astro +56 -0
  141. package/src/pages/admin/calendar/events/index.astro +408 -0
  142. package/src/pages/admin/calendar/index.ts +4 -0
  143. package/src/pages/admin/calendar/integrations.astro +221 -0
  144. package/src/pages/admin/calendar/resources/[id].astro +180 -0
  145. package/src/pages/admin/calendar/resources/index.astro +106 -0
  146. package/src/pages/admin/calendar/resources/link.astro +196 -0
  147. package/src/pages/admin/design.astro +16 -4
  148. package/src/pages/admin/email.astro +13 -0
  149. package/src/pages/admin/index.astro +237 -87
  150. package/src/pages/admin/links.astro +3 -3
  151. package/src/pages/admin/login.astro +14 -10
  152. package/src/pages/admin/navigation.astro +8 -2
  153. package/src/pages/admin/pages/[id].astro +14 -3
  154. package/src/pages/admin/pages/index.astro +41 -47
  155. package/src/pages/admin/settings.astro +475 -276
  156. package/src/pages/admin/tags.astro +4 -4
  157. package/src/pages/admin/users.astro +38 -28
  158. package/src/pages/api/agent/docs.ts +33 -0
  159. package/src/pages/api/agent/index.ts +21 -0
  160. package/src/pages/api/ai/clean.ts +13 -0
  161. package/src/pages/api/ai/draft.ts +14 -0
  162. package/src/pages/api/ai/settings.ts +53 -0
  163. package/src/pages/api/ai/test.ts +12 -0
  164. package/src/pages/api/backup.ts +8 -21
  165. package/src/pages/api/calendar/availability.ts +35 -0
  166. package/src/pages/api/calendar/bookings/[id].ts +49 -0
  167. package/src/pages/api/calendar/bookings/index.ts +73 -0
  168. package/src/pages/api/calendar/calendars.ts +35 -0
  169. package/src/pages/api/calendar/docs.ts +64 -0
  170. package/src/pages/api/calendar/events/[id].ts +63 -0
  171. package/src/pages/api/calendar/events/index.ts +112 -0
  172. package/src/pages/api/calendar/resources.ts +34 -0
  173. package/src/pages/api/design/capabilities.ts +10 -0
  174. package/src/pages/api/design/index.ts +58 -0
  175. package/src/pages/api/design/keys.ts +24 -0
  176. package/src/pages/api/design/media.ts +32 -0
  177. package/src/pages/api/email/run.ts +11 -0
  178. package/src/pages/api/email/settings.ts +61 -0
  179. package/src/pages/api/email/webhooks/[provider].ts +4 -0
  180. package/src/pages/api/mcp.ts +59 -0
  181. package/src/pages/api/media/upload.ts +2 -2
  182. package/src/pages/api/ops/status.ts +39 -0
  183. package/src/pages/api/restore.ts +27 -37
  184. package/src/pages/api/search.ts +18 -4
  185. package/src/pages/api/v1/availability.ts +7 -0
  186. package/src/pages/api/v1/bookings.ts +17 -0
  187. package/src/pages/api/v1/calendars.ts +7 -0
  188. package/src/pages/api/v1/content/[id].ts +7 -0
  189. package/src/pages/api/v1/content/index.ts +7 -0
  190. package/src/pages/api/v1/events.ts +7 -0
  191. package/src/pages/api/v1/resources.ts +7 -0
  192. package/src/pages/author/[slug].astro +43 -35
  193. package/src/pages/blog/[slug].astro +140 -66
  194. package/src/pages/blog/index.astro +95 -30
  195. package/src/pages/blog/tag/[slug].astro +45 -40
  196. package/src/pages/book/[resource]/[link].astro +70 -0
  197. package/src/pages/book/[resource]/index.astro +63 -0
  198. package/src/pages/book/[resource].ics.ts +38 -0
  199. package/src/pages/book/index.astro +47 -0
  200. package/src/pages/book/manage/[token].astro +108 -0
  201. package/src/pages/calendar/[slug].ics.ts +27 -0
  202. package/src/pages/calendar/feed.ics.ts +15 -0
  203. package/src/pages/calendar/index.astro +191 -0
  204. package/src/pages/embed/book/[resource]/[link].astro +33 -0
  205. package/src/pages/embed/calendar/[slug].astro +43 -0
  206. package/src/pages/events/[slug].astro +74 -0
  207. package/src/pages/events/[slug].ics.ts +25 -0
  208. package/src/pages/index.astro +9 -3
  209. package/src/pages/legal/privacy.astro +4 -0
  210. package/src/pages/legal/terms.astro +4 -0
  211. package/src/pages/links.astro +33 -7
  212. package/src/pages/llms.txt.ts +4 -0
  213. package/src/pages/preview/[type]/[id].astro +34 -8
  214. package/src/pages/search.astro +40 -20
  215. package/src/pages/sitemap.xml.ts +16 -1
  216. package/src/styles/admin.css +928 -105
  217. package/src/styles/cards.css +101 -60
  218. package/src/styles/editor.css +532 -102
  219. package/src/styles/email-settings.css +117 -0
  220. package/src/styles/sections.css +12 -0
  221. package/src/styles/site.css +938 -278
  222. package/src/styles/theme-editor.css +135 -0
  223. package/src/theme-sdk/contract.d.mts +619 -0
  224. package/src/theme-sdk/contract.mjs +80 -0
  225. package/src/theme-sdk/index.ts +15 -0
  226. package/src/theme-sdk/tokens.d.mts +148 -0
  227. package/src/theme-sdk/tokens.mjs +52 -0
  228. package/src/themes/editorial/templates/home.astro +4 -4
  229. package/src/themes/editorial/templates/post-minimal.astro +2 -2
  230. package/src/themes/editorial/theme.css +22 -37
  231. package/src/themes/editorial/theme.json +23 -3
  232. package/src/themes/field/theme.json +26 -9
  233. package/src/themes/ink/theme.json +30 -12
  234. package/src/themes/night/theme.css +4 -7
  235. package/src/themes/night/theme.json +27 -10
  236. package/src/virtual.d.ts +14 -0
  237. package/src/worker.ts +5 -1
@@ -2,6 +2,10 @@
2
2
  import { getRepo } from '../lib/repo';
3
3
  import { BRAND } from '../lib/brand';
4
4
  import { CONTENT } from '../lib/content';
5
+ import { CALENDAR, bookingsEnabled } from '../lib/calendar/config';
6
+ import { validateSession, SESSION_COOKIE, OWNER_ENV } from '../lib/auth';
7
+ import ThemeScript from '../components/ThemeScript.astro';
8
+ import CommandPalette from '../components/CommandPalette';
5
9
  import '../styles/admin.css';
6
10
 
7
11
  interface Props {
@@ -14,12 +18,24 @@ interface Props {
14
18
  const { title, section, wide = false } = Astro.props;
15
19
 
16
20
  const repo = await getRepo(Astro.locals);
17
- const [pages, posts, links, home] = await Promise.all([
21
+ const sessionUserId = (await validateSession(Astro.cookies.get(SESSION_COOKIE)?.value, Astro.locals));
22
+ const [pages, posts, links, home, settings, sessionUser] = await Promise.all([
18
23
  repo.listPages(),
19
24
  repo.listPosts(),
20
25
  repo.listLinks(),
21
26
  repo.getHomePage(),
27
+ repo.getSettings(),
28
+ sessionUserId && sessionUserId !== OWNER_ENV ? repo.getUser(sessionUserId) : Promise.resolve(null),
22
29
  ]);
30
+ // pending booking requests, shown as a count on the Bookings nav item
31
+ const pendingBookings = bookingsEnabled() ? (await repo.listBookings({ from: Date.now(), statuses: ['pending'] })).length : 0;
32
+ const me = {
33
+ name: sessionUser?.name || 'Admin',
34
+ role: sessionUserId === OWNER_ENV ? 'Owner' : sessionUser?.role === 'owner' ? 'Owner' : 'Editor',
35
+ avatar: sessionUser?.avatar || '',
36
+ };
37
+ const initials = me.name.split(/\s+/).map((w) => w[0] ?? '').join('').slice(0, 2).toUpperCase();
38
+ const siteName = settings.siteTitle || BRAND.name;
23
39
 
24
40
  const icons: Record<string, string> = {
25
41
  dashboard: 'M3 3h7v9H3zM14 3h7v5h-7zM14 12h7v9h-7zM3 16h7v5H3z',
@@ -30,34 +46,107 @@ const icons: Record<string, string> = {
30
46
  links: 'M10 14a4 4 0 0 0 6 0l3-3a4 4 0 0 0-6-6l-1.5 1.5M14 10a4 4 0 0 0-6 0l-3 3a4 4 0 0 0 6 6l1.5-1.5',
31
47
  tags: 'M3 12V3h9l9 9-9 9zM7.5 7.5h0',
32
48
  users: 'M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2M9 11a4 4 0 1 0 0-8 4 4 0 0 0 0 8M22 21v-2a4 4 0 0 0-3-3.87M15 3.13a4 4 0 0 1 0 7.75',
33
- settings: 'M12 8a4 4 0 1 0 0 8 4 4 0 0 0 0-8zM19 12a7 7 0 0 0-.1-1.2l2-1.6-2-3.4-2.4 1a7 7 0 0 0-2-1.2L14 3h-4l-.5 2.6a7 7 0 0 0-2 1.2l-2.4-1-2 3.4 2 1.6A7 7 0 0 0 5 12a7 7 0 0 0 .1 1.2l-2 1.6 2 3.4 2.4-1a7 7 0 0 0 2 1.2L10 21h4l.5-2.6a7 7 0 0 0 2-1.2l2.4 1 2-3.4-2-1.6A7 7 0 0 0 19 12z',
49
+ settings: 'M4 7h10M18 7h2M4 17h4M12 17h8M14 5v4M8 15v4',
34
50
  design: 'M12 3c3.2 3.7 6 7 6 10.2a6 6 0 1 1-12 0C6 10 8.8 6.7 12 3zM9.5 14.5a2.5 2.5 0 0 0 5 0',
35
51
  navigation: 'M4 6h16M4 12h10M4 18h13M19 11l3 3-3 3',
52
+ search: 'M11 4a7 7 0 1 0 0 14 7 7 0 0 0 0-14zM20 20l-4-4',
53
+ write: 'M4 20h4L19 9l-4-4L4 16zM13.5 6.5l4 4',
54
+ chev: 'M6 9l6 6 6-6',
55
+ chevr: 'M9 6l6 6-6 6',
56
+ updown: 'M8 9l4-4 4 4M8 15l4 4 4-4',
57
+ side: 'M4 4h16v16H4zM9 4v16',
58
+ sun: 'M12 8a4 4 0 1 0 0 8 4 4 0 0 0 0-8zM12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4',
59
+ moon: 'M20 14.5A8 8 0 0 1 9.5 4 8 8 0 1 0 20 14.5z',
60
+ monitor: 'M3 4h18v12H3zM8 20h8M12 16v4',
61
+ globe: 'M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18zM3 12h18M12 3c2.5 2.4 3.8 5.6 3.8 9S14.5 18.6 12 21c-2.5-2.4-3.8-5.6-3.8-9S9.5 5.4 12 3z',
62
+ out: 'M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4M10 17l5-5-5-5M15 12H3',
63
+ more: 'M5 12h.01M12 12h.01M19 12h.01',
64
+ user: 'M12 12a4 4 0 1 0 0-8 4 4 0 0 0 0 8zM4 21a8 8 0 0 1 16 0',
65
+ calendar: 'M3 5.5h18v15H3zM3 10h18M8 3v4M16 3v4',
66
+ calendars: 'M4 6h.01M4 12h.01M4 18h.01M8 6h12M8 12h12M8 18h12',
67
+ bookings: 'M12 21a9 9 0 1 0 0-18 9 9 0 0 0 0 18zM12 7v5l3 2',
68
+ resources: 'M3 21V9l9-5 9 5v12M9 21v-6h6v6',
69
+ plug: 'M9 7V3M15 7V3M6 7h12v4a6 6 0 0 1-12 0zM12 17v4',
36
70
  };
37
71
 
38
72
  // one sidebar entry per configured collection, in config order
39
73
  const byCollection = new Map<string, number>();
40
74
  for (const p of posts) byCollection.set(p.collection, (byCollection.get(p.collection) ?? 0) + 1);
41
- const collectionItems = Object.entries(CONTENT.collections).map(([key, c]) => ({
75
+ const collections = Object.entries(CONTENT.collections);
76
+ const firstCollection = collections[0]?.[0] ?? 'posts';
77
+ const collectionItems = collections.map(([key, c]) => ({
42
78
  key: `c:${key}`,
43
79
  label: c.plural,
44
80
  href: `/admin/c/${key}`,
45
81
  icon: icons.posts,
46
- count: byCollection.get(key) ?? 0,
82
+ count: byCollection.get(key) ?? 0 as number | null,
47
83
  }));
48
84
 
49
- const items = [
50
- { key: 'dashboard', label: 'Dashboard', href: '/admin', icon: icons.dashboard, count: null },
51
- { key: 'home', label: 'Home Page', href: home ? `/admin/pages/${home.id}` : '/admin/pages', icon: icons.home, count: null },
52
- { key: 'pages', label: 'Pages', href: '/admin/pages', icon: icons.pages, count: pages.length },
53
- ...collectionItems,
54
- { key: 'tags', label: 'Tags', href: '/admin/tags', icon: icons.tags, count: null },
55
- { key: 'media', label: 'Media', href: '/admin/media', icon: icons.media, count: null },
56
- { key: 'links', label: 'Links', href: '/admin/links', icon: icons.links, count: links.length },
57
- { key: 'users', label: 'Users', href: '/admin/users', icon: icons.users, count: null },
58
- { key: 'design', label: 'Design', href: '/admin/design', icon: icons.design, count: null },
59
- { key: 'navigation', label: 'Navigation', href: '/admin/navigation', icon: icons.navigation, count: null },
60
- { key: 'settings', label: 'Settings', href: '/admin/settings', icon: icons.settings, count: null },
85
+ type Item = { key: string; label: string; href: string; icon: string; count: number | null; attention?: boolean };
86
+ const groups: { id: string; label: string; items: Item[] }[] = [
87
+ {
88
+ id: 'main',
89
+ label: '',
90
+ items: [
91
+ { key: 'dashboard', label: 'Overview', href: '/admin', icon: icons.dashboard, count: null },
92
+ { key: 'home', label: 'Home page', href: home ? `/admin/pages/${home.id}` : '/admin/pages', icon: icons.home, count: null },
93
+ ],
94
+ },
95
+ {
96
+ id: 'content',
97
+ label: 'Content',
98
+ items: [
99
+ ...collectionItems,
100
+ { key: 'pages', label: 'Pages', href: '/admin/pages', icon: icons.pages, count: pages.length },
101
+ { key: 'tags', label: 'Tags', href: '/admin/tags', icon: icons.tags, count: null },
102
+ { key: 'media', label: 'Media', href: '/admin/media', icon: icons.media, count: null },
103
+ ],
104
+ },
105
+ ...(CALENDAR.enabled
106
+ ? [{
107
+ id: 'calendar',
108
+ label: 'Calendar',
109
+ items: [
110
+ { key: 'calendar:events', label: 'Events', href: '/admin/calendar/events', icon: icons.calendar, count: null },
111
+ ...(bookingsEnabled()
112
+ ? [
113
+ { key: 'calendar:bookings', label: 'Bookings', href: '/admin/calendar/bookings', icon: icons.bookings, count: pendingBookings || null, attention: pendingBookings > 0 },
114
+ { key: 'calendar:resources', label: 'Resources', href: '/admin/calendar/resources', icon: icons.resources, count: null },
115
+ ]
116
+ : []),
117
+ { key: 'calendar:calendars', label: 'Calendars', href: '/admin/calendar/calendars', icon: icons.calendars, count: null },
118
+ { key: 'calendar:integrations', label: 'API & webhooks', href: '/admin/calendar/integrations', icon: icons.plug, count: null },
119
+ ],
120
+ }]
121
+ : []),
122
+ {
123
+ id: 'site',
124
+ label: 'Site',
125
+ items: [
126
+ { key: 'design', label: 'Design', href: '/admin/design', icon: icons.design, count: null },
127
+ { key: 'navigation', label: 'Navigation', href: '/admin/navigation', icon: icons.navigation, count: null },
128
+ { key: 'links', label: 'Links page', href: '/admin/links', icon: icons.links, count: links.length },
129
+ ],
130
+ },
131
+ {
132
+ id: 'team',
133
+ label: 'Team',
134
+ items: [
135
+ { key: 'users', label: 'Users', href: '/admin/users', icon: icons.users, count: null },
136
+ { key: 'ai', label: 'Document import', href: '/admin/ai', icon: icons.posts, count: null },
137
+ { key: 'settings', label: 'Settings', href: '/admin/settings', icon: icons.settings, count: null },
138
+ ],
139
+ },
140
+ ];
141
+ const activeGroup = groups.find((g) => g.items.some((i) => i.key === section));
142
+ const crumbGroup = activeGroup?.label ?? '';
143
+
144
+ // bottom tab bar (phones): Home · Content · Links · Search · More
145
+ const tabActive = section === 'dashboard' ? 'home' : section.startsWith('c:') || section === 'pages' ? 'content' : section === 'links' ? 'links' : 'more';
146
+ const themeOpts = [
147
+ { key: 'light', label: 'Light', icon: icons.sun },
148
+ { key: 'dark', label: 'Dark', icon: icons.moon },
149
+ { key: 'system', label: 'Auto', icon: icons.monitor },
61
150
  ];
62
151
  ---
63
152
 
@@ -65,55 +154,206 @@ const items = [
65
154
  <html lang="en">
66
155
  <head>
67
156
  <meta charset="utf-8" />
68
- <meta name="viewport" content="width=device-width, initial-scale=1" />
157
+ <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
69
158
  <meta name="robots" content="noindex" />
70
159
  <title>{title} · {BRAND.name} admin</title>
160
+ <ThemeScript />
161
+ <script is:inline>
162
+ // restore the collapsed rail before paint
163
+ try { if (localStorage.getItem('acms-rail') === '1') document.documentElement.classList.add('rail-pending'); } catch {}
164
+ </script>
71
165
  <link rel="preconnect" href="https://fonts.googleapis.com" />
72
166
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
73
167
  </head>
74
168
  <body>
75
- <div class="shell">
76
- <aside class="sidebar">
169
+ <div class="shell" id="acms-shell">
170
+ <aside class="sidebar" aria-label="Admin navigation">
77
171
  <div class="brand">
78
- <svg width="22" height="22" viewBox="0 0 24 24" role="img" aria-label={BRAND.name}>
79
- <rect x="1" y="3" width="22" height="4.4" rx="1.9" fill="#e7e8ec" />
80
- <rect x="1" y="10" width="12.5" height="4.4" rx="1.9" fill="#ff8a4c" />
81
- <rect x="1" y="17" width="17.5" height="4.4" rx="1.9" fill="#e7e8ec" opacity="0.48" />
172
+ <svg width="20" height="20" viewBox="0 0 24 24" role="img" aria-label={BRAND.name}>
173
+ <rect x="1" y="3" width="22" height="4.4" rx="1.9" fill="currentColor" opacity="0.85" />
174
+ <rect x="1" y="10" width="12.5" height="4.4" rx="1.9" fill="var(--accent)" />
175
+ <rect x="1" y="17" width="17.5" height="4.4" rx="1.9" fill="currentColor" opacity="0.35" />
82
176
  </svg>
83
- <div>
84
- <div class="brand-name">{BRAND.name}</div>
85
- <div class="brand-sub">v{BRAND.version} · {BRAND.stack}</div>
86
- </div>
177
+ <span class="brand-name" title={`v${BRAND.version} · ${BRAND.stack}`}>{siteName}</span>
178
+ <button class="icon-btn" type="button" id="acms-rail-toggle" aria-label="Collapse sidebar" title="Collapse sidebar">
179
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d={icons.side} /></svg>
180
+ </button>
87
181
  </div>
88
- {items.map((it) => (
89
- <a class={`nav-item ${section === it.key ? 'active' : ''}`} href={it.href}>
90
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
91
- <path d={it.icon} />
92
- </svg>
93
- {it.label}
94
- {it.count !== null && <span class="count">{it.count}</span>}
95
- </a>
96
- ))}
97
- <div class="nav-sep" />
98
- <a class="nav-item" href="/" target="_blank" rel="noopener">
99
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
100
- <path d="M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18zM3 12h18M12 3c2.5 2.4 3.8 5.6 3.8 9S14.5 18.6 12 21c-2.5-2.4-3.8-5.6-3.8-9S9.5 5.4 12 3z" />
101
- </svg>
102
- View site
182
+ <button class="side-search" type="button" data-cmdk title="Search or jump to (⌘K)">
183
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><path d={icons.search} /></svg>
184
+ <span>Search</span><kbd>⌘K</kbd>
185
+ </button>
186
+ <a class="nav-item" href={`/admin/c/${firstCollection}?new=1`} title={`Write a ${(CONTENT.collections[firstCollection] ?? CONTENT.posts).singular.toLowerCase()}`}>
187
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d={icons.write} /></svg>
188
+ <span>Write a {(CONTENT.collections[firstCollection] ?? CONTENT.posts).singular.toLowerCase()}</span>
103
189
  </a>
190
+ <nav class="nav-scroll">
191
+ {groups.map((g) => {
192
+ const links = g.items.map((it) => (
193
+ <a class={`nav-item ${section === it.key ? 'active' : ''}`} href={it.href} title={it.label} aria-current={section === it.key ? 'page' : undefined}>
194
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d={it.icon} /></svg>
195
+ <span>{it.label}</span>
196
+ {it.count !== null && <span class={`count${it.attention ? ' is-attention' : ''}`}>{it.count}</span>}
197
+ </a>
198
+ ));
199
+ return g.label ? (
200
+ <details class="nav-group" data-group={g.id} open>
201
+ <summary>
202
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d={icons.chev} /></svg>
203
+ {g.label}
204
+ </summary>
205
+ {links}
206
+ </details>
207
+ ) : <div>{links}</div>;
208
+ })}
209
+ </nav>
104
210
  <div class="sidebar-foot">
105
- Signed in · <a href="/admin/logout">Sign out</a>
211
+ <button class="account" type="button" id="acms-account" aria-haspopup="menu" aria-expanded="false">
212
+ <span class="avatar">{me.avatar ? <img src={me.avatar} alt="" /> : initials}</span>
213
+ <span class="who"><b>{me.name}</b><small>{me.role}</small></span>
214
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d={icons.updown} /></svg>
215
+ </button>
216
+ <div class="menu-pop" id="acms-account-menu" role="menu" hidden>
217
+ <div class="menu-label">Appearance</div>
218
+ <div class="seg block" role="group" aria-label="Theme">
219
+ {themeOpts.map((t) => (
220
+ <button type="button" data-set-theme={t.key}>
221
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d={t.icon} /></svg>{t.label}
222
+ </button>
223
+ ))}
224
+ </div>
225
+ <div class="menu-sep" />
226
+ <a class="menu-item" href="/" target="_blank" rel="noopener" role="menuitem">
227
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d={icons.globe} /></svg>View site
228
+ </a>
229
+ <a class="menu-item" href="/admin/logout" role="menuitem">
230
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d={icons.out} /></svg>Sign out
231
+ </a>
232
+ </div>
106
233
  </div>
107
234
  </aside>
235
+
108
236
  <div class="main">
109
- <div class="topbar">
110
- <div class="crumb">{BRAND.name} / <b>{title}</b></div>
111
- <a class="btn sm ghost" href="/" target="_blank" rel="noopener">Open site ↗</a>
237
+ <main class="panel" id="acms-panel">
238
+ <div class="topbar">
239
+ <div class="crumb">
240
+ {crumbGroup && <span>{crumbGroup}</span>}
241
+ {crumbGroup && <span class="sl">/</span>}
242
+ <b>{title}</b>
243
+ </div>
244
+ <div class="topbar-actions">
245
+ <slot name="topbar" />
246
+ <a class="btn sm ghost" href="/" target="_blank" rel="noopener">View site ↗</a>
247
+ </div>
248
+ </div>
249
+ <div class={`content ${wide ? 'content-wide' : ''}`}>
250
+ <slot />
251
+ </div>
252
+ </main>
253
+ </div>
254
+ </div>
255
+
256
+ <nav class="tabbar" aria-label="Sections">
257
+ <a href="/admin" class={tabActive === 'home' ? 'active' : ''}>
258
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d={icons.home} /></svg>Home
259
+ </a>
260
+ <a href={`/admin/c/${firstCollection}`} class={tabActive === 'content' ? 'active' : ''}>
261
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d={icons.posts} /></svg>Content
262
+ </a>
263
+ <a href="/admin/links" class={tabActive === 'links' ? 'active' : ''}>
264
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d={icons.links} /></svg>Links
265
+ </a>
266
+ <button type="button" data-cmdk>
267
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d={icons.search} /></svg>Search
268
+ </button>
269
+ <button type="button" class={tabActive === 'more' ? 'active' : ''} id="acms-more-open">
270
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d={icons.more} /></svg>More
271
+ </button>
272
+ </nav>
273
+
274
+ <dialog class="sheet" id="acms-more" aria-label="More">
275
+ <div class="sheet-grab" />
276
+ <div class="sheet-body">
277
+ <div class="sheet-title">More</div>
278
+ <div class="glist">
279
+ <div class="glist-row">
280
+ <span class="avatar" style="width:34px;height:34px;font-size:12px">{me.avatar ? <img src={me.avatar} alt="" /> : initials}</span>
281
+ <span class="grow"><b>{me.name}</b><br /><small class="hint">{me.role} · {siteName}</small></span>
282
+ </div>
112
283
  </div>
113
- <div class={`content ${wide ? 'content-wide' : ''}`}>
114
- <slot />
284
+ <div class="sheet-label">Appearance</div>
285
+ <div class="seg" role="group" aria-label="Theme" style="display:flex;margin-bottom:12px">
286
+ {themeOpts.map((t) => (
287
+ <button type="button" data-set-theme={t.key} style="flex:1;padding:8px 4px">
288
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d={t.icon} /></svg>{t.label}
289
+ </button>
290
+ ))}
291
+ </div>
292
+ {groups.map((g) => (
293
+ <div class="glist">
294
+ {g.items
295
+ .filter((it) => !['dashboard', `c:${firstCollection}`, 'links'].includes(it.key))
296
+ .map((it) => (
297
+ <a href={it.href} aria-current={section === it.key ? 'page' : undefined}>
298
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d={it.icon} /></svg>
299
+ <span class="grow">{it.label}</span>
300
+ {it.count !== null && <span class="count">{it.count}</span>}
301
+ <svg class="chev" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d={icons.chevr} /></svg>
302
+ </a>
303
+ ))}
304
+ </div>
305
+ ))}
306
+ <div class="glist">
307
+ <a href="/" target="_blank" rel="noopener">
308
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d={icons.globe} /></svg>
309
+ <span class="grow">View site</span>
310
+ </a>
311
+ <a href="/admin/logout" class="danger">
312
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d={icons.out} /></svg>
313
+ <span class="grow">Sign out</span>
314
+ </a>
115
315
  </div>
116
316
  </div>
117
- </div>
317
+ </dialog>
318
+
319
+ <CommandPalette client:idle collections={collections.map(([key, c]) => ({ key, singular: c.singular, plural: c.plural }))} />
320
+
321
+ <script>
322
+ const shell = document.getElementById('acms-shell');
323
+ // collapsed rail, remembered per browser
324
+ if (document.documentElement.classList.contains('rail-pending')) shell?.classList.add('is-rail');
325
+ document.getElementById('acms-rail-toggle')?.addEventListener('click', () => {
326
+ const on = !shell?.classList.contains('is-rail');
327
+ shell?.classList.toggle('is-rail', on);
328
+ try { localStorage.setItem('acms-rail', on ? '1' : '0'); } catch {}
329
+ });
330
+
331
+ // nav groups remember open/closed
332
+ document.querySelectorAll<HTMLDetailsElement>('details.nav-group').forEach((d) => {
333
+ const k = `acms-nav-${d.dataset.group}`;
334
+ try { if (localStorage.getItem(k) === '0' && !d.querySelector('.active')) d.open = false; } catch {}
335
+ d.addEventListener('toggle', () => { try { localStorage.setItem(k, d.open ? '1' : '0'); } catch {} });
336
+ });
337
+
338
+ // account menu
339
+ const acct = document.getElementById('acms-account');
340
+ const menu = document.getElementById('acms-account-menu');
341
+ const close = () => { if (menu) menu.hidden = true; acct?.setAttribute('aria-expanded', 'false'); };
342
+ acct?.addEventListener('click', (e) => {
343
+ e.stopPropagation();
344
+ if (!menu) return;
345
+ menu.hidden = !menu.hidden;
346
+ acct.setAttribute('aria-expanded', String(!menu.hidden));
347
+ });
348
+ document.addEventListener('click', (e) => {
349
+ if (menu && !menu.hidden && !(e.target instanceof Node && menu.contains(e.target))) close();
350
+ });
351
+ document.addEventListener('keydown', (e) => { if (e.key === 'Escape') close(); });
352
+
353
+ // More sheet (phones)
354
+ const sheet = document.getElementById('acms-more') as HTMLDialogElement | null;
355
+ document.getElementById('acms-more-open')?.addEventListener('click', () => sheet?.showModal());
356
+ sheet?.addEventListener('click', (e) => { if (e.target === sheet) sheet.close(); });
357
+ </script>
118
358
  </body>
119
359
  </html>