micro-sidebar 2.1.0__py3-none-any.whl → 2.2.0__py3-none-any.whl

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: micro-sidebar
3
- Version: 2.1.0
3
+ Version: 2.2.0
4
4
  Summary: A Reusable RTL Django Sidebar App
5
5
  Home-page: https://github.com/debeski/micro-sidebar
6
6
  Author: DeBeski
@@ -230,3 +230,4 @@ While it may theoretically work in LTR environments if standard Bootstrap files
230
230
  | **v1.2.2** | **CSP Compliance:** Added `nonce` attribute support to inline scripts for Content Security Policy compliance. |
231
231
  | **v2.0.0** | **Auto-Discovery:** New feature that introspects Django URL patterns and models to automatically generate sidebar navigation items. Adds `{% auto_sidebar %}` template tag, context processor, and configuration options. |
232
232
  | **v2.1.0** | **Refactor & Enhancements:** Decoupled customization from models by introducing `DEFAULT_ITEMS` setting for overriding auto-discovered items' labels/icons/order. Added `EXTRA_ITEMS` setting for manual, permission-aware sidebar links grouped in accordions with `{% extra_sidebar %}` tag. Removed deprecated model-level `sidebar_*` attributes. |
233
+ | **v2.2.0** | **Drag-and-Drop Reordering:** New reorder toggle in sidebar toolbar (visible in expanded mode only). Click to enable reorder mode with shake animation. Drag items to reorder with visual drop indicator. Order persists to localStorage. Default order applies only if no user customization exists. Accordion headers remain fixed. |
@@ -5,9 +5,11 @@ sidebar/discovery.py,sha256=6rBpji-g057Pwg3fS7Vp1JV61wz3ygmhZfosGMNXjAY,5636
5
5
  sidebar/urls.py,sha256=UL_9e1RLNMxZXkah65m7GRU1dbViZRGeNPBIiSZpOYg,142
6
6
  sidebar/views.py,sha256=MebyJ1ZiylSOPESXFkkQ8QTg-ClrkJn-oYLN6KrcgiM,418
7
7
  sidebar/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
- sidebar/static/sidebar/sidebar.css,sha256=2mO5IlDZawzFds5KE9NCpM1UwB-e5HHlnRj2gfK28vs,5995
8
+ sidebar/static/sidebar/sidebar.css,sha256=fojQ5zofvpCz1SNe4zCHPB3Bx7kF0KiVKb_W3SSrA8g,6044
9
9
  sidebar/static/sidebar/sidebar.js,sha256=gcurW6nl0Dt9jPTzk749DX7uQ3U_7De4F6Gj8ci6W40,5233
10
+ sidebar/static/sidebar/css/reorder.css,sha256=zOBzg6nJwbY0QRCQKAVjaKxqCXqpICY1kFmXe2qO-og,2177
10
11
  sidebar/static/sidebar/css/theme_picker.css,sha256=S2u9p_4rXZALf7hoCOsgyAU91NP3Hoy54WYnc4aGyww,4345
12
+ sidebar/static/sidebar/js/reorder.js,sha256=BRAI-S9aTBeq7kwg1Z_ixgIrPdHB3A7LIdZbe8poRB4,9478
11
13
  sidebar/static/sidebar/js/theme_picker.js,sha256=Kt4S2fd0ocFjqeuOQXDcvWVLrLDAVxs7zANZqzIqA4g,2292
12
14
  sidebar/static/themes/blue.css,sha256=_CMBLoX8xuSkEdiSYP4HhM-_M368q5Zus9n2kGILBQw,1651
13
15
  sidebar/static/themes/dark.css,sha256=JmLG6UWSB7Erwogz0tCcpZUL1hgwxqgfyx-WSCXBtLU,14854
@@ -17,13 +19,13 @@ sidebar/static/themes/light.css,sha256=XqStIa8wTzWswWyy2nqbk4tMPKUuIR4967PBoJoPu
17
19
  sidebar/static/themes/main.css,sha256=NaPBDAPL-PkTphtcIto7Qjb5CkiR9T_X5YztDT5SwwE,121
