domma-cms 0.10.0 → 0.12.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 (119) hide show
  1. package/CLAUDE.md +248 -159
  2. package/admin/css/admin.css +1 -1
  3. package/admin/js/api.js +1 -1
  4. package/admin/js/app.js +7 -3
  5. package/admin/js/config/sidebar-config.js +1 -1
  6. package/admin/js/http-interceptor.js +1 -0
  7. package/admin/js/lib/safe-html.js +1 -0
  8. package/admin/js/templates/layouts.html +5 -4
  9. package/admin/js/templates/notifications.html +14 -0
  10. package/admin/js/templates/plugin-marketplace.html +16 -0
  11. package/admin/js/templates/plugins.html +17 -5
  12. package/admin/js/views/index.js +1 -1
  13. package/admin/js/views/layouts.js +1 -16
  14. package/admin/js/views/notifications.js +1 -0
  15. package/admin/js/views/plugin-marketplace.js +1 -0
  16. package/admin/js/views/plugins.js +16 -16
  17. package/config/navigation.json +5 -72
  18. package/config/plugins.json +10 -14
  19. package/config/presets.json +50 -13
  20. package/config/site.json +11 -63
  21. package/package.json +2 -1
  22. package/plugins/_template/admin/templates/index.html +17 -0
  23. package/plugins/_template/admin/views/index.js +19 -0
  24. package/plugins/_template/config.js +8 -0
  25. package/plugins/_template/plugin.js +23 -0
  26. package/plugins/_template/plugin.json +34 -0
  27. package/plugins/analytics/plugin.json +41 -31
  28. package/plugins/blog/admin/templates/blog.html +22 -0
  29. package/plugins/blog/admin/templates/categories.html +7 -0
  30. package/plugins/blog/admin/templates/comments.html +11 -0
  31. package/plugins/blog/admin/templates/post-editor.html +97 -0
  32. package/plugins/blog/admin/templates/settings.html +11 -0
  33. package/plugins/blog/admin/views/blog.js +183 -0
  34. package/plugins/blog/admin/views/categories.js +235 -0
  35. package/plugins/blog/admin/views/comments.js +187 -0
  36. package/plugins/blog/admin/views/post-editor.js +291 -0
  37. package/plugins/blog/admin/views/settings.js +100 -0
  38. package/plugins/blog/collections/categories/schema.json +12 -0
  39. package/plugins/blog/collections/comments/schema.json +16 -0
  40. package/plugins/blog/collections/posts/schema.json +19 -0
  41. package/plugins/blog/config.js +8 -0
  42. package/plugins/blog/plugin.js +352 -0
  43. package/plugins/blog/plugin.json +96 -0
  44. package/plugins/blog/roles/blog-author.json +10 -0
  45. package/plugins/blog/roles/blog-editor.json +12 -0
  46. package/plugins/blog/templates/author.html +9 -0
  47. package/plugins/blog/templates/category.html +9 -0
  48. package/plugins/blog/templates/index.html +9 -0
  49. package/plugins/blog/templates/post.html +17 -0
  50. package/plugins/blog/templates/tag.html +9 -0
  51. package/plugins/contacts/collections/user-contact-groups/schema.json +1 -1
  52. package/plugins/contacts/collections/user-contacts/schema.json +1 -1
  53. package/plugins/contacts/plugin.js +4 -10
  54. package/plugins/contacts/plugin.json +13 -3
  55. package/plugins/notes/collections/user-notes/schema.json +1 -1
  56. package/plugins/notes/plugin.js +3 -9
  57. package/plugins/notes/plugin.json +13 -3
  58. package/plugins/site-search/plugin.json +5 -2
  59. package/plugins/theme-switcher/plugin.json +1 -1
  60. package/plugins/todo/collections/todos/schema.json +1 -1
  61. package/plugins/todo/plugin.js +3 -9
  62. package/plugins/todo/plugin.json +13 -3
  63. package/public/css/site.css +1 -1
  64. package/scripts/build.js +48 -0
  65. package/scripts/create-plugin.js +113 -0
  66. package/scripts/fresh.js +6 -7
  67. package/scripts/gen-instance-secret.js +46 -0
  68. package/scripts/reset.js +3 -3
  69. package/scripts/setup.js +31 -13
  70. package/server/middleware/auth.js +48 -0
  71. package/server/middleware/managerAuth.js +36 -0
  72. package/server/routes/api/actions.js +1 -1
  73. package/server/routes/api/auth.js +4 -3
  74. package/server/routes/api/layouts.js +173 -49
  75. package/server/routes/api/notifications.js +155 -0
  76. package/server/routes/api/plugin-marketplace.js +75 -0
  77. package/server/routes/api/users.js +1 -1
  78. package/server/routes/api/views.js +1 -1
  79. package/server/routes/public.js +4 -9
  80. package/server/server.js +32 -3
  81. package/server/services/actions.js +1 -1
  82. package/server/services/managerClient.js +182 -0
  83. package/server/services/permissionRegistry.js +245 -173
  84. package/server/services/pluginInstaller.js +301 -0
  85. package/server/services/plugins.js +117 -10
  86. package/server/services/presetCollections.js +66 -251
  87. package/server/services/renderer.js +99 -0
  88. package/server/services/roles.js +191 -39
  89. package/server/services/users.js +1 -1
  90. package/server/services/views.js +1 -1
  91. package/server/templates/page.html +2 -2
  92. package/plugins/docs/admin/templates/docs.html +0 -69
  93. package/plugins/docs/admin/views/docs.js +0 -276
  94. package/plugins/docs/config.js +0 -8
  95. package/plugins/docs/data/documents/57e003f0-68f2-47dc-9c36-ed4b10ed3deb.json +0 -11
  96. package/plugins/docs/data/folders.json +0 -9
  97. package/plugins/docs/data/templates.json +0 -1
  98. package/plugins/docs/data/versions/57e003f0-68f2-47dc-9c36-ed4b10ed3deb/1.json +0 -5
  99. package/plugins/docs/plugin.js +0 -375
  100. package/plugins/docs/plugin.json +0 -23
  101. package/plugins/form-builder/data/forms/contacts.json +0 -66
  102. package/plugins/form-builder/data/forms/enquiries.json +0 -103
  103. package/plugins/form-builder/data/forms/feedback.json +0 -131
  104. package/plugins/form-builder/data/forms/notes.json +0 -79
  105. package/plugins/form-builder/data/forms/to-do.json +0 -100
  106. package/plugins/form-builder/data/submissions/contacts.json +0 -1
  107. package/plugins/form-builder/data/submissions/enquiries.json +0 -1
  108. package/plugins/form-builder/data/submissions/feedback.json +0 -1
  109. package/plugins/form-builder/data/submissions/notes.json +0 -1
  110. package/plugins/form-builder/data/submissions/to-do.json +0 -1
  111. package/plugins/garage/admin/templates/garage.html +0 -111
  112. package/plugins/garage/admin/views/garage.js +0 -622
  113. package/plugins/garage/collections/garage-vehicles/schema.json +0 -101
  114. package/plugins/garage/config.js +0 -18
  115. package/plugins/garage/data/vehicles.json +0 -70
  116. package/plugins/garage/plugin.js +0 -398
  117. package/plugins/garage/plugin.json +0 -33
  118. package/scripts/seed.js +0 -1996
  119. package/server/services/userTypes.js +0 -227
