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,291 @@
1
+ <script lang="ts">
2
+ import {
3
+ backend,
4
+ content,
5
+ pickDirectory,
6
+ requestPermission,
7
+ connectGitHub,
8
+ } from '../js/state/state.svelte';
9
+
10
+ // Whether the stored FSA handle needs re-authorization
11
+ const needsReauth = $derived(
12
+ backend.type === 'fsa' && backend.permission === 'prompt',
13
+ );
14
+
15
+ // GitHub form state
16
+ let token = $state('');
17
+ let repo = $state('');
18
+ let connecting = $state(false);
19
+ let githubError = $state<string | null>(null);
20
+
21
+ /**
22
+ * Handles the GitHub connect form submission.
23
+ * @return {Promise<void>}
24
+ */
25
+ async function handleGitHubConnect(): Promise<void> {
26
+ if (!token || !repo) return;
27
+ connecting = true;
28
+ githubError = null;
29
+ try {
30
+ await connectGitHub(token, repo);
31
+ } catch (err) {
32
+ githubError = err instanceof Error ? err.message : String(err);
33
+ } finally {
34
+ connecting = false;
35
+ }
36
+ }
37
+ </script>
38
+
39
+ {#if needsReauth}
40
+ <div class="picker">
41
+ <p>This folder requires re-authorization to continue.</p>
42
+ <button class="btn btn--primary btn--primary-lg" onclick={requestPermission}
43
+ >Re-authorize folder</button
44
+ >
45
+ </div>
46
+ {:else}
47
+ <div class="picker">
48
+ <h2 class="picker-title">Connect to your project</h2>
49
+
50
+ <div class="picker-options">
51
+ <div class="picker-option">
52
+ <h3>Local Folder</h3>
53
+ <p>Select your project folder on this device.</p>
54
+ <button class="btn btn--primary btn--primary-lg" onclick={pickDirectory}
55
+ >Choose project folder</button
56
+ >
57
+ </div>
58
+
59
+ <div class="picker-option">
60
+ <h3>GitHub Repository</h3>
61
+ <p>Connect using a Personal Access Token.</p>
62
+ <form
63
+ onsubmit={(e) => {
64
+ e.preventDefault();
65
+ handleGitHubConnect();
66
+ }}
67
+ >
68
+ <label>
69
+ <span class="field-label">
70
+ Personal Access Token
71
+ <button
72
+ class="icon-btn info-btn"
73
+ type="button"
74
+ title="Required permissions"
75
+ interestfor="pat-info"
76
+ commandfor="pat-info"
77
+ command="toggle-popover"
78
+ >
79
+ <span class="icon">info</span>
80
+ </button>
81
+ </span>
82
+ <input type="password" bind:value={token} placeholder="ghp_..." />
83
+ </label>
84
+ <div id="pat-info" class="pat-tooltip" popover="hint" role="tooltip">
85
+ <p class="pat-tooltip-heading">
86
+ Create a <a
87
+ href="https://github.com/settings/personal-access-tokens"
88
+ target="_blank"
89
+ rel="noopener noreferrer">fine-grained PAT</a
90
+ > with:
91
+ </p>
92
+ <dl class="pat-permissions">
93
+ <div class="pat-permission">
94
+ <dt>Contents</dt>
95
+ <dd>Read and write</dd>
96
+ </div>
97
+ <div class="pat-permission">
98
+ <dt>Metadata</dt>
99
+ <dd>
100
+ Read-only <span class="pat-note">(included by default)</span>
101
+ </dd>
102
+ </div>
103
+ </dl>
104
+ </div>
105
+ <label>
106
+ <span class="field-label">Repository</span>
107
+ <input type="text" bind:value={repo} placeholder="owner/repo" />
108
+ </label>
109
+ <button
110
+ class="btn btn--primary btn--primary-lg"
111
+ type="submit"
112
+ disabled={!token || !repo || connecting}
113
+ >
114
+ {connecting ? 'Connecting...' : 'Connect'}
115
+ </button>
116
+ </form>
117
+ {#if githubError}
118
+ <p class="error">{githubError}</p>
119
+ {/if}
120
+ </div>
121
+ </div>
122
+
123
+ {#if content.error}
124
+ <p class="error">{content.error}</p>
125
+ {/if}
126
+ </div>
127
+ {/if}
128
+
129
+ <style>
130
+ .picker {
131
+ display: grid;
132
+ place-items: center;
133
+ gap: 1rem;
134
+ padding: var(--spacing);
135
+ min-height: 50vh;
136
+ text-align: center;
137
+ }
138
+
139
+ .picker-title {
140
+ font-size: 1.5rem;
141
+ margin-bottom: 0.5rem;
142
+ }
143
+
144
+ .picker-options {
145
+ display: grid;
146
+ grid-template-columns: 1fr 1fr;
147
+ gap: 2rem;
148
+ max-width: 40rem;
149
+ width: 100%;
150
+ }
151
+
152
+ .picker-option {
153
+ display: grid;
154
+ /* auto rows for content, last row (button/form) aligns to bottom */
155
+ grid-template-rows: auto auto 1fr;
156
+ align-items: start;
157
+ gap: 0.75rem;
158
+ padding: 1.5rem;
159
+ border: 1px solid var(--cms-border);
160
+ border-radius: 0.5rem;
161
+ text-align: left;
162
+
163
+ h3 {
164
+ font-size: 1.25rem;
165
+ margin: 0;
166
+ }
167
+
168
+ p {
169
+ font-size: 0.875rem;
170
+ color: var(--cms-muted);
171
+ margin: 0;
172
+ }
173
+
174
+ /* Align the last child (button or form) to the bottom of the card */
175
+ > :last-child {
176
+ align-self: end;
177
+ }
178
+ }
179
+
180
+ form {
181
+ display: grid;
182
+ gap: 0.75rem;
183
+ }
184
+
185
+ label {
186
+ display: grid;
187
+ gap: 0.25rem;
188
+ }
189
+
190
+ /* Flex to align label text and info icon inline */
191
+ .field-label {
192
+ display: flex;
193
+ align-items: center;
194
+ gap: 0.25rem;
195
+ font-size: 0.875rem;
196
+ color: var(--cms-muted);
197
+ }
198
+
199
+ /* Anchor positioning and icon size for the PAT info tooltip trigger */
200
+ .info-btn {
201
+ anchor-name: --pat-info-btn;
202
+ interest-delay: 0s;
203
+ border-radius: 0;
204
+
205
+ .icon {
206
+ font-size: 1rem;
207
+ }
208
+ }
209
+
210
+ .pat-tooltip {
211
+ position-anchor: --pat-info-btn;
212
+ position: fixed;
213
+ inset: unset;
214
+ top: anchor(bottom);
215
+ right: anchor(right);
216
+ margin-top: 0.25rem;
217
+ background: var(--cms-bg);
218
+ border: 1px solid var(--cms-border);
219
+ border-radius: 0.25rem;
220
+ padding: 0.5rem 0.75rem;
221
+ max-width: 16rem;
222
+ text-align: left;
223
+ font-size: 0.75rem;
224
+
225
+ /* Invisible bridge so hover interest isn't broken by the gap */
226
+ &::before {
227
+ content: '';
228
+ position: absolute;
229
+ bottom: 100%;
230
+ left: 0;
231
+ right: 0;
232
+ height: 0.25rem;
233
+ }
234
+ }
235
+
236
+ /* Higher specificity to override .picker-option p { color: var(--cms-muted) } */
237
+ .pat-tooltip .pat-tooltip-heading {
238
+ color: var(--cms-fg);
239
+ margin: 0 0 0.25rem;
240
+ font-size: 0.75rem;
241
+
242
+ a {
243
+ color: var(--cms-fg);
244
+ text-decoration: underline;
245
+ }
246
+ }
247
+
248
+ .pat-permissions {
249
+ margin: 0;
250
+ }
251
+
252
+ .pat-permission {
253
+ display: grid;
254
+ grid-template-columns: auto 1fr;
255
+ gap: 0.5rem;
256
+ font-size: 0.75rem;
257
+
258
+ dt {
259
+ color: var(--cms-fg);
260
+ font-weight: 600;
261
+ }
262
+
263
+ dd {
264
+ margin: 0;
265
+ color: var(--cms-fg);
266
+ }
267
+ }
268
+
269
+ .pat-note {
270
+ color: var(--cms-muted);
271
+ }
272
+
273
+ input {
274
+ width: 100%;
275
+ padding: 0.5rem 0.75rem;
276
+ background: var(--cms-bg);
277
+ border: 1px solid var(--cms-border);
278
+ border-radius: 0.25rem;
279
+ color: var(--cms-fg);
280
+ font-size: 0.875rem;
281
+
282
+ &::placeholder {
283
+ color: var(--cms-muted);
284
+ }
285
+ }
286
+
287
+ .error {
288
+ color: var(--light-red);
289
+ font-size: 0.875rem;
290
+ }
291
+ </style>
@@ -0,0 +1,46 @@
1
+ <script lang="ts">
2
+ /**
3
+ * A small colored badge for indicating draft or outdated status in the sidebar.
4
+ */
5
+ interface Props {
6
+ // The chip variant — "draft" is yellow, "outdated" is red
7
+ variant: 'draft' | 'outdated';
8
+ }
9
+
10
+ let { variant }: Props = $props();
11
+ </script>
12
+
13
+ <span
14
+ class="chip"
15
+ class:chip--draft={variant === 'draft'}
16
+ class:chip--outdated={variant === 'outdated'}
17
+ >
18
+ {variant === 'draft' ? 'draft' : 'outdated'}
19
+ </span>
20
+
21
+ <style>
22
+ /*
23
+ * Visually sized closer to the subtitle than the main label text —
24
+ * exempt from font size scale and .25rem padding-block rule per CLAUDE.md.
25
+ */
26
+ .chip {
27
+ font-size: 0.6rem;
28
+ font-weight: normal;
29
+ text-transform: lowercase;
30
+ padding-inline: 0.25rem;
31
+ padding-block: 0.15rem;
32
+ border-radius: 5rem;
33
+ line-height: 1;
34
+ white-space: nowrap;
35
+ }
36
+
37
+ .chip--draft {
38
+ background: var(--gold);
39
+ color: var(--cms-bg);
40
+ }
41
+
42
+ .chip--outdated {
43
+ background: var(--light-red);
44
+ color: var(--cms-fg);
45
+ }
46
+ </style>
@@ -0,0 +1,56 @@
1
+ <script lang="ts">
2
+ import type { SchemaNode } from '../js/utils/schema-utils';
3
+ import {
4
+ getFieldsForTab,
5
+ getProperties,
6
+ getRequiredFields,
7
+ } from '../js/utils/schema-utils';
8
+ import { editor, updateFormField } from '../js/editor/editor.svelte';
9
+ import SchemaField from './fields/SchemaField.svelte';
10
+
11
+ /**
12
+ * Props for the MetadataForm component, which renders the set of schema fields assigned to a given editor tab (or all fields when no tab is specified).
13
+ */
14
+ interface Props {
15
+ // The JSON Schema for the collection
16
+ schema: SchemaNode;
17
+ // Tab name to filter by, or null for Metadata (all fields)
18
+ tab?: string | null;
19
+ }
20
+
21
+ let { schema, tab = null }: Props = $props();
22
+
23
+ // List of property names to render for this tab
24
+ const fieldNames = $derived(getFieldsForTab(schema, tab));
25
+
26
+ // Schema properties map
27
+ const properties = $derived(getProperties(schema) ?? {});
28
+
29
+ // Required field names
30
+ const requiredFields = $derived(getRequiredFields(schema));
31
+ </script>
32
+
33
+ <div class="metadata-form">
34
+ {#each fieldNames as fieldName}
35
+ {@const fieldSchema = properties[fieldName]}
36
+ {#if fieldSchema}
37
+ <SchemaField
38
+ name={fieldName}
39
+ schema={fieldSchema}
40
+ value={editor.data[fieldName]}
41
+ required={requiredFields.includes(fieldName)}
42
+ onchange={(v) => updateFormField([fieldName], v)}
43
+ />
44
+ {/if}
45
+ {/each}
46
+ </div>
47
+
48
+ <style>
49
+ .metadata-form {
50
+ display: grid;
51
+ gap: 1.25rem;
52
+ padding: 1.5rem;
53
+ max-width: 80ch;
54
+ margin: 0 auto;
55
+ }
56
+ </style>
@@ -0,0 +1,18 @@
1
+ <script lang="ts">
2
+ /* Tri-state theme toggle that cycles through auto / light / dark preferences. */
3
+ import { cycleTheme, theme } from '../js/state/theme.svelte';
4
+ </script>
5
+
6
+ <button
7
+ class="icon-btn theme-toggle"
8
+ aria-label="Theme: {theme.label}"
9
+ onclick={cycleTheme}
10
+ >
11
+ <span class="icon">{theme.icon}</span>
12
+ </button>
13
+
14
+ <style>
15
+ .theme-toggle .icon {
16
+ font-size: 1.25rem;
17
+ }
18
+ </style>
@@ -0,0 +1,51 @@
1
+ <script lang="ts">
2
+ /**
3
+ * Confirmation dialog for deleting a draft, using native <dialog> with modal backdrop.
4
+ */
5
+ interface Props {
6
+ // Called when the user confirms deletion
7
+ onConfirm: () => void;
8
+ // Called when the user cancels
9
+ onCancel: () => void;
10
+ }
11
+
12
+ let { onConfirm, onCancel }: Props = $props();
13
+
14
+ // Dialog element ref
15
+ let dialogEl = $state<HTMLDialogElement | null>(null);
16
+
17
+ // Open the dialog on mount
18
+ $effect(() => {
19
+ dialogEl?.showModal();
20
+ });
21
+ </script>
22
+
23
+ <dialog class="dialog confirm-dialog" bind:this={dialogEl} onclose={onCancel}>
24
+ <h2 class="dialog__title">Delete Draft?</h2>
25
+ <p class="dialog__message">This cannot be undone.</p>
26
+ <div class="dialog__actions">
27
+ <button class="btn btn--cancel" type="button" onclick={onCancel}
28
+ >Cancel</button
29
+ >
30
+ <button class="btn btn--danger" type="button" onclick={onConfirm}
31
+ >Delete</button
32
+ >
33
+ </div>
34
+ </dialog>
35
+
36
+ <style>
37
+ .confirm-dialog {
38
+ min-width: 18rem;
39
+ }
40
+
41
+ /* Tighter title spacing for the short confirmation dialog */
42
+ .dialog__title {
43
+ margin-bottom: 0.5rem;
44
+ }
45
+
46
+ .dialog__message {
47
+ font-size: 0.875rem;
48
+ color: var(--cms-muted);
49
+ margin-bottom: 1.25rem;
50
+ }
51
+ </style>
@@ -0,0 +1,129 @@
1
+ <script lang="ts">
2
+ import { slugify } from '../../js/utils/slug';
3
+
4
+ /**
5
+ * Props for the FilenameDialog, a native <dialog> that prompts the user to set a filename before publishing.
6
+ */
7
+ interface Props {
8
+ // The title from formData, used to pre-populate the slug suggestion
9
+ title: string;
10
+ // Existing filenames (both live and draft) to validate uniqueness against
11
+ existingFilenames: string[];
12
+ // Called with the chosen filename (including .md extension) when confirmed
13
+ onConfirm: (filename: string) => void;
14
+ // Called when the dialog is cancelled
15
+ onCancel: () => void;
16
+ }
17
+
18
+ let { title, existingFilenames, onConfirm, onCancel }: Props = $props();
19
+
20
+ // The dialog element ref for imperative showModal/close
21
+ let dialogEl = $state<HTMLDialogElement | null>(null);
22
+
23
+ // The slug input value, initialized from slugified title
24
+ let slug = $state(slugify(title));
25
+
26
+ // Validation error message
27
+ const error = $derived.by(() => {
28
+ if (!slug.trim()) return 'Filename cannot be empty';
29
+ const full = `${slug}.md`;
30
+ if (existingFilenames.includes(full)) {
31
+ return 'A file with this name already exists';
32
+ }
33
+ return null;
34
+ });
35
+
36
+ // Open the dialog on mount
37
+ $effect(() => {
38
+ dialogEl?.showModal();
39
+ });
40
+
41
+ /**
42
+ * Handles the confirm action — calls onConfirm with the full filename.
43
+ * @return {void}
44
+ */
45
+ function handleConfirm(): void {
46
+ if (error) return;
47
+ onConfirm(`${slug}.md`);
48
+ }
49
+
50
+ /**
51
+ * Handles the cancel action — closes the dialog and calls onCancel.
52
+ * @return {void}
53
+ */
54
+ function handleCancel(): void {
55
+ dialogEl?.close();
56
+ onCancel();
57
+ }
58
+ </script>
59
+
60
+ <dialog
61
+ class="dialog filename-dialog"
62
+ bind:this={dialogEl}
63
+ onclose={handleCancel}
64
+ >
65
+ <h2 class="dialog__title">Set Filename</h2>
66
+ <div class="input-row">
67
+ <input
68
+ class="slug-input"
69
+ type="text"
70
+ bind:value={slug}
71
+ onkeydown={(e) => {
72
+ if (e.key === 'Enter' && !error) handleConfirm();
73
+ }}
74
+ />
75
+ <span class="extension">.md</span>
76
+ </div>
77
+ {#if error}
78
+ <p class="error">{error}</p>
79
+ {/if}
80
+ <div class="dialog__actions">
81
+ <button class="btn btn--cancel" type="button" onclick={handleCancel}
82
+ >Cancel</button
83
+ >
84
+ <button
85
+ class="btn btn--primary"
86
+ type="button"
87
+ disabled={!!error}
88
+ onclick={handleConfirm}>Confirm</button
89
+ >
90
+ </div>
91
+ </dialog>
92
+
93
+ <style>
94
+ .filename-dialog {
95
+ min-width: 20rem;
96
+ }
97
+
98
+ .input-row {
99
+ display: grid;
100
+ grid-template-columns: 1fr auto;
101
+ align-items: center;
102
+ gap: 0.25rem;
103
+ }
104
+
105
+ .slug-input {
106
+ width: 100%;
107
+ padding: 0.5rem;
108
+ background: var(--cms-bg);
109
+ border: 1px solid var(--cms-border);
110
+ border-radius: 0.25rem;
111
+ color: var(--cms-fg);
112
+ font-size: 1rem;
113
+ }
114
+
115
+ .extension {
116
+ color: var(--cms-muted);
117
+ font-size: 1rem;
118
+ }
119
+
120
+ .error {
121
+ color: var(--light-red);
122
+ font-size: 0.875rem;
123
+ margin-top: 0.5rem;
124
+ }
125
+
126
+ .dialog__actions {
127
+ margin-top: 1.25rem;
128
+ }
129
+ </style>