@keenmate/pure-admin-core 2.9.0-rc03 → 2.9.0-rc05

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 (35) hide show
  1. package/README.md +55 -13
  2. package/dist/css/main.css +554 -7
  3. package/package.json +3 -1
  4. package/snippets/buttons.html +51 -0
  5. package/snippets/cards.html +132 -47
  6. package/snippets/comparison.html +26 -22
  7. package/snippets/manifest.json +180 -115
  8. package/snippets/range-group.html +125 -0
  9. package/snippets/splitter.html +44 -38
  10. package/snippets/statistics.html +31 -0
  11. package/src/js/btn-split-auto-absorb.js +327 -0
  12. package/src/js/command-palette.js +472 -0
  13. package/src/js/file-selector.js +1275 -0
  14. package/src/js/internal/logging.js +121 -0
  15. package/src/js/logic-tree-renderer.js +303 -0
  16. package/src/js/modal-dialogs.js +460 -0
  17. package/src/js/overflow.js +371 -0
  18. package/src/js/pa-stat-fit.js +184 -0
  19. package/src/js/range-group.js +663 -0
  20. package/src/js/search-autocomplete-v2.js +907 -0
  21. package/src/js/search-autocomplete.js +434 -0
  22. package/src/js/settings-panel.js +245 -0
  23. package/src/js/split-button.js +141 -0
  24. package/src/js/splitter.js +1323 -0
  25. package/src/js/toast-service.js +302 -0
  26. package/src/js/tooltips-popovers.js +275 -0
  27. package/src/js/virtual-scroll.js +143 -0
  28. package/src/js/virtual-textbox.js +803 -0
  29. package/src/scss/_core.scss +7 -0
  30. package/src/scss/core-components/_buttons.scss +44 -0
  31. package/src/scss/core-components/_cards.scss +95 -6
  32. package/src/scss/core-components/_overflow.scss +50 -0
  33. package/src/scss/core-components/_range-group.scss +474 -0
  34. package/src/scss/core-components/_statistics.scss +163 -0
  35. package/src/scss/variables/_components.scss +41 -2