@@ -2,6 +2,9 @@
2
2
  * Roles Service
3
3
  * Preset Collection — seeds roles on first startup, caches them in memory.
4
4
  * Auth middleware calls getRoleMap() / getPermissionsFor() at request time.
5
+ *
6
+ * Base roles: super-admin (0), admin (1), user (2).
7
+ * Plugin-contributed roles are tagged with a `plugin` field and removed on plugin teardown.
5
8
  */
6
9
  import fs from 'fs/promises';
7
10
  import path from 'path';
@@ -15,6 +18,15 @@ const DIR = path.join(COLLECTIONS_DIR, SLUG);
15
18
  const SCHEMA_PATH = path.join(DIR, 'schema.json');
16
19
  const DATA_PATH = path.join(DIR, 'data.json');
17
20
 
21
+ /** Users dir — derived from collections dir parent (content/users/) */
22
+ const USERS_DIR = path.resolve(path.dirname(COLLECTIONS_DIR), 'users');
23
+
24
+ /** Base role slugs that should always exist */
25
+ const BASE_ROLE_NAMES = ['super-admin', 'admin', 'user'];
26
+
27
+ /** Role slugs that were seeded by the old 4-role system — used to detect legacy data */
28
+ const DEFUNCT_ROLE_NAMES = ['manager', 'editor', 'subscriber'];
29
+
18
30
  export {RESOURCES, ACTIONS};
