nebula-cms 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 (241) hide show
  1. package/.claude/settings.local.json +42 -0
  2. package/.github/workflows/ci.yml +31 -0
  3. package/.mcp.json +12 -0
  4. package/.prettierignore +5 -0
  5. package/.prettierrc.cjs +22 -0
  6. package/AGENTS.md +183 -0
  7. package/LICENSE +201 -0
  8. package/README.md +128 -0
  9. package/package.json +74 -0
  10. package/playground/.claude/settings.local.json +5 -0
  11. package/playground/astro.config.mjs +7 -0
  12. package/playground/node_modules/.bin/astro +21 -0
  13. package/playground/node_modules/.bin/rollup +21 -0
  14. package/playground/node_modules/.bin/tsc +21 -0
  15. package/playground/node_modules/.bin/tsserver +21 -0
  16. package/playground/node_modules/.bin/vite +21 -0
  17. package/playground/node_modules/.vite/_svelte_metadata.json +1 -0
  18. package/playground/node_modules/.vite/deps/@astrojs_svelte_client__js.js +80 -0
  19. package/playground/node_modules/.vite/deps/@astrojs_svelte_client__js.js.map +7 -0
  20. package/playground/node_modules/.vite/deps/_metadata.json +184 -0
  21. package/playground/node_modules/.vite/deps/astro___aria-query.js +6776 -0
  22. package/playground/node_modules/.vite/deps/astro___aria-query.js.map +7 -0
  23. package/playground/node_modules/.vite/deps/astro___axobject-query.js +3754 -0
  24. package/playground/node_modules/.vite/deps/astro___axobject-query.js.map +7 -0
  25. package/playground/node_modules/.vite/deps/astro___html-escaper.js +34 -0
  26. package/playground/node_modules/.vite/deps/astro___html-escaper.js.map +7 -0
  27. package/playground/node_modules/.vite/deps/chunk-AJXJMYAF.js +0 -0
  28. package/playground/node_modules/.vite/deps/chunk-AJXJMYAF.js.map +7 -0
  29. package/playground/node_modules/.vite/deps/chunk-ALJIOON6.js +1005 -0
  30. package/playground/node_modules/.vite/deps/chunk-ALJIOON6.js.map +7 -0
  31. package/playground/node_modules/.vite/deps/chunk-BUSYA2B4.js +8 -0
  32. package/playground/node_modules/.vite/deps/chunk-BUSYA2B4.js.map +7 -0
  33. package/playground/node_modules/.vite/deps/chunk-CNYJBM5F.js +21 -0
  34. package/playground/node_modules/.vite/deps/chunk-CNYJBM5F.js.map +7 -0
  35. package/playground/node_modules/.vite/deps/chunk-DBPNBGEI.js +223 -0
  36. package/playground/node_modules/.vite/deps/chunk-DBPNBGEI.js.map +7 -0
  37. package/playground/node_modules/.vite/deps/chunk-G3C2FXJT.js +204 -0
  38. package/playground/node_modules/.vite/deps/chunk-G3C2FXJT.js.map +7 -0
  39. package/playground/node_modules/.vite/deps/chunk-GKDKFWC5.js +27 -0
  40. package/playground/node_modules/.vite/deps/chunk-GKDKFWC5.js.map +7 -0
  41. package/playground/node_modules/.vite/deps/chunk-HNCLEOC5.js +4376 -0
  42. package/playground/node_modules/.vite/deps/chunk-HNCLEOC5.js.map +7 -0
  43. package/playground/node_modules/.vite/deps/chunk-JICYXBFU.js +688 -0
  44. package/playground/node_modules/.vite/deps/chunk-JICYXBFU.js.map +7 -0
  45. package/playground/node_modules/.vite/deps/chunk-KCUTL6DD.js +5099 -0
  46. package/playground/node_modules/.vite/deps/chunk-KCUTL6DD.js.map +7 -0
  47. package/playground/node_modules/.vite/deps/chunk-ZP4UNCSN.js +23 -0
  48. package/playground/node_modules/.vite/deps/chunk-ZP4UNCSN.js.map +7 -0
  49. package/playground/node_modules/.vite/deps/chunk-ZREFNRZZ.js +148 -0
  50. package/playground/node_modules/.vite/deps/chunk-ZREFNRZZ.js.map +7 -0
  51. package/playground/node_modules/.vite/deps/package.json +3 -0
  52. package/playground/node_modules/.vite/deps/smol-toml.js +843 -0
  53. package/playground/node_modules/.vite/deps/smol-toml.js.map +7 -0
  54. package/playground/node_modules/.vite/deps/svelte.js +55 -0
  55. package/playground/node_modules/.vite/deps/svelte.js.map +7 -0
  56. package/playground/node_modules/.vite/deps/svelte___clsx.js +9 -0
  57. package/playground/node_modules/.vite/deps/svelte___clsx.js.map +7 -0
  58. package/playground/node_modules/.vite/deps/svelte_animate.js +57 -0
  59. package/playground/node_modules/.vite/deps/svelte_animate.js.map +7 -0
  60. package/playground/node_modules/.vite/deps/svelte_attachments.js +15 -0
  61. package/playground/node_modules/.vite/deps/svelte_attachments.js.map +7 -0
  62. package/playground/node_modules/.vite/deps/svelte_easing.js +67 -0
  63. package/playground/node_modules/.vite/deps/svelte_easing.js.map +7 -0
  64. package/playground/node_modules/.vite/deps/svelte_events.js +11 -0
  65. package/playground/node_modules/.vite/deps/svelte_events.js.map +7 -0
  66. package/playground/node_modules/.vite/deps/svelte_internal.js +5 -0
  67. package/playground/node_modules/.vite/deps/svelte_internal.js.map +7 -0
  68. package/playground/node_modules/.vite/deps/svelte_internal_client.js +402 -0
  69. package/playground/node_modules/.vite/deps/svelte_internal_client.js.map +7 -0
  70. package/playground/node_modules/.vite/deps/svelte_internal_disclose-version.js +10 -0
  71. package/playground/node_modules/.vite/deps/svelte_internal_disclose-version.js.map +7 -0
  72. package/playground/node_modules/.vite/deps/svelte_internal_flags_async.js +8 -0
  73. package/playground/node_modules/.vite/deps/svelte_internal_flags_async.js.map +7 -0
  74. package/playground/node_modules/.vite/deps/svelte_internal_flags_legacy.js +8 -0
  75. package/playground/node_modules/.vite/deps/svelte_internal_flags_legacy.js.map +7 -0
  76. package/playground/node_modules/.vite/deps/svelte_internal_flags_tracing.js +8 -0
  77. package/playground/node_modules/.vite/deps/svelte_internal_flags_tracing.js.map +7 -0
  78. package/playground/node_modules/.vite/deps/svelte_legacy.js +35 -0
  79. package/playground/node_modules/.vite/deps/svelte_legacy.js.map +7 -0
  80. package/playground/node_modules/.vite/deps/svelte_motion.js +545 -0
  81. package/playground/node_modules/.vite/deps/svelte_motion.js.map +7 -0
  82. package/playground/node_modules/.vite/deps/svelte_reactivity.js +29 -0
  83. package/playground/node_modules/.vite/deps/svelte_reactivity.js.map +7 -0
  84. package/playground/node_modules/.vite/deps/svelte_reactivity_window.js +127 -0
  85. package/playground/node_modules/.vite/deps/svelte_reactivity_window.js.map +7 -0
  86. package/playground/node_modules/.vite/deps/svelte_store.js +103 -0
  87. package/playground/node_modules/.vite/deps/svelte_store.js.map +7 -0
  88. package/playground/node_modules/.vite/deps/svelte_transition.js +208 -0
  89. package/playground/node_modules/.vite/deps/svelte_transition.js.map +7 -0
  90. package/playground/package.json +16 -0
  91. package/playground/pnpm-lock.yaml +3167 -0
  92. package/playground/src/content/authors/jane-doe.json +8 -0
  93. package/playground/src/content/config/build.toml +2 -0
  94. package/playground/src/content/courses/web-fundamentals.json +29 -0
  95. package/playground/src/content/docs/advanced.mdx +6 -0
  96. package/playground/src/content/docs/intro.md +6 -0
  97. package/playground/src/content/guides/getting-started.mdx +6 -0
  98. package/playground/src/content/posts/hello-world.md +7 -0
  99. package/playground/src/content/products/t-shirt.json +16 -0
  100. package/playground/src/content/recipes/pancakes.mdoc +8 -0
  101. package/playground/src/content/settings/site.yml +2 -0
  102. package/playground/src/content.config.ts +198 -0
  103. package/playground/src/env.d.ts +1 -0
  104. package/playground/src/pages/index.astro +11 -0
  105. package/playground/src/pages/nebula.astro +14 -0
  106. package/pnpm-workspace.yaml +2 -0
  107. package/scripts/subset-icons.mjs +178 -0
  108. package/src/astro/index.ts +295 -0
  109. package/src/client/Admin.svelte +283 -0
  110. package/src/client/components/BackendPicker.svelte +291 -0
  111. package/src/client/components/DraftChip.svelte +46 -0
  112. package/src/client/components/MetadataForm.svelte +56 -0
  113. package/src/client/components/ThemeToggle.svelte +18 -0
  114. package/src/client/components/dialogs/DeleteDraftDialog.svelte +51 -0
  115. package/src/client/components/dialogs/FilenameDialog.svelte +129 -0
  116. package/src/client/components/editor/EditorPane.svelte +227 -0
  117. package/src/client/components/editor/EditorTabs.svelte +81 -0
  118. package/src/client/components/editor/EditorToolbar.svelte +131 -0
  119. package/src/client/components/editor/FormatSelector.svelte +66 -0
  120. package/src/client/components/editor/Toolbar.svelte +17 -0
  121. package/src/client/components/fields/ArrayField.svelte +339 -0
  122. package/src/client/components/fields/ArrayItem.svelte +325 -0
  123. package/src/client/components/fields/BooleanField.svelte +114 -0
  124. package/src/client/components/fields/DateField.svelte +82 -0
  125. package/src/client/components/fields/EnumField.svelte +74 -0
  126. package/src/client/components/fields/FieldWrapper.svelte +96 -0
  127. package/src/client/components/fields/NumberField.svelte +99 -0
  128. package/src/client/components/fields/ObjectField.svelte +121 -0
  129. package/src/client/components/fields/SchemaField.svelte +107 -0
  130. package/src/client/components/fields/StringField.svelte +104 -0
  131. package/src/client/components/sidebar/AdminSidebar.svelte +339 -0
  132. package/src/client/components/sidebar/AdminSidebarSort.svelte +123 -0
  133. package/src/client/css/a11y.css +14 -0
  134. package/src/client/css/btn.css +113 -0
  135. package/src/client/css/dialog.css +29 -0
  136. package/src/client/css/field-input.css +39 -0
  137. package/src/client/css/reset.css +59 -0
  138. package/src/client/css/theme.css +77 -0
  139. package/src/client/index.ts +1 -0
  140. package/src/client/js/drafts/merge.svelte.ts +121 -0
  141. package/src/client/js/drafts/ops.svelte.ts +227 -0
  142. package/src/client/js/drafts/storage.ts +108 -0
  143. package/src/client/js/drafts/workers/diff.ts +40 -0
  144. package/src/client/js/editor/editor.svelte.ts +343 -0
  145. package/src/client/js/editor/languages.ts +98 -0
  146. package/src/client/js/editor/link-wrap.ts +45 -0
  147. package/src/client/js/editor/markdown-shortcuts.ts +261 -0
  148. package/src/client/js/handlers/admin.ts +246 -0
  149. package/src/client/js/state/dialogs.svelte.ts +35 -0
  150. package/src/client/js/state/router.svelte.ts +156 -0
  151. package/src/client/js/state/schema.svelte.ts +140 -0
  152. package/src/client/js/state/state.svelte.ts +334 -0
  153. package/src/client/js/state/theme.svelte.ts +173 -0
  154. package/src/client/js/storage/adapter.ts +102 -0
  155. package/src/client/js/storage/client.ts +150 -0
  156. package/src/client/js/storage/db.ts +36 -0
  157. package/src/client/js/storage/fsa.ts +110 -0
  158. package/src/client/js/storage/github.ts +297 -0
  159. package/src/client/js/storage/storage.ts +83 -0
  160. package/src/client/js/storage/workers/frontmatter.ts +320 -0
  161. package/src/client/js/storage/workers/storage.ts +177 -0
  162. package/src/client/js/storage/workers/toml-parser.ts +106 -0
  163. package/src/client/js/storage/workers/yaml-parser.ts +132 -0
  164. package/src/client/js/utils/file-types.ts +192 -0
  165. package/src/client/js/utils/format.ts +16 -0
  166. package/src/client/js/utils/frontmatter.ts +38 -0
  167. package/src/client/js/utils/schema-utils.ts +295 -0
  168. package/src/client/js/utils/slug.ts +18 -0
  169. package/src/client/js/utils/sort.ts +84 -0
  170. package/src/client/js/utils/stable-stringify.ts +27 -0
  171. package/src/client/js/utils/url-utils.ts +38 -0
  172. package/src/types.ts +25 -0
  173. package/src/virtual.d.ts +22 -0
  174. package/svelte.config.js +4 -0
  175. package/tests/astro/build.test.ts +63 -0
  176. package/tests/astro/index.test.ts +689 -0
  177. package/tests/client/components/Admin.test.ts +446 -0
  178. package/tests/client/components/BackendPicker.test.ts +239 -0
  179. package/tests/client/components/DraftChip.test.ts +53 -0
  180. package/tests/client/components/MetadataForm.test.ts +164 -0
  181. package/tests/client/components/dialogs/DeleteDraftDialog.test.ts +91 -0
  182. package/tests/client/components/dialogs/FilenameDialog.test.ts +209 -0
  183. package/tests/client/components/dialogs/dialog-stubs.ts +19 -0
  184. package/tests/client/components/editor/EditorPane.test.ts +100 -0
  185. package/tests/client/components/editor/EditorTabs.test.ts +253 -0
  186. package/tests/client/components/editor/EditorToolbar.test.ts +252 -0
  187. package/tests/client/components/editor/fixtures.ts +31 -0
  188. package/tests/client/components/fields/ArrayField.test.ts +197 -0
  189. package/tests/client/components/fields/BooleanField.test.ts +206 -0
  190. package/tests/client/components/fields/DateField.test.ts +210 -0
  191. package/tests/client/components/fields/EnumField.test.ts +246 -0
  192. package/tests/client/components/fields/NumberField.test.ts +240 -0
  193. package/tests/client/components/fields/ObjectField.test.ts +157 -0
  194. package/tests/client/components/fields/SchemaField.test.ts +190 -0
  195. package/tests/client/components/fields/StringField.test.ts +223 -0
  196. package/tests/client/components/sidebar/AdminSidebar.test.ts +285 -0
  197. package/tests/client/components/sidebar/AdminSidebarSort.test.ts +135 -0
  198. package/tests/client/components/sidebar/sort-mock.ts +23 -0
  199. package/tests/client/js/drafts/fixtures.ts +22 -0
  200. package/tests/client/js/drafts/merge.test.ts +282 -0
  201. package/tests/client/js/drafts/ops.test.ts +658 -0
  202. package/tests/client/js/drafts/storage.test.ts +200 -0
  203. package/tests/client/js/drafts/workers/diff.test.ts +165 -0
  204. package/tests/client/js/editor/editor.test.ts +616 -0
  205. package/tests/client/js/editor/link-wrap.test.ts +225 -0
  206. package/tests/client/js/editor/markdown-shortcuts.test.ts +370 -0
  207. package/tests/client/js/handlers/admin.test.ts +467 -0
  208. package/tests/client/js/state/router.test.ts +619 -0
  209. package/tests/client/js/state/schema.test.ts +266 -0
  210. package/tests/client/js/state/state.test.ts +328 -0
  211. package/tests/client/js/storage/adapter.test.ts +115 -0
  212. package/tests/client/js/storage/client.test.ts +250 -0
  213. package/tests/client/js/storage/db.test.ts +59 -0
  214. package/tests/client/js/storage/fsa.test.ts +284 -0
  215. package/tests/client/js/storage/github.test.ts +349 -0
  216. package/tests/client/js/storage/mock-port.ts +95 -0
  217. package/tests/client/js/storage/storage.test.ts +77 -0
  218. package/tests/client/js/storage/workers/frontmatter.test.ts +479 -0
  219. package/tests/client/js/storage/workers/storage.test.ts +299 -0
  220. package/tests/client/js/storage/workers/toml-parser.test.ts +169 -0
  221. package/tests/client/js/storage/workers/yaml-parser.test.ts +168 -0
  222. package/tests/client/js/utils/file-types.test.ts +268 -0
  223. package/tests/client/js/utils/frontmatter.test.ts +87 -0
  224. package/tests/client/js/utils/schema-utils.test.ts +318 -0
  225. package/tests/client/js/utils/slug.test.ts +58 -0
  226. package/tests/client/js/utils/sort.test.ts +276 -0
  227. package/tests/client/js/utils/stable-stringify.test.ts +68 -0
  228. package/tests/client/js/utils/url-utils.test.ts +70 -0
  229. package/tests/e2e/backend-connection.test.ts +301 -0
  230. package/tests/e2e/draft-lifecycle.test.ts +388 -0
  231. package/tests/e2e/editing.test.ts +355 -0
  232. package/tests/e2e/github-adapter.test.ts +330 -0
  233. package/tests/e2e/helpers/mock-adapter.ts +166 -0
  234. package/tests/e2e/helpers/test-app.ts +155 -0
  235. package/tests/e2e/navigation.test.ts +358 -0
  236. package/tests/e2e/publishing.test.ts +345 -0
  237. package/tests/e2e/unsaved-changes.test.ts +317 -0
  238. package/tests/setup.ts +2 -0
  239. package/tests/stubs/codemirror.ts +197 -0
  240. package/tsconfig.json +19 -0
  241. package/vitest.config.ts +178 -0
