webspresso 0.0.63 → 0.0.65

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.
@@ -14,10 +14,10 @@ module.exports = {
14
14
  meta.label || name,
15
15
  required ? m('span.text-red-500', ' *') : null
16
16
  ),
17
- m('.border.border-gray-300.rounded.p-4', [
17
+ m('.border.border-gray-300 dark:border-slate-600.rounded.p-4', [
18
18
  items.map((item, index) =>
19
19
  m('.flex.items-center.gap-2.mb-2', [
20
- m('input.flex-1.px-3.py-2.border.border-gray-300.rounded', {
20
+ m('input.flex-1.px-3.py-2.border.border-gray-300 dark:border-slate-600.rounded', {
21
21
  type: 'text',
22
22
  value: String(item),
23
23
  oninput: (e) => {
@@ -15,7 +15,7 @@ module.exports = {
15
15
  meta.label || name,
16
16
  required ? m('span.text-red-500', ' *') : null
17
17
  ),
18
- m('input.w-full.px-3.py-2.border.border-gray-300.rounded', {
18
+ m('input.w-full.px-3.py-2.border.border-gray-300 dark:border-slate-600.rounded', {
19
19
  id: name,
20
20
  name,
21
21
  type: 'text',
@@ -43,7 +43,7 @@ module.exports = {
43
43
  meta.label || name,
44
44
  required ? m('span.text-red-500', ' *') : null
45
45
  ),
46
- m('textarea.w-full.px-3.py-2.border.border-gray-300.rounded', {
46
+ m('textarea.w-full.px-3.py-2.border.border-gray-300 dark:border-slate-600.rounded', {
47
47
  id: name,
48
48
  name,
49
49
  rows: meta.rows || 5,
@@ -69,7 +69,7 @@ module.exports = {
69
69
  meta.label || name,
70
70
  required ? m('span.text-red-500', ' *') : null
71
71
  ),
72
- m('input.w-full.px-3.py-2.border.border-gray-300.rounded', {
72
+ m('input.w-full.px-3.py-2.border.border-gray-300 dark:border-slate-600.rounded', {
73
73
  id: name,
74
74
  name,
75
75
  type: 'number',
@@ -127,7 +127,7 @@ module.exports = {
127
127
  meta.label || name,
128
128
  required ? m('span.text-red-500', ' *') : null
129
129
  ),
130
- m('input.w-full.px-3.py-2.border.border-gray-300.rounded', {
130
+ m('input.w-full.px-3.py-2.border.border-gray-300 dark:border-slate-600.rounded', {
131
131
  id: name,
132
132
  name,
133
133
  type: 'date',
@@ -155,7 +155,7 @@ module.exports = {
155
155
  meta.label || name,
156
156
  required ? m('span.text-red-500', ' *') : null
157
157
  ),
158
- m('input.w-full.px-3.py-2.border.border-gray-300.rounded', {
158
+ m('input.w-full.px-3.py-2.border.border-gray-300 dark:border-slate-600.rounded', {
159
159
  id: name,
160
160
  name,
161
161
  type: 'datetime-local',
@@ -183,7 +183,7 @@ module.exports = {
183
183
  meta.label || name,
184
184
  required ? m('span.text-red-500', ' *') : null
185
185
  ),
186
- m('select.w-full.px-3.py-2.border.border-gray-300.rounded', {
186
+ m('select.w-full.px-3.py-2.border.border-gray-300 dark:border-slate-600.rounded', {
187
187
  id: name,
188
188
  name,
189
189
  value: String(value || ''),
@@ -63,7 +63,7 @@ module.exports = {
63
63
  meta.label || name,
64
64
  required ? m('span.text-red-500', ' *') : null
65
65
  ),
66
- m('div#drop-zone-' + name + '.border-2.border-dashed.border-gray-300.rounded.p-8.text-center', {
66
+ m('div#drop-zone-' + name + '.border-2.border-dashed.border-gray-300 dark:border-slate-600.rounded.p-8.text-center', {
67
67
  style: 'cursor: pointer;'
68
68
  }, [
69
69
  m('input[type=file]', {
@@ -77,7 +77,7 @@ module.exports = {
77
77
  }
78
78
  }),
79
79
  m('div', [
80
- m('p.text-gray-600.mb-2', 'Drag and drop a file here, or'),
80
+ m('p.text-gray-600 dark:text-slate-400.mb-2', 'Drag and drop a file here, or'),
81
81
  m('label.text-blue-600.hover:text-blue-800.cursor-pointer', {
82
82
  for: 'file-input-' + name
83
83
  }, 'browse'),
@@ -22,7 +22,7 @@ module.exports = {
22
22
  meta.label || name,
23
23
  required ? m('span.text-red-500', ' *') : null
24
24
  ),
25
- m('textarea.w-full.px-3.py-2.border.border-gray-300.rounded.font-mono.text-sm', {
25
+ m('textarea.w-full.px-3.py-2.border.border-gray-300 dark:border-slate-600.rounded.font-mono.text-sm', {
26
26
  rows: 10,
27
27
  value: jsonString,
28
28
  oninput: (e) => {
@@ -18,7 +18,7 @@ module.exports = {
18
18
  meta.label || name,
19
19
  required ? m('span.text-red-500', ' *') : null
20
20
  ),
21
- m('select.w-full.px-3.py-2.border.border-gray-300.rounded', {
21
+ m('select.w-full.px-3.py-2.border.border-gray-300 dark:border-slate-600.rounded', {
22
22
  name,
23
23
  value: value ? String(value) : '',
24
24
  required,
@@ -58,7 +58,7 @@ module.exports = {
58
58
  meta.label || name,
59
59
  required ? m('span.text-red-500', ' *') : null
60
60
  ),
61
- m('.border.border-gray-300.rounded.p-4.max-h-64.overflow-y-auto', [
61
+ m('.border.border-gray-300 dark:border-slate-600.rounded.p-4.max-h-64.overflow-y-auto', [
62
62
  relationData.map(item => {
63
63
  const itemValue = String(item[valueKey]);
64
64
  const itemDisplay = item[displayKey] || itemValue;
@@ -95,11 +95,11 @@ module.exports = {
95
95
  const editorId = 'quill-editor-' + name;
96
96
 
97
97
  return m('.mb-4', [
98
- m('label.block.text-sm.font-medium.text-gray-700.mb-1', { for: name },
98
+ m('label.block.text-sm.font-medium.text-gray-700 dark:text-slate-300.mb-1', { for: name },
99
99
  label,
100
100
  required ? m('span.text-red-500', ' *') : null
101
101
  ),
102
- m('div.border.border-gray-300.rounded', {
102
+ m('div.border.border-gray-300 dark:border-slate-600.rounded', {
103
103
  id: editorId,
104
104
  style: 'min-height: 200px;'
105
105
  }),
@@ -108,7 +108,7 @@ module.exports = {
108
108
  id: name + '-value',
109
109
  value: value || '',
110
110
  }),
111
- hint ? m('p.text-xs.text-gray-500.mt-1', hint) : null,
111
+ hint ? m('p.text-xs.text-gray-500 dark:text-slate-400.mt-1', hint) : null,
112
112
  ]);
113
113
  }
114
114
  },
@@ -66,14 +66,16 @@ function adminPanelPlugin(options = {}) {
66
66
  description: 'Modular admin panel for Webspresso with extensions support',
67
67
 
68
68
  // CSP requirements for admin panel scripts
69
+ // Note: cdn.quilljs.com 301-redirects to cdn.jsdelivr.net; CSP is enforced on the final URL.
69
70
  csp: {
70
- styleSrc: ['https://cdn.quilljs.com'],
71
+ styleSrc: ['https://cdn.quilljs.com', 'https://cdn.jsdelivr.net'],
71
72
  scriptSrc: [
72
73
  'https://cdn.quilljs.com',
74
+ 'https://cdn.jsdelivr.net',
73
75
  'https://unpkg.com',
74
76
  'https://cdn.tailwindcss.com',
75
77
  ],
76
- connectSrc: ['https://unpkg.com', 'https://cdn.tailwindcss.com'],
78
+ connectSrc: ['https://unpkg.com', 'https://cdn.tailwindcss.com', 'https://cdn.jsdelivr.net'],
77
79
  },
78
80
  enabled,
79
81
  registry, // Expose registry for external configuration
@@ -338,19 +340,48 @@ function generateAdminPanelHtml(adminPath, registry) {
338
340
  <meta charset="UTF-8">
339
341
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
340
342
  <title>${settings.title || 'Admin Panel'}</title>
343
+ <script>
344
+ (function () {
345
+ var K = 'webspresso-admin-theme';
346
+ function sync() {
347
+ try {
348
+ var v = localStorage.getItem(K);
349
+ var dark = (v === 'dark') || ((v !== 'light') && window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches);
350
+ document.documentElement.classList.toggle('dark', dark);
351
+ } catch (e) {}
352
+ }
353
+ sync();
354
+ window.__syncAdminTheme = sync;
355
+ try {
356
+ window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function () {
357
+ var v = localStorage.getItem(K);
358
+ if (v === 'light' || v === 'dark') return;
359
+ sync();
360
+ });
361
+ } catch (e) {}
362
+ })();
363
+ </script>
341
364
  <script src="https://unpkg.com/mithril/mithril.js"></script>
342
365
  <script src="https://cdn.tailwindcss.com"></script>
366
+ <script>tailwind.config = { darkMode: 'class' };</script>
343
367
  <style>
344
368
  body { margin: 0; font-family: system-ui, -apple-system, sans-serif; }
369
+ html.dark { color-scheme: dark; }
345
370
  :root {
346
371
  --primary-color: ${settings.primaryColor || '#3B82F6'};
347
372
  }
348
373
  .bg-primary { background-color: var(--primary-color); }
349
374
  .text-primary { color: var(--primary-color); }
350
375
  .border-primary { border-color: var(--primary-color); }
376
+ .dark .ql-toolbar.ql-snow { border-color: #475569; background: #1e293b; }
377
+ .dark .ql-container.ql-snow { border-color: #475569; background: #0f172a; }
378
+ .dark .ql-editor { color: #f1f5f9; min-height: 8rem; }
379
+ .dark .ql-snow .ql-stroke { stroke: #94a3b8; }
380
+ .dark .ql-snow .ql-fill { fill: #94a3b8; }
381
+ .dark .ql-picker { color: #cbd5e1; }
351
382
  </style>
352
383
  </head>
353
- <body>
384
+ <body class="min-h-screen bg-gray-50 dark:bg-slate-950 text-gray-900 dark:text-slate-100 antialiased">
354
385
  <div id="app"></div>
355
386
  <script>
356
387
  window.__ADMIN_PATH__ = ${JSON.stringify(adminPath)};
@@ -366,7 +397,7 @@ function generateAdminPanelHtml(adminPath, registry) {
366
397
 
367
398
  // Spinner Component
368
399
  const Spinner = {
369
- view: () => m('div.animate-spin.rounded-full.h-6.w-6.border-2.border-blue-500.border-t-transparent'),
400
+ view: () => m('div.animate-spin.rounded-full.h-6.w-6.border-2.border-blue-500.dark:border-blue-400.border-t-transparent'),
370
401
  };
371
402
 
372
403
  ${menuComponent}
@@ -163,13 +163,13 @@ const BulkActionsBar = {
163
163
 
164
164
  if (selectedCount === 0) return null;
165
165
 
166
- return m('div.fixed.bottom-0.left-0.right-0.bg-white.border-t.shadow-lg.p-4.z-50', [
166
+ return m('div.fixed.bottom-0.left-0.right-0.bg-white dark:bg-slate-800.border-t.shadow-lg.p-4.z-50', [
167
167
  m('div.max-w-7xl.mx-auto.flex.items-center.justify-between', [
168
168
  m('div.flex.items-center.gap-4', [
169
169
  m('span.text-sm.font-medium.text-gray-700',
170
170
  selectedCount + ' record' + (selectedCount !== 1 ? 's' : '') + ' selected'
171
171
  ),
172
- m('button.text-sm.text-gray-500.hover:text-gray-700.underline', {
172
+ m('button.text-sm.text-gray-500 dark:text-slate-400.hover:text-gray-700 dark:hover:text-slate-200 dark:hover:text-slate-200.underline', {
173
173
  onclick: onClearSelection,
174
174
  }, 'Clear selection'),
175
175
  ]),
@@ -182,7 +182,7 @@ const BulkActionsBar = {
182
182
  ? 'bg-green-100 text-green-700 hover:bg-green-200'
183
183
  : action.color === 'blue'
184
184
  ? 'bg-blue-100 text-blue-700 hover:bg-blue-200'
185
- : 'bg-gray-100 text-gray-700 hover:bg-gray-200',
185
+ : 'bg-gray-100 text-gray-700 dark:text-slate-300 hover:bg-gray-200 dark:hover:bg-slate-600 dark:hover:bg-slate-600',
186
186
  onclick: () => onAction(action),
187
187
  }, [
188
188
  action.icon && m(Icon, { name: action.icon, class: 'w-4 h-4' }),
@@ -205,13 +205,13 @@ const ConfirmModal = {
205
205
  if (e.target === e.currentTarget) onCancel();
206
206
  },
207
207
  }, [
208
- m('div.bg-white.rounded-lg.shadow-xl.max-w-md.w-full.mx-4', [
208
+ m('div.bg-white dark:bg-slate-800.rounded-lg.shadow-xl.max-w-md.w-full.mx-4', [
209
209
  m('div.p-6', [
210
210
  m('h3.text-lg.font-medium.text-gray-900', title || 'Confirm Action'),
211
211
  m('p.mt-2.text-sm.text-gray-500', message),
212
212
  ]),
213
- m('div.bg-gray-50.px-6.py-4.flex.justify-end.gap-3.rounded-b-lg', [
214
- m('button.px-4.py-2.text-sm.font-medium.text-gray-700.bg-white.border.border-gray-300.rounded.hover:bg-gray-50', {
213
+ m('div.bg-gray-50 dark:bg-slate-900.px-6.py-4.flex.justify-end.gap-3.rounded-b-lg', [
214
+ m('button.px-4.py-2.text-sm.font-medium.text-gray-700 dark:text-slate-300.bg-white dark:bg-slate-800.border.border-gray-300 dark:border-slate-600.rounded.hover:bg-gray-50 dark:hover:bg-slate-800/50 dark:hover:bg-slate-800/50', {
215
215
  onclick: onCancel,
216
216
  }, 'Cancel'),
217
217
  m('button.px-4.py-2.text-sm.font-medium.text-white.rounded', {
@@ -182,7 +182,7 @@ function createCustomPage(pageConfig) {
182
182
 
183
183
  m('div.mb-6', [
184
184
  m('h1.text-2xl.font-bold.text-gray-900', pageConfig.title),
185
- pageConfig.description && m('p.text-gray-500.mt-1', pageConfig.description),
185
+ pageConfig.description && m('p.text-gray-500 dark:text-slate-400.mt-1', pageConfig.description),
186
186
  ]),
187
187
 
188
188
  loading
@@ -191,7 +191,7 @@ function createCustomPage(pageConfig) {
191
191
  ? m('div.bg-red-50.border.border-red-200.rounded.p-4.text-red-700', error)
192
192
  : pageConfig.render
193
193
  ? pageConfig.render(data, vnode)
194
- : m('div.bg-white.rounded-lg.shadow.p-6', [
194
+ : m('div.bg-white dark:bg-slate-800.rounded-lg.shadow.p-6', [
195
195
  data
196
196
  ? m('pre.text-sm.overflow-auto', JSON.stringify(data, null, 2))
197
197
  : m('p.text-gray-500', 'No content'),
@@ -239,11 +239,11 @@ const SettingsPage = {
239
239
 
240
240
  error && m('div.bg-red-50.border.border-red-200.rounded.p-4.text-red-700.mb-4', error),
241
241
 
242
- m('div.bg-white.rounded-lg.shadow', [
242
+ m('div.bg-white dark:bg-slate-800.rounded-lg.shadow', [
243
243
  m('div.p-6.space-y-4', [
244
244
  m('div', [
245
245
  m('label.block.text-sm.font-medium.text-gray-700', 'Panel Title'),
246
- m('input.mt-1.block.w-full.rounded.border-gray-300.shadow-sm.focus:border-blue-500.focus:ring-blue-500', {
246
+ m('input.mt-1.block.w-full.rounded.border-gray-300 dark:border-slate-600.shadow-sm.focus:border-blue-500.focus:ring-blue-500', {
247
247
  type: 'text',
248
248
  value: formData.title || '',
249
249
  oninput: (e) => { formData.title = e.target.value; },
@@ -252,7 +252,7 @@ const SettingsPage = {
252
252
 
253
253
  m('div', [
254
254
  m('label.block.text-sm.font-medium.text-gray-700', 'Primary Color'),
255
- m('input.mt-1.block.w-24.h-10.rounded.border-gray-300.shadow-sm', {
255
+ m('input.mt-1.block.w-24.h-10.rounded.border-gray-300 dark:border-slate-600.shadow-sm', {
256
256
  type: 'color',
257
257
  value: formData.primaryColor || '#3B82F6',
258
258
  oninput: (e) => { formData.primaryColor = e.target.value; },
@@ -261,7 +261,7 @@ const SettingsPage = {
261
261
 
262
262
  m('div', [
263
263
  m('label.block.text-sm.font-medium.text-gray-700', 'Records Per Page'),
264
- m('input.mt-1.block.w-32.rounded.border-gray-300.shadow-sm.focus:border-blue-500.focus:ring-blue-500', {
264
+ m('input.mt-1.block.w-32.rounded.border-gray-300 dark:border-slate-600.shadow-sm.focus:border-blue-500.focus:ring-blue-500', {
265
265
  type: 'number',
266
266
  min: 5,
267
267
  max: 100,
@@ -271,7 +271,7 @@ const SettingsPage = {
271
271
  ]),
272
272
  ]),
273
273
 
274
- m('div.bg-gray-50.px-6.py-4.flex.justify-end.gap-3.rounded-b-lg', [
274
+ m('div.bg-gray-50 dark:bg-slate-900.px-6.py-4.flex.justify-end.gap-3.rounded-b-lg', [
275
275
  m('button.px-4.py-2.text-sm.font-medium.text-white.bg-blue-600.rounded.hover:bg-blue-700.disabled:opacity-50', {
276
276
  disabled: saving,
277
277
  onclick: async () => {
@@ -173,7 +173,7 @@ const WidgetRenderers = {
173
173
  render: (data) => {
174
174
  if (!data || !Array.isArray(data)) return m('div.text-gray-500', 'No data');
175
175
  return m('div.grid.grid-cols-1.md:grid-cols-2.lg:grid-cols-3.gap-4', data.map(stat =>
176
- m('a.block.bg-white.rounded-lg.shadow.hover:shadow-md.transition-shadow.overflow-hidden', {
176
+ m('a.block.bg-white dark:bg-slate-800.rounded-lg.shadow.hover:shadow-md dark:hover:shadow-slate-900/40 dark:hover:shadow-slate-900/40.transition-shadow.overflow-hidden', {
177
177
  href: '/models/' + stat.name,
178
178
  onclick: (e) => {
179
179
  e.preventDefault();
@@ -193,17 +193,17 @@ const WidgetRenderers = {
193
193
  ]),
194
194
  ]),
195
195
  // Stats row
196
- m('div.px-4.py-3.bg-gray-50.grid.grid-cols-3.gap-2.text-center', [
196
+ m('div.px-4.py-3.bg-gray-50 dark:bg-slate-900.grid.grid-cols-3.gap-2.text-center', [
197
197
  m('div', [
198
- m('p.text-xs.text-gray-400.uppercase', 'Columns'),
198
+ m('p.text-xs.text-gray-400 dark:text-slate-500.uppercase', 'Columns'),
199
199
  m('p.text-sm.font-semibold.text-gray-700', stat.columnCount || '-'),
200
200
  ]),
201
201
  m('div', [
202
- m('p.text-xs.text-gray-400.uppercase', 'Table'),
203
- m('p.text-sm.font-semibold.text-gray-700.truncate', { title: stat.table }, stat.table || '-'),
202
+ m('p.text-xs.text-gray-400 dark:text-slate-500.uppercase', 'Table'),
203
+ m('p.text-sm.font-semibold.text-gray-700 dark:text-slate-300.truncate', { title: stat.table }, stat.table || '-'),
204
204
  ]),
205
205
  m('div', [
206
- m('p.text-xs.text-gray-400.uppercase', 'Last Update'),
206
+ m('p.text-xs.text-gray-400 dark:text-slate-500.uppercase', 'Last Update'),
207
207
  m('p.text-sm.font-semibold.text-gray-700',
208
208
  stat.lastUpdated || stat.lastCreated
209
209
  ? formatRelativeTime(stat.lastUpdated || stat.lastCreated)
@@ -220,10 +220,10 @@ const WidgetRenderers = {
220
220
  'recent-activity': {
221
221
  render: (data) => {
222
222
  if (!data?.enabled) {
223
- return m('div.text-gray-500.text-center.py-4', 'Activity logging not enabled');
223
+ return m('div.text-gray-500 dark:text-slate-400.text-center.py-4', 'Activity logging not enabled');
224
224
  }
225
225
  if (!data.activities?.length) {
226
- return m('div.text-gray-500.text-center.py-4', 'No recent activity');
226
+ return m('div.text-gray-500 dark:text-slate-400.text-center.py-4', 'No recent activity');
227
227
  }
228
228
  return m('div.divide-y', data.activities.map(activity =>
229
229
  m('div.py-3.flex.items-center.gap-3', [
@@ -240,7 +240,7 @@ const WidgetRenderers = {
240
240
  }),
241
241
  ]),
242
242
  m('div.flex-1.min-w-0', [
243
- m('p.text-sm.text-gray-900.truncate', activity.description || \`\${activity.action} on \${activity.model}\`),
243
+ m('p.text-sm.text-gray-900 dark:text-slate-100.truncate', activity.description || \`\${activity.action} on \${activity.model}\`),
244
244
  m('p.text-xs.text-gray-500', formatDate(activity.created_at)),
245
245
  ]),
246
246
  activity.user_name && m('span.text-xs.text-gray-400', activity.user_name),
@@ -253,10 +253,10 @@ const WidgetRenderers = {
253
253
  'quick-actions': {
254
254
  render: (data) => {
255
255
  if (!data || !Array.isArray(data) || data.length === 0) {
256
- return m('div.text-gray-500.text-center.py-4', 'No quick actions available');
256
+ return m('div.text-gray-500 dark:text-slate-400.text-center.py-4', 'No quick actions available');
257
257
  }
258
258
  return m('div.space-y-2', data.map(action =>
259
- m('a.flex.items-center.gap-2.px-3.py-2.bg-gray-50.rounded.hover:bg-gray-100.transition-colors', {
259
+ m('a.flex.items-center.gap-2.px-3.py-2.bg-gray-50 dark:bg-slate-900.rounded.hover:bg-gray-100 dark:hover:bg-slate-700 dark:hover:bg-slate-700.transition-colors', {
260
260
  href: action.path,
261
261
  onclick: (e) => {
262
262
  e.preventDefault();
@@ -299,7 +299,7 @@ const WidgetRenderers = {
299
299
  default: {
300
300
  render: (data) => {
301
301
  if (!data) return m('div.text-gray-500', 'No data');
302
- return m('pre.text-xs.bg-gray-50.p-2.rounded.overflow-auto', JSON.stringify(data, null, 2));
302
+ return m('pre.text-xs.bg-gray-50 dark:bg-slate-900.p-2.rounded.overflow-auto', JSON.stringify(data, null, 2));
303
303
  },
304
304
  },
305
305
  };
@@ -340,7 +340,7 @@ const Widget = {
340
340
  full: 'col-span-full',
341
341
  };
342
342
 
343
- return m('div.bg-white.rounded-lg.shadow', {
343
+ return m('div.bg-white dark:bg-slate-800.rounded-lg.shadow', {
344
344
  class: sizeClasses[widget.size] || sizeClasses.md,
345
345
  }, [
346
346
  m('div.px-4.py-3.border-b.border-gray-200', [
@@ -391,7 +391,7 @@ const Dashboard = {
391
391
  return m(Layout, [
392
392
  m('div.mb-6', [
393
393
  m('h1.text-2xl.font-bold.text-gray-900', config?.settings?.title || 'Dashboard'),
394
- m('p.text-gray-500.mt-1', 'Welcome back, ' + (state.user?.name || 'Admin')),
394
+ m('p.text-gray-500 dark:text-slate-400.mt-1', 'Welcome back, ' + (state.user?.name || 'Admin')),
395
395
  ]),
396
396
 
397
397
  widgets.length > 0
@@ -99,6 +99,9 @@ const Icon = {
99
99
  logout: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" />',
100
100
  menu: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />',
101
101
  tool: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />',
102
+ sun: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />',
103
+ moon: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />',
104
+ monitor: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />',
102
105
  };
103
106
 
104
107
  const path = icons[name] || icons.database;
@@ -113,6 +116,43 @@ const Icon = {
113
116
  },
114
117
  };
115
118
 
119
+ var ADMIN_THEME_KEY = 'webspresso-admin-theme';
120
+ function getAdminThemePref() {
121
+ try { return localStorage.getItem(ADMIN_THEME_KEY); } catch (e) { return null; }
122
+ }
123
+ function setAdminThemePref(mode) {
124
+ try {
125
+ if (mode === 'system') localStorage.removeItem(ADMIN_THEME_KEY);
126
+ else localStorage.setItem(ADMIN_THEME_KEY, mode);
127
+ } catch (e) {}
128
+ if (window.__syncAdminTheme) window.__syncAdminTheme();
129
+ m.redraw();
130
+ }
131
+ const ThemeToggle = {
132
+ view() {
133
+ var pref = getAdminThemePref();
134
+ var isSystem = !pref || pref === 'system';
135
+ var isLight = pref === 'light';
136
+ var isDark = pref === 'dark';
137
+ var btn = function(mode, title, iconName) {
138
+ var active = (mode === 'system' && isSystem) || (mode === 'light' && isLight) || (mode === 'dark' && isDark);
139
+ return m('button.p-1.5.rounded-md.transition-colors', {
140
+ type: 'button',
141
+ title: title,
142
+ class: active
143
+ ? 'bg-white dark:bg-slate-600 text-blue-600 dark:text-blue-300 shadow-sm'
144
+ : 'text-gray-500 dark:text-slate-400 hover:bg-gray-100 dark:hover:bg-slate-700',
145
+ onclick: function() { setAdminThemePref(mode); },
146
+ }, m(Icon, { name: iconName, class: 'w-4 h-4' }));
147
+ };
148
+ return m('div.flex.items-center.gap-0.5.p-0.5.rounded-lg.border.border-gray-200 dark:border-slate-600.bg-gray-50 dark:bg-slate-900/80', [
149
+ btn('system', 'Match system', 'monitor'),
150
+ btn('light', 'Light', 'sun'),
151
+ btn('dark', 'Dark', 'moon'),
152
+ ]);
153
+ },
154
+ };
155
+
116
156
  // Menu Item Component
117
157
  const MenuItem = {
118
158
  view(vnode) {
@@ -121,8 +161,8 @@ const MenuItem = {
121
161
  return m('a.flex.items-center.gap-3.px-3.py-2.rounded-lg.text-sm.font-medium.transition-colors', {
122
162
  href: item.path,
123
163
  class: active
124
- ? 'bg-blue-100 text-blue-700'
125
- : 'text-gray-700 hover:bg-gray-100',
164
+ ? 'bg-blue-100 dark:bg-blue-900/40 text-blue-700 dark:text-blue-300'
165
+ : 'text-gray-700 dark:text-slate-300 hover:bg-gray-100 dark:hover:bg-slate-700',
126
166
  onclick: (e) => {
127
167
  e.preventDefault();
128
168
  sidebarOpen = false;
@@ -131,7 +171,7 @@ const MenuItem = {
131
171
  }, [
132
172
  item.icon && m(Icon, { name: item.icon, class: 'w-5 h-5 flex-shrink-0' }),
133
173
  m('span.truncate', item.label),
134
- item.badge && m('span.ml-auto.bg-blue-100.text-blue-600.text-xs.px-2.py-0.5.rounded-full', item.badge),
174
+ item.badge && m('span.ml-auto.bg-blue-100.dark:bg-blue-900/50.text-blue-600.dark:text-blue-300.text-xs.px-2.py-0.5.rounded-full', item.badge),
135
175
  ]);
136
176
  },
137
177
  };
@@ -154,7 +194,7 @@ const MenuGroup = {
154
194
  return m('div.mb-2', [
155
195
  // Group header
156
196
  group.collapsible !== false
157
- ? m('button.w-full.flex.items-center.gap-3.px-3.py-2.text-xs.font-semibold.text-gray-500.uppercase.tracking-wider.hover:text-gray-700', {
197
+ ? m('button.w-full.flex.items-center.gap-3.px-3.py-2.text-xs.font-semibold.text-gray-500 dark:text-slate-400.uppercase.tracking-wider.hover:text-gray-700 dark:hover:text-slate-200', {
158
198
  onclick: () => { vnode.state.collapsed = !collapsed; },
159
199
  }, [
160
200
  group.icon && m(Icon, { name: group.icon, class: 'w-4 h-4' }),
@@ -164,7 +204,7 @@ const MenuGroup = {
164
204
  class: 'w-4 h-4 transition-transform'
165
205
  }),
166
206
  ])
167
- : m('div.px-3.py-2.text-xs.font-semibold.text-gray-500.uppercase.tracking-wider', [
207
+ : m('div.px-3.py-2.text-xs.font-semibold.text-gray-500 dark:text-slate-400.uppercase.tracking-wider', [
168
208
  group.icon && m(Icon, { name: group.icon, class: 'w-4 h-4 inline mr-2' }),
169
209
  group.label,
170
210
  ]),
@@ -217,24 +257,26 @@ const Sidebar = {
217
257
  onclick: () => { sidebarOpen = false; },
218
258
  }),
219
259
 
220
- m('aside.w-64.bg-white.border-r.border-gray-200.flex.flex-col.h-screen.fixed.left-0.top-0.z-40.transition-transform.duration-200', {
260
+ m('aside.w-64.bg-white dark:bg-slate-800.border-r.border-gray-200 dark:border-slate-600.flex.flex-col.h-screen.fixed.left-0.top-0.z-40.transition-transform.duration-200', {
221
261
  class: sidebarOpen ? 'translate-x-0' : '-translate-x-full lg:translate-x-0',
222
262
  }, [
223
263
  // Logo/Title
224
- m('div.h-16.flex.items-center.justify-between.px-4.border-b.border-gray-200', [
225
- m('a.flex.items-center.gap-2.text-lg.font-bold.text-gray-900', {
264
+ m('div.h-16.flex.items-center.justify-between.gap-2.px-4.border-b.border-gray-200.dark:border-slate-600', [
265
+ m('a.flex.items-center.gap-2.text-lg.font-bold.text-gray-900.dark:text-slate-100.min-w-0', {
226
266
  href: '/',
227
267
  onclick: (e) => { e.preventDefault(); sidebarOpen = false; m.route.set('/'); },
228
268
  }, [
229
- m('div.w-8.h-8.bg-blue-600.rounded-lg.flex.items-center.justify-center', [
269
+ m('div.w-8.h-8.bg-blue-600.rounded-lg.flex.items-center.justify-center.flex-shrink-0', [
230
270
  m('span.text-white.font-bold', 'A'),
231
271
  ]),
232
- m('span', settings?.title || 'Admin'),
272
+ m('span.truncate', settings?.title || 'Admin'),
273
+ ]),
274
+ m('div.flex.items-center.gap-2.flex-shrink-0', [
275
+ m(ThemeToggle),
276
+ m('button.p-1.text-gray-400.dark:text-slate-500.hover:text-gray-600.dark:hover:text-slate-300.lg:hidden', {
277
+ onclick: () => { sidebarOpen = false; },
278
+ }, m(Icon, { name: 'x', class: 'w-5 h-5' })),
233
279
  ]),
234
- // Close button (mobile only)
235
- m('button.p-1.text-gray-400.hover:text-gray-600.lg:hidden', {
236
- onclick: () => { sidebarOpen = false; },
237
- }, m(Icon, { name: 'x', class: 'w-5 h-5' })),
238
280
  ]),
239
281
 
240
282
  // Menu
@@ -249,18 +291,18 @@ const Sidebar = {
249
291
  ]),
250
292
 
251
293
  // User section
252
- state.user && m('div.p-4.border-t.border-gray-200', [
294
+ state.user && m('div.p-4.border-t.border-gray-200.dark:border-slate-600', [
253
295
  m('div.flex.items-center.gap-3', [
254
- m('div.w-8.h-8.bg-gray-200.rounded-full.flex.items-center.justify-center', [
255
- m('span.text-sm.font-medium.text-gray-600',
296
+ m('div.w-8.h-8.bg-gray-200 dark:bg-slate-700.rounded-full.flex.items-center.justify-center', [
297
+ m('span.text-sm.font-medium.text-gray-600.dark:text-slate-300',
256
298
  (state.user.name || state.user.email || 'A').charAt(0).toUpperCase()
257
299
  ),
258
300
  ]),
259
301
  m('div.flex-1.min-w-0', [
260
- m('p.text-sm.font-medium.text-gray-900.truncate', state.user.name || 'Admin'),
261
- m('p.text-xs.text-gray-500.truncate', state.user.email),
302
+ m('p.text-sm.font-medium.text-gray-900 dark:text-slate-100.truncate', state.user.name || 'Admin'),
303
+ m('p.text-xs.text-gray-500 dark:text-slate-400.truncate', state.user.email),
262
304
  ]),
263
- m('button.p-1.text-gray-400.hover:text-gray-600', {
305
+ m('button.p-1.text-gray-400 dark:text-slate-500.hover:text-gray-600 dark:hover:text-slate-300', {
264
306
  title: 'Logout',
265
307
  onclick: async () => {
266
308
  await api.post('/auth/logout');
@@ -279,11 +321,14 @@ const Sidebar = {
279
321
  // Mobile Header with hamburger button
280
322
  const MobileHeader = {
281
323
  view(vnode) {
282
- return m('div.lg:hidden.fixed.top-0.left-0.right-0.h-14.bg-white.border-b.border-gray-200.flex.items-center.px-4.z-20', [
283
- m('button.p-2.-ml-1.text-gray-600.hover:text-gray-900.rounded-lg.hover:bg-gray-100', {
284
- onclick: () => { sidebarOpen = true; },
285
- }, m(Icon, { name: 'menu', class: 'w-6 h-6' })),
286
- m('span.ml-3.text-lg.font-semibold.text-gray-900', 'Admin'),
324
+ return m('div.lg:hidden.fixed.top-0.left-0.right-0.h-14.bg-white.dark:bg-slate-800.border-b.border-gray-200.dark:border-slate-600.flex.items-center.justify-between.px-4.z-20', [
325
+ m('div.flex.items-center.min-w-0', [
326
+ m('button.p-2.-ml-1.text-gray-600.dark:text-slate-400.hover:text-gray-900.dark:hover:text-slate-100.rounded-lg.hover:bg-gray-100.dark:hover:bg-slate-700', {
327
+ onclick: () => { sidebarOpen = true; },
328
+ }, m(Icon, { name: 'menu', class: 'w-6 h-6' })),
329
+ m('span.ml-3.text-lg.font-semibold.text-gray-900.dark:text-slate-100.truncate', 'Admin'),
330
+ ]),
331
+ m(ThemeToggle),
287
332
  ]);
288
333
  },
289
334
  };
@@ -291,7 +336,7 @@ const MobileHeader = {
291
336
  // Layout Component (with sidebar)
292
337
  const Layout = {
293
338
  view(vnode) {
294
- return m('div.min-h-screen.bg-gray-50', [
339
+ return m('div.min-h-screen.bg-gray-50.dark:bg-slate-950', [
295
340
  m(MobileHeader),
296
341
  m(Sidebar),
297
342
  m('main.lg:ml-64.p-6.pt-20.lg:pt-6', vnode.children),
package/plugins/index.js CHANGED
@@ -14,6 +14,7 @@ const auditLogPlugin = require('./audit-log');
14
14
  const recaptchaPlugin = require('./recaptcha');
15
15
  const swaggerPlugin = require('./swagger');
16
16
  const healthCheckPlugin = require('./health-check');
17
+ const restResourcePlugin = require('./rest-resources');
17
18
 
18
19
  module.exports = {
19
20
  sitemapPlugin,
@@ -27,5 +28,6 @@ module.exports = {
27
28
  recaptchaPlugin,
28
29
  swaggerPlugin,
29
30
  healthCheckPlugin,
31
+ restResourcePlugin,
30
32
  };
31
33