19
31
 
20
32
  const PRESET_SCHEMA = {
@@ -35,7 +47,8 @@ const PRESET_SCHEMA = {
35
47
  {
36
48
  name: 'badgeClass', label: 'Badge Class', type: 'select',
37
49
  options: ['badge-danger', 'badge-warning', 'badge-info', 'badge-secondary', 'badge-success', 'badge-primary']
38
- }
50
+ },
51
+ {name: 'plugin', label: 'Plugin', type: 'text', required: false}
39
52
  ],
40
53
  api: {
41
54
  create: {enabled: false, access: 'admin'},
@@ -46,22 +59,31 @@ const PRESET_SCHEMA = {
46
59
  };
47
60
 
48
61
  const SEED_ENTRIES = [
49
- {name: 'admin', label: 'Admin', level: 0, permissions: RESOURCES, badgeClass: 'badge-danger'},
50
62
  {
51
- name: 'manager',
52
- label: 'Manager',
63
+ name: 'super-admin',
64
+ label: 'Super Admin',
65
+ level: 0,
66
+ permissions: RESOURCES,
67
+ badgeClass: 'badge-danger'
68
+ },
69
+ {
70
+ name: 'admin',
71
+ label: 'Admin',
53
72
  level: 1,
54
- permissions: ['pages', 'settings', 'navigation', 'layouts', 'media', 'users', 'collections', 'views', 'actions'],
73
+ permissions: [
74
+ 'pages', 'media', 'blocks', 'navigation', 'layouts',
75
+ 'collections', 'views', 'actions',
76
+ 'users', 'settings', 'notifications'
77
+ ],
55
78
  badgeClass: 'badge-warning'
56
79
  },
57
80
  {
58
- name: 'editor',
59
- label: 'Editor',
81
+ name: 'user',
82
+ label: 'User',
60
83
  level: 2,
61
- permissions: ['pages.read', 'pages.create', 'pages.update', 'media'],
62
- badgeClass: 'badge-info'
63
- },
64
- {name: 'subscriber', label: 'Subscriber', level: 3, permissions: [], badgeClass: 'badge-secondary'}
84
+ permissions: ['notifications'],
85
+ badgeClass: 'badge-secondary'
86
+ }
65
87
  ];
66
88
 
67
89
  // ---------------------------------------------------------------------------
@@ -80,7 +102,7 @@ let rawPermissionsMap = new Map();
80
102
  /**
81
103
  * Build in-memory maps from an array of data entries.
82
104
  * Supports both bare resource names ('pages') and dotted action strings ('pages.read').
83
- * Bare names expand to all four actions for backward compatibility.
105
+ * Bare names expand to all actions for backward compatibility.
84
106
  *
85
107
  * @param {object[]} entries
86
108
  */
