web-mojo 2.1.550 → 2.1.626

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 (66) hide show
  1. package/dist/admin.cjs.js +1 -1
  2. package/dist/admin.cjs.js.map +1 -1
  3. package/dist/admin.es.js +19 -10
  4. package/dist/admin.es.js.map +1 -1
  5. package/dist/auth.cjs.js +1 -1
  6. package/dist/auth.cjs.js.map +1 -1
  7. package/dist/auth.es.js +3 -3
  8. package/dist/auth.es.js.map +1 -1
  9. package/dist/charts.cjs.js +1 -1
  10. package/dist/charts.es.js +3 -3
  11. package/dist/chunks/{ChatView-DXUzOG8o.js → ChatView-0e0k3QSK.js} +2 -2
  12. package/dist/chunks/{ChatView-DXUzOG8o.js.map → ChatView-0e0k3QSK.js.map} +1 -1
  13. package/dist/chunks/{ChatView-C3oW0hvN.js → ChatView-BxbA5ob6.js} +6 -6
  14. package/dist/chunks/{ChatView-C3oW0hvN.js.map → ChatView-BxbA5ob6.js.map} +1 -1
  15. package/dist/chunks/{ContextMenu-BTEcH8VJ.js → ContextMenu-ATInMbaI.js} +3 -3
  16. package/dist/chunks/{ContextMenu-BTEcH8VJ.js.map → ContextMenu-ATInMbaI.js.map} +1 -1
  17. package/dist/chunks/{ContextMenu-B6VXD0nZ.js → ContextMenu-Ckttp-rD.js} +10 -3
  18. package/dist/chunks/{ContextMenu-B6VXD0nZ.js.map → ContextMenu-Ckttp-rD.js.map} +1 -1
  19. package/dist/chunks/{DataView-CCrESxij.js → DataView-BQOKPprj.js} +2 -2
  20. package/dist/chunks/{DataView-CCrESxij.js.map → DataView-BQOKPprj.js.map} +1 -1
  21. package/dist/chunks/{DataView-DX2DcGKA.js → DataView-BsWK0Ul7.js} +2 -2
  22. package/dist/chunks/{DataView-DX2DcGKA.js.map → DataView-BsWK0Ul7.js.map} +1 -1
  23. package/dist/chunks/{Dialog-CY3xpb40.js → Dialog-aTTrQ6uW.js} +5 -5
  24. package/dist/chunks/{Dialog-CY3xpb40.js.map → Dialog-aTTrQ6uW.js.map} +1 -1
  25. package/dist/chunks/{Dialog-DJkVf-r4.js → Dialog-t4J3qOjC.js} +2 -2
  26. package/dist/chunks/{Dialog-DJkVf-r4.js.map → Dialog-t4J3qOjC.js.map} +1 -1
  27. package/dist/chunks/{FormView-BFa00Ukw.js → FormView-DjypPrEw.js} +2 -2
  28. package/dist/chunks/{FormView-BFa00Ukw.js.map → FormView-DjypPrEw.js.map} +1 -1
  29. package/dist/chunks/{FormView-Czl1459d.js → FormView-DvKBq99H.js} +2 -2
  30. package/dist/chunks/{FormView-Czl1459d.js.map → FormView-DvKBq99H.js.map} +1 -1
  31. package/dist/chunks/{MetricsChart-Kx9ZS9n8.js → MetricsChart-BHB2dubV.js} +2 -2
  32. package/dist/chunks/{MetricsChart-Kx9ZS9n8.js.map → MetricsChart-BHB2dubV.js.map} +1 -1
  33. package/dist/chunks/{MetricsChart-CoKYs18I.js → MetricsChart-DJ_AMjGg.js} +3 -3
  34. package/dist/chunks/{MetricsChart-CoKYs18I.js.map → MetricsChart-DJ_AMjGg.js.map} +1 -1
  35. package/dist/chunks/{PDFViewer-CPRZHTMD.js → PDFViewer-DXoC6y5o.js} +2 -2
  36. package/dist/chunks/{PDFViewer-CPRZHTMD.js.map → PDFViewer-DXoC6y5o.js.map} +1 -1
  37. package/dist/chunks/{PDFViewer-CObvQFg-.js → PDFViewer-kDd18mqo.js} +3 -3
  38. package/dist/chunks/{PDFViewer-CObvQFg-.js.map → PDFViewer-kDd18mqo.js.map} +1 -1
  39. package/dist/chunks/{Page-csKPSoVL.js → Page-BrWp1FsC.js} +2 -2
  40. package/dist/chunks/{Page-csKPSoVL.js.map → Page-BrWp1FsC.js.map} +1 -1
  41. package/dist/chunks/{Page-oNlGYNf5.js → Page-DgKaDD9p.js} +2 -2
  42. package/dist/chunks/{Page-oNlGYNf5.js.map → Page-DgKaDD9p.js.map} +1 -1
  43. package/dist/chunks/TopNav-DGtRj5F4.js +1039 -0
  44. package/dist/chunks/TopNav-DGtRj5F4.js.map +1 -0
  45. package/dist/chunks/TopNav-DoAl9Rpd.js +2 -0
  46. package/dist/chunks/TopNav-DoAl9Rpd.js.map +1 -0
  47. package/dist/chunks/{WebApp-Z3SCGNp2.js → WebApp-DWIriU_w.js} +2 -2
  48. package/dist/chunks/{WebApp-Z3SCGNp2.js.map → WebApp-DWIriU_w.js.map} +1 -1
  49. package/dist/chunks/{WebApp-6ncOIVXa.js → WebApp-ciExPk0c.js} +21 -14
  50. package/dist/chunks/{WebApp-6ncOIVXa.js.map → WebApp-ciExPk0c.js.map} +1 -1
  51. package/dist/css/web-mojo.css +1 -1
  52. package/dist/docit.cjs.js +1 -1
  53. package/dist/docit.es.js +5 -5
  54. package/dist/index.cjs.js +1 -1
  55. package/dist/index.cjs.js.map +1 -1
  56. package/dist/index.es.js +237 -449
  57. package/dist/index.es.js.map +1 -1
  58. package/dist/lightbox.cjs.js +1 -1
  59. package/dist/lightbox.es.js +4 -4
  60. package/dist/portal.css +222 -0
  61. package/dist/table.css +0 -13
  62. package/package.json +1 -1
  63. package/dist/chunks/TopNav-CWXnES46.js +0 -2
  64. package/dist/chunks/TopNav-CWXnES46.js.map +0 -1
  65. package/dist/chunks/TopNav-Szt3ULMS.js +0 -381
  66. package/dist/chunks/TopNav-Szt3ULMS.js.map +0 -1
