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.
- package/.claude/settings.local.json +42 -0
- package/.github/workflows/ci.yml +31 -0
- package/.mcp.json +12 -0
- package/.prettierignore +5 -0
- package/.prettierrc.cjs +22 -0
- package/AGENTS.md +183 -0
- package/LICENSE +201 -0
- package/README.md +128 -0
- package/package.json +74 -0
- package/playground/.claude/settings.local.json +5 -0
- package/playground/astro.config.mjs +7 -0
- package/playground/node_modules/.bin/astro +21 -0
- package/playground/node_modules/.bin/rollup +21 -0
- package/playground/node_modules/.bin/tsc +21 -0
- package/playground/node_modules/.bin/tsserver +21 -0
- package/playground/node_modules/.bin/vite +21 -0
- package/playground/node_modules/.vite/_svelte_metadata.json +1 -0
- package/playground/node_modules/.vite/deps/@astrojs_svelte_client__js.js +80 -0
- package/playground/node_modules/.vite/deps/@astrojs_svelte_client__js.js.map +7 -0
- package/playground/node_modules/.vite/deps/_metadata.json +184 -0
- package/playground/node_modules/.vite/deps/astro___aria-query.js +6776 -0
- package/playground/node_modules/.vite/deps/astro___aria-query.js.map +7 -0
- package/playground/node_modules/.vite/deps/astro___axobject-query.js +3754 -0
- package/playground/node_modules/.vite/deps/astro___axobject-query.js.map +7 -0
- package/playground/node_modules/.vite/deps/astro___html-escaper.js +34 -0
- package/playground/node_modules/.vite/deps/astro___html-escaper.js.map +7 -0
- package/playground/node_modules/.vite/deps/chunk-AJXJMYAF.js +0 -0
- package/playground/node_modules/.vite/deps/chunk-AJXJMYAF.js.map +7 -0
- package/playground/node_modules/.vite/deps/chunk-ALJIOON6.js +1005 -0
- package/playground/node_modules/.vite/deps/chunk-ALJIOON6.js.map +7 -0
- package/playground/node_modules/.vite/deps/chunk-BUSYA2B4.js +8 -0
- package/playground/node_modules/.vite/deps/chunk-BUSYA2B4.js.map +7 -0
- package/playground/node_modules/.vite/deps/chunk-CNYJBM5F.js +21 -0
- package/playground/node_modules/.vite/deps/chunk-CNYJBM5F.js.map +7 -0
- package/playground/node_modules/.vite/deps/chunk-DBPNBGEI.js +223 -0
- package/playground/node_modules/.vite/deps/chunk-DBPNBGEI.js.map +7 -0
- package/playground/node_modules/.vite/deps/chunk-G3C2FXJT.js +204 -0
- package/playground/node_modules/.vite/deps/chunk-G3C2FXJT.js.map +7 -0
- package/playground/node_modules/.vite/deps/chunk-GKDKFWC5.js +27 -0
- package/playground/node_modules/.vite/deps/chunk-GKDKFWC5.js.map +7 -0
- package/playground/node_modules/.vite/deps/chunk-HNCLEOC5.js +4376 -0
- package/playground/node_modules/.vite/deps/chunk-HNCLEOC5.js.map +7 -0
- package/playground/node_modules/.vite/deps/chunk-JICYXBFU.js +688 -0
- package/playground/node_modules/.vite/deps/chunk-JICYXBFU.js.map +7 -0
- package/playground/node_modules/.vite/deps/chunk-KCUTL6DD.js +5099 -0
- package/playground/node_modules/.vite/deps/chunk-KCUTL6DD.js.map +7 -0
- package/playground/node_modules/.vite/deps/chunk-ZP4UNCSN.js +23 -0
- package/playground/node_modules/.vite/deps/chunk-ZP4UNCSN.js.map +7 -0
- package/playground/node_modules/.vite/deps/chunk-ZREFNRZZ.js +148 -0
- package/playground/node_modules/.vite/deps/chunk-ZREFNRZZ.js.map +7 -0
- package/playground/node_modules/.vite/deps/package.json +3 -0
- package/playground/node_modules/.vite/deps/smol-toml.js +843 -0
- package/playground/node_modules/.vite/deps/smol-toml.js.map +7 -0
- package/playground/node_modules/.vite/deps/svelte.js +55 -0
- package/playground/node_modules/.vite/deps/svelte.js.map +7 -0
- package/playground/node_modules/.vite/deps/svelte___clsx.js +9 -0
- package/playground/node_modules/.vite/deps/svelte___clsx.js.map +7 -0
- package/playground/node_modules/.vite/deps/svelte_animate.js +57 -0
- package/playground/node_modules/.vite/deps/svelte_animate.js.map +7 -0
- package/playground/node_modules/.vite/deps/svelte_attachments.js +15 -0
- package/playground/node_modules/.vite/deps/svelte_attachments.js.map +7 -0
- package/playground/node_modules/.vite/deps/svelte_easing.js +67 -0
- package/playground/node_modules/.vite/deps/svelte_easing.js.map +7 -0
- package/playground/node_modules/.vite/deps/svelte_events.js +11 -0
- package/playground/node_modules/.vite/deps/svelte_events.js.map +7 -0
- package/playground/node_modules/.vite/deps/svelte_internal.js +5 -0
- package/playground/node_modules/.vite/deps/svelte_internal.js.map +7 -0
- package/playground/node_modules/.vite/deps/svelte_internal_client.js +402 -0
- package/playground/node_modules/.vite/deps/svelte_internal_client.js.map +7 -0
- package/playground/node_modules/.vite/deps/svelte_internal_disclose-version.js +10 -0
- package/playground/node_modules/.vite/deps/svelte_internal_disclose-version.js.map +7 -0
- package/playground/node_modules/.vite/deps/svelte_internal_flags_async.js +8 -0
- package/playground/node_modules/.vite/deps/svelte_internal_flags_async.js.map +7 -0
- package/playground/node_modules/.vite/deps/svelte_internal_flags_legacy.js +8 -0
- package/playground/node_modules/.vite/deps/svelte_internal_flags_legacy.js.map +7 -0
- package/playground/node_modules/.vite/deps/svelte_internal_flags_tracing.js +8 -0
- package/playground/node_modules/.vite/deps/svelte_internal_flags_tracing.js.map +7 -0
- package/playground/node_modules/.vite/deps/svelte_legacy.js +35 -0
- package/playground/node_modules/.vite/deps/svelte_legacy.js.map +7 -0
- package/playground/node_modules/.vite/deps/svelte_motion.js +545 -0
- package/playground/node_modules/.vite/deps/svelte_motion.js.map +7 -0
- package/playground/node_modules/.vite/deps/svelte_reactivity.js +29 -0
- package/playground/node_modules/.vite/deps/svelte_reactivity.js.map +7 -0
- package/playground/node_modules/.vite/deps/svelte_reactivity_window.js +127 -0
- package/playground/node_modules/.vite/deps/svelte_reactivity_window.js.map +7 -0
- package/playground/node_modules/.vite/deps/svelte_store.js +103 -0
- package/playground/node_modules/.vite/deps/svelte_store.js.map +7 -0
- package/playground/node_modules/.vite/deps/svelte_transition.js +208 -0
- package/playground/node_modules/.vite/deps/svelte_transition.js.map +7 -0
- package/playground/package.json +16 -0
- package/playground/pnpm-lock.yaml +3167 -0
- package/playground/src/content/authors/jane-doe.json +8 -0
- package/playground/src/content/config/build.toml +2 -0
- package/playground/src/content/courses/web-fundamentals.json +29 -0
- package/playground/src/content/docs/advanced.mdx +6 -0
- package/playground/src/content/docs/intro.md +6 -0
- package/playground/src/content/guides/getting-started.mdx +6 -0
- package/playground/src/content/posts/hello-world.md +7 -0
- package/playground/src/content/products/t-shirt.json +16 -0
- package/playground/src/content/recipes/pancakes.mdoc +8 -0
- package/playground/src/content/settings/site.yml +2 -0
- package/playground/src/content.config.ts +198 -0
- package/playground/src/env.d.ts +1 -0
- package/playground/src/pages/index.astro +11 -0
- package/playground/src/pages/nebula.astro +14 -0
- package/pnpm-workspace.yaml +2 -0
- package/scripts/subset-icons.mjs +178 -0
- package/src/astro/index.ts +295 -0
- package/src/client/Admin.svelte +283 -0
- package/src/client/components/BackendPicker.svelte +291 -0
- package/src/client/components/DraftChip.svelte +46 -0
- package/src/client/components/MetadataForm.svelte +56 -0
- package/src/client/components/ThemeToggle.svelte +18 -0
- package/src/client/components/dialogs/DeleteDraftDialog.svelte +51 -0
- package/src/client/components/dialogs/FilenameDialog.svelte +129 -0
- package/src/client/components/editor/EditorPane.svelte +227 -0
- package/src/client/components/editor/EditorTabs.svelte +81 -0
- package/src/client/components/editor/EditorToolbar.svelte +131 -0
- package/src/client/components/editor/FormatSelector.svelte +66 -0
- package/src/client/components/editor/Toolbar.svelte +17 -0
- package/src/client/components/fields/ArrayField.svelte +339 -0
- package/src/client/components/fields/ArrayItem.svelte +325 -0
- package/src/client/components/fields/BooleanField.svelte +114 -0
- package/src/client/components/fields/DateField.svelte +82 -0
- package/src/client/components/fields/EnumField.svelte +74 -0
- package/src/client/components/fields/FieldWrapper.svelte +96 -0
- package/src/client/components/fields/NumberField.svelte +99 -0
- package/src/client/components/fields/ObjectField.svelte +121 -0
- package/src/client/components/fields/SchemaField.svelte +107 -0
- package/src/client/components/fields/StringField.svelte +104 -0
- package/src/client/components/sidebar/AdminSidebar.svelte +339 -0
- package/src/client/components/sidebar/AdminSidebarSort.svelte +123 -0
- package/src/client/css/a11y.css +14 -0
- package/src/client/css/btn.css +113 -0
- package/src/client/css/dialog.css +29 -0
- package/src/client/css/field-input.css +39 -0
- package/src/client/css/reset.css +59 -0
- package/src/client/css/theme.css +77 -0
- package/src/client/index.ts +1 -0
- package/src/client/js/drafts/merge.svelte.ts +121 -0
- package/src/client/js/drafts/ops.svelte.ts +227 -0
- package/src/client/js/drafts/storage.ts +108 -0
- package/src/client/js/drafts/workers/diff.ts +40 -0
- package/src/client/js/editor/editor.svelte.ts +343 -0
- package/src/client/js/editor/languages.ts +98 -0
- package/src/client/js/editor/link-wrap.ts +45 -0
- package/src/client/js/editor/markdown-shortcuts.ts +261 -0
- package/src/client/js/handlers/admin.ts +246 -0
- package/src/client/js/state/dialogs.svelte.ts +35 -0
- package/src/client/js/state/router.svelte.ts +156 -0
- package/src/client/js/state/schema.svelte.ts +140 -0
- package/src/client/js/state/state.svelte.ts +334 -0
- package/src/client/js/state/theme.svelte.ts +173 -0
- package/src/client/js/storage/adapter.ts +102 -0
- package/src/client/js/storage/client.ts +150 -0
- package/src/client/js/storage/db.ts +36 -0
- package/src/client/js/storage/fsa.ts +110 -0
- package/src/client/js/storage/github.ts +297 -0
- package/src/client/js/storage/storage.ts +83 -0
- package/src/client/js/storage/workers/frontmatter.ts +320 -0
- package/src/client/js/storage/workers/storage.ts +177 -0
- package/src/client/js/storage/workers/toml-parser.ts +106 -0
- package/src/client/js/storage/workers/yaml-parser.ts +132 -0
- package/src/client/js/utils/file-types.ts +192 -0
- package/src/client/js/utils/format.ts +16 -0
- package/src/client/js/utils/frontmatter.ts +38 -0
- package/src/client/js/utils/schema-utils.ts +295 -0
- package/src/client/js/utils/slug.ts +18 -0
- package/src/client/js/utils/sort.ts +84 -0
- package/src/client/js/utils/stable-stringify.ts +27 -0
- package/src/client/js/utils/url-utils.ts +38 -0
- package/src/types.ts +25 -0
- package/src/virtual.d.ts +22 -0
- package/svelte.config.js +4 -0
- package/tests/astro/build.test.ts +63 -0
- package/tests/astro/index.test.ts +689 -0
- package/tests/client/components/Admin.test.ts +446 -0
- package/tests/client/components/BackendPicker.test.ts +239 -0
- package/tests/client/components/DraftChip.test.ts +53 -0
- package/tests/client/components/MetadataForm.test.ts +164 -0
- package/tests/client/components/dialogs/DeleteDraftDialog.test.ts +91 -0
- package/tests/client/components/dialogs/FilenameDialog.test.ts +209 -0
- package/tests/client/components/dialogs/dialog-stubs.ts +19 -0
- package/tests/client/components/editor/EditorPane.test.ts +100 -0
- package/tests/client/components/editor/EditorTabs.test.ts +253 -0
- package/tests/client/components/editor/EditorToolbar.test.ts +252 -0
- package/tests/client/components/editor/fixtures.ts +31 -0
- package/tests/client/components/fields/ArrayField.test.ts +197 -0
- package/tests/client/components/fields/BooleanField.test.ts +206 -0
- package/tests/client/components/fields/DateField.test.ts +210 -0
- package/tests/client/components/fields/EnumField.test.ts +246 -0
- package/tests/client/components/fields/NumberField.test.ts +240 -0
- package/tests/client/components/fields/ObjectField.test.ts +157 -0
- package/tests/client/components/fields/SchemaField.test.ts +190 -0
- package/tests/client/components/fields/StringField.test.ts +223 -0
- package/tests/client/components/sidebar/AdminSidebar.test.ts +285 -0
- package/tests/client/components/sidebar/AdminSidebarSort.test.ts +135 -0
- package/tests/client/components/sidebar/sort-mock.ts +23 -0
- package/tests/client/js/drafts/fixtures.ts +22 -0
- package/tests/client/js/drafts/merge.test.ts +282 -0
- package/tests/client/js/drafts/ops.test.ts +658 -0
- package/tests/client/js/drafts/storage.test.ts +200 -0
- package/tests/client/js/drafts/workers/diff.test.ts +165 -0
- package/tests/client/js/editor/editor.test.ts +616 -0
- package/tests/client/js/editor/link-wrap.test.ts +225 -0
- package/tests/client/js/editor/markdown-shortcuts.test.ts +370 -0
- package/tests/client/js/handlers/admin.test.ts +467 -0
- package/tests/client/js/state/router.test.ts +619 -0
- package/tests/client/js/state/schema.test.ts +266 -0
- package/tests/client/js/state/state.test.ts +328 -0
- package/tests/client/js/storage/adapter.test.ts +115 -0
- package/tests/client/js/storage/client.test.ts +250 -0
- package/tests/client/js/storage/db.test.ts +59 -0
- package/tests/client/js/storage/fsa.test.ts +284 -0
- package/tests/client/js/storage/github.test.ts +349 -0
- package/tests/client/js/storage/mock-port.ts +95 -0
- package/tests/client/js/storage/storage.test.ts +77 -0
- package/tests/client/js/storage/workers/frontmatter.test.ts +479 -0
- package/tests/client/js/storage/workers/storage.test.ts +299 -0
- package/tests/client/js/storage/workers/toml-parser.test.ts +169 -0
- package/tests/client/js/storage/workers/yaml-parser.test.ts +168 -0
- package/tests/client/js/utils/file-types.test.ts +268 -0
- package/tests/client/js/utils/frontmatter.test.ts +87 -0
- package/tests/client/js/utils/schema-utils.test.ts +318 -0
- package/tests/client/js/utils/slug.test.ts +58 -0
- package/tests/client/js/utils/sort.test.ts +276 -0
- package/tests/client/js/utils/stable-stringify.test.ts +68 -0
- package/tests/client/js/utils/url-utils.test.ts +70 -0
- package/tests/e2e/backend-connection.test.ts +301 -0
- package/tests/e2e/draft-lifecycle.test.ts +388 -0
- package/tests/e2e/editing.test.ts +355 -0
- package/tests/e2e/github-adapter.test.ts +330 -0
- package/tests/e2e/helpers/mock-adapter.ts +166 -0
- package/tests/e2e/helpers/test-app.ts +155 -0
- package/tests/e2e/navigation.test.ts +358 -0
- package/tests/e2e/publishing.test.ts +345 -0
- package/tests/e2e/unsaved-changes.test.ts +317 -0
- package/tests/setup.ts +2 -0
- package/tests/stubs/codemirror.ts +197 -0
- package/tsconfig.json +19 -0
- package/vitest.config.ts +178 -0
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { EditorView, keymap } from '@codemirror/view';
|
|
3
|
+
import { Compartment, EditorState } from '@codemirror/state';
|
|
4
|
+
import { defaultKeymap, history, historyKeymap } from '@codemirror/commands';
|
|
5
|
+
import { getEditorFile, updateBody } from '../../js/editor/editor.svelte';
|
|
6
|
+
import {
|
|
7
|
+
getTypeForFilename,
|
|
8
|
+
stripExtension,
|
|
9
|
+
} from '../../js/utils/file-types';
|
|
10
|
+
import { getLanguageExtension } from '../../js/editor/languages';
|
|
11
|
+
import { linkWrapPlugin } from '../../js/editor/link-wrap';
|
|
12
|
+
import {
|
|
13
|
+
markdownShortcutsKeymap,
|
|
14
|
+
markdownShortcutsExtensions,
|
|
15
|
+
} from '../../js/editor/markdown-shortcuts';
|
|
16
|
+
import Toolbar from './Toolbar.svelte';
|
|
17
|
+
|
|
18
|
+
// Container element for CodeMirror
|
|
19
|
+
let container: HTMLDivElement;
|
|
20
|
+
// The CodeMirror EditorView instance
|
|
21
|
+
let view: EditorView | undefined;
|
|
22
|
+
// Compartment isolating the language extension for runtime reconfiguration
|
|
23
|
+
const langCompartment = new Compartment();
|
|
24
|
+
|
|
25
|
+
// Base editor theme matching the admin color scheme
|
|
26
|
+
const editorTheme = EditorView.theme({
|
|
27
|
+
'&': {
|
|
28
|
+
fontSize: '1rem',
|
|
29
|
+
},
|
|
30
|
+
'.cm-content': {
|
|
31
|
+
fontFamily: "'SF Mono', 'Fira Code', 'Cascadia Code', monospace",
|
|
32
|
+
caretColor: 'var(--editor-caret)',
|
|
33
|
+
padding: '1rem',
|
|
34
|
+
},
|
|
35
|
+
'.cm-scroller': {
|
|
36
|
+
overflow: 'auto',
|
|
37
|
+
},
|
|
38
|
+
'&.cm-focused': {
|
|
39
|
+
outline: 'none',
|
|
40
|
+
},
|
|
41
|
+
'.cm-line': {
|
|
42
|
+
padding: '0 0.25rem',
|
|
43
|
+
},
|
|
44
|
+
'.cm-cursor': {
|
|
45
|
+
borderLeftColor: 'var(--editor-caret)',
|
|
46
|
+
},
|
|
47
|
+
'.cm-selectionBackground': {
|
|
48
|
+
background: 'var(--plum) !important',
|
|
49
|
+
},
|
|
50
|
+
'&.cm-focused .cm-selectionBackground': {
|
|
51
|
+
background: 'var(--plum) !important',
|
|
52
|
+
},
|
|
53
|
+
'.cm-activeLine': {
|
|
54
|
+
backgroundColor: 'var(--editor-active-line)',
|
|
55
|
+
},
|
|
56
|
+
'.cm-gutters': {
|
|
57
|
+
display: 'none',
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Creates the full set of CodeMirror extensions. The language extension
|
|
63
|
+
* is wrapped in a Compartment so it can be swapped at runtime without
|
|
64
|
+
* rebuilding the entire editor state.
|
|
65
|
+
* @param {string} fileType - The file type identifier for language selection
|
|
66
|
+
* @return {import('@codemirror/state').Extension[]} The array of CodeMirror extensions
|
|
67
|
+
*/
|
|
68
|
+
function createExtensions(fileType: string) {
|
|
69
|
+
return [
|
|
70
|
+
editorTheme,
|
|
71
|
+
history(),
|
|
72
|
+
keymap.of([
|
|
73
|
+
...markdownShortcutsKeymap,
|
|
74
|
+
...defaultKeymap,
|
|
75
|
+
...historyKeymap,
|
|
76
|
+
]),
|
|
77
|
+
langCompartment.of(getLanguageExtension(fileType)),
|
|
78
|
+
EditorView.lineWrapping,
|
|
79
|
+
linkWrapPlugin,
|
|
80
|
+
...markdownShortcutsExtensions,
|
|
81
|
+
EditorView.updateListener.of((update) => {
|
|
82
|
+
if (update.docChanged) {
|
|
83
|
+
updateBody(update.state.doc.toString());
|
|
84
|
+
}
|
|
85
|
+
}),
|
|
86
|
+
EditorView.contentAttributes.of({ 'aria-label': 'Content editor' }),
|
|
87
|
+
];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Track the last loaded file identity to detect file changes
|
|
91
|
+
let lastFileKey = '';
|
|
92
|
+
// Track the last configured language type to avoid redundant compartment reconfigures
|
|
93
|
+
let lastLangType = '';
|
|
94
|
+
|
|
95
|
+
$effect(() => {
|
|
96
|
+
const file = getEditorFile();
|
|
97
|
+
|
|
98
|
+
if (!file) {
|
|
99
|
+
// No file open — destroy editor if it exists
|
|
100
|
+
if (view) {
|
|
101
|
+
view.destroy();
|
|
102
|
+
view = undefined;
|
|
103
|
+
lastFileKey = '';
|
|
104
|
+
}
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Wait for body to load before creating/updating CodeMirror
|
|
109
|
+
if (!file.bodyLoaded) return;
|
|
110
|
+
|
|
111
|
+
// Use slug (without extension) so format changes don't trigger a rebuild
|
|
112
|
+
const fileKey = file.draftId ?? stripExtension(file.filename);
|
|
113
|
+
|
|
114
|
+
if (!view && container) {
|
|
115
|
+
// First mount — create the editor
|
|
116
|
+
const fileType = getTypeForFilename(file.filename) ?? 'md';
|
|
117
|
+
lastFileKey = fileKey;
|
|
118
|
+
lastLangType = fileType;
|
|
119
|
+
const state = EditorState.create({
|
|
120
|
+
doc: file.body,
|
|
121
|
+
extensions: createExtensions(fileType),
|
|
122
|
+
});
|
|
123
|
+
view = new EditorView({ state, parent: container });
|
|
124
|
+
} else if (view && fileKey !== lastFileKey) {
|
|
125
|
+
// Different file selected — replace document
|
|
126
|
+
const fileType = getTypeForFilename(file.filename) ?? 'md';
|
|
127
|
+
lastFileKey = fileKey;
|
|
128
|
+
lastLangType = fileType;
|
|
129
|
+
view.setState(
|
|
130
|
+
EditorState.create({
|
|
131
|
+
doc: file.body,
|
|
132
|
+
extensions: createExtensions(fileType),
|
|
133
|
+
}),
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// Reconfigure the language compartment when the file type changes
|
|
139
|
+
$effect(() => {
|
|
140
|
+
if (!view) return;
|
|
141
|
+
const file = getEditorFile();
|
|
142
|
+
if (!file?.filename) return;
|
|
143
|
+
const fileType = getTypeForFilename(file.filename) ?? 'md';
|
|
144
|
+
// Skip if the language is already configured (avoids redundant reconfigure on initial mount)
|
|
145
|
+
if (fileType === lastLangType) return;
|
|
146
|
+
lastLangType = fileType;
|
|
147
|
+
view.dispatch({
|
|
148
|
+
effects: langCompartment.reconfigure(getLanguageExtension(fileType)),
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// Cleanup on component destroy
|
|
153
|
+
$effect(() => {
|
|
154
|
+
return () => {
|
|
155
|
+
view?.destroy();
|
|
156
|
+
view = undefined;
|
|
157
|
+
};
|
|
158
|
+
});
|
|
159
|
+
</script>
|
|
160
|
+
|
|
161
|
+
<div class="editor-wrapper">
|
|
162
|
+
<div class="editor-box">
|
|
163
|
+
<Toolbar />
|
|
164
|
+
<div class="editor-pane" bind:this={container}></div>
|
|
165
|
+
</div>
|
|
166
|
+
</div>
|
|
167
|
+
|
|
168
|
+
<style>
|
|
169
|
+
.editor-wrapper {
|
|
170
|
+
padding: 1.5rem;
|
|
171
|
+
max-width: 80ch;
|
|
172
|
+
margin: 0 auto;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
.editor-box {
|
|
176
|
+
display: grid;
|
|
177
|
+
grid-template-rows: auto 1fr;
|
|
178
|
+
border: 1px solid var(--cms-border);
|
|
179
|
+
border-radius: 4px;
|
|
180
|
+
overflow: hidden;
|
|
181
|
+
/* Subtract the toolbar, tabs, and wrapper padding from viewport height */
|
|
182
|
+
height: calc(100dvh - 9rem);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
.editor-pane {
|
|
186
|
+
height: 100%;
|
|
187
|
+
overflow: auto;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/*
|
|
191
|
+
* Fill the entire editor box so clicking anywhere starts editing, even on empty documents.
|
|
192
|
+
* cm-editor uses display:flex column by default — min-height stretches the container,
|
|
193
|
+
* and flex-grow on cm-scroller makes the scrollable/clickable area fill it.
|
|
194
|
+
*/
|
|
195
|
+
.editor-pane :global(.cm-editor) {
|
|
196
|
+
min-height: 100%;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
.editor-pane :global(.cm-scroller) {
|
|
200
|
+
flex-grow: 1;
|
|
201
|
+
/*
|
|
202
|
+
* CodeMirror sets align-items: flex-start which prevents cm-content from stretching
|
|
203
|
+
* vertically. Override to stretch so the editable area fills the entire scroller.
|
|
204
|
+
*/
|
|
205
|
+
align-items: stretch !important;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/*
|
|
209
|
+
* Forces .cm-content to shrink below its longest word so overflow-wrap can break long URLs.
|
|
210
|
+
* min-height fills the scroller so the entire editor area is clickable on empty documents.
|
|
211
|
+
* Both need !important to override CodeMirror's inline theme styles.
|
|
212
|
+
*/
|
|
213
|
+
.editor-pane :global(.cm-content) {
|
|
214
|
+
min-width: 0 !important;
|
|
215
|
+
min-height: 100% !important;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/* Wraps long URLs at word boundaries where possible, breaking mid-word only when necessary */
|
|
219
|
+
.editor-pane :global(.cm-link-wrap) {
|
|
220
|
+
overflow-wrap: break-word;
|
|
221
|
+
word-break: break-all;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
.editor-pane :global(.cm-link-wrap span:nth-of-type(2)) {
|
|
225
|
+
word-break: break-word;
|
|
226
|
+
}
|
|
227
|
+
</style>
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { SchemaNode } from '../../js/utils/schema-utils';
|
|
3
|
+
import { extractTabs } from '../../js/utils/schema-utils';
|
|
4
|
+
import { toTitleCase } from '../../js/utils/format';
|
|
5
|
+
import {
|
|
6
|
+
editor,
|
|
7
|
+
setActiveTab,
|
|
8
|
+
getEditorFile,
|
|
9
|
+
} from '../../js/editor/editor.svelte';
|
|
10
|
+
import { hasBodyEditor } from '../../js/utils/file-types';
|
|
11
|
+
|
|
12
|
+
// Props for the EditorTabs component, which renders the tab bar above the editor, including the default Metadata and Body tabs plus any custom schema-defined tabs.
|
|
13
|
+
interface Props {
|
|
14
|
+
// The JSON Schema for the current collection (null if not loaded yet)
|
|
15
|
+
schema: SchemaNode | null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
let { schema }: Props = $props();
|
|
19
|
+
|
|
20
|
+
// Custom tab names derived from schema, sorted alphabetically
|
|
21
|
+
const customTabs = $derived(schema ? extractTabs(schema) : []);
|
|
22
|
+
|
|
23
|
+
// Current open file — null when no file is loaded
|
|
24
|
+
const file = $derived(getEditorFile());
|
|
25
|
+
|
|
26
|
+
/*
|
|
27
|
+
* Show the Body tab for files that have a body editor (markdown, MDX, Markdoc).
|
|
28
|
+
* Defaults to true when no file is open to preserve the prior behavior.
|
|
29
|
+
*/
|
|
30
|
+
const showBody = $derived(file ? hasBodyEditor(file.filename) : true);
|
|
31
|
+
|
|
32
|
+
// All tabs: Metadata, conditionally Body, then custom tabs
|
|
33
|
+
const allTabs = $derived([
|
|
34
|
+
'metadata',
|
|
35
|
+
...(showBody ? ['body'] : []),
|
|
36
|
+
...customTabs,
|
|
37
|
+
]);
|
|
38
|
+
</script>
|
|
39
|
+
|
|
40
|
+
<nav class="tabs" aria-label="Editor tabs">
|
|
41
|
+
{#each allTabs as tab}
|
|
42
|
+
<button
|
|
43
|
+
class="tabs__tab"
|
|
44
|
+
class:tabs__tab--active={editor.tab === tab}
|
|
45
|
+
type="button"
|
|
46
|
+
onclick={() => setActiveTab(tab)}
|
|
47
|
+
aria-selected={editor.tab === tab}
|
|
48
|
+
role="tab"
|
|
49
|
+
>
|
|
50
|
+
{toTitleCase(tab)}
|
|
51
|
+
</button>
|
|
52
|
+
{/each}
|
|
53
|
+
</nav>
|
|
54
|
+
|
|
55
|
+
<style>
|
|
56
|
+
/* Tab bar sits below the editor toolbar, separated by a border */
|
|
57
|
+
.tabs {
|
|
58
|
+
display: flex;
|
|
59
|
+
border-bottom: 1px solid var(--cms-border);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.tabs__tab {
|
|
63
|
+
padding: 0.5rem 1rem;
|
|
64
|
+
font-size: 0.875rem;
|
|
65
|
+
color: var(--cms-muted);
|
|
66
|
+
background: none;
|
|
67
|
+
border: none;
|
|
68
|
+
/* Bottom border reserves space to avoid layout shift on active state */
|
|
69
|
+
border-bottom: 2px solid transparent;
|
|
70
|
+
cursor: pointer;
|
|
71
|
+
|
|
72
|
+
&:hover {
|
|
73
|
+
color: var(--cms-fg);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.tabs__tab--active {
|
|
78
|
+
color: var(--cms-fg);
|
|
79
|
+
border-bottom-color: var(--plum);
|
|
80
|
+
}
|
|
81
|
+
</style>
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { getEditorFile } from '../../js/editor/editor.svelte';
|
|
3
|
+
import { nav } from '../../js/state/router.svelte';
|
|
4
|
+
import { schema } from '../../js/state/schema.svelte';
|
|
5
|
+
import {
|
|
6
|
+
handleSave,
|
|
7
|
+
handlePublish,
|
|
8
|
+
computePublishDisabled,
|
|
9
|
+
} from '../../js/handlers/admin';
|
|
10
|
+
import { dialog } from '../../js/state/dialogs.svelte';
|
|
11
|
+
|
|
12
|
+
// Current editor file state
|
|
13
|
+
const file = $derived(getEditorFile());
|
|
14
|
+
|
|
15
|
+
// Active collection derived from route, needed by save/publish handlers
|
|
16
|
+
const activeCollection = $derived(
|
|
17
|
+
nav.route.view !== 'home' ? nav.route.collection : null,
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
// Whether publish is disabled due to missing required fields
|
|
21
|
+
const publishDisabled = $derived(
|
|
22
|
+
computePublishDisabled(schema.active, file?.formData ?? {}),
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
// Display title from formData, falling back to filename or "Untitled Draft"
|
|
26
|
+
const title = $derived(
|
|
27
|
+
file && typeof file.formData.title === 'string'
|
|
28
|
+
? file.formData.title
|
|
29
|
+
: file?.filename || 'Untitled Draft',
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Publishes the current file, showing the filename dialog if a filename is needed first.
|
|
34
|
+
* @return {Promise<void>}
|
|
35
|
+
*/
|
|
36
|
+
async function onPublish(): Promise<void> {
|
|
37
|
+
const result = await handlePublish(activeCollection);
|
|
38
|
+
if (result.status === 'needs-filename') {
|
|
39
|
+
dialog.open('filename');
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
</script>
|
|
43
|
+
|
|
44
|
+
{#if file}
|
|
45
|
+
<header class="toolbar">
|
|
46
|
+
<div class="toolbar__info">
|
|
47
|
+
<h1 class="toolbar__title">
|
|
48
|
+
{title}
|
|
49
|
+
<span
|
|
50
|
+
class="dirty-indicator"
|
|
51
|
+
class:dirty-indicator--visible={file.dirty}
|
|
52
|
+
title={file.dirty ? 'Unsaved changes' : ''}>•</span
|
|
53
|
+
>
|
|
54
|
+
</h1>
|
|
55
|
+
{#if file.filename}
|
|
56
|
+
<p class="toolbar__filename">{file.filename}</p>
|
|
57
|
+
{/if}
|
|
58
|
+
</div>
|
|
59
|
+
<div class="toolbar__actions">
|
|
60
|
+
{#if file.draftId}
|
|
61
|
+
<button
|
|
62
|
+
class="btn btn--danger-outline btn--compact"
|
|
63
|
+
type="button"
|
|
64
|
+
onclick={() => dialog.open('delete')}
|
|
65
|
+
>
|
|
66
|
+
Delete Draft
|
|
67
|
+
</button>
|
|
68
|
+
{/if}
|
|
69
|
+
<button
|
|
70
|
+
class="btn btn--save-outline btn--compact"
|
|
71
|
+
type="button"
|
|
72
|
+
disabled={!file.dirty || file.saving}
|
|
73
|
+
onclick={() => handleSave(activeCollection)}
|
|
74
|
+
>
|
|
75
|
+
{file.saving ? 'Saving...' : 'Save'}
|
|
76
|
+
</button>
|
|
77
|
+
<button
|
|
78
|
+
class="btn btn--primary btn--compact"
|
|
79
|
+
type="button"
|
|
80
|
+
disabled={publishDisabled || file.saving}
|
|
81
|
+
onclick={onPublish}
|
|
82
|
+
>
|
|
83
|
+
Publish
|
|
84
|
+
</button>
|
|
85
|
+
</div>
|
|
86
|
+
</header>
|
|
87
|
+
{/if}
|
|
88
|
+
|
|
89
|
+
<style>
|
|
90
|
+
.toolbar {
|
|
91
|
+
display: grid;
|
|
92
|
+
grid-template-columns: 1fr auto;
|
|
93
|
+
align-items: center;
|
|
94
|
+
padding: 0.5rem 1rem;
|
|
95
|
+
border-bottom: 1px solid var(--cms-border);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.toolbar__info {
|
|
99
|
+
display: grid;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.toolbar__title {
|
|
103
|
+
font-size: 1rem;
|
|
104
|
+
font-weight: normal;
|
|
105
|
+
color: var(--cms-fg);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.toolbar__filename {
|
|
109
|
+
font-size: 0.75rem;
|
|
110
|
+
color: var(--cms-muted);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/* Always rendered to reserve space and prevent layout shift when toggling */
|
|
114
|
+
.dirty-indicator {
|
|
115
|
+
color: transparent;
|
|
116
|
+
font-size: 1.25rem;
|
|
117
|
+
vertical-align: middle;
|
|
118
|
+
margin-left: 0.25rem;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.dirty-indicator--visible {
|
|
122
|
+
color: var(--gold);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.toolbar__actions {
|
|
126
|
+
display: grid;
|
|
127
|
+
grid-auto-flow: column;
|
|
128
|
+
align-items: center;
|
|
129
|
+
gap: 0.5rem;
|
|
130
|
+
}
|
|
131
|
+
</style>
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import {
|
|
3
|
+
getDefaultExtension,
|
|
4
|
+
getTypeForFilename,
|
|
5
|
+
} from '../../js/utils/file-types';
|
|
6
|
+
import {
|
|
7
|
+
changeFileFormat,
|
|
8
|
+
getEditorFile,
|
|
9
|
+
} from '../../js/editor/editor.svelte';
|
|
10
|
+
import { schema } from '../../js/state/schema.svelte';
|
|
11
|
+
|
|
12
|
+
// Type identifiers from the schema's files array (e.g. ['md', 'mdx'])
|
|
13
|
+
const fileTypes = $derived(
|
|
14
|
+
Array.isArray(schema.active?.['files'])
|
|
15
|
+
? (schema.active['files'] as string[])
|
|
16
|
+
: [],
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
// The type identifier of the currently open file (e.g. 'md', 'mdx')
|
|
20
|
+
const activeType = $derived.by(() => {
|
|
21
|
+
const file = getEditorFile();
|
|
22
|
+
if (!file?.filename) return fileTypes[0] ?? '';
|
|
23
|
+
return getTypeForFilename(file.filename) ?? fileTypes[0] ?? '';
|
|
24
|
+
});
|
|
25
|
+
</script>
|
|
26
|
+
|
|
27
|
+
{#if fileTypes.length > 1}
|
|
28
|
+
<div class="format-selector">
|
|
29
|
+
<span class="format-selector__label">Format</span>
|
|
30
|
+
<select
|
|
31
|
+
class="format-selector__select"
|
|
32
|
+
value={activeType}
|
|
33
|
+
onchange={(e) => changeFileFormat((e.target as HTMLSelectElement).value)}
|
|
34
|
+
>
|
|
35
|
+
{#each fileTypes as type}
|
|
36
|
+
<option value={type}>{getDefaultExtension(type) ?? type}</option>
|
|
37
|
+
{/each}
|
|
38
|
+
</select>
|
|
39
|
+
</div>
|
|
40
|
+
{/if}
|
|
41
|
+
|
|
42
|
+
<style>
|
|
43
|
+
/* Inline layout for label + select inside the editor body toolbar */
|
|
44
|
+
.format-selector {
|
|
45
|
+
display: flex;
|
|
46
|
+
align-items: center;
|
|
47
|
+
gap: 0.5rem;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.format-selector__label {
|
|
51
|
+
font-size: 0.875rem;
|
|
52
|
+
color: var(--cms-muted);
|
|
53
|
+
text-transform: uppercase;
|
|
54
|
+
letter-spacing: 0.05em;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.format-selector__select {
|
|
58
|
+
background: var(--cms-bg);
|
|
59
|
+
border: 1px solid var(--cms-border);
|
|
60
|
+
border-radius: 0.25rem;
|
|
61
|
+
color: var(--cms-fg);
|
|
62
|
+
font-size: 0.875rem;
|
|
63
|
+
padding: 0.25rem 0.5rem;
|
|
64
|
+
max-width: 10rem;
|
|
65
|
+
}
|
|
66
|
+
</style>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import FormatSelector from './FormatSelector.svelte';
|
|
3
|
+
</script>
|
|
4
|
+
|
|
5
|
+
<div class="editor-body-toolbar">
|
|
6
|
+
<FormatSelector />
|
|
7
|
+
</div>
|
|
8
|
+
|
|
9
|
+
<style>
|
|
10
|
+
.editor-body-toolbar {
|
|
11
|
+
display: flex;
|
|
12
|
+
align-items: center;
|
|
13
|
+
padding: 0.25rem 0.5rem;
|
|
14
|
+
border-bottom: 1px solid var(--cms-border);
|
|
15
|
+
background: var(--cms-bg);
|
|
16
|
+
}
|
|
17
|
+
</style>
|