@@ -101,44 +123,111 @@ function buildCache(entries) {
101
123
  for (const perm of (d.permissions || [])) {
102
124
  if (perm.includes('.')) {
103
125
  const [res] = perm.split('.');
104
- addTo(perm, d.name); // 'pages.read' → [role]
105
- addTo(res, d.name); // 'pages' → [role] (any-action union)
126
+ addTo(perm, d.name);
127
+ addTo(res, d.name);
106
128
  } else {
107
- addTo(perm, d.name); // 'pages' (bare) → [role]
129
+ addTo(perm, d.name);
108
130
  for (const action of getActionsForResource(perm)) {
109
- addTo(`${perm}.${action}`, d.name); // expand to resource's actual actions
131
+ addTo(`${perm}.${action}`, d.name);
110
132
  }
111
133
  }
112
134
  }
113
135
  }
114
136
  }
115
137
 
138
+ // ---------------------------------------------------------------------------
139
+ // Internal helpers
140
+ // ---------------------------------------------------------------------------
141
+
142
+ async function readData() {
143
+ const raw = await fs.readFile(DATA_PATH, 'utf8');
144
+ return JSON.parse(raw);
145
+ }
146
+
147
+ async function writeData(entries) {
148
+ await fs.writeFile(DATA_PATH, JSON.stringify(entries, null, 2) + '\n', 'utf8');
149
+ }
150
+
151
+ function makeEntry(data) {
152
+ return {
153
+ id: uuidv4(),
154
+ data,
155
+ createdAt: new Date().toISOString(),
156
+ updatedAt: new Date().toISOString()
157
+ };
158
+ }
159
+
116
160
  // ---------------------------------------------------------------------------
117
161
  // Public API
118
162
  // ---------------------------------------------------------------------------
119
163
 
120
164
  /**
121
- * Seed the preset collection on first startup (no-op if data.json already exists).
165
+ * Seed the preset collection on first startup.
166
+ * Always writes schema. Writes base roles if missing; migrates legacy on-disk data.
167
+ * Scans content/users/ and rewrites any defunct role assignments to 'user'.
122
168
  *
123
169
  * @returns {Promise<void>}
124
170
  */
125
171
  export async function seed() {
126
172
  await fs.mkdir(DIR, {recursive: true});
127
-
128
- // Always write schema (overwrite to keep in sync with code)
129
173
  await fs.writeFile(SCHEMA_PATH, JSON.stringify(PRESET_SCHEMA, null, 2) + '\n', 'utf8');
130
174
 
131
- // Only write data if it doesn't exist yet
175
+ let entries;
132
176
  try {
133
177
  await fs.access(DATA_PATH);
178
+ entries = await readData();
179
+
180
+ // Migrate: if the data still carries defunct base roles, replace the base layer
181
+ const hasDefunct = entries.some(e => DEFUNCT_ROLE_NAMES.includes(e.data?.name));
182
+ const missingBase = !entries.some(e => e.data?.name === 'super-admin');
183
+ if (hasDefunct || missingBase) {
184
+ // Keep only plugin-contributed entries, rebuild base entries
185
+ const pluginEntries = entries.filter(e => e.data?.plugin);
186
+ const baseEntries = SEED_ENTRIES.map(data => makeEntry(data));
187
+ entries = [...baseEntries, ...pluginEntries];
188
+ await writeData(entries);
189
+ }
190
+ } catch {
191
+ // data.json doesn't exist — fresh install
192
+ entries = SEED_ENTRIES.map(data => makeEntry(data));
193
+ await writeData(entries);
194
+ }
195
+
196
+ // Migrate existing user files whose role is no longer recognised
197
+ await migrateUserRoles(entries);
198
+ }
199
+
200
+ /**
201
+ * Rewrite any user file whose role is not in the current role set to 'user'.
202
+ *
203
+ * @param {object[]} roleEntries - Current on-disk role entries
204
+ * @returns {Promise<void>}
205
+ */
206
+ async function migrateUserRoles(roleEntries) {
207
+ const knownRoles = new Set(roleEntries.map(e => e.data?.name).filter(Boolean));
208
+
209
+ let files;
210
+ try {
211
+ files = await fs.readdir(USERS_DIR);
134
212
  } catch {
135
- const entries = SEED_ENTRIES.map(data => ({
136
- id: uuidv4(),
137
- data,
138
- createdAt: new Date().toISOString(),
139
- updatedAt: new Date().toISOString()
140
- }));
141
- await fs.writeFile(DATA_PATH, JSON.stringify(entries, null, 2) + '\n', 'utf8');
213
+ return; // users dir doesn't exist yet — nothing to migrate
214
+ }
215
+
216
+ for (const file of files) {
217
+ if (!file.endsWith('.json')) continue;
218
+ const filePath = path.join(USERS_DIR, file);
219
+ try {
220
+ const raw = await fs.readFile(filePath, 'utf8');
221
+ const user = JSON.parse(raw);
222
+ if (user.role && !knownRoles.has(user.role)) {
223
+ const oldRole = user.role;
224
+ user.role = 'user';
225
+ await fs.writeFile(filePath, JSON.stringify(user, null, 2) + '\n', 'utf8');
226
+ console.log(`[roles] Migrated user ${user.email ?? file} from '${oldRole}' to 'user'`);
227
+ }
228
+ } catch {
229
+ // skip unreadable files
230
+ }
142
231
  }
143
232
  }