package/dist/index.es.js CHANGED
@@ -1,442 +1,16 @@
1
- import { V as View, W as WebApp, d as dataFormatter, M as Mustache } from "./chunks/WebApp-6ncOIVXa.js";
2
- import { B, D, g, E, h, r, R, b, a, c, e, f } from "./chunks/WebApp-6ncOIVXa.js";
3
- import { P as Page } from "./chunks/Page-csKPSoVL.js";
4
- import { G as GroupList, T as ToastService, U as User, a as Group } from "./chunks/ContextMenu-B6VXD0nZ.js";
5
- import { C, b as b2, c as c2, M, f as f2, g as g2, h as h2, i, j, e as e2, d } from "./chunks/ContextMenu-B6VXD0nZ.js";
6
- import { M as Member } from "./chunks/ChatView-C3oW0hvN.js";
7
- import { f as f3, e as e3, C as C2, E as E2, k, j as j2, r as r2, t, s, x, z, y, u, w, v, F, g as g3, ap, aq, D as D2, I, B as B2, A, H, Q, R as R2, G, N, O, J, K, Y, Z, a2, a3, $, _, a0, a1, a5, a7, a6, a4, L, c as c3, a8, a9, l, n, m, ab, aa, ae, ac, ad, P, aj, an, ak, al, am, af, ag, ah, ao, ai, W, X, U, V, S, i as i2, h as h3, o, q, p, d as d2, b as b3, a as a10, T, ar, aw, av, as, at, au } from "./chunks/ChatView-C3oW0hvN.js";
8
- import { T as TopNav } from "./chunks/TopNav-Szt3ULMS.js";
1
+ import { V as View, W as WebApp, d as dataFormatter, M as Mustache } from "./chunks/WebApp-ciExPk0c.js";
2
+ import { B, D, g, E, h, r, R, b, a, c, e, f } from "./chunks/WebApp-ciExPk0c.js";
3
+ import { P as Page } from "./chunks/Page-BrWp1FsC.js";
4
+ import { G as GroupList, T as ToastService, U as User, a as Group } from "./chunks/ContextMenu-Ckttp-rD.js";
5
+ import { C, b as b2, c as c2, M, f as f2, g as g2, h as h2, i, j, e as e2, d } from "./chunks/ContextMenu-Ckttp-rD.js";
6
+ import { M as Member } from "./chunks/ChatView-BxbA5ob6.js";
7
+ import { f as f3, e as e3, C as C2, E as E2, k, j as j2, r as r2, t, s, x, z, y, u, w, v, F, g as g3, ap, aq, D as D2, I, B as B2, A, H, Q, R as R2, G, N, O, J, K, Y, Z, a2, a3, $, _, a0, a1, a5, a7, a6, a4, L, c as c3, a8, a9, l, n, m, ab, aa, ae, ac, ad, P, aj, an, ak, al, am, af, ag, ah, ao, ai, W, X, U, V, S, i as i2, h as h3, o, q, p, d as d2, b as b3, a as a10, T, ar, aw, av, as, at, au } from "./chunks/ChatView-BxbA5ob6.js";
8
+ import { S as SimpleSearchView, T as TopNav } from "./chunks/TopNav-DGtRj5F4.js";
9
9
  import { T as TokenManager } from "./chunks/TokenManager-Fjt083wv.js";
