webspresso 0.0.97 → 0.0.99

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 (136) hide show
  1. package/.agents/ADMIN.md +163 -5
  2. package/.agents/AGENTS.md +1 -1
  3. package/.agents/ARCHITECTURE.md +10 -0
  4. package/.agents/AUTH.md +169 -0
  5. package/.agents/CLI.md +195 -1
  6. package/.agents/ORM.md +88 -4
  7. package/.agents/PLUGINS.md +221 -9
  8. package/.agents/QUEUE.md +127 -0
  9. package/.agents/ROUTING.md +127 -7
  10. package/AGENTS.md +1 -0
  11. package/bin/commands/add-esbuild.js +80 -0
  12. package/bin/commands/add-tailwind.js +76 -31
  13. package/bin/commands/analytics-prune.js +57 -0
  14. package/bin/commands/api.js +207 -15
  15. package/bin/commands/build.js +99 -0
  16. package/bin/commands/dev.js +64 -35
  17. package/bin/commands/doctor.js +26 -0
  18. package/bin/commands/email-prune.js +47 -0
  19. package/bin/commands/make-job.js +86 -0
  20. package/bin/commands/make-model.js +250 -0
  21. package/bin/commands/new.js +45 -28
  22. package/bin/commands/openapi.js +102 -0
  23. package/bin/commands/page.js +6 -3
  24. package/bin/commands/queue-prune.js +59 -0
  25. package/bin/commands/routes.js +52 -0
  26. package/bin/commands/service.js +6 -3
  27. package/bin/commands/typegen.js +106 -0
  28. package/bin/commands/upload-prune.js +79 -0
  29. package/bin/webspresso.js +22 -0
  30. package/core/applySchema.js +13 -6
  31. package/core/auth/hash.js +21 -12
  32. package/core/auth/index.js +60 -0
  33. package/core/auth/manager.js +134 -0
  34. package/core/auth/middleware.js +88 -0
  35. package/core/auth/oauth.js +517 -0
  36. package/core/auth/qrcode.js +499 -0
  37. package/core/auth/totp.js +504 -0
  38. package/core/compression/middleware.js +23 -17
  39. package/core/openapi/build-from-api-routes.js +160 -29
  40. package/core/orm/cache/fingerprint.js +15 -1
  41. package/core/orm/cache/memory-provider.js +27 -2
  42. package/core/orm/eager-loader.js +144 -10
  43. package/core/orm/index.js +20 -7
  44. package/core/orm/model.js +53 -7
  45. package/core/orm/query-builder.js +90 -4
  46. package/core/orm/repository.js +44 -3
  47. package/core/orm/search.js +213 -0
  48. package/core/queue/adapters/database.js +94 -0
  49. package/core/queue/adapters/memory.js +62 -0
  50. package/core/queue/adapters/redis.js +80 -0
  51. package/core/queue/cron.js +331 -0
  52. package/core/queue/index.js +8 -1
  53. package/core/queue/manager.js +205 -1
  54. package/core/queue/scheduler.js +359 -0
  55. package/core/sse/index.js +19 -0
  56. package/core/sse/stream.js +430 -0
  57. package/core/ssr/stream.js +11 -0
  58. package/index.d.ts +1188 -10
  59. package/index.js +88 -96
  60. package/package.json +1 -1
  61. package/plugins/admin-panel/api.js +279 -17
  62. package/plugins/admin-panel/app.js +114 -22
  63. package/plugins/admin-panel/client/parts/01-state-api-breadcrumb.js +314 -9
  64. package/plugins/admin-panel/client/parts/04-field-renderers.js +264 -67
  65. package/plugins/admin-panel/client/parts/05-rich-text-file-helpers.js +333 -36
  66. package/plugins/admin-panel/client/parts/06-login-setup-forms.js +119 -28
  67. package/plugins/admin-panel/client/parts/07-model-list.js +49 -8
  68. package/plugins/admin-panel/client/parts/08-record-list.js +248 -23
  69. package/plugins/admin-panel/client/parts/09-record-form.js +59 -12
  70. package/plugins/admin-panel/client/parts/10-export-registry.js +23 -0
  71. package/plugins/admin-panel/client/verify-spa-parts.js +2 -0
  72. package/plugins/admin-panel/core/admin-module.js +13 -7
  73. package/plugins/admin-panel/core/api-extensions.js +30 -2
  74. package/plugins/admin-panel/core/registry.js +96 -16
  75. package/plugins/admin-panel/field-renderers/file-upload.js +187 -104
  76. package/plugins/admin-panel/field-renderers/index.js +2 -0
  77. package/plugins/admin-panel/index.js +96 -0
  78. package/plugins/admin-panel/lib/login-rate-limiter.js +119 -0
  79. package/plugins/admin-panel/modules/admin-users.js +16 -1
  80. package/plugins/admin-panel/modules/bulk-actions.js +13 -5
  81. package/plugins/admin-panel/modules/custom-pages.js +492 -3
  82. package/plugins/admin-panel/modules/dashboard.js +283 -0
  83. package/plugins/admin-panel/modules/menu.js +56 -18
  84. package/plugins/admin-panel/modules/user-management.js +24 -6
  85. package/plugins/api-key/crypto.js +101 -0
  86. package/plugins/api-key/index.js +288 -0
  87. package/plugins/api-key/middleware.js +461 -0
  88. package/plugins/api-key/migration-template.js +48 -0
  89. package/plugins/api-key/model.js +50 -0
  90. package/plugins/api-key/scopes.js +125 -0
  91. package/plugins/audit-log/admin-component.js +487 -59
  92. package/plugins/audit-log/api-handlers.js +148 -0
  93. package/plugins/audit-log/index.js +153 -3
  94. package/plugins/audit-log/purge.js +60 -10
  95. package/plugins/dashboard/app.js +152 -27
  96. package/plugins/dashboard/index.js +143 -39
  97. package/plugins/dashboard/styles.js +78 -0
  98. package/plugins/data-exchange/export-xlsx.js +9 -2
  99. package/plugins/data-exchange/import.js +9 -2
  100. package/plugins/data-exchange/index.js +6 -3
  101. package/plugins/email/index.js +132 -2
  102. package/plugins/email/purge.js +60 -10
  103. package/plugins/file-manager/admin-component.js +20 -4
  104. package/plugins/file-manager/index.js +21 -4
  105. package/plugins/index.js +10 -0
  106. package/plugins/oauth/index.js +338 -0
  107. package/plugins/orm-cache-admin/index.js +4 -2
  108. package/plugins/queue/index.js +15 -0
  109. package/plugins/schema-explorer/admin-component.js +714 -0
  110. package/plugins/schema-explorer/relations-graph.js +205 -0
  111. package/plugins/schema-explorer.js +248 -69
  112. package/plugins/site-analytics/admin-component.js +23 -0
  113. package/plugins/site-analytics/api-handlers.js +42 -0
  114. package/plugins/site-analytics/index.js +147 -2
  115. package/plugins/site-analytics/purge.js +168 -0
  116. package/plugins/swagger.js +261 -13
  117. package/plugins/upload/index.js +26 -0
  118. package/plugins/upload/orphan-pruner.js +292 -0
  119. package/src/api/api-loader.js +4 -1
  120. package/src/bundle/index.js +517 -0
  121. package/src/bundle/manifest.js +165 -0
  122. package/src/dev/banner.js +296 -0
  123. package/src/dev/colors.js +70 -0
  124. package/src/dev/index.js +62 -0
  125. package/src/dev/profiler.js +354 -0
  126. package/src/dev/typegen.js +497 -0
  127. package/src/dev-reload/index.js +611 -0
  128. package/src/discovery/file-route-parser.js +44 -1
  129. package/src/discovery/scan-routes.js +2 -2
  130. package/src/file-router.js +347 -162
  131. package/src/helpers.js +967 -98
  132. package/src/njk-frontmatter.js +12 -0
  133. package/src/pages/page-loader.js +61 -11
  134. package/src/routing/cascading.js +378 -0
  135. package/src/routing/middleware-resolver.js +92 -0
  136. package/src/server.js +635 -63