@@ -0,0 +1,434 @@
1
+ /**
2
+ * Search Field Autocomplete with Token-Based Query Builder
3
+ * Provides field name suggestions when user types ":"
4
+ * Then provides operator suggestions based on field type
5
+ * Resolved fragments appear as token badges
6
+ * Example: :name starts_with abc → [name][starts_with][abc]
7
+ */
8
+
9
+ class SearchAutocomplete {
10
+ constructor(inputElement, fields, options = {}) {
11
+ this.input = inputElement;
12
+ this.fields = fields;
13
+ this.popup = null;
14
+ this.tokensContainer = null;
15
+ this.activeIndex = -1;
16
+ this.isOpen = false;
17
+ this.resolvedTokens = []; // { type: 'field'|'operator'|'value', value: string, variant: string }
18
+ this.currentQueryState = 'idle'; // 'idle' | 'expecting-operator' | 'expecting-value'
19
+ this.autocompleteResults = [];
20
+
21
+ // Operator definitions by field type with shortcuts
22
+ this.operators = {
23
+ text: [
24
+ { value: 'equals', label: 'Equals', shortcuts: ['eq', '='], description: 'Exact match' },
25
+ { value: 'not_equals', label: 'Not Equals', shortcuts: ['neq', 'ne', '!='], description: 'Does not match' },
26
+ { value: 'starts_with', label: 'Starts With', shortcuts: ['sw', 'startswith'], description: 'Starts with text' },
27
+ { value: 'ends_with', label: 'Ends With', shortcuts: ['ew', 'endswith'], description: 'Ends with text' },
28
+ { value: 'contains', label: 'Contains', shortcuts: ['c', 'has'], description: 'Contains text' },
29
+ { value: 'not_contains', label: 'Not Contains', shortcuts: ['nc', 'notcontains'], description: 'Does not contain' }
30
+ ],
31
+ number: [
32
+ { value: '=', label: 'Equals', shortcuts: ['eq', '=='], description: 'Exact value' },
33
+ { value: '!=', label: 'Not Equals', shortcuts: ['neq', 'ne'], description: 'Different value' },
34
+ { value: '>', label: 'Greater Than', shortcuts: ['gt'], description: 'Greater than value' },
35
+ { value: '<', label: 'Less Than', shortcuts: ['lt'], description: 'Less than value' },
36
+ { value: 'between', label: 'Between', shortcuts: ['bw', 'btw'], description: 'Between two values' },
37
+ { value: '>=', label: 'Greater or Equal', shortcuts: ['gte', 'ge'], description: 'Greater or equal' },
38
+ { value: '<=', label: 'Less or Equal', shortcuts: ['lte', 'le'], description: 'Less or equal' }
39
+ ],
40
+ date: [
41
+ { value: 'equals', label: 'On Date', shortcuts: ['eq', '=', 'on'], description: 'Exact date' },
42
+ { value: 'before', label: 'Before', shortcuts: ['bf', '<'], description: 'Before date' },
43
+ { value: 'after', label: 'After', shortcuts: ['af', '>'], description: 'After date' },
44
+ { value: 'between', label: 'Between', shortcuts: ['bw', 'btw'], description: 'Date range' },
45
+ { value: 'in_last', label: 'In Last', shortcuts: ['last', 'past'], description: 'In last X days' },
46
+ { value: 'in_next', label: 'In Next', shortcuts: ['next', 'future'], description: 'In next X days' }
47
+ ]
48
+ };
49
+
50
+ this.init();
51
+ }
52
+
53
+ init() {
54
+ // Find or create tokens container
55
+ const wrapper = this.input.parentElement;
56
+ this.tokensContainer = wrapper.querySelector('.pa-search-tokens');
57
+
58
+ if (!this.tokensContainer) {
59
+ this.tokensContainer = document.createElement('div');
60
+ this.tokensContainer.className = 'pa-search-tokens';
61
+ wrapper.insertBefore(this.tokensContainer, this.input);
62
+ }
63
+
64
+ // Create popup element
65
+ this.popup = document.createElement('div');
66
+ this.popup.className = 'pa-search-autocomplete';
67
+ this.popup.style.display = 'none';
68
+ document.body.appendChild(this.popup);
69
+
70
+ // Event listeners
71
+ this.input.addEventListener('input', (e) => this.handleInput(e));
72
+ this.input.addEventListener('keydown', (e) => this.handleKeydown(e));
73
+ document.addEventListener('click', (e) => this.handleClickOutside(e));
74
+
75
+ // Initialize state
76
+ this.updateQueryState();
77
+ }
78
+
79
+ /**
80
+ * Update query state based on number of tokens
81
+ */
82
+ updateQueryState() {
83
+ const tokenCount = this.resolvedTokens.length % 3;
84
+
85
+ if (tokenCount === 0) {
86
+ this.currentQueryState = 'idle';
87
+ } else if (tokenCount === 1) {
88
+ this.currentQueryState = 'expecting-operator';
89
+ } else if (tokenCount === 2) {
90
+ this.currentQueryState = 'expecting-value';
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Search for autocomplete suggestions
96
+ */
97
+ searchAutocomplete(query) {
98
+ if (this.currentQueryState === 'idle') {
99
+ // Search for fields (must start with :)
100
+ if (query.startsWith(':')) {
101
+ const searchTerm = query.substring(1).toLowerCase();
102
+ return this.fields.filter(field =>
103
+ field.name.toLowerCase().includes(searchTerm)
104
+ ).map(field => ({
105
+ value: field.name,
106
+ label: field.name,
107
+ description: `${field.type} field`,
108
+ type: field.type
109
+ }));
110
+ }
111
+ } else if (this.currentQueryState === 'expecting-operator') {
112
+ // Get the last resolved field token
113
+ const lastFieldToken = this.resolvedTokens[this.resolvedTokens.length - 1];
114
+ const fieldDef = this.fields.find(f => f.name === lastFieldToken.value);
115
+
116
+ if (!fieldDef) return [];
117
+
118
+ const operators = this.operators[fieldDef.type] || [];
119
+ const searchTerm = query.toLowerCase();
120
+
121
+ return operators.filter(op =>
122
+ searchTerm === '' ||
123
+ op.value.toLowerCase().includes(searchTerm) ||
124
+ op.label.toLowerCase().includes(searchTerm) ||
125
+ (op.shortcuts && op.shortcuts.some(s => s.toLowerCase().includes(searchTerm)))
126
+ );
127
+ }
128
+
129
+ return [];
130
+ }
131
+
132
+ /**
133
+ * Render autocomplete dropdown
134
+ */
135
+ renderAutocomplete() {
136
+ if (this.autocompleteResults.length === 0) {
137
+ this.close();
138
+ return;
139
+ }
140
+
141
+ const isFieldSearch = this.currentQueryState === 'idle';
142
+ let html = '';
143
+
144
+ this.autocompleteResults.forEach((item, index) => {
145
+ const isActive = index === this.activeIndex;
146
+ const icon = isFieldSearch ? '🔍' : '⚡';
147
+
148
+ html += `
149
+ <div class="pa-search-autocomplete__item ${isActive ? 'pa-search-autocomplete__item--active' : ''}" data-index="${index}">
150
+ <span class="pa-search-autocomplete__item-icon">${icon}</span>
151
+ <div class="pa-search-autocomplete__item-content">
152
+ <span class="pa-search-autocomplete__item-name">${item.label}</span>
153
+ <span class="pa-search-autocomplete__item-type">${item.description}</span>
154
+ </div>
155
+ <span class="pa-search-autocomplete__item-badge">${item.value}</span>
156
+ </div>
157
+ `;
158
+ });
159
+
160
+ this.popup.innerHTML = html;
161
+
162
+ // Add click handlers
163
+ this.popup.querySelectorAll('.pa-search-autocomplete__item').forEach((item, index) => {
164
+ item.addEventListener('click', () => {
165
+ this.selectAutocomplete(this.autocompleteResults[index]);
166
+ });
167
+ });
168
+
169
+ this.position();
170
+ this.open();
171
+ }
172
+
173
+ /**
174
+ * Select autocomplete item
175
+ */
176
+ selectAutocomplete(item) {
177
+ if (!item) return;
178
+
179
+ // Determine token type and variant
180
+ let tokenType, variant;
181
+
182
+ if (this.currentQueryState === 'idle') {
183
+ tokenType = 'field';
184
+ variant = 'primary';
185
+ } else if (this.currentQueryState === 'expecting-operator') {
186
+ tokenType = 'operator';
187
+ variant = 'secondary';
188
+ }
189
+
190
+ // Add token
191
+ this.resolvedTokens.push({
192
+ type: tokenType,
193
+ value: item.value,
194
+ variant: variant
195
+ });
196
+
197
+ // Update state
198
+ this.updateQueryState();
199
+
200
+ // Clear input and autocomplete
201
+ this.input.value = '';
202
+ this.autocompleteResults = [];
203
+ this.activeIndex = -1;
204
+
205
+ // Re-render
206
+ this.renderTokens();
207
+ this.close();
208
+ this.input.focus();
209
+
210
+ // If we just selected a field, automatically show operators
211
+ if (tokenType === 'field') {
212
+ // Trigger operator autocomplete immediately
213
+ setTimeout(() => {
214
+ this.autocompleteResults = this.searchAutocomplete('');
215
+ this.activeIndex = this.autocompleteResults.length > 0 ? 0 : -1;
216
+ if (this.autocompleteResults.length > 0) {
217
+ this.renderAutocomplete();
218
+ }
219
+ }, 0);
220
+ }
221
+ }
222
+
223
+ /**
224
+ * Resolve value token (when user presses Enter or Tab)
225
+ */
226
+ resolveValueToken() {
227
+ const value = this.input.value.trim();
228
+
229
+ if (value && this.currentQueryState === 'expecting-value') {
230
+ this.resolvedTokens.push({
231
+ type: 'value',
232
+ value: value,
233
+ variant: 'success'
234
+ });
235
+
236
+ this.updateQueryState();
237
+ this.input.value = '';
238
+ this.renderTokens();
239
+ }
240
+ }
241
+
242
+ /**
243
+ * Render tokens as badge groups
244
+ */
245
+ renderTokens() {
246
+ this.tokensContainer.innerHTML = '';
247
+
248
+ // Group tokens into sets of 3 (field, operator, value)
249
+ for (let i = 0; i < this.resolvedTokens.length; i += 3) {
250
+ const queryTokens = this.resolvedTokens.slice(i, i + 3);
251
+
252
+ // Create group container
253
+ const group = document.createElement('div');
254
+ group.className = 'pa-search-token-group';
255
+
256
+ queryTokens.forEach((token, index) => {
257
+ const badge = document.createElement('span');
258
+ badge.className = `pa-badge pa-badge--${token.variant}`;
259
+ badge.textContent = token.value;
260
+ badge.style.margin = '0';
261
+ badge.style.borderRadius = '0';
262
+
263
+ // Round corners on first and last badges
264
+ if (index === 0) {
265
+ badge.style.borderTopLeftRadius = '4px';
266
+ badge.style.borderBottomLeftRadius = '4px';
267
+ }
268
+ if (index === queryTokens.length - 1 || i + index === this.resolvedTokens.length - 1) {
269
+ badge.style.borderTopRightRadius = '4px';
270
+ badge.style.borderBottomRightRadius = '4px';
271
+ }
272
+
273
+ group.appendChild(badge);
274
+ });
275
+
276
+ // Add remove button
277
+ const removeBtn = document.createElement('button');
278
+ removeBtn.className = 'pa-search-token-remove';
279
+ removeBtn.innerHTML = '×';
280
+ removeBtn.onclick = (e) => {
281
+ e.preventDefault();
282
+ e.stopPropagation();
283
+ this.removeTokenGroup(i);
284
+ };
285
+
286
+ group.appendChild(removeBtn);
287
+ this.tokensContainer.appendChild(group);
288
+ }
289
+ }
290
+
291
+ /**
292
+ * Remove a token group
293
+ */
294
+ removeTokenGroup(startIndex) {
295
+ this.resolvedTokens.splice(startIndex, 3);
296
+ this.updateQueryState();
297
+ this.renderTokens();
298
+ this.input.focus();
299
+ }
300
+
301
+ /**
302
+ * Handle input changes
303
+ */
304
+ handleInput(e) {
305
+ const inputValue = this.input.value;
306
+
307
+ // Check for autocomplete
308
+ if (this.currentQueryState === 'idle' || this.currentQueryState === 'expecting-operator') {
309
+ this.autocompleteResults = this.searchAutocomplete(inputValue);
310
+ this.activeIndex = this.autocompleteResults.length > 0 ? 0 : -1;
311
+
312
+ if (this.autocompleteResults.length > 0) {
313
+ this.renderAutocomplete();
314
+ } else {
315
+ this.close();
316
+ }
317
+ }
318
+ }
319
+
320
+ /**
321
+ * Handle keyboard navigation
322
+ */
323
+ handleKeydown(e) {
324
+ // Handle autocomplete navigation
325
+ if (this.autocompleteResults.length > 0 && this.isOpen) {
326
+ if (e.key === 'ArrowUp') {
327
+ e.preventDefault();
328
+ this.navigateAutocompletePrevious();
329
+ return;
330
+ }
331
+ if (e.key === 'ArrowDown') {
332
+ e.preventDefault();
333
+ this.navigateAutocompleteNext();
334
+ return;
335
+ }
336
+ if (e.key === 'Enter' || e.key === 'Tab') {
337
+ e.preventDefault();
338
+ this.selectAutocomplete(this.autocompleteResults[this.activeIndex]);
339
+ return;
340
+ }
341
+ }
342
+
343
+ // Handle value resolution
344
+ if (this.currentQueryState === 'expecting-value' && (e.key === 'Enter' || e.key === 'Tab')) {
345
+ e.preventDefault();
346
+ this.resolveValueToken();
347
+ return;
348
+ }
349
+
350
+ // Backspace at start - remove last token
351
+ if (e.key === 'Backspace' && this.input.value === '' && this.resolvedTokens.length > 0) {
352
+ e.preventDefault();
353
+ this.resolvedTokens.pop();
354
+ this.updateQueryState();
355
+ this.renderTokens();
356
+ return;
357
+ }
358
+
359
+ // Escape to close
360
+ if (e.key === 'Escape') {
361
+ this.close();
362
+ }
363
+ }
364
+
365
+ /**
366
+ * Navigate autocomplete suggestions
367
+ */
368
+ navigateAutocompletePrevious() {
369
+ if (this.autocompleteResults.length === 0) return;
370
+
371
+ this.activeIndex = this.activeIndex <= 0
372
+ ? this.autocompleteResults.length - 1
373
+ : this.activeIndex - 1;
374
+
375
+ this.renderAutocomplete();
376
+ }
377
+
378
+ navigateAutocompleteNext() {
379
+ if (this.autocompleteResults.length === 0) return;
380
+
381
+ this.activeIndex = this.activeIndex >= this.autocompleteResults.length - 1
382
+ ? 0
383
+ : this.activeIndex + 1;
384
+
385
+ this.renderAutocomplete();
386
+ }
387
+
388
+ /**
389
+ * Position popup below input
390
+ */
391
+ position() {
392
+ const inputRect = this.input.getBoundingClientRect();
393
+ this.popup.style.position = 'absolute';
394
+ this.popup.style.left = inputRect.left + 'px';
395
+ this.popup.style.top = (inputRect.bottom + 4) + 'px';
396
+ this.popup.style.minWidth = Math.max(inputRect.width, 250) + 'px';
397
+ }
398
+
399
+ /**
400
+ * Open popup
401
+ */
402
+ open() {
403
+ this.popup.style.display = 'block';
404
+ this.isOpen = true;
405
+ }
406
+
407
+ /**
408
+ * Close popup
409
+ */
410
+ close() {
411
+ this.popup.style.display = 'none';
412
+ this.isOpen = false;
413
+ this.activeIndex = -1;
414
+ this.autocompleteResults = [];
415
+ }
416
+
417
+ /**
418
+ * Handle click outside
419
+ */
420
+ handleClickOutside(e) {
421
+ if (this.isOpen && !this.popup.contains(e.target) && e.target !== this.input) {
422
+ this.close();
423
+ }
424
+ }
425
+
426
+ /**
427
+ * Destroy instance
428
+ */
429
+ destroy() {
430
+ if (this.popup && this.popup.parentNode) {
431
+ this.popup.parentNode.removeChild(this.popup);
432
+ }
433
+ }
434
+ }
@@ -0,0 +1,245 @@
1
+ /**
2
+ * Pure Admin Settings Panel
3
+ * Global settings management for theme, layout, sidebar, fonts, and display options
4
+ */
5
+
6
+ (function() {
7
+ 'use strict';
8
+
9
+ // Wait for DOM to be ready
10
+ document.addEventListener('DOMContentLoaded', function() {
11
+ // Settings Panel Elements
12
+ const settingsPanel = document.getElementById('settingsPanel');
13
+ const settingsToggle = document.getElementById('settingsToggle');
14
+
15
+ // Check if elements exist
16
+ if (!settingsPanel || !settingsToggle) {
17
+ console.error('Settings panel elements not found');
18
+ return;
19
+ }
20
+
21
+ const themeSelector = document.getElementById('themeSelector');
22
+ const fontSizeSelector = document.getElementById('fontSizeSelector');
23
+ const fontFamilySelector = document.getElementById('fontFamilySelector');
24
+ const sidebarCollapsed = document.getElementById('sidebarCollapsed');
25
+ const sidebarBehaviorSelector = document.getElementById('sidebarBehaviorSelector');
26
+ const sidebarModeSelector = document.getElementById('sidebarModeSelector');
27
+ const compactMode = document.getElementById('compactMode');
28
+ const containerWidthSelector = document.getElementById('containerWidthSelector');
29
+ const resetSettings = document.getElementById('resetSettings');
30
+ const body = document.body;
31
+
32
+ // Load saved settings
33
+ const loadSettings = () => {
34
+ // Theme - use server-provided currentTheme from global config
35
+ const currentTheme = window.PURE_ADMIN_CONFIG?.currentTheme || 'audi';
36
+ themeSelector.value = currentTheme;
37
+
38
+ // Font size
39
+ const savedFontSize = localStorage.getItem('font-size') || 'default';
40
+ fontSizeSelector.value = savedFontSize;
41
+ document.documentElement.classList.remove('font-size-small', 'font-size-default', 'font-size-large', 'font-size-xlarge');
42
+ if (savedFontSize !== 'default') {
43
+ document.documentElement.classList.add(`font-size-${savedFontSize}`);
44
+ }
45
+
46
+ // Font family
47
+ const savedFontFamily = localStorage.getItem('font-family') || 'default';
48
+ fontFamilySelector.value = savedFontFamily;
49
+ body.classList.remove('font-family-serif', 'font-family-mono');
50
+ if (savedFontFamily !== 'default') {
51
+ body.classList.add(`font-family-${savedFontFamily}`);
52
+ }
53
+
54
+ // Sidebar collapsed
55
+ const isSidebarCollapsed = localStorage.getItem('sidebar-hidden') === 'true';
56
+ sidebarCollapsed.checked = isSidebarCollapsed;
57
+
58
+ // Sidebar behavior
59
+ const sidebarBehavior = localStorage.getItem('sidebar-behavior') || 'hide';
60
+ sidebarBehaviorSelector.value = sidebarBehavior;
61
+ const sidebar = document.querySelector('.pa-layout__sidebar');
62
+ const burgerMenu = document.querySelector('.burger-menu');
63
+ if (sidebar) {
64
+ sidebar.classList.remove('pa-layout__sidebar--icon-collapse');
65
+ if (sidebarBehavior === 'icon-collapse') {
66
+ sidebar.classList.add('pa-layout__sidebar--icon-collapse');
67
+ // In icon-collapse mode, check if expanded or collapsed
68
+ const isExpanded = !body.classList.contains('sidebar-hidden');
69
+ if (burgerMenu) {
70
+ if (isExpanded) {
71
+ burgerMenu.classList.add('active'); // Expanded, show X
72
+ } else {
73
+ burgerMenu.classList.remove('active'); // Collapsed to icons, show hamburger
74
+ }
75
+ }
76
+ } else {
77
+ // In hide mode, burger active = sidebar visible
78
+ if (burgerMenu) {
79
+ if (body.classList.contains('sidebar-hidden')) {
80
+ burgerMenu.classList.remove('active'); // Hidden, show hamburger
81
+ } else {
82
+ burgerMenu.classList.add('active'); // Visible, show X
83
+ }
84
+ }
85
+ }
86
+ }
87
+
88
+ // Compact mode
89
+ const isCompactMode = localStorage.getItem('compact-mode') === 'true';
90
+ compactMode.checked = isCompactMode;
91
+ if (isCompactMode) {
92
+ body.classList.add('compact-mode');
93
+ }
94
+
95
+ // Container width - read from body class
96
+ const containerWidthClasses = ['pa-container-sm', 'pa-container-md', 'pa-container-lg', 'pa-container-xl', 'pa-container-2xl'];
97
+ let currentContainerWidth = 'fluid';
98
+ for (const cls of containerWidthClasses) {
99
+ if (body.classList.contains(cls)) {
100
+ currentContainerWidth = cls.replace('pa-container-', '');
101
+ break;
102
+ }
103
+ }
104
+ containerWidthSelector.value = currentContainerWidth;
105
+
106
+ // Sidebar mode - check if body has sticky class
107
+ const currentSidebarMode = body.classList.contains('pa-layout--sticky') ? 'sticky' : '';
108
+ sidebarModeSelector.value = currentSidebarMode;
109
+ };
110
+
111
+ // Toggle panel
112
+ settingsToggle.addEventListener('click', () => {
113
+ settingsPanel.classList.toggle('pa-settings-panel--open');
114
+ });
115
+
116
+ // Close panel when clicking outside
117
+ document.addEventListener('click', (e) => {
118
+ if (!settingsPanel.contains(e.target) && settingsPanel.classList.contains('pa-settings-panel--open')) {
119
+ settingsPanel.classList.remove('pa-settings-panel--open');
120
+ }
121
+ });
122
+
123
+ // Theme change
124
+ themeSelector.addEventListener('change', (e) => {
125
+ const theme = e.target.value;
126
+ switchTheme(theme);
127
+ });
128
+
129
+ // Font size change
130
+ fontSizeSelector.addEventListener('change', (e) => {
131
+ const size = e.target.value;
132
+ document.documentElement.classList.remove('font-size-small', 'font-size-default', 'font-size-large', 'font-size-xlarge');
133
+ if (size !== 'default') {
134
+ document.documentElement.classList.add(`font-size-${size}`);
135
+ }
136
+ localStorage.setItem('font-size', size);
137
+ });
138
+
139
+ // Font family change
140
+ fontFamilySelector.addEventListener('change', (e) => {
141
+ const family = e.target.value;
142
+ body.classList.remove('font-family-serif', 'font-family-mono');
143
+ if (family !== 'default') {
144
+ body.classList.add(`font-family-${family}`);
145
+ }
146
+ localStorage.setItem('font-family', family);
147
+ });
148
+
149
+ // Sidebar toggle
150
+ sidebarCollapsed.addEventListener('change', (e) => {
151
+ toggleSidebar();
152
+ });
153
+
154
+ // Compact mode toggle
155
+ compactMode.addEventListener('change', (e) => {
156
+ if (e.target.checked) {
157
+ body.classList.add('compact-mode');
158
+ localStorage.setItem('compact-mode', 'true');
159
+ } else {
160
+ body.classList.remove('compact-mode');
161
+ localStorage.setItem('compact-mode', 'false');
162
+ }
163
+ });
164
+
165
+ // Container width change - reload page with query param to set cookie
166
+ containerWidthSelector.addEventListener('change', (e) => {
167
+ const width = e.target.value;
168
+ const url = new URL(window.location);
169
+ url.searchParams.set('containerWidth', width);
170
+ window.location.href = url.toString();
171
+ });
172
+
173
+ // Sidebar mode change
174
+ sidebarModeSelector.addEventListener('change', (e) => {
175
+ const mode = e.target.value;
176
+ switchSidebarMode(mode);
177
+ });
178
+
179
+ // Sidebar behavior change
180
+ sidebarBehaviorSelector.addEventListener('change', (e) => {
181
+ const behavior = e.target.value;
182
+ const sidebar = document.querySelector('.pa-layout__sidebar');
183
+ const burgerMenu = document.querySelector('.burger-menu');
184
+
185
+ if (sidebar) {
186
+ sidebar.classList.remove('pa-layout__sidebar--icon-collapse');
187
+ document.body.classList.remove('sidebar-hidden');
188
+
189
+ if (behavior === 'icon-collapse') {
190
+ // Show icon-only sidebar in collapsed state
191
+ sidebar.classList.add('pa-layout__sidebar--icon-collapse');
192
+ // Start in collapsed state (icon bar showing)
193
+ if (burgerMenu) burgerMenu.classList.remove('active'); // Not expanded, show hamburger
194
+ localStorage.setItem('sidebar-hidden', 'false'); // Not fully hidden, just in icon mode
195
+ } else if (behavior === 'hide') {
196
+ // Hide sidebar completely
197
+ document.body.classList.add('sidebar-hidden');
198
+ if (burgerMenu) burgerMenu.classList.remove('active'); // Hidden, show hamburger to open
199
+ localStorage.setItem('sidebar-hidden', 'true');
200
+ } else {
201
+ // Default behavior - show full sidebar
202
+ if (burgerMenu) burgerMenu.classList.add('active'); // Visible, show X to close
203
+ localStorage.setItem('sidebar-hidden', 'false');
204
+ }
205
+
206
+ localStorage.setItem('sidebar-behavior', behavior);
207
+ }
208
+ });
209
+
210
+ // Reset settings
211
+ resetSettings.addEventListener('click', () => {
212
+ // Clear localStorage settings
213
+ localStorage.removeItem('font-size');
214
+ localStorage.removeItem('font-family');
215
+ localStorage.removeItem('sidebar-hidden');
216
+ localStorage.removeItem('sidebar-behavior');
217
+ localStorage.removeItem('compact-mode');
218
+
219
+ // Reset theme, container width, and sidebar mode to defaults
220
+ const url = new URL(window.location);
221
+ url.searchParams.set('theme', 'audi');
222
+ url.searchParams.set('containerWidth', 'fluid');
223
+ url.searchParams.set('sidebarMode', '');
224
+ window.location.href = url.toString();
225
+ });
226
+
227
+ // Load settings on init
228
+ loadSettings();
229
+ });
230
+
231
+ // Helper functions (need to be globally accessible for other scripts)
232
+ window.switchTheme = function(theme) {
233
+ // Redirect to current page with theme parameter
234
+ const url = new URL(window.location);
235
+ url.searchParams.set('theme', theme);
236
+ window.location.href = url.toString();
237
+ };
238
+
239
+ window.switchSidebarMode = function(mode) {
240
+ // Redirect to current page with sidebar mode parameter
241
+ const url = new URL(window.location);
242
+ url.searchParams.set('sidebarMode', mode);
243
+ window.location.href = url.toString();
244
+ };
245
+ })();