18
20
  sidebar/static/themes/main.js,sha256=DSz07M8c1KTXSlj7woxPq0Kf9VFb2uNCI13Cz87gghs,1412
19
21
  sidebar/static/themes/red.css,sha256=t8OTmvgszQXX4YtYxlIM86zjx1jp-SvfVx2G3X0FjDI,1613
20
- sidebar/templates/sidebar/auto.html,sha256=nPKOcWMQ9aYNnH6z2EswsBE76zuxL4mUYUv8JyvsyWA,476
21
- sidebar/templates/sidebar/extra_groups.html,sha256=uMjaXhCuDitIattfChKdvyl6KQj6OVuMG4-bkg8Lb9E,1525
22
- sidebar/templates/sidebar/main.html,sha256=5-cPxjVAJ_lLvjI6IMwM3Lrov7gBZQAb4fh_vVTM160,3521
22
+ sidebar/templates/sidebar/auto.html,sha256=LQRLUJjiodRbRyWKxkj1MGorYrKAF67gsX_LC7LowH4,538
23
+ sidebar/templates/sidebar/extra_groups.html,sha256=uyiT2BcsY1-GM5GXHoudajckccF1cDCrCftm2vxOflo,1568
24
+ sidebar/templates/sidebar/main.html,sha256=djayBODnzWFaZerWG1THyHfvFV-4gYajIrx8BhBF_FY,6512
23
25
  sidebar/templatetags/__init__.py,sha256=RC19QrlHdcgslFc_19Se9UhOe-7_WH9GtNGYU_cZlXg,48
24
26
  sidebar/templatetags/sidebar_tags.py,sha256=KoZzjqRpMtiGcITKFUQBcs5RdbvtuzCaNrCWEzIbBlk,2245
