micro-sidebar 1.2.2__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: 1.2.2
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
@@ -66,6 +66,119 @@ Requires-Dist: Django (>=5.1)
66
66
  ]
67
67
  ```
68
68
 
69
+ ## Auto-Discovery Mode (New in v2.0.0)
70
+
71
+ The sidebar can **automatically discover** your list views and generate navigation items!
72
+
73
+ ### Setup
74
+
75
+ 1. **Add context processor** to `settings.py`:
76
+ ```python
77
+ TEMPLATES = [{
78
+ 'OPTIONS': {
79
+ 'context_processors': [
80
+ ...
81
+ 'sidebar.context_processors.sidebar_context',
82
+ ],
83
+ },
84
+ }]
85
+ ```
86
+
87
+ 2. **Use in your sidebar template:**
88
+ ```html
89
+ {% extends "sidebar/main.html" %}
90
+ {% load sidebar_tags %}
91
+
92
+ {% block items %}
93
+ {% auto_sidebar %}
94
+ {% endblock %}
95
+ ```
96
+
97
+ That's it! The sidebar will automatically find all URLs with `list` in their name (e.g., `decree_list`), match them to models, and display `verbose_name_plural` as labels.
98
+
99
+ ### Configuration (Optional)
100
+
101
+ Add to `settings.py`:
102
+ ```python
103
+ SIDEBAR_AUTO = {
104
+ 'ENABLED': True, # Enable auto-discovery
105
+ 'URL_PATTERNS': ['list'], # Keywords to match in URL names
106
+ 'EXCLUDE_APPS': ['admin', 'auth'], # Apps to exclude
107
+ 'EXCLUDE_MODELS': [], # Specific models to exclude
108
+ 'CACHE_TIMEOUT': 3600, # Cache timeout in seconds
109
+ 'DEFAULT_ICON': 'bi-list', # Default Bootstrap icon
110
+ 'DEFAULT_ITEMS': {}, # See "Default Items" section below
111
+ 'EXTRA_ITEMS': {}, # See "Extra Items" section below
112
+ }
113
+ ```
114
+
115
+ ### Default Items Configuration
116
+
117
+ Use `DEFAULT_ITEMS` to customize auto-discovered items (labels, icons, ordering) without modifying your models:
118
+
119
+ ```python
120
+ SIDEBAR_AUTO = {
121
+ # ...
122
+ 'DEFAULT_ITEMS': {
123
+ 'decree_list': { # Key is the URL name
124
+ 'label': 'Decisions', # Override display label
125
+ 'icon': 'bi-gavel', # Override icon
126
+ 'order': 10, # Sort order (lower = first)
127
+ },
128
+ 'incoming_list': {
129
+ 'order': 20, # Only setting order
130
+ }
131
+ }
132
+ }
133
+ ```
134
+
135
+ Items not in `DEFAULT_ITEMS` will use defaults:
136
+ - **Label**: Model's `verbose_name_plural`
137
+ - **Icon**: `DEFAULT_ICON`
138
+ - **Order**: 100
139
+
140
+ ### Extra Items (Non-Model URLs)
141
+
142
+ For URLs that don't map to a model (e.g., management pages), use `EXTRA_ITEMS`:
143
+
144
+ ```python
145
+ SIDEBAR_AUTO = {
146
+ # ... other config ...
147
+ 'EXTRA_ITEMS': {
148
+ 'الإدارة': { # Group name (accordion header)
149
+ 'icon': 'bi-gear',
150
+ 'items': [
151
+ {
152
+ 'url_name': 'manage_sections',
153
+ 'label': 'إدارة الأقسام',
154
+ 'icon': 'bi-diagram-3',
155
+ 'permission': 'documents.manage_sections',
156
+ },
157
+ {
158
+ 'url_name': 'manage_users',
159
+ 'label': 'إدارة المستخدمين',
160
+ 'icon': 'bi-people',
161
+ 'permission': 'is_staff', # or a special check to Only show to staff users.
162
+ },
163
+ ]
164
+ }
165
+ }
166
+ }
167
+ ```
168
+
169
+ Then in your sidebar template:
170
+ ```html
171
+ {% extends "sidebar/main.html" %}
172
+ {% load sidebar_tags %}
173
+
174
+ {% block items %}
175
+ {% auto_sidebar %}
176
+ {% extra_sidebar %}
177
+ {% endblock %}
178
+ ```
179
+
180
+ Extra items appear at the bottom of the sidebar, grouped in Bootstrap accordions.
181
+
69
182
  ## Customization
70
183
 
71
184
  ### Override Default Menu
@@ -115,3 +228,6 @@ While it may theoretically work in LTR environments if standard Bootstrap files
115
228
  | **v1.2.0** | **New Theme Implementation:** Redesigned UI with rounded pill-shaped items, tactile micro-animations, and a refined color palette. Improved responsiveness with dynamic top-offset calculations and inline FOUC fixes for small screens. Fixed tooltip stickiness bug. |
116
229
  | **v1.2.1** | **Positioning Fix:** Added `align-self: flex-start` to resolve 60px vertical offset in flex containers. Removed legacy `sidebar-top-offset` CSS variable and JS calculations. Added `box-shadow: none` and `outline: none` to accordion buttons to remove focus ring. Fixed page flickering on wider screens by constraining sidebar height with `calc(100vh - header-height)`. |
117
230
  | **v1.2.2** | **CSP Compliance:** Added `nonce` attribute support to inline scripts for Content Security Policy compliance. |
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
+ | **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. |
@@ -0,0 +1,31 @@
1
+ sidebar/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ sidebar/apps.py,sha256=iF89pf0Vfs32KLibizTjb9crYPOb3sKcpS0MddBYGrM,381
3
+ sidebar/context_processors.py,sha256=g4TmjkAdyJDqGmQ7c1EhRtDUyt7gHQTPsNzbzy0o_sY,4221
4
+ sidebar/discovery.py,sha256=6rBpji-g057Pwg3fS7Vp1JV61wz3ygmhZfosGMNXjAY,5636
5
+ sidebar/urls.py,sha256=UL_9e1RLNMxZXkah65m7GRU1dbViZRGeNPBIiSZpOYg,142
6
+ sidebar/views.py,sha256=MebyJ1ZiylSOPESXFkkQ8QTg-ClrkJn-oYLN6KrcgiM,418
7
+ sidebar/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
+ sidebar/static/sidebar/sidebar.css,sha256=fojQ5zofvpCz1SNe4zCHPB3Bx7kF0KiVKb_W3SSrA8g,6044
9
+ sidebar/static/sidebar/sidebar.js,sha256=gcurW6nl0Dt9jPTzk749DX7uQ3U_7De4F6Gj8ci6W40,5233
10
+ sidebar/static/sidebar/css/reorder.css,sha256=zOBzg6nJwbY0QRCQKAVjaKxqCXqpICY1kFmXe2qO-og,2177
11
+ sidebar/static/sidebar/css/theme_picker.css,sha256=S2u9p_4rXZALf7hoCOsgyAU91NP3Hoy54WYnc4aGyww,4345
12
+ sidebar/static/sidebar/js/reorder.js,sha256=BRAI-S9aTBeq7kwg1Z_ixgIrPdHB3A7LIdZbe8poRB4,9478
13
+ sidebar/static/sidebar/js/theme_picker.js,sha256=Kt4S2fd0ocFjqeuOQXDcvWVLrLDAVxs7zANZqzIqA4g,2292
14
+ sidebar/static/themes/blue.css,sha256=_CMBLoX8xuSkEdiSYP4HhM-_M368q5Zus9n2kGILBQw,1651
15
+ sidebar/static/themes/dark.css,sha256=JmLG6UWSB7Erwogz0tCcpZUL1hgwxqgfyx-WSCXBtLU,14854
16
+ sidebar/static/themes/gold.css,sha256=2Ry4ba5CIRvgYFTiAaeh5K5DUjvbX1uyVIcolVTPiwE,1619
17
+ sidebar/static/themes/green.css,sha256=y-P-ShJ6ISOqT6FnuS2PNKNQX0kr4Whx-uMLozhPkvY,1951
18
+ sidebar/static/themes/light.css,sha256=XqStIa8wTzWswWyy2nqbk4tMPKUuIR4967PBoJoPuOc,1717
19
+ sidebar/static/themes/main.css,sha256=NaPBDAPL-PkTphtcIto7Qjb5CkiR9T_X5YztDT5SwwE,121
20
+ sidebar/static/themes/main.js,sha256=DSz07M8c1KTXSlj7woxPq0Kf9VFb2uNCI13Cz87gghs,1412
21
+ sidebar/static/themes/red.css,sha256=t8OTmvgszQXX4YtYxlIM86zjx1jp-SvfVx2G3X0FjDI,1613
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
25
+ sidebar/templatetags/__init__.py,sha256=RC19QrlHdcgslFc_19Se9UhOe-7_WH9GtNGYU_cZlXg,48
26
+ sidebar/templatetags/sidebar_tags.py,sha256=KoZzjqRpMtiGcITKFUQBcs5RdbvtuzCaNrCWEzIbBlk,2245
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,,
sidebar/apps.py CHANGED
@@ -1,5 +1,15 @@
1
1
  from django.apps import AppConfig
2
2
 
3
+
3
4
  class SidebarConfig(AppConfig):
4
5
  default_auto_field = 'django.db.models.BigAutoField'
5
6
  name = 'sidebar'
7
+
8
+ def ready(self):
9
+ """
10
+ Called when Django starts.
11
+ Import discovery module to ensure it's available.
12
+ """
13
+ # Import to make discovery available
14
+ from . import discovery # noqa: F401
15
+
@@ -0,0 +1,127 @@
1
+ """
2
+ Context processor for auto-discovered sidebar items.
3
+
4
+ Injects sidebar navigation items into every request context,
5
+ filtered by user permissions.
6
+ """
7
+ import hashlib
8
+ import json
9
+ from django.core.cache import cache
10
+ from django.urls import reverse, NoReverseMatch
11
+ from .discovery import discover_list_urls, get_sidebar_config
12
+
13
+
14
+ def _get_config_hash(config):
15
+ """Generate a hash of the config for cache key."""
16
+ # Exclude EXTRA_ITEMS from hash since they're processed separately
17
+ config_copy = {k: v for k, v in config.items() if k != 'EXTRA_ITEMS'}
18
+ config_str = json.dumps(config_copy, sort_keys=True)
19
+ return hashlib.md5(config_str.encode()).hexdigest()[:8]
20
+
21
+
22
+ def _process_extra_items(config, request):
23
+ """
24
+ Process EXTRA_ITEMS config into sidebar-ready format.
25
+
26
+ Returns dict of groups, each with icon and list of items with resolved URLs.
27
+ """
28
+ extra_items = config.get('EXTRA_ITEMS', {})
29
+ processed_groups = {}
30
+
31
+ for group_name, group_config in extra_items.items():
32
+ group_icon = group_config.get('icon', 'bi-gear')
33
+ items = []
34
+
35
+ for item in group_config.get('items', []):
36
+ url_name = item.get('url_name', '')
37
+
38
+ # Check permission if specified
39
+ permission = item.get('permission')
40
+ if permission:
41
+ if permission == 'is_staff' and not request.user.is_staff:
42
+ continue
43
+ elif permission == 'is_superuser' and not request.user.is_superuser:
44
+ continue
45
+ elif permission not in ['is_staff', 'is_superuser'] and not request.user.has_perm(permission):
46
+ continue
47
+
48
+ # Resolve URL
49
+ try:
50
+ url = reverse(url_name)
51
+ active = request.path == url or request.path.startswith(url.rstrip('/') + '/')
52
+ except NoReverseMatch:
53
+ url = '#'
54
+ active = False
55
+
56
+ items.append({
57
+ 'url_name': url_name,
58
+ 'url': url,
59
+ 'label': item.get('label', url_name),
60
+ 'icon': item.get('icon', 'bi-link'),
61
+ 'active': active,
62
+ })
63
+
64
+ if items: # Only add group if it has visible items
65
+ processed_groups[group_name] = {
66
+ 'icon': group_icon,
67
+ 'items': items,
68
+ 'has_active': any(item['active'] for item in items),
69
+ }
70
+
71
+ return processed_groups
72
+
73
+
74
+ def sidebar_context(request):
75
+ """
76
+ Add auto-discovered sidebar items to template context.
77
+
78
+ Items are cached for performance and filtered by user permissions.
79
+ Only authenticated users see sidebar items, and only items they
80
+ have view permission for.
81
+
82
+ Returns:
83
+ Dictionary with 'sidebar_auto_items' and 'sidebar_extra_groups' keys.
84
+ """
85
+ config = get_sidebar_config()
86
+ # Include config hash in cache key so settings changes invalidate cache
87
+ cache_key = f'sidebar_auto_items_{_get_config_hash(config)}'
88
+ items = cache.get(cache_key)
89
+
90
+ if items is None:
91
+ items = discover_list_urls()
92
+ cache.set(cache_key, items, timeout=config['CACHE_TIMEOUT'])
93
+
94
+ # Filter by user permissions
95
+ if request.user.is_authenticated:
96
+ if request.user.is_superuser:
97
+ # Superusers see everything
98
+ visible = items
99
+ else:
100
+ visible = []
101
+ for item in items:
102
+ if not item.get('permissions'):
103
+ visible.append(item)
104
+ elif any(request.user.has_perm(p) for p in item['permissions']):
105
+ visible.append(item)
106
+ items = visible
107
+
108
+ # Process extra items for authenticated users
109
+ extra_groups = _process_extra_items(config, request)
110
+ else:
111
+ items = []
112
+ extra_groups = {}
113
+
114
+ return {
115
+ 'sidebar_auto_items': items,
116
+ 'sidebar_extra_groups': extra_groups,
117
+ }
118
+
119
+
120
+ def clear_sidebar_cache():
121
+ """
122
+ Clear the sidebar items cache.
123
+
124
+ Call this when models or URLs change and sidebar needs refresh.
125
+ """
126
+ cache.delete('sidebar_auto_items')
127
+
sidebar/discovery.py ADDED
@@ -0,0 +1,151 @@
1
+ """
2
+ Auto-discovery module for sidebar navigation items.
3
+
4
+ Scans Django URL patterns for list views and matches them to models
5
+ to automatically generate sidebar navigation items.
6
+ """
7
+ from django.urls import get_resolver, URLPattern, URLResolver
8
+ from django.apps import apps
9
+ from django.conf import settings
10
+ from difflib import get_close_matches
11
+
12
+
13
+ def get_sidebar_config():
14
+ """Get sidebar configuration from Django settings with defaults."""
15
+ defaults = {
16
+ 'ENABLED': True,
17
+ 'URL_PATTERNS': ['list'],
18
+ 'EXCLUDE_APPS': ['admin', 'auth', 'contenttypes', 'sessions'],
19
+ 'EXCLUDE_MODELS': [],
20
+ 'CACHE_TIMEOUT': 3600,
21
+ 'DEFAULT_ICON': 'bi-list',
22
+ # Extra items that don't require model matching
23
+ # Format: {'group_name': {'icon': 'bi-gear', 'items': [{'url_name': 'x', 'label': 'X', 'icon': 'bi-x'}]}}
24
+ 'EXTRA_ITEMS': {},
25
+ # Default overrides for auto-discovered items
26
+ # Format: {'url_name': {'label': 'X', 'icon': 'bi-x', 'order': 10}}
27
+ 'DEFAULT_ITEMS': {},
28
+ }
29
+ user_config = getattr(settings, 'SIDEBAR_AUTO', {})
30
+ return {**defaults, **user_config}
31
+
32
+
33
+
34
+ def discover_list_urls():
35
+ """
36
+ Scan all URL patterns for names containing configured keywords.
37
+ Match them to Django models and extract verbose_name_plural.
38
+
39
+ Returns:
40
+ List of sidebar item dictionaries sorted by order.
41
+ """
42
+ config = get_sidebar_config()
43
+
44
+ if not config['ENABLED']:
45
+ return []
46
+
47
+ resolver = get_resolver()
48
+ items = []
49
+
50
+ for pattern in _iterate_patterns(resolver.url_patterns):
51
+ url_name = getattr(pattern, 'name', '') or ''
52
+
53
+ # Check if URL name contains any of the configured patterns
54
+ matched_keyword = None
55
+ for kw in config['URL_PATTERNS']:
56
+ if kw in url_name.lower():
57
+ matched_keyword = kw
58
+ break
59
+
60
+ if matched_keyword:
61
+ # Extract model hint (e.g., 'decree' from 'decree_list')
62
+ model_hint = url_name
63
+ for kw in config['URL_PATTERNS']:
64
+ model_hint = model_hint.replace(f'_{kw}', '').replace(f'-{kw}', '').replace(kw, '')
65
+
66
+ model_hint = model_hint.strip('_-')
67
+
68
+ # Try to find model - first with stripped hint, then with keyword itself
69
+ model = None
70
+ if model_hint:
71
+ model = _find_model(model_hint, config)
72
+
73
+ # If no model found and keyword looks like it could be a model name, try that
74
+ if not model and matched_keyword not in ['list', 'index', 'view', 'page']:
75
+ model = _find_model(matched_keyword, config)
76
+
77
+ if model:
78
+ # Check exclusions
79
+ if model._meta.app_label in config['EXCLUDE_APPS']:
80
+ continue
81
+ if f"{model._meta.app_label}.{model.__name__}" in config['EXCLUDE_MODELS']:
82
+ continue
83
+
84
+ items.append({
85
+ 'url_name': url_name,
86
+ 'label': getattr(model._meta, 'sidebar_label', None) or str(model._meta.verbose_name_plural),
87
+ 'icon': getattr(model._meta, 'sidebar_icon', config['DEFAULT_ICON']),
88
+ 'order': getattr(model._meta, 'sidebar_order', 100),
89
+ 'app_label': model._meta.app_label,
90
+ 'model_name': model._meta.model_name,
91
+ 'permissions': [f'{model._meta.app_label}.view_{model._meta.model_name}'],
92
+ })
93
+
94
+ # Override with DEFAULT_ITEMS if present
95
+ default_items = config.get('DEFAULT_ITEMS', {})
96
+ if url_name in default_items:
97
+ item_config = default_items[url_name]
98
+ # Update last added item
99
+ if items:
100
+ item = items[-1]
101
+ if 'label' in item_config:
102
+ item['label'] = item_config['label']
103
+ if 'icon' in item_config:
104
+ item['icon'] = item_config['icon']
105
+ if 'order' in item_config:
106
+ item['order'] = item_config['order']
107
+
108
+ return sorted(items, key=lambda x: (x['order'], x['label']))
109
+
110
+
111
+ def _iterate_patterns(patterns, prefix=''):
112
+ """Recursively iterate through URL patterns."""
113
+ for pattern in patterns:
114
+ if isinstance(pattern, URLResolver):
115
+ yield from _iterate_patterns(pattern.url_patterns, prefix + str(pattern.pattern))
116
+ elif isinstance(pattern, URLPattern):
117
+ yield pattern
118
+
119
+
120
+ def _find_model(hint, config):
121
+ """
122
+ Find model by name using exact and fuzzy matching.
123
+
124
+ Args:
125
+ hint: The model name hint extracted from URL
126
+ config: Sidebar configuration dictionary
127
+
128
+ Returns:
129
+ Model class or None if not found
130
+ """
131
+ all_models = apps.get_models()
132
+
133
+ # Build lookup excluding configured apps
134
+ model_names = {}
135
+ for m in all_models:
136
+ if m._meta.app_label not in config['EXCLUDE_APPS']:
137
+ model_names[m.__name__.lower()] = m
138
+
139
+ hint_lower = hint.lower()
140
+
141
+ # Exact match first
142
+ if hint_lower in model_names:
143
+ return model_names[hint_lower]
144
+
145
+ # Handle common pluralization (simple 's' suffix)
146
+ if hint_lower.endswith('s') and hint_lower[:-1] in model_names:
147
+ return model_names[hint_lower[:-1]]
148
+
149
+ # Fuzzy match as fallback
150
+ matches = get_close_matches(hint_lower, model_names.keys(), n=1, cutoff=0.8)
151
+ return model_names[matches[0]] if matches else None
@@ -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
+ }