10
- import Dialog from "./chunks/Dialog-CY3xpb40.js";
11
- import { default as default2 } from "./chunks/DataView-CCrESxij.js";
12
- import { F as F2, a as a11 } from "./chunks/FormView-BFa00Ukw.js";
10
+ import Dialog from "./chunks/Dialog-aTTrQ6uW.js";
11
+ import { default as default2 } from "./chunks/DataView-BQOKPprj.js";
12
+ import { F as F2, a as a11 } from "./chunks/FormView-DjypPrEw.js";
13
13
  import { W as W2 } from "./chunks/WebSocketClient-B6ribe3B.js";
14
- class ResultsView extends View {
15
- constructor(options = {}) {
16
- super({
17
- className: "search-results-view flex-grow-1 overflow-auto d-flex flex-column",
18
- template: `
19
- <div class="flex-grow-1 overflow-auto">
20
- {{#data.loading}}
21
- <div class="text-center p-4">
22
- <div class="spinner-border spinner-border-sm text-muted" role="status">
23
- <span class="visually-hidden">Loading...</span>
24
- </div>
25
- <div class="mt-2 small text-muted">{{data.loadingText}}</div>
26
- </div>
27
- {{/data.loading}}
28
-
29
- {{^data.loading}}
30
- {{#data.items}}
31
- <div class="simple-search-item position-relative"
32
- data-action="select-item"
33
- data-item-index="{{index}}">
34
- {{{itemContent}}}
35
- <i class="bi bi-chevron-right position-absolute end-0 top-50 translate-middle-y me-3 text-muted"></i>
36
- </div>
37
- {{/data.items}}
38
-
39
- {{#data.showNoResults}}
40
- <div class="text-center p-4">
41
- <i class="bi bi-search text-muted mb-2" style="font-size: 1.5rem;"></i>
42
- <div class="text-muted small">{{data.noResultsText}}</div>
43
- <button type="button"
44
- class="btn btn-link btn-sm mt-2 p-0"
45
- data-action="clear-search">
46
- Clear search
47
- </button>
48
- </div>
49
- {{/data.showNoResults}}
50
-
51
- {{#data.showEmpty}}
52
- <div class="text-center p-4">
53
- <i class="{{data.emptyIcon}} text-muted mb-2" style="font-size: 2rem;"></i>
54
- <div class="text-muted small mb-2">{{data.emptyText}}</div>
55
- {{#data.emptySubtext}}
56
- <div class="text-muted" style="font-size: 0.75rem;">
57
- {{data.emptySubtext}}
58
- </div>
59
- {{/data.emptySubtext}}
60
- </div>
61
- {{/data.showEmpty}}
62
- {{/data.loading}}
63
- </div>
64
-
65
- {{#data.showResultsCount}}
66
- <div class="border-top bg-light p-2 text-center">
67
- <small class="text-muted">
68
- {{data.filteredCount}} of {{data.totalCount}}
69
- </small>
70
- </div>
71
- {{/data.showResultsCount}}
72
- `,
73
- ...options
74
- });
75
- this.parentView = options.parentView;
76
- }
77
- async handleActionSelectItem(event, element) {
78
- event.preventDefault();
79
- const itemIndex = parseInt(element.getAttribute("data-item-index"));
80
- if (this.parentView) {
81
- this.parentView.handleItemSelection(itemIndex);
82
- }
83
- }
84
- async handleActionClearSearch(event, _element) {
85
- event.preventDefault();
86
- if (this.parentView) {
87
- this.parentView.clearSearch();
88
- }
89
- }
90
- }
91
- class SimpleSearchView extends View {
92
- constructor(options = {}) {
93
- super({
94
- className: "simple-search-view h-100 d-flex flex-column",
95
- template: `
96
- <div class="p-3 border-bottom bg-light">
97
- <div class="d-flex justify-content-between align-items-start mb-3">
98
- <h6 class="text-muted fw-semibold mb-0">
99
- {{#data.headerIcon}}<i class="{{data.headerIcon}} me-2"></i>{{/data.headerIcon}}
100
- {{{data.headerText}}}
101
- </h6>
102
- {{#data.showExitButton}}
103
- <button class="btn btn-link p-0 text-muted simple-search-exit-btn"
104
- type="button"
105
- data-action="exit-view"
106
- title="Exit"
107
- aria-label="Exit view">
108
- <i class="bi bi-x-lg" aria-hidden="true"></i>
109
- </button>
110
- {{/data.showExitButton}}
111
- </div>
112
- <div class="position-relative">
113
- <input type="text"
114
- class="form-control form-control-sm pe-5"
115
- placeholder="{{data.searchPlaceholder}}"
116
- value="{{data.searchValue}}"
117
- data-filter="live-search"
118
- data-filter-debounce="{{data.debounceMs}}"
119
- data-change-action="search-items">
120
- <button class="btn btn-link p-0 position-absolute top-50 end-0 translate-middle-y me-2 text-muted simple-search-clear-btn"
121
- type="button"
122
- data-action="clear-search"
123
- title="Clear search"
124
- aria-label="Clear search">
125
- <i class="bi bi-x-circle-fill" aria-hidden="true"></i>
126
- </button>
127
- </div>
128
- </div>
129
-
130
- <div data-container="results"></div>
131
-
132
- {{#data.showFooter}}
133
- <div class="p-3 border-top bg-light">
134
- <small class="text-muted">
135
- <i class="{{data.footerIcon}} me-1"></i>
136
- {{{data.footerContent}}}
137
- </small>
138
- </div>
139
- {{/data.showFooter}}
140
- `,
141
- ...options
142
- });
143
- this.Collection = options.Collection;
144
- this.collection = options.collection;
145
- this.itemTemplate = options.itemTemplate || this.getDefaultItemTemplate();
146
- this.searchFields = options.searchFields || ["name"];
147
- this.collectionParams = { size: 25, ...options.collectionParams };
148
- this.headerText = options.headerText || "Select Item";
149
- this.headerIcon = options.headerIcon || "bi bi-list";
150
- this.searchPlaceholder = options.searchPlaceholder || "Search...";
151
- this.loadingText = options.loadingText || "Loading items...";
152
- this.noResultsText = options.noResultsText || "No items match your search";
153
- this.emptyText = options.emptyText || "No items available";
154
- this.emptySubtext = options.emptySubtext || null;
155
- this.emptyIcon = options.emptyIcon || "bi bi-inbox";
156
- this.footerContent = options.footerContent || null;
157
- this.footerIcon = options.footerIcon || "bi bi-info-circle";
158
- this.showExitButton = options.showExitButton || false;
159
- this.searchValue = "";
160
- this.filteredItems = [];
161
- this.loading = false;
162
- this.hasSearched = false;
163
- this.searchTimer = null;
164
- this.debounceMs = options.debounceMs || 800;
165
- this.resultsView = new ResultsView({
166
- parentView: this
167
- });
168
- if (!this.collection && this.Collection) {
169
- this.collection = new this.Collection();
170
- }
171
- this.addChild(this.resultsView);
172
- }
173
- onInit() {
174
- if (this.collection) {
175
- this.setupCollection();
176
- }
177
- if (this.collection && this.options.autoLoad !== false) {
178
- this.loadItems();
179
- }
180
- }
181
- setupCollection() {
182
- Object.assign(this.collection.params, this.collectionParams);
183
- this.collection.on("fetch:success", () => {
184
- this.loading = false;
185
- this.updateFilteredItems();
186
- });
187
- this.collection.on("fetch:error", () => {
188
- this.loading = false;
189
- });
190
- }
191
- async loadItems() {
192
- if (!this.collection) {
193
- console.warn("SimpleSearchView: No collection provided");
194
- return;
195
- }
196
- this.loading = true;
197
- this.updateResultsView();
198
- try {
199
- await this.collection.fetch();
200
- this.updateFilteredItems();
201
- } catch (error) {
202
- console.error("Error loading items:", error);
203
- const app = this.getApp();
204
- app?.showError?.("Failed to load items. Please try again.");
205
- } finally {
206
- this.loading = false;
207
- this.updateFilteredItems();
208
- }
209
- }
210
- updateFilteredItems() {
211
- if (!this.collection) {
212
- this.filteredItems = [];
213
- return;
214
- }
215
- const items = this.collection.toJSON();
216
- if (!this.searchValue || !this.searchValue.trim()) {
217
- this.filteredItems = items;
218
- } else {
219
- const searchTerm = this.searchValue.toLowerCase().trim();
220
- this.filteredItems = items.filter((item) => {
221
- return this.searchFields.some((field) => {
222
- const value = this.getNestedValue(item, field);
223
- return value && value.toString().toLowerCase().includes(searchTerm);
224
- });
225
- });
226
- }
227
- this.updateResultsView();
228
- }
229
- getNestedValue(obj, path) {
230
- return path.split(".").reduce((current, key) => current?.[key], obj);
231
- }
232
- async getViewData() {
233
- return {
234
- searchValue: this.searchValue,
235
- showFooter: !!this.footerContent,
236
- showExitButton: this.showExitButton,
237
- debounceMs: this.debounceMs,
238
- // UI text
239
- headerText: this.headerText,
240
- headerIcon: this.headerIcon,
241
- searchPlaceholder: this.searchPlaceholder,
242
- footerContent: this.footerContent,
243
- footerIcon: this.footerIcon
244
- };
245
- }
246
- updateResultsView() {
247
- if (!this.resultsView) return;
248
- const hasItems = this.collection && this.collection.length() > 0;
249
- const hasFilteredItems = this.filteredItems.length > 0;
250
- const hasSearchValue = this.searchValue.length > 0;
251
- const processedItems = this.filteredItems.map((item, index2) => {
252
- return {
253
- ...item,
254
- index: index2,
255
- itemContent: this.processItemTemplate(item)
256
- };
257
- });
258
- this.resultsView.data = {
259
- loading: this.loading,
260
- items: processedItems,
261
- showEmpty: !this.loading && !hasItems,
262
- showNoResults: !this.loading && hasItems && !hasFilteredItems && hasSearchValue,
263
- showResultsCount: !this.loading && hasItems,
264
- filteredCount: this.filteredItems.length,
265
- totalCount: this.collection?.restEnabled ? this.collection?.meta?.count || 0 : this.collection?.length() || 0,
266
- // UI text
267
- loadingText: this.loadingText,
268
- noResultsText: this.noResultsText,
269
- emptyText: this.emptyText,
270
- emptySubtext: this.emptySubtext,
271
- emptyIcon: this.emptyIcon
272
- };
273
- this.resultsView.render();
274
- }
275
- processItemTemplate(item) {
276
- let template = this.itemTemplate;
277
- template = template.replace(/\{\{(\w+)\}\}/g, (match, prop) => {
278
- return this.getNestedValue(item, prop) || "";
279
- });
280
- return template;
281
- }
282
- getDefaultItemTemplate() {
283
- return `
284
- <div class="p-3 border-bottom">
285
- <div class="fw-semibold text-dark">{{name}}</div>
286
- <small class="text-muted">{{id}}</small>
287
- </div>
288
- `;
289
- }
290
- async onPassThruActionSearchItems(event, element) {
291
- const searchValue = element.value || "";
292
- console.log("search change...");
293
- this.searchValue = searchValue;
294
- this.hasSearched = true;
295
- if (this.searchTimer) {
296
- clearTimeout(this.searchTimer);
297
- }
298
- this.performSearch();
299
- }
300
- async performSearch() {
301
- const searchParams = { ...this.collectionParams };
302
- if (this.searchValue && this.searchValue.length > 1) {
303
- searchParams.search = this.searchValue.trim();
304
- }
305
- this.collection.setParams(searchParams, true);
306
- }
307
- handleItemSelection(itemIndex) {
308
- if (isNaN(itemIndex) || itemIndex < 0 || itemIndex >= this.filteredItems.length) {
309
- console.error("Invalid item index:", itemIndex);
310
- return;
311
- }
312
- const item = this.filteredItems[itemIndex];
313
- const model = this.collection ? this.collection.get(item.id) : null;
314
- this.emit("item:selected", {
315
- item,
316
- model,
317
- index: itemIndex
318
- });
319
- }
320
- /**
321
- * Set the collection for this search view
322
- */
323
- setCollection(collection) {
324
- this.collection = collection;
325
- this.setupCollection();
326
- return this;
327
- }
328
- /**
329
- * Set the item template
330
- */
331
- setItemTemplate(template) {
332
- this.itemTemplate = template;
333
- this.updateResultsView();
334
- return this;
335
- }
336
- /**
337
- * Set search fields
338
- */
339
- setSearchFields(fields) {
340
- this.searchFields = Array.isArray(fields) ? fields : [fields];
341
- return this;
342
- }
343
- /**
344
- * Refresh items list
345
- */
346
- async refresh() {
347
- await this.loadItems();
348
- }
349
- /**
350
- * Focus the search input
351
- */
352
- focusSearch() {
353
- const searchInput = this.element?.querySelector('input[data-action="search-items"]');
354
- if (searchInput) {
355
- searchInput.focus();
356
- }
357
- }
358
- /**
359
- * Handle exit button click - emits event instead of closing
360
- */
361
- async handleActionExitView(event, element) {
362
- this.emit("exit", { view: this });
363
- }
364
- /**
365
- * Clear search and reset
366
- */
367
- async handleActionClearSearch(event, element) {
368
- this.clearSearch();
369
- }
370
- clearSearch() {
371
- this.searchValue = "";
372
- this.hasSearched = false;
373
- const searchInput = this.element?.querySelector('input[data-change-action="search-items"]');
374
- if (searchInput) {
375
- searchInput.value = "";
376
- searchInput.focus();
377
- }
378
- this.performSearch();
379
- }
380
- /**
381
- * Get the number of available items
382
- */
383
- getItemCount() {
384
- return this.collection ? this.collection.length() : 0;
385
- }
386
- /**
387
- * Get the number of filtered items
388
- */
389
- getFilteredItemCount() {
390
- return this.filteredItems.length;
391
- }
392
- /**
393
- * Check if items are loaded
394
- */
395
- hasItems() {
396
- return this.getItemCount() > 0;
397
- }
398
- /**
399
- * Get current search value
400
- */
401
- getSearchValue() {
402
- return this.searchValue;
403
- }
404
- /**
405
- * Set search value programmatically
406
- */
407
- setSearchValue(value) {
408
- this.searchValue = value || "";
409
- this.hasSearched = !!this.searchValue;
410
- const searchInput = this.element?.querySelector('input[data-action="search-items"]');
411
- if (searchInput) {
412
- searchInput.value = this.searchValue;
413
- }
414
- this.performSearch();
415
- return this;
416
- }
417
- async onAfterRender() {
418
- await super.onAfterRender();
419
- if (this.resultsView && !this.resultsView.isMounted()) {
420
- const container = this.element?.querySelector('[data-container="results"]');
421
- if (container) {
422
- await this.resultsView.render(true, container);
423
- }
424
- }
425
- this.updateResultsView();
426
- }
427
- /**
428
- * Cleanup on destroy
429
- */
430
- async onBeforeDestroy() {
431
- if (this.searchTimer) {
432
- clearTimeout(this.searchTimer);
433
- }
434
- if (this.collection) {
435
- this.collection.off("update");
436
- }
437
- await super.onBeforeDestroy();
438
- }
439
- }
440
14
  class Sidebar extends View {
441
15
  constructor(options = {}) {
442
16
  super({
@@ -452,6 +26,7 @@ class Sidebar extends View {
452
26
  this.isCollapsed = false;
453
27
  this.sidebarTheme = options.theme || "sidebar-light";
454
28
  this.customView = null;
29
+ if (this.options.groupHeader) this.groupHeader = this.options.groupHeader;
455
30
  if (this.sidebarTheme) {
456
31
  this.addClass(this.sidebarTheme);
457
32
  }
@@ -461,6 +36,12 @@ class Sidebar extends View {
461
36
  this.setupResponsiveBehavior();
462
37
  }
463
38
  }
39
+ groupHeader = `
40
+ <div class="sidebar-group-header py-3" data-action="show-group-search">
41
+ <div class='text-center fs-5 px-1 collapsed-hidden'>{{group.name}}</div>
42
+ <div class='text-center fs-6 collapsed-hidden'>kind: {{group.kind}}</div>
43
+ </div>
44
+ `;
464
45
  /**
465
46
  * Initialize sidebar and auto-switch to correct menu based on current route
466
47
  */
@@ -767,13 +348,7 @@ class Sidebar extends View {
767
348
  };
768
349
  }
769
350
  getGroupHeader() {
770
- return `
771
- <div class="sidebar-group-header py-3" data-action="show-group-search">
772
- <div class='text-center text-muted fs-7 collapsed-hidden'>active group</div>
773
- <div class='text-center fs-5 px-1 collapsed-hidden'>{{group.name}}</div>
774
- <div class='text-center fs-6 collapsed-hidden'>kind: {{group.kind}}</div>
775
- </div>
776
- `;
351
+ return this.groupHeader;
777
352
  }
778
353
  /**
779
354
  * Add a menu configuration
@@ -1491,6 +1066,182 @@ class Sidebar extends View {
1491
1066
  return this;
1492
1067
  }
1493
1068
  }
1069
+ class PageHeader extends View {
1070
+ constructor(options = {}) {
1071
+ super({
1072
+ tagName: "div",
1073
+ className: "page-header",
1074
+ ...options
1075
+ });
1076
+ this.style = options.style || "default";
1077
+ this.size = options.size || "md";
1078
+ this.showIcon = options.showIcon !== false;
1079
+ this.showDescription = options.showDescription !== false;
1080
+ this.showBreadcrumbs = options.showBreadcrumbs || false;
1081
+ this.currentPage = null;
1082
+ }
1083
+ async getTemplate() {
1084
+ if (this.style === "minimal") {
1085
+ return this.getMinimalTemplate();
1086
+ } else if (this.style === "breadcrumb") {
1087
+ return this.getBreadcrumbTemplate();
1088
+ }
1089
+ return this.getDefaultTemplate();
1090
+ }
1091
+ getDefaultTemplate() {
1092
+ return `
1093
+ {{#data.hasPage}}
1094
+ <div class="page-header-content page-header-{{data.size}}">
1095
+ <div class="page-header-main">
1096
+ <div class="page-header-info">
1097
+ {{#data.showIcon}}
1098
+ {{#data.pageIcon}}
1099
+ <div class="page-icon">
1100
+ <i class="{{data.pageIcon}}"></i>
1101
+ </div>
1102
+ {{/data.pageIcon}}
1103
+ {{/data.showIcon}}
1104
+
1105
+ <div class="page-title-group">
1106
+ <h1 class="page-title">{{data.pageTitle}}</h1>
1107
+ {{#data.showDescription}}
1108
+ {{#data.pageDescription}}
1109
+ <p class="page-description text-muted">{{data.pageDescription}}</p>
1110
+ {{/data.pageDescription}}
1111
+ {{/data.showDescription}}
1112
+ </div>
1113
+ </div>
1114
+
1115
+ {{#data.hasActions}}
1116
+ <div class="page-actions">
1117
+ {{#data.actions}}
1118
+ <button class="btn {{buttonClass}}"
1119
+ data-action="{{action}}"
1120
+ type="button">
1121
+ {{#icon}}<i class="{{icon}} me-1"></i>{{/icon}}
1122
+ {{label}}
1123
+ </button>
1124
+ {{/data.actions}}
1125
+ </div>
1126
+ {{/data.hasActions}}
1127
+ </div>
1128
+ </div>
1129
+ {{/data.hasPage}}
1130
+ `;
1131
+ }
1132
+ getMinimalTemplate() {
1133
+ return `
1134
+ {{#data.hasPage}}
1135
+ <div class="page-header-content page-header-minimal">
1136
+ <h1 class="page-title">
1137
+ {{#data.showIcon}}
1138
+ {{#data.pageIcon}}<i class="{{data.pageIcon}} me-2"></i>{{/data.pageIcon}}
1139
+ {{/data.showIcon}}
1140
+ {{data.pageTitle}}
1141
+ </h1>
1142
+ </div>
1143
+ {{/data.hasPage}}
1144
+ `;
1145
+ }
1146
+ getBreadcrumbTemplate() {
1147
+ return `
1148
+ {{#data.hasPage}}
1149
+ <div class="page-header-content page-header-breadcrumb">
1150
+ {{#data.showBreadcrumbs}}
1151
+ <nav aria-label="breadcrumb">
1152
+ <ol class="breadcrumb mb-2">
1153
+ {{#data.breadcrumbs}}
1154
+ <li class="breadcrumb-item {{#active}}active{{/active}}">
1155
+ {{#href}}<a href="{{href}}">{{label}}</a>{{/href}}
1156
+ {{^href}}{{label}}{{/href}}
1157
+ </li>
1158
+ {{/data.breadcrumbs}}
1159
+ </ol>
1160
+ </nav>
1161
+ {{/data.showBreadcrumbs}}
1162
+
1163
+ <div class="d-flex justify-content-between align-items-start">
1164
+ <h1 class="page-title">
1165
+ {{#data.showIcon}}
1166
+ {{#data.pageIcon}}<i class="{{data.pageIcon}} me-2"></i>{{/data.pageIcon}}
1167
+ {{/data.showIcon}}
1168
+ {{data.pageTitle}}
1169
+ </h1>
1170
+
1171
+ {{#data.hasActions}}
1172
+ <div class="page-actions">
1173
+ {{#data.actions}}
1174
+ <button class="btn {{buttonClass}}"
1175
+ data-action="{{action}}"
1176
+ type="button">
1177
+ {{#icon}}<i class="{{icon}} me-1"></i>{{/icon}}
1178
+ {{label}}
1179
+ </button>
1180
+ {{/data.actions}}
1181
+ </div>
1182
+ {{/data.hasActions}}
1183
+ </div>
1184
+
1185
+ {{#data.showDescription}}
1186
+ {{#data.pageDescription}}
1187
+ <p class="page-description text-muted mt-2">{{data.pageDescription}}</p>
1188
+ {{/data.pageDescription}}
1189
+ {{/data.showDescription}}
1190
+ </div>
1191
+ {{/data.hasPage}}
1192
+ `;
1193
+ }
1194
+ async onBeforeRender() {
1195
+ await super.onBeforeRender();
1196
+ const page = this.currentPage;
1197
+ const hasPage = !!page;
1198
+ const headerActions = page?.options?.headerActions || page?.headerActions || page?.constructor?.prototype?.headerActions || [];
1199
+ this.data = {
1200
+ hasPage,
1201
+ pageTitle: page?.title || page?.name || "",
1202
+ pageIcon: page?.icon || page?.pageIcon || "",
1203
+ pageDescription: page?.description || "",
1204
+ showIcon: this.showIcon,
1205
+ showDescription: this.showDescription,
1206
+ showBreadcrumbs: this.showBreadcrumbs,
1207
+ breadcrumbs: page?.options?.breadcrumbs || page?.breadcrumbs || [],
1208
+ actions: headerActions,
1209
+ hasActions: headerActions.length > 0,
1210
+ size: this.size
1211
+ };
1212
+ }
1213
+ /**
1214
+ * Set the current page to display
1215
+ */
1216
+ setPage(page) {
1217
+ this.currentPage = page;
1218
+ if (this.mounted) {
1219
+ this.render();
1220
+ }
1221
+ }
1222
+ /**
1223
+ * Get the current page
1224
+ */
1225
+ getPage() {
1226
+ return this.currentPage;
1227
+ }
1228
+ /**
1229
+ * Handle action clicks from page header buttons
1230
+ */
1231
+ async onActionDefault(action, event, element) {
1232
+ if (this.currentPage && typeof this.currentPage.onHeaderAction === "function") {
1233
+ await this.currentPage.onHeaderAction(action, event, element);
1234
+ return true;
1235
+ }
1236
+ this.emit("action", {
1237
+ action,
1238
+ event,
1239
+ element,
1240
+ page: this.currentPage
1241
+ });
1242
+ return false;
1243
+ }
1244
+ }
1494
1245
  class DeniedPage extends Page {
1495
1246
  constructor(options = {}) {
1496
1247
  super({
@@ -1826,9 +1577,12 @@ class PortalApp extends WebApp {
1826
1577
  if (config.topnav && !config.topbar) {
1827
1578
  this.topbarConfig = config.topnav;
1828
1579
  }
1580
+ this.showPageHeader = config.showPageHeader || false;
1581
+ this.pageHeaderConfig = config.pageHeader || {};
1829
1582
  this.sidebar = null;
1830
1583
  this.topbar = null;
1831
1584
  this.topnav = null;
1585
+ this.pageHeader = null;
1832
1586
  this.tokenManager = new TokenManager();
1833
1587
  this.activeGroup = null;
1834
1588
  if (!this.isMobile()) {
@@ -1926,6 +1680,7 @@ class PortalApp extends WebApp {
1926
1680
  this.activeUser.member = new Member();
1927
1681
  await this.activeUser.member.fetchForGroup(group.id);
1928
1682
  }
1683
+ this.events.emit("group:loaded", { group: this.activeGroup });
1929
1684
  console.log("Loaded active group:", group.get("name"));
1930
1685
  } catch (error) {
1931
1686
  console.warn("Failed to load active group:", error);
@@ -1938,6 +1693,7 @@ class PortalApp extends WebApp {
1938
1693
  const fallbackGroup = new Group({ id: storedGroupId });
1939
1694
  await fallbackGroup.fetch();
1940
1695
  this.activeGroup = fallbackGroup;
1696
+ this.events.emit("group:loaded", { group: this.activeGroup });
1941
1697
  console.log("Fell back to stored active group:", fallbackGroup.get("name"));
1942
1698
  } catch (fallbackError) {
1943
1699
  console.warn("Fallback to stored group also failed:", fallbackError);
@@ -2061,14 +1817,24 @@ class PortalApp extends WebApp {
2061
1817
  }
2062
1818
  const showSidebar = this.sidebarConfig && Object.keys(this.sidebarConfig).length > 0;
2063
1819
  const showTopbar = this.topbarConfig && Object.keys(this.topbarConfig).length > 0;
1820
+ const contentMarkup = this.showPageHeader ? `
1821
+ <div class="portal-content" id="portal-content">
1822
+ <div id="page-header"></div>
1823
+ <div id="page-container">
1824
+ <!-- Pages render here -->
1825
+ </div>
1826
+ </div>
1827
+ ` : `
1828
+ <div class="portal-content" id="page-container">
1829
+ <!-- Pages render here -->
1830
+ </div>
1831
+ `;
2064
1832
  container.innerHTML = `
2065
1833
  <div class="portal-layout hide-sidebar">
2066
1834
  ${showSidebar ? '<div id="portal-sidebar"></div>' : ""}
2067
1835
  <div class="portal-body">
2068
1836
  ${showTopbar ? '<div id="portal-topnav"></div>' : ""}
2069
- <div class="portal-content" id="page-container">
2070
- <!-- Pages render here -->
2071
- </div>
1837
+ ${contentMarkup}
2072
1838
  </div>
2073
1839
  </div>
2074
1840
  `;
@@ -2083,6 +1849,7 @@ class PortalApp extends WebApp {
2083
1849
  async setupPortalComponents() {
2084
1850
  await this.setupSidebar();
2085
1851
  await this.setupTopbar();
1852
+ await this.setupPageHeader();
2086
1853
  this.setupPortalEvents();
2087
1854
  }
2088
1855
  /**
@@ -2115,6 +1882,24 @@ class PortalApp extends WebApp {
2115
1882
  await this.topbar.render();
2116
1883
  this.topnav = this.topbar;
2117
1884
  }
1885
+ /**
1886
+ * Setup page header component
1887
+ */
1888
+ async setupPageHeader() {
1889
+ if (!this.showPageHeader) return;
1890
+ this.pageHeader = new PageHeader({
1891
+ containerId: "page-header",
1892
+ style: this.pageHeaderConfig.style || "default",
1893
+ showIcon: this.pageHeaderConfig.showIcon !== false,
1894
+ showDescription: this.pageHeaderConfig.showDescription !== false,
1895
+ showBreadcrumbs: this.pageHeaderConfig.showBreadcrumbs || false,
1896
+ ...this.pageHeaderConfig
1897
+ });
1898
+ const headerContainer = document.getElementById("page-header");
1899
+ if (headerContainer) {
1900
+ await this.pageHeader.render(true, headerContainer);
1901
+ }
1902
+ }
2118
1903
  /**
2119
1904
  * Setup portal event handling
2120
1905
  */
@@ -2190,8 +1975,8 @@ class PortalApp extends WebApp {
2190
1975
  if (this.hasMobileLayout()) {
2191
1976
  this.getPortalContainer().classList.add("hide-sidebar");
2192
1977
  }
2193
- if (result && this.currentPageInstance) {
2194
- this.updateNavigation(this.currentPageInstance);
1978
+ if (this.currentPage) {
1979
+ this.updateNavigation(this.currentPage);
2195
1980
  }
2196
1981
  return result;
2197
1982
  }
@@ -2205,6 +1990,9 @@ class PortalApp extends WebApp {
2205
1990
  if (this.topbar && this.topbar.setActivePage) {
2206
1991
  this.topbar.setActivePage(page.route);
2207
1992
  }
1993
+ if (this.pageHeader) {
1994
+ this.pageHeader.setPage(page);
1995
+ }
2208
1996
  this.events.emit("portal:page-changed", { page });
2209
1997
  }
2210
1998
  /**