25
- micro_sidebar-2.1.0.dist-info/LICENSE,sha256=Fco89ULLSSxKkC2KKnx57SaT0R7WOkZfuk8IYcGiN50,1063
26
- micro_sidebar-2.1.0.dist-info/METADATA,sha256=Qu2mhQ1Log9XThokGmFAtUc4haOkaIQeMU7n7MPFh3c,8264
27
- micro_sidebar-2.1.0.dist-info/WHEEL,sha256=pkctZYzUS4AYVn6dJ-7367OJZivF2e8RA9b_ZBjif18,92
28
- micro_sidebar-2.1.0.dist-info/top_level.txt,sha256=ih69sjMhU1wOB9HzUV90yEY98aiPuGhzPBBBE-YtJ3w,8
29
- micro_sidebar-2.1.0.dist-info/RECORD,,
27
+ micro_sidebar-2.2.0.dist-info/LICENSE,sha256=Fco89ULLSSxKkC2KKnx57SaT0R7WOkZfuk8IYcGiN50,1063
28
+ micro_sidebar-2.2.0.dist-info/METADATA,sha256=NMtlGHTlPMAP1SvLxA2BzGqsbK_XYfWkzfFzYsPT8o0,8607
29
+ micro_sidebar-2.2.0.dist-info/WHEEL,sha256=pkctZYzUS4AYVn6dJ-7367OJZivF2e8RA9b_ZBjif18,92
30
+ micro_sidebar-2.2.0.dist-info/top_level.txt,sha256=ih69sjMhU1wOB9HzUV90yEY98aiPuGhzPBBBE-YtJ3w,8
31
+ micro_sidebar-2.2.0.dist-info/RECORD,,
@@ -0,0 +1,91 @@
1
+ /* Reorder Toggle Button */
2
+ .reorder-toggle {
3
+ font-size: 1.2rem;
4
+ color: #8c98a4;
5
+ cursor: pointer;
6
+ padding: 5px;
7
+ margin-left: 10px;
8
+ border-radius: 50%;
9
+ transition: all 0.2s ease;
10
+ pointer-events: auto;
11
+ }
12
+
13
+ .reorder-toggle:hover {
14
+ color: var(--primal, #2363c3);
15
+ transform: scale(1.1);
16
+ }
17
+
18
+ .reorder-toggle.active {
19
+ color: var(--primal, #2363c3);
20
+ background-color: rgba(35, 99, 195, 0.1);
21
+ }
22
+
23
+ /* Hide reorder toggle in collapsed sidebar */
24
+ .sidebar.collapsed .reorder-toggle {
25
+ display: none;
26
+ }
27
+
28
+ /* Shake Animation for reorder mode */
29
+ @keyframes shake {
30
+ 0%, 100% { transform: translateX(0); }
31
+ 10%, 30%, 50%, 70%, 90% { transform: translateX(-2px); }
32
+ 20%, 40%, 60%, 80% { transform: translateX(2px); }
33
+ }
34
+
35
+ .sidebar.reorder-mode .list-group-item[draggable="true"],
36
+ .sidebar.reorder-mode .accordion-body .list-group-item[draggable="true"] {
37
+ animation: shake 1.5s ease-in-out infinite;
38
+ cursor: grab;
39
+ }
40
+
41
+ .sidebar.reorder-mode .list-group-item[draggable="true"]:hover {
42
+ animation-play-state: paused;
43
+ background-color: rgba(35, 99, 195, 0.08) !important;
44
+ }
45
+
46
+ /* Dragging State */
47
+ .sidebar .list-group-item.dragging {
48
+ opacity: 0.4;
49
+ cursor: grabbing;
50
+ animation: none !important;
51
+ }
52
+
53
+ /* Drop Indicator Line */
54
+ .drop-indicator {
55
+ height: 3px;
56
+ background: var(--primal, #2363c3);
57
+ border-radius: 2px;
58
+ margin: 2px 10px;
59
+ pointer-events: none;
60
+ box-shadow: 0 0 8px rgba(35, 99, 195, 0.5);
61
+ animation: pulseIndicator 0.8s ease-in-out infinite;
62
+ }
63
+
64
+ @keyframes pulseIndicator {
65
+ 0%, 100% { opacity: 1; }
66
+ 50% { opacity: 0.6; }
67
+ }
68
+
69
+ /* Ensure theme indicator stays in position */
70
+ .sidebar.collapsed .sidebar-toolbar {
71
+ justify-content: center;
72
+ }
73
+
74
+ /* Dark Mode Overrides */
75
+ :root.theme-dark .reorder-toggle {
76
+ color: rgba(255, 255, 255, 0.6);
77
+ }
78
+
79
+ :root.theme-dark .reorder-toggle:hover,
80
+ :root.theme-dark .reorder-toggle.active {
81
+ color: var(--primal, #3b82f6);
82
+ }
83
+
84
+ :root.theme-dark .reorder-toggle.active {
85
+ background-color: rgba(59, 130, 246, 0.2);
86
+ }
87
+
88
+ :root.theme-dark .drop-indicator {
89
+ background: var(--primal, #3b82f6);
90
+ box-shadow: 0 0 8px rgba(59, 130, 246, 0.5);
91
+ }
@@ -0,0 +1,267 @@
1
+ (function() {
2
+ 'use strict';
3
+
4
+ const STORAGE_KEY_AUTO = 'sidebar_auto_order';
5
+ const STORAGE_KEY_PREFIX_EXTRA = 'sidebar_extra_';
6
+
7
+ let isReorderMode = false;
8
+ let draggedElement = null;
9
+ let dropIndicator = null;
10
+
11
+ // Expose restore function globally for immediate FOUC fix
12
+ window.restoreSidebarOrder = restoreOrder;
13
+
14
+ document.addEventListener('DOMContentLoaded', () => {
15
+ const sidebar = document.getElementById('sidebar');
16
+ const reorderToggle = document.getElementById('sidebarReorderToggle');
17
+
18
+ if (!sidebar || !reorderToggle) return;
19
+
20
+ // Create drop indicator element
21
+ dropIndicator = document.createElement('div');
22
+ dropIndicator.className = 'drop-indicator';
23
+ dropIndicator.style.display = 'none';
24
+
25
+ // Restore is now called immediately via inline script for FOUC prevention
26
+ // But call again here as fallback if inline script didn't run
27
+ if (!window._sidebarOrderRestored) {
28
+ restoreOrder();
29
+ }
30
+
31
+ // Toggle reorder mode
32
+ reorderToggle.addEventListener('click', (e) => {
33
+ e.stopPropagation();
34
+ isReorderMode = !isReorderMode;
35
+ reorderToggle.classList.toggle('active', isReorderMode);
36
+ sidebar.classList.toggle('reorder-mode', isReorderMode);
37
+
38
+ if (isReorderMode) {
39
+ enableDragAndDrop();
40
+ } else {
41
+ disableDragAndDrop();
42
+ }
43
+ });
44
+
45
+ // Close reorder mode when clicking outside
46
+ document.addEventListener('click', (e) => {
47
+ if (isReorderMode && !sidebar.contains(e.target)) {
48
+ isReorderMode = false;
49
+ reorderToggle.classList.remove('active');
50
+ sidebar.classList.remove('reorder-mode');
51
+ disableDragAndDrop();
52
+ }
53
+ });
54
+ });
55
+
56
+ function enableDragAndDrop() {
57
+ // Auto items in .sidebar-auto-items or direct .list-group children
58
+ const autoContainer = document.getElementById('sidebarAutoItems') ||
59
+ document.querySelector('.sidebar .list-group');
60
+ if (autoContainer) {
61
+ setupDraggableContainer(autoContainer, STORAGE_KEY_AUTO);
62
+ }
63
+
64
+ // Extra group items in accordion bodies
65
+ const accordionBodies = document.querySelectorAll('.sidebar .accordion-body');
66
+ accordionBodies.forEach(body => {
67
+ const groupName = body.dataset.groupName || body.closest('.accordion-item')?.querySelector('.accordion-button span')?.textContent?.trim();
68
+ if (groupName) {
69
+ const key = STORAGE_KEY_PREFIX_EXTRA + slugify(groupName);
70
+ setupDraggableContainer(body, key);
71
+ }
72
+ });
73
+ }
74
+
75
+ function disableDragAndDrop() {
76
+ const items = document.querySelectorAll('.sidebar .list-group-item[draggable="true"]');
77
+ items.forEach(item => {
78
+ item.removeAttribute('draggable');
79
+ item.removeEventListener('dragstart', handleDragStart);
80
+ item.removeEventListener('dragend', handleDragEnd);
81
+ item.removeEventListener('dragover', handleDragOver);
82
+ item.removeEventListener('drop', handleDrop);
83
+ });
84
+
85
+ // Hide drop indicator
86
+ if (dropIndicator) {
87
+ dropIndicator.style.display = 'none';
88
+ if (dropIndicator.parentNode) {
89
+ dropIndicator.parentNode.removeChild(dropIndicator);
90
+ }
91
+ }
92
+ }
93
+
94
+ function setupDraggableContainer(container, storageKey) {
95
+ const items = container.querySelectorAll(':scope > .list-group-item');
96
+
97
+ items.forEach(item => {
98
+ // Skip accordion buttons - they're not reorderable
99
+ if (item.classList.contains('accordion-button')) return;
100
+
101
+ item.setAttribute('draggable', 'true');
102
+ item.dataset.storageKey = storageKey;
103
+
104
+ item.addEventListener('dragstart', handleDragStart);
105
+ item.addEventListener('dragend', handleDragEnd);
106
+ item.addEventListener('dragover', handleDragOver);
107
+ item.addEventListener('drop', handleDrop);
108
+ });
109
+
110
+ // Add event listeners to container for drag events
111
+ container.addEventListener('dragover', handleContainerDragOver);
112
+ container.addEventListener('drop', handleContainerDrop);
113
+ }
114
+
115
+ function handleDragStart(e) {
116
+ draggedElement = this;
117
+ this.classList.add('dragging');
118
+ e.dataTransfer.effectAllowed = 'move';
119
+ e.dataTransfer.setData('text/plain', ''); // Required for Firefox
120
+
121
+ // Add drop indicator to DOM
122
+ if (dropIndicator && this.parentNode) {
123
+ this.parentNode.appendChild(dropIndicator);
124
+ }
125
+ }
126
+
127
+ function handleDragEnd(e) {
128
+ this.classList.remove('dragging');
129
+
130
+ // Hide drop indicator
131
+ if (dropIndicator) {
132
+ dropIndicator.style.display = 'none';
133
+ }
134
+
135
+ // Save new order
136
+ if (draggedElement) {
137
+ saveOrder(draggedElement.parentNode, draggedElement.dataset.storageKey);
138
+ }
139
+
140
+ draggedElement = null;
141
+ }
142
+
143
+ function handleDragOver(e) {
144
+ e.preventDefault();
145
+ e.dataTransfer.dropEffect = 'move';
146
+
147
+ if (!draggedElement || draggedElement === this) return;
148
+ if (draggedElement.dataset.storageKey !== this.dataset.storageKey) return;
149
+
150
+ const rect = this.getBoundingClientRect();
151
+ const midY = rect.top + rect.height / 2;
152
+
153
+ // Show drop indicator
154
+ if (dropIndicator) {
155
+ dropIndicator.style.display = 'block';
156
+ if (e.clientY < midY) {
157
+ this.parentNode.insertBefore(dropIndicator, this);
158
+ } else {
159
+ this.parentNode.insertBefore(dropIndicator, this.nextSibling);
160
+ }
161
+ }
162
+ }
163
+
164
+ function handleContainerDragOver(e) {
165
+ e.preventDefault();
166
+ }
167
+
168
+ function handleDrop(e) {
169
+ e.preventDefault();
170
+ e.stopPropagation();
171
+
172
+ if (!draggedElement || draggedElement === this) return;
173
+ if (draggedElement.dataset.storageKey !== this.dataset.storageKey) return;
174
+
175
+ const rect = this.getBoundingClientRect();
176
+ const midY = rect.top + rect.height / 2;
177
+
178
+ if (e.clientY < midY) {
179
+ this.parentNode.insertBefore(draggedElement, this);
180
+ } else {
181
+ this.parentNode.insertBefore(draggedElement, this.nextSibling);
182
+ }
183
+ }
184
+
185
+ function handleContainerDrop(e) {
186
+ e.preventDefault();
187
+ // Item drops are handled by individual items
188
+ }
189
+
190
+ function saveOrder(container, storageKey) {
191
+ if (!container || !storageKey) return;
192
+
193
+ const items = container.querySelectorAll(':scope > .list-group-item[data-url-name]');
194
+ const order = Array.from(items).map(item => item.dataset.urlName);
195
+
196
+ try {
197
+ localStorage.setItem(storageKey, JSON.stringify(order));
198
+ } catch (e) {
199
+ console.warn('Could not save sidebar order:', e);
200
+ }
201
+ }
202
+
203
+ function restoreOrder() {
204
+ // Restore auto items order
205
+ const autoContainer = document.getElementById('sidebarAutoItems') ||
206
+ document.querySelector('.sidebar .list-group');
207
+ if (autoContainer) {
208
+ restoreContainerOrder(autoContainer, STORAGE_KEY_AUTO);
209
+ }
210
+
211
+ // Restore extra group items order
212
+ const accordionBodies = document.querySelectorAll('.sidebar .accordion-body');
213
+ accordionBodies.forEach(body => {
214
+ const groupName = body.dataset.groupName || body.closest('.accordion-item')?.querySelector('.accordion-button span')?.textContent?.trim();
215
+ if (groupName) {
216
+ const key = STORAGE_KEY_PREFIX_EXTRA + slugify(groupName);
217
+ restoreContainerOrder(body, key);
218
+ }
219
+ });
220
+
221
+ // Mark as restored
222
+ window._sidebarOrderRestored = true;
223
+ }
224
+
225
+ function restoreContainerOrder(container, storageKey) {
226
+ let savedOrder;
227
+ try {
228
+ const saved = localStorage.getItem(storageKey);
229
+ if (!saved) return; // No saved order, use default
230
+ savedOrder = JSON.parse(saved);
231
+ } catch (e) {
232
+ return; // Invalid JSON, use default
233
+ }
234
+
235
+ if (!Array.isArray(savedOrder) || savedOrder.length === 0) return;
236
+
237
+ const items = container.querySelectorAll(':scope > .list-group-item[data-url-name]');
238
+ const itemMap = new Map();
239
+ items.forEach(item => {
240
+ itemMap.set(item.dataset.urlName, item);
241
+ });
242
+
243
+ // Reorder based on saved order
244
+ savedOrder.forEach(urlName => {
245
+ const item = itemMap.get(urlName);
246
+ if (item) {
247
+ container.appendChild(item);
248
+ itemMap.delete(urlName);
249
+ }
250
+ });
251
+
252
+ // Append any remaining items (new items not in saved order)
253
+ itemMap.forEach(item => {
254
+ container.appendChild(item);
255
+ });
256
+ }
257
+
258
+ function slugify(text) {
259
+ return text
260
+ .toString()
261
+ .toLowerCase()
262
+ .trim()
263
+ .replace(/\s+/g, '-')
264
+ .replace(/[^\w\-]+/g, '')
265
+ .replace(/\-\-+/g, '-');
266
+ }
267
+ })();
@@ -227,4 +227,6 @@
227
227
  width: 100%;
228
228
  background: transparent;
229
229
  pointer-events: none; /* Let clicks pass through to content if needed, but we override for items */
230
+ display: flex;
231
+ justify-content: flex-end
230
232
  }
@@ -1,3 +1,4 @@
1
+ <div class="sidebar-auto-items" id="sidebarAutoItems">
1
2
  {% for item in items %}
2
3
  <a href="{{ item.url }}"
3
4
  class="list-group-item list-group-item-action{% if item.active %} active{% endif %}"
@@ -11,3 +12,4 @@
11
12
  <p class="mb-0">لا توجد عناصر</p>
12
13
  </div>
13
14
  {% endfor %}
15
+ </div>
@@ -16,7 +16,7 @@
16
16
  <div id="extraGroup{{ forloop.counter }}"
17
17
  class="accordion-collapse collapse{% if group.has_active %} show{% endif %}"
18
18
  data-bs-parent="#sidebarExtraAccordion">
19
- <div class="accordion-body p-0">
19
+ <div class="accordion-body p-0" data-group-name="{{ group_name|slugify }}">
20
20
  {% for item in group.items %}
21
21
  <a href="{{ item.url }}"
22
22
  class="list-group-item list-group-item-action{% if item.active %} active{% endif %}"
@@ -1,10 +1,12 @@
1
1
  {% load static %}
2
2
  <link rel="stylesheet" href="{% static 'sidebar/sidebar.css' %}">
3
3
  <link rel="stylesheet" href="{% static 'sidebar/css/theme_picker.css' %}">
4
+ <link rel="stylesheet" href="{% static 'sidebar/css/reorder.css' %}">
4
5
  <link rel="stylesheet" href="{% static 'themes/main.css' %}">
5
6
  <script src="{% static 'sidebar/sidebar.js' %}" nonce="{{ request.csp_nonce }}" defer></script>
6
7
  <script src="{% static 'themes/main.js' %}" nonce="{{ request.csp_nonce }}" defer></script>
7
8
  <script src="{% static 'sidebar/js/theme_picker.js' %}" nonce="{{ request.csp_nonce }}" defer></script>
9
+ <script src="{% static 'sidebar/js/reorder.js' %}" nonce="{{ request.csp_nonce }}" defer></script>
8
10
  <!-- Ghost Sidebar for small screens layout stability -->
9
11
  {% if request.user.is_authenticated %}
10
12
  <!-- <div class="sidebar-ghost"></div> -->
@@ -31,7 +33,7 @@
31
33
  {% block items %}
32
34
 
33
35
  <!-- DEFAULT CONTENT / INSTRUCTIONS -->
34
- <div class="p-3 text-center text-muted">
36
+ <!-- <div class="p-3 text-center text-muted">
35
37
  <i class="bi bi-info-circle mb-2" style="font-size: 24px;"></i>
36
38
  <p class="small">
37
39
  <strong>Default Sidebar</strong><br>
@@ -39,18 +41,82 @@
39
41
  <code>sidebar/main.html</code><br>
40
42
  and override the <code>items</code> block.
41
43
  </p>
42
- </div>
44
+ </div> -->
43
45
 
44
- <a href="#" class="list-group-item list-group-item-action">
46
+ <!-- <a href="#" class="list-group-item list-group-item-action">
45
47
  <i class="bi bi-house me-2" style="font-size: 24px;"></i>
46
48
  <span>Example Home</span>
47
- </a>
49
+ </a> -->
48
50
 
49
51
  {% endblock %}
50
52
  </div>
53
+ <script nonce="{{ request.csp_nonce }}">
54
+ // Immediate order restore to prevent FOUC - runs before browser paint
55
+ (function() {
56
+ var STORAGE_KEY_AUTO = 'sidebar_auto_order';
57
+ var STORAGE_KEY_PREFIX_EXTRA = 'sidebar_extra_';
58
+
59
+ function restoreContainer(container, storageKey) {
60
+ var saved;
61
+ try {
62
+ saved = localStorage.getItem(storageKey);
63
+ if (!saved) return;
64
+ saved = JSON.parse(saved);
65
+ } catch(e) { return; }
66
+ if (!Array.isArray(saved) || saved.length === 0) return;
67
+
68
+ var items = container.querySelectorAll(':scope > .list-group-item[data-url-name]');
69
+ var itemMap = {};
70
+ for (var i = 0; i < items.length; i++) {
71
+ itemMap[items[i].dataset.urlName] = items[i];
72
+ }
73
+ for (var j = 0; j < saved.length; j++) {
74
+ var item = itemMap[saved[j]];
75
+ if (item) {
76
+ container.appendChild(item);
77
+ delete itemMap[saved[j]];
78
+ }
79
+ }
80
+ for (var key in itemMap) {
81
+ container.appendChild(itemMap[key]);
82
+ }
83
+ }
84
+
85
+ function slugify(text) {
86
+ return text.toString().toLowerCase().trim()
87
+ .replace(/\s+/g, '-').replace(/[^\w\-]+/g, '').replace(/\-\-+/g, '-');
88
+ }
89
+
90
+ // Restore auto items
91
+ var autoContainer = document.getElementById('sidebarAutoItems');
92
+ if (autoContainer) {
93
+ restoreContainer(autoContainer, STORAGE_KEY_AUTO);
94
+ }
95
+
96
+ // Restore extra groups
97
+ var accordionBodies = document.querySelectorAll('.sidebar .accordion-body');
98
+ for (var k = 0; k < accordionBodies.length; k++) {
99
+ var body = accordionBodies[k];
100
+ var groupName = body.dataset.groupName;
101
+ if (!groupName) {
102
+ var btn = body.closest('.accordion-item');
103
+ if (btn) {
104
+ var span = btn.querySelector('.accordion-button span');
105
+ if (span) groupName = span.textContent.trim();
106
+ }
107
+ }
108
+ if (groupName) {
109
+ restoreContainer(body, STORAGE_KEY_PREFIX_EXTRA + slugify(groupName));
110
+ }
111
+ }
112
+
113
+ window._sidebarOrderRestored = true;
114
+ })();
115
+ </script>
51
116
 
52
- <!-- Sidebar Toolbar (Theme Picker, etc.) -->
117
+ <!-- Sidebar Toolbar (Theme Picker, Reorder, etc.) -->
53
118
  <div class="sidebar-toolbar no-print">
119
+ <i class="bi bi-arrows-move reorder-toggle" id="sidebarReorderToggle" title="إعادة الترتيب"></i>
54
120
  <i class="bi bi-chevron-up theme-arrow" id="sidebarThemeArrow"></i>
55
121
  <div class="current-theme-indicator" id="sidebarThemeIndicator" title="تغيير المظهر"></div>
56
122
  <div class="theme-popup" id="sidebarThemePopup">