@@ -9,6 +9,50 @@ module.exports = components + `
9
9
  // Set route prefix to admin path
10
10
  m.route.prefix = window.__ADMIN_PATH__ || '/_admin';
11
11
 
12
+ // Title formatting and synchronization helper
13
+ function formatAdminTitle(pageTitle, baseTitle, template) {
14
+ baseTitle = baseTitle || 'Admin Panel';
15
+ if (!pageTitle || pageTitle === baseTitle) {
16
+ return baseTitle;
17
+ }
18
+ if (typeof template === 'function') {
19
+ return template(pageTitle, baseTitle);
20
+ }
21
+ if (typeof template === 'string' && template.trim()) {
22
+ var formatted = template;
23
+ if (formatted.indexOf('%s') !== -1) {
24
+ formatted = formatted.replace(/%s/g, pageTitle);
25
+ }
26
+ if (formatted.indexOf('%b') !== -1) {
27
+ formatted = formatted.replace(/%b/g, baseTitle);
28
+ }
29
+ return formatted;
30
+ }
31
+ return pageTitle + ' | ' + baseTitle;
32
+ }
33
+
34
+ function setAdminPageTitle(pageTitle, options) {
35
+ try {
36
+ var config = window.__ADMIN_CONFIG__ || {};
37
+ var settings = config.settings || {};
38
+ var baseTitle = settings.title || 'Admin Panel';
39
+ var template = (options && options.template) || settings.titleTemplate || '%s | %b';
40
+
41
+ if (options && options.raw) {
42
+ document.title = pageTitle || baseTitle;
43
+ return;
44
+ }
45
+
46
+ document.title = formatAdminTitle(pageTitle, baseTitle, template);
47
+ } catch (e) {
48
+ if (pageTitle) {
49
+ document.title = pageTitle;
50
+ }
51
+ }
52
+ }
53
+ window.formatAdminTitle = formatAdminTitle;
54
+ window.setAdminPageTitle = setAdminPageTitle;
55
+
12
56
  // Helper to check auth status
13
57
  async function checkAuth() {
14
58
  if (state.user) return true;
@@ -45,7 +89,7 @@ function getUserManagementModel() {
45
89
  async function guardUserManagementRoutes() {
46
90
  var isAuth = await checkAuth();
47
91
  if (!isAuth) {
48
- redirectToLogin();
92
+ redirectToLogin('expired');
49
93
  return false;
50
94
  }
51
95
  if (!getUserManagementModel()) {
@@ -98,13 +142,20 @@ const UserSessionsPage = {
98
142
  m('td.px-3.py-2.text-gray-700.dark:text-slate-300', r.created_at ? new Date(r.created_at).toLocaleString() : ''),
99
143
  m('td.px-3.py-2', m('button.text-red-600.dark:text-red-400.text-xs', {
100
144
  onclick: async function () {
101
- if (!confirm('Revoke this session?')) return;
145
+ var ok = await confirmAction({
146
+ title: 'Revoke Session',
147
+ message: 'Revoke this session?',
148
+ danger: true,
149
+ confirmText: 'Revoke'
150
+ });
151
+ if (!ok) return;
102
152
  try {
103
153
  await api.delete('/users/sessions/' + encodeURIComponent(r.token));
104
154
  vnode.state.rows = vnode.state.rows.filter(function (x) { return x.token !== r.token; });
155
+ notifySuccess('Session revoked successfully');
105
156
  m.redraw();
106
157
  } catch (err) {
107
- alert(err.message || err);
158
+ notifyError(err.message || String(err));
108
159
  }
109
160
  },
110
161
  }, 'Revoke')),
@@ -122,12 +173,15 @@ var routes = {
122
173
  onmatch: async () => {
123
174
  const needsSetup = await checkSetupNeeded();
124
175
  if (needsSetup) {
176
+ setAdminPageTitle('Setup');
125
177
  return SetupForm;
126
178
  }
127
179
  const isAuth = await checkAuth();
128
180
  if (!isAuth) {
181
+ setAdminPageTitle('Sign In');
129
182
  return LoginForm;
130
183
  }
184
+ setAdminPageTitle('Dashboard');
131
185
  return Dashboard;
132
186
  }
133
187
  },
@@ -138,6 +192,7 @@ var routes = {
138
192
  m.route.set(consumeIntendedRoute('/'));
139
193
  return;
140
194
  }
195
+ setAdminPageTitle('Sign In');
141
196
  return LoginForm;
142
197
  }
143
198
  },
@@ -148,6 +203,7 @@ var routes = {
148
203
  m.route.set('/login');
149
204
  return;
150
205
  }
206
+ setAdminPageTitle('Setup');
151
207
  return SetupForm;
152
208
  }
153
209
  },
@@ -155,9 +211,10 @@ var routes = {
155
211
  onmatch: async () => {
156
212
  const isAuth = await checkAuth();
157
213
  if (!isAuth) {
158
- redirectToLogin();
214
+ redirectToLogin('expired');
159
215
  return;
160
216
  }
217
+ setAdminPageTitle('Settings');
161
218
  return SettingsPage;
162
219
  }
163
220
  },
@@ -165,9 +222,10 @@ var routes = {
165
222
  onmatch: async () => {
166
223
  const isAuth = await checkAuth();
167
224
  if (!isAuth) {
168
- redirectToLogin();
225
+ redirectToLogin('expired');
169
226
  return;
170
227
  }
228
+ setAdminPageTitle('Profile');
171
229
  return ProfilePage;
172
230
  }
173
231
  },
@@ -175,15 +233,17 @@ var routes = {
175
233
  onmatch: async () => {
176
234
  const isAuth = await checkAuth();
177
235
  if (!isAuth) {
178
- redirectToLogin();
236
+ redirectToLogin('expired');
179
237
  return;
180
238
  }
239
+ setAdminPageTitle('Staff Users');
181
240
  return AdminUsersPage;
182
241
  }
183
242
  },
184
243
  '/users/sessions': {
185
244
  onmatch: async () => {
186
245
  if (!(await guardUserManagementRoutes())) return;
246
+ setAdminPageTitle('Active Sessions');
187
247
  return UserSessionsPage;
188
248
  }
189
249
  },
@@ -213,9 +273,11 @@ var routes = {
213
273
  onmatch: async () => {
214
274
  const isAuth = await checkAuth();
215
275
  if (!isAuth) {
216
- redirectToLogin();
276
+ redirectToLogin('expired');
217
277
  return;
218
278
  }
279
+ const model = m.route.param('model');
280
+ setAdminPageTitle(model);
219
281
  return RecordList;
220
282
  }
221
283
  },
@@ -223,9 +285,11 @@ var routes = {
223
285
  onmatch: async () => {
224
286
  const isAuth = await checkAuth();
225
287
  if (!isAuth) {
226
- redirectToLogin();
288
+ redirectToLogin('expired');
227
289
  return;
228
290
  }
291
+ const model = m.route.param('model');
292
+ setAdminPageTitle('New ' + model);
229
293
  return RecordForm;
230
294
  }
231
295
  },
@@ -233,9 +297,11 @@ var routes = {
233
297
  onmatch: async () => {
234
298
  const isAuth = await checkAuth();
235
299
  if (!isAuth) {
236
- redirectToLogin();
300
+ redirectToLogin('expired');
237
301
  return;
238
302
  }
303
+ const model = m.route.param('model');
304
+ setAdminPageTitle('Edit ' + model);
239
305
  return RecordForm;
240
306
  }
241
307
  },
@@ -243,10 +309,16 @@ var routes = {
243
309
 
244
310
  function wrapCustomPageWithLayout(page, rawComp) {
245
311
  if (!rawComp) return createCustomPage(page);
246
- if (page.layout === false) return rawComp;
247
312
 
248
313
  return {
249
314
  oninit: function(vnode) {
315
+ if (page) {
316
+ if (page.documentTitle) {
317
+ document.title = page.documentTitle;
318
+ } else if (page.title || page.id) {
319
+ setAdminPageTitle(page.title || page.id);
320
+ }
321
+ }
250
322
  try {
251
323
  const instance = typeof rawComp === 'function' ? rawComp.call(rawComp, vnode) : rawComp;
252
324
  vnode.state.instance = instance;
@@ -293,14 +365,15 @@ function wrapCustomPageWithLayout(page, rawComp) {
293
365
  view: function(vnode) {
294
366
  if (vnode.state.renderError) {
295
367
  var err = vnode.state.renderError;
296
- var breadcrumbs = [{ label: page.title, href: page.path }];
297
- return m(Layout, [
298
- m(Breadcrumb, { items: breadcrumbs }),
299
- m('.p-6.bg-red-50.border.border-red-200.rounded-xl', [
300
- m('h3.text-lg.font-semibold.text-red-800', 'Sayfa Yüklenirken Hata Oluştu'),
301
- m('p.text-sm.text-red-600.mt-1', err.message || String(err)),
302
- ])
303
- ]);
368
+ return typeof renderCustomPageError === 'function'
369
+ ? renderCustomPageError(page, err)
370
+ : m(Layout, [
371
+ m(Breadcrumb, { items: [{ label: page.title, href: page.path }] }),
372
+ m('.p-6.bg-red-50.border.border-red-200.rounded-xl', [
373
+ m('h3.text-lg.font-semibold.text-red-800', 'Custom Page Component Error'),
374
+ m('p.text-sm.text-red-600.mt-1', err.message || String(err)),
375
+ ])
376
+ ]);
304
377
  }
305
378
  const comp = vnode.state.instance || rawComp;
306
379
  var content;
@@ -314,11 +387,25 @@ function wrapCustomPageWithLayout(page, rawComp) {
314
387
  }
315
388
  } catch (err) {
316
389
  console.error('[Admin Panel] Error rendering custom page "' + page.id + '":', err);
317
- content = m('.p-6.bg-red-50.border.border-red-200.rounded-xl', [
318
- m('h3.text-lg.font-semibold.text-red-800', 'Sayfa Çizilirken Hata Oluştu'),
319
- m('p.text-sm.text-red-600.mt-1', err.message || String(err)),
390
+ return typeof renderCustomPageError === 'function'
391
+ ? renderCustomPageError(page, err)
392
+ : m(Layout, [
393
+ m(Breadcrumb, { items: [{ label: page.title, href: page.path }] }),
394
+ m('.p-6.bg-red-50.border.border-red-200.rounded-xl', [
395
+ m('h3.text-lg.font-semibold.text-red-800', 'Custom Page Component Error'),
396
+ m('p.text-sm.text-red-600.mt-1', err.message || String(err)),
397
+ ])
398
+ ]);
399
+ }
400
+
401
+ if (page.layout === false) {
402
+ return m('div.custom-canvas-page', [
403
+ content,
404
+ typeof ToastContainer !== 'undefined' ? m(ToastContainer) : null,
405
+ typeof ConfirmModalContainer !== 'undefined' ? m(ConfirmModalContainer) : null,
320
406
  ]);
321
407
  }
408
+
322
409
  if (content && (content.tag === Layout || (content.tag && content.tag.name === 'Layout'))) {
323
410
  return content;
324
411
  }
@@ -340,9 +427,14 @@ if (config && config.pages) {
340
427
  onmatch: async () => {
341
428
  const isAuth = await checkAuth();
342
429
  if (!isAuth) {
343
- redirectToLogin();
430
+ redirectToLogin('expired');
344
431
  return;
345
432
  }
433
+ if (page.documentTitle) {
434
+ document.title = page.documentTitle;
435
+ } else {
436
+ setAdminPageTitle(page.title || page.id);
437
+ }
346
438
  const comp = (window.__customPages && window.__customPages[page.id])
347
439
  ? wrapCustomPageWithLayout(page, window.__customPages[page.id])
348
440
  : createCustomPage(page);
@@ -16,11 +16,12 @@ const api = {
16
16
  if (!response.ok) {
17
17
  const error = await response.json().catch(() => ({ error: 'Request failed' }));
18
18
  if (response.status === 401 && path.indexOf('/auth/') !== 0) {
19
- redirectToLogin();
19
+ redirectToLogin('expired');
20
20
  }
21
- const err = new Error(error.error || 'Request failed');
21
+ const err = new Error(error.error || error.message || 'Request failed');
22
22
  if (error.fields) err.fields = error.fields;
23
23
  err.response = error;
24
+ err.status = response.status;
24
25
  throw err;
25
26
  }
26
27
 
@@ -90,6 +91,8 @@ const state = {
90
91
  needsSetup: false,
91
92
  loading: false,
92
93
  error: null,
94
+ notice: null,
95
+ toasts: [],
93
96
  models: [],
94
97
  currentModel: null,
95
98
  currentModelMeta: null, // Full model metadata with columns
@@ -118,14 +121,19 @@ var INTENDED_ROUTE_KEY = 'webspresso.admin.intendedRoute';
118
121
 
119
122
  function isSafeIntendedRoute(path) {
120
123
  if (!path || typeof path !== 'string') return false;
121
- if (path.charAt(0) !== '/') return false;
122
- if (path === '/login' || path === '/setup') return false;
124
+ var p = path.trim();
125
+ if (p.charAt(0) !== '/' || p.indexOf('//') === 0 || p.indexOf('/\\') === 0) return false;
126
+ if (p === '/login' || p === '/setup' || p.indexOf('/login?') === 0 || p.indexOf('/setup?') === 0) return false;
127
+ if (/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(p)) return false;
123
128
  return true;
124
129
  }
125
130
 
126
- function saveIntendedRoute() {
131
+ function saveIntendedRoute(explicitPath) {
127
132
  try {
128
- var path = m.route.get();
133
+ var path = explicitPath;
134
+ if (!path && typeof m !== 'undefined' && m.route && typeof m.route.get === 'function') {
135
+ path = m.route.get();
136
+ }
129
137
  if (!isSafeIntendedRoute(path)) return;
130
138
  sessionStorage.setItem(INTENDED_ROUTE_KEY, path);
131
139
  } catch (e) {}
@@ -139,17 +147,314 @@ function clearIntendedRoute() {
139
147
 
140
148
  function consumeIntendedRoute(defaultPath) {
141
149
  defaultPath = defaultPath || '/';
150
+
151
+ // 1. Try route parameter or query string returnTo
152
+ try {
153
+ if (typeof m !== 'undefined' && m.route && typeof m.route.param === 'function') {
154
+ var param = m.route.param('returnTo');
155
+ if (param && isSafeIntendedRoute(param)) {
156
+ clearIntendedRoute();
157
+ return param;
158
+ }
159
+ }
160
+ } catch (e) {}
161
+
162
+ try {
163
+ if (typeof window !== 'undefined' && window.location && window.location.search) {
164
+ var match = window.location.search.match(/[?&]returnTo=([^&]+)/);
165
+ if (match && match[1]) {
166
+ var decoded = decodeURIComponent(match[1]);
167
+ if (isSafeIntendedRoute(decoded)) {
168
+ clearIntendedRoute();
169
+ return decoded;
170
+ }
171
+ }
172
+ }
173
+ } catch (e) {}
174
+
175
+ // 2. Fall back to sessionStorage
142
176
  try {
143
177
  var path = sessionStorage.getItem(INTENDED_ROUTE_KEY);
144
178
  sessionStorage.removeItem(INTENDED_ROUTE_KEY);
145
179
  if (isSafeIntendedRoute(path)) return path;
146
180
  } catch (e) {}
181
+
147
182
  return defaultPath;
148
183
  }
149
184
 
150
- function redirectToLogin() {
151
- saveIntendedRoute();
152
- m.route.set('/login');
185
+ function redirectToLogin(reason) {
186
+ var currentRoute = '';
187
+ try {
188
+ if (typeof m !== 'undefined' && m.route && typeof m.route.get === 'function') {
189
+ currentRoute = m.route.get();
190
+ }
191
+ } catch (e) {}
192
+
193
+ if (isSafeIntendedRoute(currentRoute)) {
194
+ saveIntendedRoute(currentRoute);
195
+ }
196
+
197
+ var params = [];
198
+ if (isSafeIntendedRoute(currentRoute)) {
199
+ params.push('returnTo=' + encodeURIComponent(currentRoute));
200
+ }
201
+ if (reason) {
202
+ params.push('reason=' + encodeURIComponent(reason));
203
+ }
204
+
205
+ var target = '/login' + (params.length > 0 ? '?' + params.join('&') : '');
206
+ try {
207
+ if (typeof m !== 'undefined' && m.route && typeof m.route.set === 'function') {
208
+ m.route.set(target);
209
+ }
210
+ } catch (e) {}
211
+ }
212
+
213
+ // ==========================================
214
+ // Toast Notification & Confirm Modal System
215
+ // ==========================================
216
+ var toastState = (typeof window !== 'undefined' && window.toastState) ? window.toastState : {
217
+ items: [],
218
+ add: function(type, message, options) {
219
+ options = options || {};
220
+ if (typeof type === 'object') {
221
+ options = type;
222
+ type = options.type || 'info';
223
+ message = options.message || '';
224
+ }
225
+ var id = 'toast_' + Math.random().toString(36).substring(2, 9);
226
+ var duration = options.duration !== undefined ? options.duration : 4000;
227
+ var item = {
228
+ id: id,
229
+ type: type || 'info',
230
+ message: message,
231
+ title: options.title || '',
232
+ duration: duration,
233
+ };
234
+ toastState.items.push(item);
235
+ if (typeof state !== 'undefined') {
236
+ state.toasts = toastState.items;
237
+ }
238
+ if (item.duration > 0) {
239
+ setTimeout(function() {
240
+ toastState.remove(id);
241
+ }, item.duration);
242
+ }
243
+ try {
244
+ if (typeof m !== 'undefined' && typeof m.redraw === 'function') {
245
+ m.redraw();
246
+ }
247
+ } catch (e) {}
248
+ return id;
249
+ },
250
+ remove: function(id) {
251
+ toastState.items = toastState.items.filter(function(t) { return t.id !== id; });
252
+ if (typeof state !== 'undefined') {
253
+ state.toasts = toastState.items;
254
+ }
255
+ try {
256
+ if (typeof m !== 'undefined' && typeof m.redraw === 'function') {
257
+ m.redraw();
258
+ }
259
+ } catch (e) {}
260
+ },
261
+ clear: function() {
262
+ toastState.items = [];
263
+ if (typeof state !== 'undefined') {
264
+ state.toasts = [];
265
+ }
266
+ try {
267
+ if (typeof m !== 'undefined' && typeof m.redraw === 'function') {
268
+ m.redraw();
269
+ }
270
+ } catch (e) {}
271
+ },
272
+ };
273
+
274
+ function normalizeToastOptions(options) {
275
+ if (typeof options === 'number') {
276
+ return { duration: options };
277
+ }
278
+ return options || {};
279
+ }
280
+
281
+ var notify = function(message, type, options) {
282
+ var opts = normalizeToastOptions(options);
283
+ if (typeof type === 'object') {
284
+ opts = type;
285
+ type = opts.type || 'info';
286
+ }
287
+ return toastState.add(type || 'info', message, opts);
288
+ };
289
+ notify.success = function(message, options) { return toastState.add('success', message, normalizeToastOptions(options)); };
290
+ notify.error = function(message, options) { return toastState.add('error', message, normalizeToastOptions(options)); };
291
+ notify.warning = function(message, options) { return toastState.add('warning', message, normalizeToastOptions(options)); };
292
+ notify.info = function(message, options) { return toastState.add('info', message, normalizeToastOptions(options)); };
293
+ notify.remove = function(id) { return toastState.remove(id); };
294
+ notify.clear = function() { return toastState.clear(); };
295
+
296
+ var ToastContainer = {
297
+ view: function() {
298
+ var items = (typeof toastState !== 'undefined' && toastState.items) || (typeof state !== 'undefined' && state.toasts) || [];
299
+ if (!items || items.length === 0) return null;
300
+ return m('div.fixed.top-4.right-4.z-50.flex.flex-col.gap-2.5.max-w-sm.w-full.pointer-events-none.px-4.sm:px-0',
301
+ items.map(function(t) {
302
+ var typeConfig = {
303
+ success: {
304
+ border: 'border-emerald-200 dark:border-emerald-800/80',
305
+ bg: 'bg-white dark:bg-slate-900',
306
+ iconColor: 'text-emerald-600 dark:text-emerald-400 bg-emerald-50 dark:bg-emerald-950/60',
307
+ symbol: '✓',
308
+ },
309
+ error: {
310
+ border: 'border-red-200 dark:border-red-800/80',
311
+ bg: 'bg-white dark:bg-slate-900',
312
+ iconColor: 'text-red-600 dark:text-red-400 bg-red-50 dark:bg-red-950/60',
313
+ symbol: '!',
314
+ },
315
+ warning: {
316
+ border: 'border-amber-200 dark:border-amber-800/80',
317
+ bg: 'bg-white dark:bg-slate-900',
318
+ iconColor: 'text-amber-600 dark:text-amber-400 bg-amber-50 dark:bg-amber-950/60',
319
+ symbol: '⚠',
320
+ },
321
+ info: {
322
+ border: 'border-blue-200 dark:border-blue-800/80',
323
+ bg: 'bg-white dark:bg-slate-900',
324
+ iconColor: 'text-blue-600 dark:text-blue-400 bg-blue-50 dark:bg-blue-950/60',
325
+ symbol: 'ℹ',
326
+ },
327
+ };
328
+ var cfg = typeConfig[t.type] || typeConfig.info;
329
+
330
+ return m('div.pointer-events-auto.shadow-xl.rounded-xl.border.p-4.flex.items-start.gap-3.transition-all.transform', {
331
+ key: t.id,
332
+ class: cfg.border + ' ' + cfg.bg
333
+ }, [
334
+ m('div.w-7.h-7.rounded-lg.flex.items-center.justify-center.text-xs.font-bold.flex-shrink-0', {
335
+ class: cfg.iconColor
336
+ }, cfg.symbol),
337
+ m('div.flex-1.min-w-0', [
338
+ t.title ? m('h4.text-sm.font-semibold.text-gray-900.dark:text-slate-100', t.title) : null,
339
+ m('p.text-xs.text-gray-600.dark:text-slate-300.break-words', t.message)
340
+ ]),
341
+ m('button.text-gray-400.hover:text-gray-600.dark:hover:text-slate-200.p-1.text-xs', {
342
+ onclick: function() { toastState.remove(t.id); }
343
+ }, '✕')
344
+ ]);
345
+ })
346
+ );
347
+ }
348
+ };
349
+
350
+ var confirmState = (typeof window !== 'undefined' && window.confirmState) ? window.confirmState : {
351
+ current: null,
352
+ open: function(options) {
353
+ options = options || {};
354
+ if (typeof options === 'string') {
355
+ options = { message: options };
356
+ }
357
+ return new Promise(function(resolve) {
358
+ confirmState.current = {
359
+ title: options.title || 'Confirmation',
360
+ message: options.message || 'Are you sure you want to proceed?',
361
+ confirmText: options.confirmText || 'Confirm',
362
+ cancelText: options.cancelText || 'Cancel',
363
+ confirmColor: options.confirmColor || (options.danger ? 'red' : 'blue'),
364
+ danger: options.danger || false,
365
+ resolve: resolve,
366
+ };
367
+ try {
368
+ if (typeof m !== 'undefined' && typeof m.redraw === 'function') {
369
+ m.redraw();
370
+ }
371
+ } catch (e) {}
372
+ });
373
+ },
374
+ close: function(result) {
375
+ if (confirmState.current && confirmState.current.resolve) {
376
+ confirmState.current.resolve(Boolean(result));
377
+ }
378
+ confirmState.current = null;
379
+ try {
380
+ if (typeof m !== 'undefined' && typeof m.redraw === 'function') {
381
+ m.redraw();
382
+ }
383
+ } catch (e) {}
384
+ }
385
+ };
386
+
387
+ var confirmModal = function(options) {
388
+ return confirmState.open(options);
389
+ };
390
+
391
+ var ConfirmModalContainer = {
392
+ view: function() {
393
+ var cur = (typeof confirmState !== 'undefined' && confirmState.current) || null;
394
+ if (!cur) return null;
395
+ var isDanger = cur.danger || cur.confirmColor === 'red';
396
+
397
+ return m('div.fixed.inset-0.z-50.flex.items-center.justify-center.p-4', [
398
+ m('div.fixed.inset-0.bg-black/50.backdrop-blur-sm', {
399
+ onclick: function() { confirmState.close(false); }
400
+ }),
401
+ m('div.relative.bg-white.dark:bg-slate-900.rounded-xl.shadow-2xl.border.border-gray-200.dark:border-slate-700.max-w-md.w-full.p-6.z-10', [
402
+ m('div.flex.items-start.gap-4', [
403
+ m('div.w-10.h-10.rounded-full.flex.items-center.justify-center.flex-shrink-0', {
404
+ class: isDanger ? 'bg-red-100 dark:bg-red-950/60 text-red-600 dark:text-red-400' : 'bg-blue-100 dark:bg-blue-950/60 text-blue-600 dark:text-blue-400'
405
+ }, isDanger ? '⚠' : '?'),
406
+ m('div.flex-1', [
407
+ m('h3.text-base.font-bold.text-gray-900.dark:text-slate-100', cur.title),
408
+ m('p.text-xs.text-gray-500.dark:text-slate-400.mt-1.5', cur.message)
409
+ ])
410
+ ]),
411
+ m('div.mt-6.flex.items-center.justify-end.gap-3', [
412
+ m('button.px-4.py-2.text-xs.font-medium.text-gray-700.dark:text-slate-300.bg-gray-100.dark:bg-slate-800.hover:bg-gray-200.dark:hover:bg-slate-700.rounded-lg', {
413
+ onclick: function() { confirmState.close(false); }
414
+ }, cur.cancelText),
415
+ m('button.px-4.py-2.text-xs.font-medium.text-white.rounded-lg.shadow-sm', {
416
+ class: isDanger ? 'bg-red-600 hover:bg-red-700' : 'bg-blue-600 hover:bg-blue-700',
417
+ onclick: function() { confirmState.close(true); }
418
+ }, cur.confirmText)
419
+ ])
420
+ ])
421
+ ]);
422
+ }
423
+ };
424
+
425
+ // Convenience notification & confirmation helpers
426
+ function notifySuccess(msg, optionsOrTtl) {
427
+ return notify.success(msg, optionsOrTtl);
428
+ }
429
+
430
+ function notifyError(msg, optionsOrTtl) {
431
+ return notify.error(msg, optionsOrTtl);
432
+ }
433
+
434
+ function notifyWarning(msg, optionsOrTtl) {
435
+ return notify.warning(msg, optionsOrTtl);
436
+ }
437
+
438
+ function notifyInfo(msg, optionsOrTtl) {
439
+ return notify.info(msg, optionsOrTtl);
440
+ }
441
+
442
+ async function confirmAction(optionsOrMsg) {
443
+ return await confirmModal(optionsOrMsg);
444
+ }
445
+
446
+ // Global window bindings
447
+ if (typeof window !== 'undefined') {
448
+ window.toastState = toastState;
449
+ window.confirmState = confirmState;
450
+ window.notify = notify;
451
+ window.toast = notify;
452
+ window.confirmModal = confirmModal;
453
+ window.ToastContainer = ToastContainer;
454
+ window.ConfirmModalContainer = ConfirmModalContainer;
455
+ window.Admin = window.Admin || {};
456
+ window.Admin.toast = notify;
457
+ window.Admin.confirm = confirmModal;
153
458
  }
154
459
 
155
460
  // Breadcrumb Component