@@ -0,0 +1,8 @@
1
+ {
2
+ "name": "Jane Doe",
3
+ "bio": "Technical writer and web developer.",
4
+ "social": {
5
+ "github": "janedoe",
6
+ "twitter": "janedoe"
7
+ }
8
+ }
@@ -0,0 +1,2 @@
1
+ name = "production"
2
+ debug = false
@@ -0,0 +1,29 @@
1
+ {
2
+ "title": "Web Fundamentals",
3
+ "instructor": {
4
+ "name": "Dr. Smith",
5
+ "credentials": [
6
+ {
7
+ "institution": "MIT",
8
+ "year": 2015
9
+ }
10
+ ]
11
+ },
12
+ "modules": [
13
+ {
14
+ "title": "HTML Basics",
15
+ "lessons": [
16
+ {
17
+ "title": "Document Structure",
18
+ "duration": 30,
19
+ "resources": [
20
+ {
21
+ "type": "video",
22
+ "url": "https://example.com/html-structure"
23
+ }
24
+ ]
25
+ }
26
+ ]
27
+ }
28
+ ]
29
+ }
@@ -0,0 +1,6 @@
1
+ ---
2
+ title: Advanced Usage
3
+ category: advanced
4
+ ---
5
+
6
+ This guide covers advanced topics.
@@ -0,0 +1,6 @@
1
+ ---
2
+ title: Introduction
3
+ category: basics
4
+ ---
5
+
6
+ Welcome to the documentation.
@@ -0,0 +1,6 @@
1
+ ---
2
+ title: Getting Started
3
+ order: 1
4
+ ---
5
+
6
+ Welcome to the **getting started** guide.
@@ -0,0 +1,7 @@
1
+ ---
2
+ title: Hello World
3
+ date: 2026-01-15
4
+ author: jane-doe
5
+ ---
6
+
7
+ A sample post for development testing.
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "Logo T-Shirt",
3
+ "price": 29.99,
4
+ "variants": [
5
+ {
6
+ "sku": "TS-BLK-M",
7
+ "color": "black",
8
+ "sizes": ["S", "M", "L", "XL"]
9
+ },
10
+ {
11
+ "sku": "TS-WHT-M",
12
+ "color": "white",
13
+ "sizes": ["S", "M", "L"]
14
+ }
15
+ ]
16
+ }
@@ -0,0 +1,8 @@
1
+ ---
2
+ title: Fluffy Pancakes
3
+ servings: 4
4
+ ---
5
+
6
+ # Fluffy Pancakes
7
+
8
+ Mix dry ingredients, add wet, cook on griddle.
@@ -0,0 +1,2 @@
1
+ siteName: My Blog
2
+ maxPosts: 10
@@ -0,0 +1,198 @@
1
+ import { defineCollection, reference } from 'astro:content';
2
+ import { glob } from 'astro/loaders';
3
+ import { z } from 'astro/zod';
4
+
5
+ //////////////////////////////
6
+ // Content collection definitions for the playground.
7
+ //
8
+ // These exercise a range of schema shapes: flat fields, cross-collection references, nested objects, and deeply nested arrays of objects.
9
+ //////////////////////////////
10
+
11
+ // Simple flat schema with a cross-collection reference
12
+ const posts = defineCollection({
13
+ loader: glob({ base: './src/content/posts', pattern: '**/*.md' }),
14
+ schema: z
15
+ .object({
16
+ title: z.string(),
17
+ date: z.date(),
18
+ draft: z.boolean().optional(),
19
+ author: reference('authors'),
20
+ })
21
+ .meta({
22
+ title: 'Posts',
23
+ description: 'Blog posts',
24
+ files: ['md'],
25
+ }),
26
+ });
27
+
28
+ // Nested object — social has optional string fields
29
+ const authors = defineCollection({
30
+ loader: glob({ base: './src/content/authors', pattern: '**/*.json' }),
31
+ schema: z
32
+ .object({
33
+ name: z.string(),
34
+ bio: z.string(),
35
+ social: z.object({
36
+ twitter: z.string().optional(),
37
+ github: z.string().optional(),
38
+ website: z.string().optional(),
39
+ }),
40
+ })
41
+ .meta({
42
+ title: 'Authors',
43
+ description: 'Those who write',
44
+ files: ['json'],
45
+ }),
46
+ });
47
+
48
+ // Array of nested objects, each containing a nested array
49
+ const products = defineCollection({
50
+ loader: glob({ base: './src/content/products', pattern: '**/*.json' }),
51
+ schema: z
52
+ .object({
53
+ name: z.string(),
54
+ price: z.number(),
55
+ variants: z.array(
56
+ z.object({
57
+ sku: z.string(),
58
+ color: z.string(),
59
+ sizes: z.array(z.string()),
60
+ }),
61
+ ),
62
+ })
63
+ .meta({
64
+ title: 'Products',
65
+ description: 'Things to buy',
66
+ files: ['json'],
67
+ }),
68
+ });
69
+
70
+ // Deep nesting: object with array, array of objects with nested arrays of objects
71
+ const courses = defineCollection({
72
+ loader: glob({ base: './src/content/courses', pattern: '**/*.json' }),
73
+ schema: z
74
+ .object({
75
+ title: z.string(),
76
+ instructor: z.object({
77
+ name: z.string(),
78
+ credentials: z.array(
79
+ z.object({
80
+ institution: z.string(),
81
+ year: z.number(),
82
+ }),
83
+ ),
84
+ }),
85
+ modules: z.array(
86
+ z.object({
87
+ title: z.string(),
88
+ lessons: z.array(
89
+ z.object({
90
+ title: z.string(),
91
+ duration: z.number(),
92
+ resources: z.array(
93
+ z.object({
94
+ type: z.string(),
95
+ url: z.string(),
96
+ }),
97
+ ),
98
+ }),
99
+ ),
100
+ }),
101
+ ),
102
+ })
103
+ .meta({
104
+ title: 'Courses',
105
+ description: 'Things to learn',
106
+ files: ['json'],
107
+ }),
108
+ });
109
+
110
+ // How-to guides authored in MDX
111
+ const guides = defineCollection({
112
+ loader: glob({ base: './src/content/guides', pattern: '**/*.mdx' }),
113
+ schema: z
114
+ .object({
115
+ title: z.string(),
116
+ order: z.number(),
117
+ })
118
+ .meta({
119
+ title: 'Guides',
120
+ description: 'How-to guides in MDX',
121
+ files: ['mdx'],
122
+ }),
123
+ });
124
+
125
+ // Cooking recipes authored in Markdoc
126
+ const recipes = defineCollection({
127
+ loader: glob({
128
+ base: './src/content/recipes',
129
+ pattern: '**/*.{mdoc,markdoc}',
130
+ }),
131
+ schema: z
132
+ .object({
133
+ title: z.string(),
134
+ servings: z.number(),
135
+ })
136
+ .meta({
137
+ title: 'Recipes',
138
+ description: 'Cooking recipes in Markdoc',
139
+ files: ['markdoc'],
140
+ }),
141
+ });
142
+
143
+ // Site configuration stored as YAML
144
+ const settings = defineCollection({
145
+ loader: glob({ base: './src/content/settings', pattern: '**/*.{yml,yaml}' }),
146
+ schema: z
147
+ .object({
148
+ siteName: z.string(),
149
+ maxPosts: z.number(),
150
+ })
151
+ .meta({
152
+ title: 'Settings',
153
+ description: 'Site configuration in YAML',
154
+ files: ['yaml'],
155
+ }),
156
+ });
157
+
158
+ // Build configuration stored as TOML
159
+ const config = defineCollection({
160
+ loader: glob({ base: './src/content/config', pattern: '**/*.toml' }),
161
+ schema: z
162
+ .object({
163
+ name: z.string(),
164
+ debug: z.boolean(),
165
+ })
166
+ .meta({
167
+ title: 'Config',
168
+ description: 'Build configuration in TOML',
169
+ files: ['toml'],
170
+ }),
171
+ });
172
+
173
+ // Documentation pages supporting both MD and MDX
174
+ const docs = defineCollection({
175
+ loader: glob({ base: './src/content/docs', pattern: '**/*.{md,mdx}' }),
176
+ schema: z
177
+ .object({
178
+ title: z.string(),
179
+ category: z.string(),
180
+ })
181
+ .meta({
182
+ title: 'Documentation',
183
+ description: 'Docs in MD and MDX',
184
+ files: ['md', 'mdx'],
185
+ }),
186
+ });
187
+
188
+ export const collections = {
189
+ posts,
190
+ authors,
191
+ products,
192
+ courses,
193
+ guides,
194
+ recipes,
195
+ settings,
196
+ config,
197
+ docs,
198
+ };
@@ -0,0 +1 @@
1
+ /// <reference types="nebula-cms/virtual" />
@@ -0,0 +1,11 @@
1
+ <html lang="en">
2
+ <head>
3
+ <meta charset="utf-8" />
4
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
5
+ <title>Nebula CMS playground</title>
6
+ </head>
7
+ <body>
8
+ <h1>Nebula CMS</h1>
9
+ <a href="/nebula">Nebula Admin Page</a>
10
+ </body>
11
+ </html>
@@ -0,0 +1,14 @@
1
+ ---
2
+ import Admin from 'nebula-cms/client';
3
+ ---
4
+
5
+ <html lang="en">
6
+ <head>
7
+ <meta charset="utf-8" />
8
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
9
+ <title>nebula-cms playground</title>
10
+ </head>
11
+ <body>
12
+ <Admin client:only="svelte" />
13
+ </body>
14
+ </html>
@@ -0,0 +1,2 @@
1
+ packages:
2
+ - playground
@@ -0,0 +1,178 @@
1
+ /*
2
+ * Fetches a subsetted Material Symbols Outlined woff2 font from Google Fonts
3
+ * and inlines it as base64 in a self-contained CSS file. Run this script
4
+ * whenever the icon set changes to regenerate src/client/css/icons.css.
5
+ *
6
+ * Usage: node scripts/subset-icons.mjs
7
+ */
8
+
9
+ import { writeFileSync, mkdirSync } from 'fs';
10
+ import { fileURLToPath } from 'url';
11
+ import { dirname, join } from 'path';
12
+
13
+ //////////////////////////////
14
+ // Icon set definition
15
+ //////////////////////////////
16
+
17
+ // Icons used across the admin UI — keep sorted alphabetically.
18
+ const ICON_NAMES = [
19
+ 'add',
20
+ 'arrow_downward',
21
+ 'arrow_upward',
22
+ 'brightness_auto',
23
+ 'chevron_right',
24
+ 'close',
25
+ 'dark_mode',
26
+ 'drag_indicator',
27
+ 'hourglass_arrow_down',
28
+ 'hourglass_arrow_up',
29
+ 'info',
30
+ 'light_mode',
31
+ 'logout',
32
+ 'sort_by_alpha',
33
+ ];
34
+
35
+ //////////////////////////////
36
+ // Path resolution
37
+ //////////////////////////////
38
+
39
+ const __filename = fileURLToPath(import.meta.url);
40
+ const __dirname = dirname(__filename);
41
+
42
+ // Absolute path where the generated CSS will be written.
43
+ const OUTPUT_PATH = join(__dirname, '../src/client/css/icons.css');
44
+
45
+ //////////////////////////////
46
+ // Font fetch helpers
47
+ //////////////////////////////
48
+
49
+ /**
50
+ * Builds the Google Fonts CSS2 API URL for the subsetted icon font.
51
+ * @param {string[]} names - Sorted array of icon ligature names
52
+ * @return {string} The fully-formed Google Fonts API URL
53
+ */
54
+ function buildFontsURL(names) {
55
+ const joined = names.sort().join(',');
56
+ return `https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined&icon_names=${joined}&display=block`;
57
+ }
58
+
59
+ /**
60
+ * Fetches the Google Fonts CSS stylesheet for the icon subset.
61
+ * @param {string} url - The Google Fonts API URL to fetch
62
+ * @return {Promise<string>} The CSS text returned by Google Fonts
63
+ */
64
+ async function fetchFontCSS(url) {
65
+ // Spoof a modern browser UA — Google Fonts returns woff2 for modern UAs.
66
+ const res = await fetch(url, {
67
+ headers: {
68
+ 'User-Agent':
69
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
70
+ },
71
+ });
72
+
73
+ if (!res.ok) {
74
+ throw new Error(
75
+ `Google Fonts API returned ${res.status}: ${res.statusText}`,
76
+ );
77
+ }
78
+
79
+ return res.text();
80
+ }
81
+
82
+ /**
83
+ * Extracts the woff2 font binary URL from the Google Fonts CSS response.
84
+ * Google Fonts returns unquoted urls in the form: url(https://fonts.gstatic.com/...)
85
+ * The URL does not end in .woff2 — it is a signed CDN path — so we match on
86
+ * the format hint that follows it instead.
87
+ * @param {string} css - The CSS text returned by Google Fonts
88
+ * @return {string} The absolute woff2 URL
89
+ */
90
+ function extractWoff2URL(css) {
91
+ // Match unquoted url(...) followed by format('woff2')
92
+ const match = css.match(/url\((https:\/\/[^)]+)\)\s+format\('woff2'\)/);
93
+ if (!match) {
94
+ throw new Error('Could not find woff2 URL in Google Fonts CSS response');
95
+ }
96
+ return match[1];
97
+ }
98
+
99
+ /**
100
+ * Downloads the woff2 binary and returns it as a base64-encoded string.
101
+ * @param {string} url - The absolute URL to the woff2 font file
102
+ * @return {Promise<string>} Base64-encoded woff2 binary data
103
+ */
104
+ async function fetchWoff2AsBase64(url) {
105
+ const res = await fetch(url);
106
+
107
+ if (!res.ok) {
108
+ throw new Error(
109
+ `Font binary fetch returned ${res.status}: ${res.statusText}`,
110
+ );
111
+ }
112
+
113
+ const arrayBuffer = await res.arrayBuffer();
114
+ return Buffer.from(arrayBuffer).toString('base64');
115
+ }
116
+
117
+ //////////////////////////////
118
+ // CSS generation
119
+ //////////////////////////////
120
+
121
+ /**
122
+ * Generates the full icons.css content with an inlined base64 woff2 font.
123
+ * @param {string} base64Font - The base64-encoded woff2 font data
124
+ * @return {string} Complete CSS file content
125
+ */
126
+ function generateCSS(base64Font) {
127
+ return `/* Material Symbols Outlined, Subsetted
128
+ * Icons: ${ICON_NAMES.sort().join(', ')}
129
+ */
130
+
131
+ @font-face {
132
+ font-family: 'Material Symbols Outlined';
133
+ font-style: normal;
134
+ font-weight: 400;
135
+ font-display: block;
136
+ src: url('data:font/woff2;base64,${base64Font}') format('woff2');
137
+ }
138
+
139
+ .icon {
140
+ font-family: 'Material Symbols Outlined';
141
+ font-weight: normal;
142
+ font-style: normal;
143
+ font-size: 1.5rem;
144
+ line-height: 1;
145
+ letter-spacing: normal;
146
+ text-transform: none;
147
+ display: inline-block;
148
+ white-space: nowrap;
149
+ word-wrap: normal;
150
+ direction: ltr;
151
+ -webkit-font-smoothing: antialiased;
152
+ text-rendering: optimizeLegibility;
153
+ -moz-osx-font-smoothing: grayscale;
154
+ font-feature-settings: 'liga';
155
+ }
156
+ `;
157
+ }
158
+
159
+ //////////////////////////////
160
+ // Entry point
161
+ //////////////////////////////
162
+
163
+ const fontsURL = buildFontsURL(ICON_NAMES);
164
+ console.log(`Fetching icon subset CSS from Google Fonts...`);
165
+ console.log(`URL: ${fontsURL}`);
166
+
167
+ const fontCSS = await fetchFontCSS(fontsURL);
168
+ const woff2URL = extractWoff2URL(fontCSS);
169
+ console.log(`Found woff2 URL: ${woff2URL}`);
170
+
171
+ console.log(`Downloading woff2 binary...`);
172
+ const base64Font = await fetchWoff2AsBase64(woff2URL);
173
+ console.log(`Base64-encoded font: ${base64Font.length} characters`);
174
+
175
+ const css = generateCSS(base64Font);
176
+ mkdirSync(dirname(OUTPUT_PATH), { recursive: true });
177
+ writeFileSync(OUTPUT_PATH, css, 'utf8');
178
+ console.log(`Written to: ${OUTPUT_PATH}`);