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,114 @@
1
+ <script lang="ts">
2
+ import type { SchemaNode } from '../../js/utils/schema-utils';
3
+ import {
4
+ isReadOnly,
5
+ isNullable,
6
+ getLabel,
7
+ } from '../../js/utils/schema-utils';
8
+ import FieldWrapper from './FieldWrapper.svelte';
9
+
10
+ /**
11
+ * Props for the BooleanField component, which renders a labeled checkbox for a JSON Schema boolean property.
12
+ */
13
+ interface Props {
14
+ // Field name used as the input id and label fallback
15
+ name: string;
16
+ // JSON Schema node describing this field
17
+ schema: SchemaNode;
18
+ // Current field value
19
+ value: unknown;
20
+ // Whether this field is required
21
+ required?: boolean;
22
+ // Callback fired when the value changes
23
+ onchange: (value: boolean | null) => void;
24
+ // When true, visually hides FieldWrapper chrome (label/help) for inline array contexts
25
+ inline?: boolean;
26
+ }
27
+
28
+ let {
29
+ name,
30
+ schema,
31
+ value,
32
+ required = false,
33
+ onchange,
34
+ inline = false,
35
+ }: Props = $props();
36
+
37
+ // Display label — schema.title if present, otherwise title-cased name
38
+ const label = $derived(getLabel(schema, name));
39
+
40
+ // Checked state for the checkbox
41
+ const checked = $derived(typeof value === 'boolean' ? value : false);
42
+
43
+ // Whether field is read-only
44
+ const readOnly = $derived(isReadOnly(schema));
45
+
46
+ // Whether empty input should emit null (nullable anyOf-unwrapped types)
47
+ const nullable = $derived(isNullable(schema));
48
+
49
+ /**
50
+ * Handles checkbox change. Preserves null for nullable fields only while the value is already null and unchecked.
51
+ * @param {Event} e - The change event from the checkbox input element
52
+ * @return {void}
53
+ */
54
+ function handleChange(e: Event): void {
55
+ const isChecked = (e.target as HTMLInputElement).checked;
56
+ onchange(nullable && value === null && !isChecked ? null : isChecked);
57
+ }
58
+ </script>
59
+
60
+ <FieldWrapper {name} {schema} {required} hideLabel={true} {inline}>
61
+ <label class="field-label-wrap" for={name}>
62
+ <input
63
+ type="checkbox"
64
+ id={name}
65
+ class="field-checkbox"
66
+ {checked}
67
+ disabled={readOnly}
68
+ onchange={handleChange}
69
+ />
70
+ <span class="field-label-text">
71
+ {label}{#if required}<span class="field-required" aria-hidden="true"
72
+ >*</span
73
+ >{/if}
74
+ </span>
75
+ </label>
76
+ </FieldWrapper>
77
+
78
+ <style>
79
+ /* Label wraps checkbox + text in a flex row — no separate label above */
80
+ .field-label-wrap {
81
+ display: flex;
82
+ align-items: center;
83
+ gap: 0.5rem;
84
+ cursor: pointer;
85
+ width: fit-content;
86
+ }
87
+
88
+ .field-checkbox {
89
+ width: 1rem;
90
+ height: 1rem;
91
+ accent-color: var(--plum);
92
+ cursor: pointer;
93
+
94
+ &:focus {
95
+ outline: 2px solid var(--plum);
96
+ outline-offset: -1px;
97
+ }
98
+
99
+ &:disabled {
100
+ opacity: 0.6;
101
+ cursor: default;
102
+ }
103
+ }
104
+
105
+ .field-label-text {
106
+ font-size: 0.875rem;
107
+ color: var(--cms-fg);
108
+ }
109
+
110
+ .field-required {
111
+ color: var(--light-red);
112
+ margin-left: 0.25rem;
113
+ }
114
+ </style>
@@ -0,0 +1,82 @@
1
+ <script lang="ts">
2
+ import type { SchemaNode } from '../../js/utils/schema-utils';
3
+ import { isReadOnly, isNullable } from '../../js/utils/schema-utils';
4
+ import FieldWrapper from './FieldWrapper.svelte';
5
+
6
+ /**
7
+ * Props for the DateField component, which renders a date input for a JSON Schema string property with format "date-time".
8
+ */
9
+ interface Props {
10
+ // Field name used as the input id and label fallback
11
+ name: string;
12
+ // JSON Schema node describing this field
13
+ schema: SchemaNode;
14
+ // Current field value — Date object or ISO string
15
+ value: unknown;
16
+ // Whether this field is required
17
+ required?: boolean;
18
+ // Callback fired when the value changes
19
+ onchange: (value: string | null) => void;
20
+ // When true, visually hides FieldWrapper chrome (label/help) for inline array contexts
21
+ inline?: boolean;
22
+ }
23
+
24
+ let {
25
+ name,
26
+ schema,
27
+ value,
28
+ required = false,
29
+ onchange,
30
+ inline = false,
31
+ }: Props = $props();
32
+
33
+ /**
34
+ * Converts a Date or ISO string to YYYY-MM-DD for the date input, or empty string if unset.
35
+ * @param {unknown} val - The field value, which may be a Date object, ISO string, or unset
36
+ * @return {string} A YYYY-MM-DD formatted string for the date input, or empty string
37
+ */
38
+ function toDateInputValue(val: unknown): string {
39
+ if (val instanceof Date) {
40
+ // Use UTC components to avoid timezone shifts converting to local date
41
+ const y = val.getUTCFullYear();
42
+ const m = String(val.getUTCMonth() + 1).padStart(2, '0');
43
+ const d = String(val.getUTCDate()).padStart(2, '0');
44
+ return `${y}-${m}-${d}`;
45
+ }
46
+ if (typeof val === 'string' && val.length >= 10) {
47
+ // Slice the YYYY-MM-DD portion from an ISO string (e.g. "2024-01-15T00:00:00Z")
48
+ return val.slice(0, 10);
49
+ }
50
+ return '';
51
+ }
52
+
53
+ // YYYY-MM-DD string for the date input element
54
+ const inputValue = $derived(toDateInputValue(value));
55
+
56
+ // Whether field is read-only
57
+ const readOnly = $derived(isReadOnly(schema));
58
+
59
+ // Whether empty input should emit null (nullable anyOf-unwrapped types)
60
+ const nullable = $derived(isNullable(schema));
61
+
62
+ /**
63
+ * Handles date input change, emitting null for empty nullable fields.
64
+ * @param {Event} e - The input event from the date input element
65
+ * @return {void}
66
+ */
67
+ function handleChange(e: Event): void {
68
+ const raw = (e.target as HTMLInputElement).value;
69
+ onchange(nullable && raw === '' ? null : raw);
70
+ }
71
+ </script>
72
+
73
+ <FieldWrapper {name} {schema} {required} {inline}>
74
+ <input
75
+ type="date"
76
+ id={name}
77
+ class="field-input field-input--auto"
78
+ value={inputValue}
79
+ readonly={readOnly}
80
+ oninput={handleChange}
81
+ />
82
+ </FieldWrapper>
@@ -0,0 +1,74 @@
1
+ <script lang="ts">
2
+ import type { SchemaNode } from '../../js/utils/schema-utils';
3
+ import { isReadOnly, isNullable } from '../../js/utils/schema-utils';
4
+ import FieldWrapper from './FieldWrapper.svelte';
5
+
6
+ /**
7
+ * Props for the EnumField component, which renders a select dropdown for a JSON Schema enum property.
8
+ */
9
+ interface Props {
10
+ // Field name used as the select id and label fallback
11
+ name: string;
12
+ // JSON Schema node describing this field
13
+ schema: SchemaNode;
14
+ // Current field value
15
+ value: unknown;
16
+ // The enum values to render as options
17
+ options: unknown[];
18
+ // Whether this field is required
19
+ required?: boolean;
20
+ // Callback fired when the value changes
21
+ onchange: (value: string | null) => void;
22
+ // When true, visually hides FieldWrapper chrome (label/help) for inline array contexts
23
+ inline?: boolean;
24
+ }
25
+
26
+ let {
27
+ name,
28
+ schema,
29
+ value,
30
+ options,
31
+ required = false,
32
+ onchange,
33
+ inline = false,
34
+ }: Props = $props();
35
+
36
+ // String representation of the current value for select binding
37
+ const selectedValue = $derived(value != null ? String(value) : '');
38
+
39
+ // Whether field is read-only
40
+ const readOnly = $derived(isReadOnly(schema));
41
+
42
+ // Whether empty selection should emit null (nullable anyOf-unwrapped types)
43
+ const nullable = $derived(isNullable(schema));
44
+
45
+ // Whether to show the empty placeholder option — when not required or no value is set
46
+ const showEmptyOption = $derived(!required || value == null);
47
+
48
+ /**
49
+ * Handles select change, emitting null when empty option is selected on nullable fields.
50
+ * @param {Event} e - The change event from the select element
51
+ * @return {void}
52
+ */
53
+ function handleChange(e: Event): void {
54
+ const raw = (e.target as HTMLSelectElement).value;
55
+ onchange(raw === '' ? (nullable ? null : '') : raw);
56
+ }
57
+ </script>
58
+
59
+ <FieldWrapper {name} {schema} {required} {inline}>
60
+ <select
61
+ id={name}
62
+ class="field-input field-input--select"
63
+ value={selectedValue}
64
+ disabled={readOnly}
65
+ onchange={handleChange}
66
+ >
67
+ {#if showEmptyOption}
68
+ <option value="">—</option>
69
+ {/if}
70
+ {#each options as option}
71
+ <option value={String(option)}>{String(option)}</option>
72
+ {/each}
73
+ </select>
74
+ </FieldWrapper>
@@ -0,0 +1,96 @@
1
+ <script lang="ts">
2
+ import type { SchemaNode } from '../../js/utils/schema-utils';
3
+ import { getLabel } from '../../js/utils/schema-utils';
4
+ import type { Snippet } from 'svelte';
5
+
6
+ /**
7
+ * Shared wrapper for all form field components. Provides the label, required marker, deprecated dimming, description, and constraint text so individual field components only supply the input element via a Svelte snippet.
8
+ */
9
+ interface Props {
10
+ // Field name used as the input id and label fallback
11
+ name: string;
12
+ // JSON Schema node describing this field
13
+ schema: SchemaNode;
14
+ // Whether this field is required
15
+ required?: boolean;
16
+ // The input element rendered inside the wrapper
17
+ children: Snippet;
18
+ // Optional constraint text displayed after the description (e.g., "max 200", "min 0, max 100")
19
+ constraintText?: string;
20
+ // When true, hides the default label — used by BooleanField which renders its own inline checkbox+label
21
+ hideLabel?: boolean;
22
+ // When true, visually hides label and help text using sr-only for inline array contexts where the parent provides visible labels
23
+ inline?: boolean;
24
+ }
25
+
26
+ let {
27
+ name,
28
+ schema,
29
+ required = false,
30
+ children,
31
+ constraintText,
32
+ hideLabel = false,
33
+ inline = false,
34
+ }: Props = $props();
35
+
36
+ // Display label — schema.title if present, otherwise title-cased name
37
+ const label = $derived(getLabel(schema, name));
38
+
39
+ // Description from schema
40
+ const description = $derived(schema['description'] as string | undefined);
41
+
42
+ // Whether field is deprecated — dims the entire field
43
+ const deprecated = $derived(!!(schema['deprecated'] as boolean | undefined));
44
+ </script>
45
+
46
+ <div class="field" class:field--deprecated={deprecated}>
47
+ {#if !hideLabel}
48
+ <label class="field-label" class:sr-only={inline} for={name}>
49
+ {label}{#if required}<span class="field-required" aria-hidden="true"
50
+ >*</span
51
+ >{/if}
52
+ </label>
53
+ {/if}
54
+
55
+ {@render children()}
56
+
57
+ {#if description || constraintText}
58
+ <p class="field-help" class:sr-only={inline}>
59
+ {#if description}{description}{/if}
60
+ {#if description && constraintText}&ensp;{/if}
61
+ {#if constraintText}<span class="field-constraint">{constraintText}</span
62
+ >{/if}
63
+ </p>
64
+ {/if}
65
+ </div>
66
+
67
+ <style>
68
+ .field {
69
+ display: grid;
70
+ gap: 0.25rem;
71
+ }
72
+
73
+ /* Dimmed appearance for deprecated fields */
74
+ .field--deprecated {
75
+ opacity: 0.5;
76
+ }
77
+
78
+ .field-label {
79
+ font-size: 0.875rem;
80
+ color: var(--cms-fg);
81
+ }
82
+
83
+ .field-required {
84
+ color: var(--light-red);
85
+ margin-left: 0.25rem;
86
+ }
87
+
88
+ .field-help {
89
+ font-size: 0.75rem;
90
+ color: var(--cms-muted);
91
+ }
92
+
93
+ .field-constraint {
94
+ font-style: italic;
95
+ }
96
+ </style>
@@ -0,0 +1,99 @@
1
+ <script lang="ts">
2
+ import type { SchemaNode } from '../../js/utils/schema-utils';
3
+ import { isReadOnly, isNullable } from '../../js/utils/schema-utils';
4
+ import FieldWrapper from './FieldWrapper.svelte';
5
+
6
+ /**
7
+ * Props for the NumberField component, which renders a numeric input for a JSON Schema number or integer property.
8
+ */
9
+ interface Props {
10
+ // Field name used as the input id and label fallback
11
+ name: string;
12
+ // JSON Schema node describing this field
13
+ schema: SchemaNode;
14
+ // Current field value
15
+ value: unknown;
16
+ // Whether this field is required
17
+ required?: boolean;
18
+ // Callback fired when the value changes
19
+ onchange: (value: number | null) => void;
20
+ // When true, visually hides FieldWrapper chrome (label/help) for inline array contexts
21
+ inline?: boolean;
22
+ }
23
+
24
+ let {
25
+ name,
26
+ schema,
27
+ value,
28
+ required = false,
29
+ onchange,
30
+ inline = false,
31
+ }: Props = $props();
32
+
33
+ // Numeric value for the input, coerced from the value prop
34
+ const inputValue = $derived(typeof value === 'number' ? value : '');
35
+
36
+ // Min attribute: minimum takes precedence, exclusiveMinimum adds 1
37
+ const min = $derived.by(() => {
38
+ const minimum = schema['minimum'] as number | undefined;
39
+ const exclusiveMin = schema['exclusiveMinimum'] as number | undefined;
40
+ if (minimum != null) return minimum;
41
+ if (exclusiveMin != null) return exclusiveMin + 1;
42
+ return undefined;
43
+ });
44
+
45
+ // Max attribute: maximum takes precedence, exclusiveMaximum subtracts 1
46
+ const max = $derived.by(() => {
47
+ const maximum = schema['maximum'] as number | undefined;
48
+ const exclusiveMax = schema['exclusiveMaximum'] as number | undefined;
49
+ if (maximum != null) return maximum;
50
+ if (exclusiveMax != null) return exclusiveMax - 1;
51
+ return undefined;
52
+ });
53
+
54
+ // Step attribute from multipleOf
55
+ const step = $derived(schema['multipleOf'] as number | undefined);
56
+
57
+ // Whether field is read-only
58
+ const readOnly = $derived(isReadOnly(schema));
59
+
60
+ // Whether empty input should emit null (nullable anyOf-unwrapped types)
61
+ const nullable = $derived(isNullable(schema));
62
+
63
+ // Human-readable constraint summary (e.g. "min 0, max 100, step 5")
64
+ const constraintText = $derived.by(() => {
65
+ const parts: string[] = [];
66
+ if (min != null) parts.push(`min ${min}`);
67
+ if (max != null) parts.push(`max ${max}`);
68
+ if (step != null) parts.push(`step ${step}`);
69
+ return parts.length > 0 ? parts.join(', ') : undefined;
70
+ });
71
+
72
+ /**
73
+ * Handles input change, emitting null for empty nullable fields or 0 for non-nullable.
74
+ * @param {Event} e - The input event from the number input element
75
+ * @return {void}
76
+ */
77
+ function handleChange(e: Event): void {
78
+ const raw = (e.target as HTMLInputElement).value;
79
+ if (raw === '') {
80
+ onchange(nullable ? null : 0);
81
+ } else {
82
+ onchange(parseFloat(raw));
83
+ }
84
+ }
85
+ </script>
86
+
87
+ <FieldWrapper {name} {schema} {required} {constraintText} {inline}>
88
+ <input
89
+ type="number"
90
+ id={name}
91
+ class="field-input field-input--auto"
92
+ value={inputValue}
93
+ {min}
94
+ {max}
95
+ {step}
96
+ readonly={readOnly}
97
+ oninput={handleChange}
98
+ />
99
+ </FieldWrapper>
@@ -0,0 +1,121 @@
1
+ <script lang="ts">
2
+ import type { SchemaNode } from '../../js/utils/schema-utils';
3
+ import {
4
+ getProperties,
5
+ getRequiredFields,
6
+ getLabel,
7
+ } from '../../js/utils/schema-utils';
8
+ import SchemaField from './SchemaField.svelte';
9
+
10
+ /**
11
+ * Props for the ObjectField component, which renders a grouped fieldset of child SchemaField components for a JSON Schema object property.
12
+ */
13
+ interface Props {
14
+ // Property name for labeling
15
+ name: string;
16
+ // JSON Schema node describing this object
17
+ schema: SchemaNode;
18
+ // Current object value
19
+ value: unknown;
20
+ // Whether this field is required
21
+ required?: boolean;
22
+ // Callback fired when the value changes
23
+ onchange: (value: unknown) => void;
24
+ // When true, renders fields without a fieldset wrapper (used inside ArrayItem).
25
+ inline?: boolean;
26
+ }
27
+
28
+ let {
29
+ name,
30
+ schema,
31
+ value,
32
+ required = false,
33
+ onchange,
34
+ inline = false,
35
+ }: Props = $props();
36
+
37
+ // Display label from schema title or property name
38
+ const label = $derived(getLabel(schema, name));
39
+
40
+ // Properties map from the schema
41
+ const properties = $derived(getProperties(schema) ?? {});
42
+
43
+ // Required field names within this object
44
+ const requiredFields = $derived(getRequiredFields(schema));
45
+
46
+ // Current object value, defaulting to empty object
47
+ const objValue = $derived(
48
+ (typeof value === 'object' && value !== null ? value : {}) as Record<
49
+ string,
50
+ unknown
51
+ >,
52
+ );
53
+
54
+ /**
55
+ * Updates a single property and dispatches the full updated object via onchange.
56
+ * @param {string} key - The property key to update within the object
57
+ * @param {unknown} newValue - The new value for the given property key
58
+ * @return {void}
59
+ */
60
+ function handleFieldChange(key: string, newValue: unknown): void {
61
+ onchange({ ...objValue, [key]: newValue });
62
+ }
63
+ </script>
64
+
65
+ {#if inline}
66
+ <!-- Inline mode: no fieldset wrapper, used inside ArrayItem -->
67
+ <div class="object-field--inline">
68
+ {#each Object.entries(properties) as [key, propSchema]}
69
+ <SchemaField
70
+ name={key}
71
+ schema={propSchema}
72
+ value={objValue[key]}
73
+ required={requiredFields.includes(key)}
74
+ onchange={(v) => handleFieldChange(key, v)}
75
+ />
76
+ {/each}
77
+ </div>
78
+ {:else}
79
+ <fieldset class="object-field">
80
+ <legend class="object-field__legend">
81
+ {label}
82
+ {#if required}<span class="object-field__required">*</span>{/if}
83
+ </legend>
84
+ {#each Object.entries(properties) as [key, propSchema]}
85
+ <SchemaField
86
+ name={key}
87
+ schema={propSchema}
88
+ value={objValue[key]}
89
+ required={requiredFields.includes(key)}
90
+ onchange={(v) => handleFieldChange(key, v)}
91
+ />
92
+ {/each}
93
+ </fieldset>
94
+ {/if}
95
+
96
+ <style>
97
+ .object-field {
98
+ border: 1px solid var(--cms-border);
99
+ border-radius: 4px;
100
+ padding: 1rem;
101
+ display: grid;
102
+ gap: 1.25rem;
103
+ }
104
+
105
+ .object-field__legend {
106
+ font-size: 0.875rem;
107
+ color: var(--cms-fg);
108
+ padding: 0 0.5rem;
109
+ }
110
+
111
+ .object-field__required {
112
+ color: var(--light-red);
113
+ margin-left: 0.25rem;
114
+ }
115
+
116
+ /* Inline mode: no border/padding, just stack the fields */
117
+ .object-field--inline {
118
+ display: grid;
119
+ gap: 1.25rem;
120
+ }
121
+ </style>
@@ -0,0 +1,107 @@
1
+ <script lang="ts">
2
+ import { resolveFieldType } from '../../js/utils/schema-utils';
3
+ import type { SchemaNode } from '../../js/utils/schema-utils';
4
+ import StringField from './StringField.svelte';
5
+ import NumberField from './NumberField.svelte';
6
+ import BooleanField from './BooleanField.svelte';
7
+ import EnumField from './EnumField.svelte';
8
+ import DateField from './DateField.svelte';
9
+ import ArrayField from './ArrayField.svelte';
10
+ import ObjectField from './ObjectField.svelte';
11
+
12
+ /**
13
+ * Props for the SchemaField component, which resolves a JSON Schema node to the appropriate leaf field component (string, number, boolean, enum, date, array, or object).
14
+ */
15
+ interface Props {
16
+ // Property name for labeling and identification
17
+ name: string;
18
+ // JSON Schema node describing this field
19
+ schema: SchemaNode;
20
+ // Current field value
21
+ value: unknown;
22
+ // Whether this field is required
23
+ required?: boolean;
24
+ // Callback fired when the value changes
25
+ onchange: (value: unknown) => void;
26
+ // When true, object fields render without a fieldset wrapper and leaf fields hide FieldWrapper chrome (used inside ArrayItem).
27
+ inline?: boolean;
28
+ }
29
+
30
+ let {
31
+ name,
32
+ schema,
33
+ value,
34
+ required = false,
35
+ onchange,
36
+ inline = false,
37
+ }: Props = $props();
38
+
39
+ // Resolve the schema node to a field type descriptor
40
+ const fieldType = $derived(resolveFieldType(schema));
41
+
42
+ // For nullable anyOf schemas, merges outer annotations onto the inner type and sets _nullable so leaf fields emit null for empty values.
43
+ const effectiveSchema = $derived.by(() => {
44
+ if (Array.isArray(schema['anyOf'])) {
45
+ const nonNull = (schema['anyOf'] as SchemaNode[]).find(
46
+ (s) => s['type'] !== 'null',
47
+ );
48
+ if (nonNull) {
49
+ // Spread outer props (title, description, readOnly, etc.) onto the inner type; exclude anyOf to avoid recursion
50
+ const { anyOf: _, ...outerProps } = schema;
51
+ return { ...nonNull, ...outerProps, _nullable: true };
52
+ }
53
+ }
54
+ return schema;
55
+ });
56
+ </script>
57
+
58
+ {#if fieldType.kind === 'string'}
59
+ <StringField
60
+ {name}
61
+ schema={effectiveSchema}
62
+ {value}
63
+ {required}
64
+ {inline}
65
+ onchange={(v) => onchange(v)}
66
+ />
67
+ {:else if fieldType.kind === 'number'}
68
+ <NumberField
69
+ {name}
70
+ schema={effectiveSchema}
71
+ {value}
72
+ {required}
73
+ {inline}
74
+ onchange={(v) => onchange(v)}
75
+ />
76
+ {:else if fieldType.kind === 'boolean'}
77
+ <BooleanField
78
+ {name}
79
+ schema={effectiveSchema}
80
+ {value}
81
+ {inline}
82
+ onchange={(v) => onchange(v)}
83
+ />
84
+ {:else if fieldType.kind === 'enum'}
85
+ <EnumField
86
+ {name}
87
+ schema={effectiveSchema}
88
+ {value}
89
+ {required}
90
+ {inline}
91
+ options={fieldType.options}
92
+ onchange={(v) => onchange(v)}
93
+ />
94
+ {:else if fieldType.kind === 'date'}
95
+ <DateField
96
+ {name}
97
+ schema={effectiveSchema}
98
+ {value}
99
+ {required}
100
+ {inline}
101
+ onchange={(v) => onchange(v)}
102
+ />
103
+ {:else if fieldType.kind === 'array'}
104
+ <ArrayField {name} {schema} {value} {required} {onchange} />
105
+ {:else if fieldType.kind === 'object'}
106
+ <ObjectField {name} {schema} {value} {required} {onchange} {inline} />
107
+ {/if}