144
233
 
@@ -149,8 +238,7 @@ export async function seed() {
149
238
  */
150
239
  export async function load() {
151
240
  try {
152
- const raw = await fs.readFile(DATA_PATH, 'utf8');
153
- const entries = JSON.parse(raw);
241
+ const entries = await readData();
154
242
  buildCache(entries);
155
243
  } catch (err) {
156
244
  console.warn('[roles] Failed to load roles collection:', err.message);
@@ -166,6 +254,77 @@ export async function invalidate() {
166
254
  await load();
167
255
  }
168
256
 
257
+ /**
258
+ * Persist a new role entry and rebuild the cache.
259
+ * Idempotent — if a role with the same name exists, it is updated in place.
260
+ *
261
+ * @param {{name:string,label:string,level:number,permissions:string[],badgeClass?:string,plugin?:string}} data
262
+ * @returns {Promise<void>}
263
+ */
264
+ export async function createRole(data) {
265
+ let entries;
266
+ try {
267
+ entries = await readData();
268
+ } catch {
269
+ entries = [];
270
+ }
271
+
272
+ const idx = entries.findIndex(e => e.data?.name === data.name);
273
+ if (idx >= 0) {
274
+ entries[idx].data = {...entries[idx].data, ...data, updatedAt: new Date().toISOString()};
275
+ entries[idx].updatedAt = new Date().toISOString();
276
+ } else {
277
+ entries.push(makeEntry(data));
278
+ }
279
+
280
+ await writeData(entries);
281
+ buildCache(entries);
282
+ }
283
+
284
+ /**
285
+ * Remove a role entry by name and rebuild the cache.
286
+ * Refuses to remove base roles (level 0 protection is enforced elsewhere;
287
+ * here we additionally refuse by name).
288
+ *
289
+ * @param {string} name - Role slug
290
+ * @returns {Promise<void>}
291
+ */
292
+ export async function removeRole(name) {
293
+ if (BASE_ROLE_NAMES.includes(name)) {
294
+ throw new Error(`Cannot remove built-in role '${name}'`);
295
+ }
296
+
297
+ let entries;
298
+ try {
299
+ entries = await readData();
300
+ } catch {
301
+ return;
302
+ }
303
+
304
+ entries = entries.filter(e => e.data?.name !== name);
305
+ await writeData(entries);
306
+ buildCache(entries);
307
+ }
308
+
309
+ /**
310
+ * Remove all roles contributed by a plugin and rebuild the cache.
311
+ *
312
+ * @param {string} plugin - Plugin name
313
+ * @returns {Promise<void>}
314
+ */
315
+ export async function removeRolesByPlugin(plugin) {
316
+ let entries;
317
+ try {
318
+ entries = await readData();
319
+ } catch {
320
+ return;
321
+ }
322
+
323
+ entries = entries.filter(e => e.data?.plugin !== plugin);
324
+ await writeData(entries);
325
+ buildCache(entries);
326
+ }
327
+
169
328
  /**
170
329
  * Return the full role map.
171
330
  *
@@ -187,26 +346,19 @@ export function getRoleLevel(roleName) {
187
346
 
188
347
  /**
189
348
  * Return the role names allowed to access a resource (and optional action).
190
- * - getPermissionsFor('pages') → roles with ANY action on pages (backward compat)
191
- * - getPermissionsFor('pages', 'delete')→ roles with delete on pages
192
- * - getPermissionsFor('pages.delete') → same as above (dot-notation shorthand)
193
349
  *
194
350
  * @param {string} resource - Resource key, or 'resource.action' dot notation
195
351
  * @param {string} [action] - Optional action (read | create | update | delete)
196
352
  * @returns {string[]}
197
353
  */
198
354
  export function getPermissionsFor(resource, action) {
199
- if (action) {
200
- return permissionsMap.get(`${resource}.${action}`) ?? [];
201
- }
202
- if (resource.includes('.')) {
203
- return permissionsMap.get(resource) ?? [];
204
- }
355
+ if (action) return permissionsMap.get(`${resource}.${action}`) ?? [];
356
+ if (resource.includes('.')) return permissionsMap.get(resource) ?? [];
205
357
  return permissionsMap.get(resource) ?? [];
206
358
  }
207
359
 
208
360
  /**
209
- * Return the raw permissions array for a role — used by the /api/auth/permissions endpoint.
361
+ * Return the raw permissions array for a role.
210
362
  *
211
363
  * @param {string} roleName
212
364
  * @returns {string[]}
@@ -122,7 +122,7 @@ export async function getUserByEmail(email) {
122
122
  * @param {object} data - { name, email, password, role }
123
123
  * @returns {Promise<object>} Created user (password stripped)
124
124
  */
125
- export async function createUser({ name, email, password, role = 'editor' }) {
125
+ export async function createUser({ name, email, password, role = 'user' }) {
126
126
  const existing = await getUserByEmail(email);
127
127
  if (existing) throw new Error('A user with that email already exists');
128
128
 
@@ -165,7 +165,7 @@ export async function createView(data, userId = null) {
165
165
  block: display?.block || ''
166
166
  },
167
167
  access: {
168
- roles: access?.roles || ['admin'],
168
+ roles: access?.roles || ['admin', 'super-admin'],
169
169
  public: access?.public || false,
170
170
  rowLevel: access?.rowLevel || null
171
171
  },
@@ -29,7 +29,7 @@
29
29
  <!-- Late head injection — custom CSS always loads last so it can override everything -->
30
30
  {{headInjectLate}}
31
31
  </head>
32
- <body class="dm-cloaked dm-theme-{{theme}}" data-layout="{{layout}}">
32
+ <body class="dm-cloaked dm-theme-{{theme}} {{layoutBodyClass}}" data-layout="{{layout}}">
33
33
 
34
34
  {{#if showNavbar}}
35
35
  <nav id="site-navbar"></nav>
@@ -43,7 +43,7 @@
43
43
  <article class="site-content">
44
44
  <div class="container">
45
45
  {{breadcrumbsHtml}}
46
- <div class="page-body">
46
+ <div class="page-body"{{#if pageBodyStyle}} style="{{pageBodyStyle}}"{{/if}}>
47
47
  {{html}}
48
48
  </div>
49
49
  </div>
@@ -1,69 +0,0 @@
1
- <div class="docs-layout" style="display:grid;grid-template-columns:200px 260px 1fr;height:calc(100vh - 120px);gap:0;border:1px solid var(--dm-border);border-radius:var(--dm-radius);overflow:hidden;">
2
-
3
- <!-- Folder sidebar -->
4
- <div class="docs-folders" style="border-right:1px solid var(--dm-border);display:flex;flex-direction:column;">
5
- <div style="padding:0.75rem;border-bottom:1px solid var(--dm-border);display:flex;align-items:center;justify-content:space-between;">
6
- <span style="font-weight:600;font-size:0.85rem;">Folders</span>
7
- <button id="new-folder-btn" class="btn btn-sm btn-ghost" title="New Folder">
8
- <span data-icon="plus" data-icon-size="14"></span>
9
- </button>
10
- </div>
11
- <div id="folder-sidebar" style="flex:1;overflow-y:auto;padding:0.5rem;"></div>
12
- </div>
13
-
14
- <!-- Document list -->
15
- <div class="docs-list-pane" style="border-right:1px solid var(--dm-border);display:flex;flex-direction:column;">
16
- <div style="padding:0.75rem;border-bottom:1px solid var(--dm-border);">
17
- <input id="doc-search" class="form-input form-input-sm" placeholder="Search documents..." style="width:100%;">
18
- </div>
19
- <div style="padding:0.5rem;border-bottom:1px solid var(--dm-border);display:flex;gap:0.5rem;">
20
- <button id="new-doc-btn" class="btn btn-sm btn-primary" style="flex:1;">
21
- <span data-icon="plus" data-icon-size="14"></span> New
22
- </button>
23
- <button id="new-from-template-btn" class="btn btn-sm btn-outline" title="New from Template">
24
- <span data-icon="layout" data-icon-size="14"></span>
25
- </button>
26
- </div>
27
- <div id="doc-list" style="flex:1;overflow-y:auto;"></div>
28
- </div>
29
-
30
- <!-- Editor pane -->
31
- <div style="display:flex;flex-direction:column;overflow:hidden;">
32
-
33
- <!-- Placeholder when no doc is selected -->
34
- <div id="editor-placeholder" style="flex:1;display:flex;align-items:center;justify-content:center;color:var(--dm-text-muted);">
35
- <div style="text-align:center;">
36
- <span data-icon="book-open" data-icon-size="48" style="display:block;margin-bottom:1rem;opacity:0.4;"></span>
37
- <p>Select a document to start editing</p>
38
- </div>
39
- </div>
40
-
41
- <!-- Editor when a doc is open -->
42
- <div id="editor-pane" style="display:none;flex-direction:column;flex:1;overflow:hidden;">
43
- <!-- Toolbar -->
44
- <div style="padding:0.5rem 0.75rem;border-bottom:1px solid var(--dm-border);display:flex;align-items:center;gap:0.5rem;flex-shrink:0;">
45
- <input id="doc-title-input" class="form-input" placeholder="Document title"
46
- style="flex:1;border:none;font-weight:600;font-size:1rem;background:transparent;outline:none;padding:0.25rem;">
47
- <button id="save-doc-btn" class="btn btn-sm btn-primary">
48
- <span data-icon="save" data-icon-size="14"></span> Save
49
- </button>
50
- <button id="find-replace-btn" class="btn btn-sm btn-ghost" title="Find &amp; Replace">
51
- <span data-icon="search" data-icon-size="14"></span>
52
- </button>
53
- <button id="version-history-btn" class="btn btn-sm btn-ghost" title="Version History">
54
- <span data-icon="clock" data-icon-size="14"></span>
55
- </button>
56
- <button id="duplicate-doc-btn" class="btn btn-sm btn-ghost" title="Duplicate Document">
57
- <span data-icon="copy" data-icon-size="14"></span>
58
- </button>
59
- <button id="delete-doc-btn" class="btn btn-sm btn-ghost btn-danger" title="Delete Document">
60
- <span data-icon="trash" data-icon-size="14"></span>
61
- </button>
62
- </div>
63
- <!-- Content area -->
64
- <div id="doc-editor-content" style="flex:1;overflow-y:auto;padding:1rem;"></div>
65
- </div>
66
-
67
- </div>
68
-
69
- </div>