aztomiq 1.0.2 → 1.0.3

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.
@@ -0,0 +1,264 @@
1
+ /* ============================================
2
+ UNIVERSAL SIDEBAR LAYOUT SYSTEM
3
+ All pages use consistent sidebar + content
4
+ ============================================ */
5
+
6
+ /* Page Wrapper */
7
+ .page-wrapper {
8
+ width: 100%;
9
+ padding: 2rem 0;
10
+ }
11
+
12
+ /* Universal Layout - Sidebar + Content */
13
+ .sidebar-layout {
14
+ display: flex;
15
+ gap: 2rem;
16
+ max-width: 1400px;
17
+ margin: 0 auto;
18
+ padding: 0 1rem;
19
+ min-height: 70vh;
20
+ }
21
+
22
+ /* Sidebar - Left Column */
23
+ .page-sidebar {
24
+ width: 250px;
25
+ flex-shrink: 0;
26
+ display: flex;
27
+ flex-direction: column;
28
+ gap: 1rem;
29
+ position: sticky;
30
+ top: 80px;
31
+ height: fit-content;
32
+ padding: 1.5rem;
33
+ background: var(--card-bg);
34
+ border: 1px solid var(--border-color);
35
+ border-radius: var(--radius);
36
+ box-shadow: var(--shadow);
37
+ }
38
+
39
+ /* Sidebar Title */
40
+ .sidebar-title {
41
+ font-size: 0.9rem;
42
+ font-weight: 600;
43
+ color: var(--text-muted);
44
+ text-transform: uppercase;
45
+ letter-spacing: 0.5px;
46
+ margin-bottom: 0.5rem;
47
+ padding-bottom: 0.5rem;
48
+ border-bottom: 1px solid var(--border-color);
49
+ }
50
+
51
+ /* Sidebar Navigation */
52
+ .sidebar-nav {
53
+ display: flex;
54
+ flex-direction: column;
55
+ gap: 0.5rem;
56
+ }
57
+
58
+ .sidebar-nav-item {
59
+ display: flex;
60
+ align-items: center;
61
+ gap: 0.75rem;
62
+ padding: 0.75rem 1rem;
63
+ border: 1px solid var(--border-color);
64
+ background: var(--bg-hover);
65
+ border-radius: 8px;
66
+ cursor: pointer;
67
+ transition: all 0.2s;
68
+ text-decoration: none;
69
+ color: var(--text-color);
70
+ font-size: 0.95rem;
71
+ }
72
+
73
+ .sidebar-nav-item:hover {
74
+ border-color: var(--primary-color);
75
+ background: var(--card-bg);
76
+ transform: translateX(5px);
77
+ }
78
+
79
+ .sidebar-nav-item.active {
80
+ background: var(--primary-color);
81
+ color: white;
82
+ border-color: var(--primary-color);
83
+ }
84
+
85
+ .sidebar-nav-item i {
86
+ font-size: 1.1rem;
87
+ opacity: 0.8;
88
+ }
89
+
90
+ /* Sidebar Info Box */
91
+ .sidebar-info {
92
+ padding: 1rem;
93
+ background: rgba(var(--primary-rgb), 0.1);
94
+ border: 1px solid rgba(var(--primary-rgb), 0.2);
95
+ border-radius: 8px;
96
+ font-size: 0.85rem;
97
+ color: var(--text-muted);
98
+ line-height: 1.5;
99
+ }
100
+
101
+ /* Sidebar Quick Actions */
102
+ .sidebar-actions {
103
+ display: flex;
104
+ flex-direction: column;
105
+ gap: 0.5rem;
106
+ margin-top: auto;
107
+ padding-top: 1rem;
108
+ border-top: 1px solid var(--border-color);
109
+ }
110
+
111
+ .sidebar-action-btn {
112
+ padding: 0.75rem;
113
+ background: var(--primary-color);
114
+ color: white;
115
+ border: none;
116
+ border-radius: 8px;
117
+ cursor: pointer;
118
+ font-weight: 600;
119
+ transition: all 0.2s;
120
+ text-align: center;
121
+ text-decoration: none;
122
+ display: block;
123
+ }
124
+
125
+ .sidebar-action-btn:hover {
126
+ background: var(--primary-hover);
127
+ transform: translateY(-2px);
128
+ }
129
+
130
+ /* Main Content - Right Column */
131
+ .page-content {
132
+ flex: 1;
133
+ min-width: 0;
134
+ padding: 2rem;
135
+ background: var(--card-bg);
136
+ border: 1px solid var(--border-color);
137
+ border-radius: var(--radius);
138
+ box-shadow: var(--shadow);
139
+ }
140
+
141
+ /* Content Header */
142
+ .content-header {
143
+ margin-bottom: 2rem;
144
+ padding-bottom: 1.5rem;
145
+ border-bottom: 1px solid var(--border-color);
146
+ }
147
+
148
+ .content-header h1 {
149
+ font-size: 2rem;
150
+ margin-bottom: 0.5rem;
151
+ color: var(--text-color);
152
+ }
153
+
154
+ .content-header p {
155
+ color: var(--text-muted);
156
+ font-size: 1.1rem;
157
+ line-height: 1.6;
158
+ }
159
+
160
+ /* Content Sections */
161
+ .content-section {
162
+ margin-bottom: 2rem;
163
+ }
164
+
165
+ .content-section:last-child {
166
+ margin-bottom: 0;
167
+ }
168
+
169
+ .content-section h2 {
170
+ font-size: 1.5rem;
171
+ margin-bottom: 1rem;
172
+ color: var(--text-color);
173
+ }
174
+
175
+ .content-section h3 {
176
+ font-size: 1.2rem;
177
+ margin-bottom: 0.75rem;
178
+ color: var(--text-color);
179
+ }
180
+
181
+ /* Content Cards */
182
+ .content-card {
183
+ background: var(--bg-hover);
184
+ border: 1px solid var(--border-color);
185
+ border-radius: 8px;
186
+ padding: 1.5rem;
187
+ margin-bottom: 1rem;
188
+ }
189
+
190
+ .content-card:last-child {
191
+ margin-bottom: 0;
192
+ }
193
+
194
+ /* Responsive - Mobile */
195
+ @media (max-width: 1000px) {
196
+ .sidebar-layout {
197
+ flex-direction: column;
198
+ }
199
+
200
+ .page-sidebar {
201
+ width: 100%;
202
+ position: static;
203
+ flex-direction: row;
204
+ overflow-x: auto;
205
+ padding: 1rem;
206
+ }
207
+
208
+ .sidebar-nav {
209
+ flex-direction: row;
210
+ flex-wrap: nowrap;
211
+ }
212
+
213
+ .sidebar-nav-item {
214
+ white-space: nowrap;
215
+ }
216
+
217
+ .sidebar-actions {
218
+ margin-top: 0;
219
+ margin-left: auto;
220
+ padding-top: 0;
221
+ padding-left: 1rem;
222
+ border-top: none;
223
+ border-left: 1px solid var(--border-color);
224
+ }
225
+
226
+ .page-content {
227
+ padding: 1.5rem;
228
+ }
229
+
230
+ .content-header h1 {
231
+ font-size: 1.5rem;
232
+ }
233
+ }
234
+
235
+ @media (max-width: 768px) {
236
+ .page-wrapper {
237
+ padding: 1rem 0;
238
+ }
239
+
240
+ .sidebar-layout {
241
+ padding: 0 0.75rem;
242
+ gap: 1rem;
243
+ }
244
+
245
+ .page-sidebar {
246
+ flex-direction: column;
247
+ }
248
+
249
+ .sidebar-nav {
250
+ flex-direction: column;
251
+ }
252
+
253
+ .sidebar-actions {
254
+ margin-left: 0;
255
+ padding-left: 0;
256
+ border-left: none;
257
+ padding-top: 1rem;
258
+ border-top: 1px solid var(--border-color);
259
+ }
260
+
261
+ .page-content {
262
+ padding: 1rem;
263
+ }
264
+ }
@@ -0,0 +1,29 @@
1
+ <svg
2
+ width="100"
3
+ height="100"
4
+ viewBox="0 0 100 100"
5
+ xmlns="http://www.w3.org/2000/svg"
6
+ >
7
+ <!-- Symmetrical Z with minimal padding -->
8
+ <path
9
+ d="M10 15
10
+ H90
11
+ L10 85
12
+ H90"
13
+ fill="none"
14
+ stroke="#22d3ee"
15
+ stroke-width="14"
16
+ stroke-linecap="round"
17
+ stroke-linejoin="round"
18
+ />
19
+
20
+ <!-- Hidden A stroke (refined) -->
21
+ <path
22
+ d="M45 50
23
+ L65 88"
24
+ fill="none"
25
+ stroke="#116974"
26
+ stroke-width="14"
27
+ stroke-linecap="round"
28
+ />
29
+ </svg>
@@ -0,0 +1,373 @@
1
+ document.addEventListener('DOMContentLoaded', () => {
2
+ const toggleBtn = document.getElementById('theme-toggle');
3
+ const htmlEl = document.documentElement;
4
+
5
+ // Update button icon based on current theme
6
+ const updateIcon = (theme) => {
7
+ const iconName = theme === 'dark' ? 'sun' : 'moon';
8
+ toggleBtn.innerHTML = `<i data-lucide="${iconName}"></i>`;
9
+ if (window.lucide) lucide.createIcons();
10
+ };
11
+
12
+ updateIcon(htmlEl.getAttribute('data-theme'));
13
+
14
+ toggleBtn.addEventListener('click', () => {
15
+ const currentTheme = htmlEl.getAttribute('data-theme');
16
+ const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
17
+
18
+ htmlEl.setAttribute('data-theme', newTheme);
19
+ localStorage.setItem('theme', newTheme);
20
+ updateIcon(newTheme);
21
+ });
22
+
23
+ console.log('AZtomiq Global JS Loaded');
24
+
25
+ // Register Service Worker for PWA
26
+ if ('serviceWorker' in navigator) {
27
+ window.addEventListener('load', () => {
28
+ navigator.serviceWorker.register('/sw.js')
29
+ .then(req => console.log('SW Registered!', req.scope))
30
+ .catch(err => console.error('SW Registration Failed', err));
31
+ });
32
+ }
33
+ // --- Global Navigation & Mega Menu Logic ---
34
+ const navItems = document.querySelectorAll('.nav-item');
35
+
36
+ // 1. Logo/Menu Toggle
37
+ navItems.forEach(item => {
38
+ const toggle = item.querySelector('.dropdown-toggle');
39
+ const menu = item.querySelector('.dropdown-menu');
40
+
41
+ if (toggle && menu) {
42
+ toggle.addEventListener('click', (e) => {
43
+ e.preventDefault();
44
+ e.stopPropagation();
45
+
46
+ // Close other open menus first
47
+ document.querySelectorAll('.dropdown-menu.show').forEach(m => {
48
+ if (m !== menu) m.classList.remove('show');
49
+ });
50
+
51
+ menu.classList.toggle('show');
52
+ toggle.setAttribute('aria-expanded', menu.classList.contains('show'));
53
+ });
54
+ }
55
+ });
56
+
57
+ // 2. Mega Menu Category Toggles (Expand/Collapse)
58
+ document.querySelectorAll('.mega-header').forEach(header => {
59
+ header.addEventListener('click', (e) => {
60
+ e.stopPropagation(); // prevent closing the menu itself
61
+ e.preventDefault();
62
+ const expanded = header.getAttribute('aria-expanded') === 'true';
63
+ header.setAttribute('aria-expanded', !expanded);
64
+ });
65
+ });
66
+
67
+ // 3. Close dropdown when clicking outside
68
+ document.addEventListener('click', (e) => {
69
+ if (!e.target.closest('.nav-item')) {
70
+ document.querySelectorAll('.dropdown-menu.show').forEach(menu => {
71
+ menu.classList.remove('show');
72
+ const toggle = menu.parentElement.querySelector('.dropdown-toggle');
73
+ if (toggle) toggle.setAttribute('aria-expanded', 'false');
74
+ });
75
+ }
76
+ });
77
+
78
+ // --- Search Modal Logic ---
79
+ const searchBtn = document.getElementById('search-btn');
80
+ const searchModal = document.getElementById('search-modal');
81
+ const searchInput = document.getElementById('search-input');
82
+ const searchResults = document.getElementById('search-results');
83
+ const closeSearch = document.getElementById('close-search');
84
+ const searchOverlay = document.querySelector('.search-overlay');
85
+
86
+ let toolsData = [];
87
+ let i18nData = {};
88
+ try {
89
+ const toolsScript = document.getElementById('tools-data');
90
+ if (toolsScript) toolsData = JSON.parse(toolsScript.textContent);
91
+
92
+ const i18nScript = document.getElementById('i18n-data');
93
+ if (i18nScript) i18nData = JSON.parse(i18nScript.textContent);
94
+ } catch (e) { console.error('Error loading search/i18n data', e); }
95
+
96
+ const openSearch = () => {
97
+ searchModal.classList.add('show');
98
+ document.body.style.overflow = 'hidden';
99
+ setTimeout(() => searchInput.focus(), 50);
100
+ renderResults('');
101
+ };
102
+
103
+ const hideSearch = () => {
104
+ searchModal.classList.remove('show');
105
+ document.body.style.overflow = '';
106
+ searchInput.value = '';
107
+ };
108
+
109
+ if (searchBtn) searchBtn.addEventListener('click', openSearch);
110
+ const searchBtnMobile = document.getElementById('search-btn-mobile');
111
+ if (searchBtnMobile) searchBtnMobile.addEventListener('click', openSearch);
112
+
113
+ const headerSearchBox = document.getElementById('header-search-box');
114
+ if (headerSearchBox) {
115
+ headerSearchBox.addEventListener('click', openSearch);
116
+
117
+ // Smart visibility for Homepage Hero Search
118
+ const heroSearch = document.querySelector('.search-box-hero');
119
+ if (heroSearch) {
120
+ // Check initial state
121
+ headerSearchBox.classList.add('search-hidden');
122
+
123
+ window.addEventListener('scroll', () => {
124
+ const rect = heroSearch.getBoundingClientRect();
125
+ // If hero search bottom is above viewport (scrolled past)
126
+ if (rect.bottom < 0) {
127
+ headerSearchBox.classList.remove('search-hidden');
128
+ } else {
129
+ headerSearchBox.classList.add('search-hidden');
130
+ }
131
+ }, { passive: true });
132
+ }
133
+ }
134
+
135
+ if (closeSearch) closeSearch.addEventListener('click', hideSearch);
136
+ if (searchOverlay) searchOverlay.addEventListener('click', hideSearch);
137
+
138
+ // Keyboard Shortcuts
139
+ document.addEventListener('keydown', (e) => {
140
+ if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
141
+ e.preventDefault();
142
+ openSearch();
143
+ }
144
+ if (e.key === 'Escape') hideSearch();
145
+ });
146
+
147
+ // Search Logic
148
+ searchInput.addEventListener('input', (e) => {
149
+ renderResults(e.target.value.trim());
150
+ });
151
+
152
+ function renderResults(query) {
153
+ if (!searchResults) return;
154
+
155
+ const q = query.toLowerCase();
156
+ const filtered = toolsData.filter(tool => {
157
+ if (!q) return true;
158
+ return tool.title.toLowerCase().includes(q) ||
159
+ tool.desc.toLowerCase().includes(q) ||
160
+ tool.id.toLowerCase().includes(q);
161
+ }).slice(0, 10);
162
+
163
+ if (filtered.length === 0) {
164
+ const emptyMsg = i18nData.search_no_results || 'No results for';
165
+ searchResults.innerHTML = `<div class="search-no-results">${emptyMsg} "${query}"</div>`;
166
+ return;
167
+ }
168
+
169
+ searchResults.innerHTML = filtered.map((tool, index) => `
170
+ <a href="${tool.link}" class="search-result-item ${index === 0 ? 'selected' : ''}" data-index="${index}">
171
+ <div class="result-icon"><i data-lucide="${tool.icon}"></i></div>
172
+ <div class="result-info">
173
+ <span class="result-title">${tool.title}</span>
174
+ <span class="result-desc">${tool.desc}</span>
175
+ </div>
176
+ <span class="result-cat">${tool.category}</span>
177
+ </a>
178
+ `).join('');
179
+
180
+ if (window.lucide) lucide.createIcons();
181
+
182
+ // Handle Keyboard Navigation within results
183
+ setupResultNavigation();
184
+ }
185
+
186
+ function setupResultNavigation() {
187
+ let selectedIndex = 0;
188
+ const items = searchResults.querySelectorAll('.search-result-item');
189
+
190
+ searchInput.onkeydown = (e) => {
191
+ if (e.key === 'ArrowDown') {
192
+ e.preventDefault();
193
+ items[selectedIndex].classList.remove('selected');
194
+ selectedIndex = (selectedIndex + 1) % items.length;
195
+ items[selectedIndex].classList.add('selected');
196
+ items[selectedIndex].scrollIntoView({ block: 'nearest' });
197
+ } else if (e.key === 'ArrowUp') {
198
+ e.preventDefault();
199
+ items[selectedIndex].classList.remove('selected');
200
+ selectedIndex = (selectedIndex - 1 + items.length) % items.length;
201
+ items[selectedIndex].classList.add('selected');
202
+ items[selectedIndex].scrollIntoView({ block: 'nearest' });
203
+ } else if (e.key === 'Enter') {
204
+ e.preventDefault();
205
+ const selected = searchResults.querySelector('.search-result-item.selected');
206
+ if (selected) selected.click();
207
+ }
208
+ };
209
+ }
210
+
211
+ // --- Changelog Modal Logic ---
212
+ const changelogBtn = document.getElementById('open-changelog');
213
+ const changelogModal = document.getElementById('changelog-modal');
214
+ const closeChangelog = document.getElementById('close-changelog');
215
+ const changelogOverlay = document.getElementById('close-changelog-overlay');
216
+
217
+ if (changelogBtn && changelogModal) {
218
+ changelogBtn.addEventListener('click', () => {
219
+ changelogModal.classList.add('show');
220
+ document.body.style.overflow = 'hidden';
221
+ });
222
+
223
+ const hideChangelog = () => {
224
+ changelogModal.classList.remove('show');
225
+ document.body.style.overflow = '';
226
+ };
227
+
228
+ if (closeChangelog) closeChangelog.addEventListener('click', hideChangelog);
229
+ if (changelogOverlay) changelogOverlay.addEventListener('click', hideChangelog);
230
+ }
231
+
232
+ // --- End Search & Changelog Logic ---
233
+
234
+ // Track clicks on tool items (both in Mega Menu and Homepage Grid)
235
+ document.querySelectorAll('.tool-item, .mega-link, .search-result-item').forEach(link => {
236
+ link.addEventListener('click', (e) => {
237
+ const url = link.getAttribute('href');
238
+ trackUsage(url);
239
+
240
+ // Close any open mega-menu if this was a menu click
241
+ if (link.classList.contains('mega-link')) {
242
+ document.querySelectorAll('.dropdown-menu.show').forEach(menu => {
243
+ menu.classList.remove('show');
244
+ const toggle = menu.parentElement.querySelector('.dropdown-toggle');
245
+ if (toggle) toggle.setAttribute('aria-expanded', 'false');
246
+ });
247
+ }
248
+ });
249
+ });
250
+
251
+ function trackUsage(toolUrl) {
252
+ if (!toolUrl) return;
253
+ try {
254
+ const parts = toolUrl.split('/').filter(Boolean);
255
+ const id = parts[parts.length - 1];
256
+ const now = Date.now();
257
+
258
+ // 1. Recently Used
259
+ let recent = JSON.parse(localStorage.getItem('aztomiq_recent') || '{}');
260
+ recent[id] = now;
261
+ localStorage.setItem('aztomiq_recent', JSON.stringify(recent));
262
+
263
+ // 2. Most Used (Personal)
264
+ let usage = JSON.parse(localStorage.getItem('aztomiq_usage') || '{}');
265
+ usage[id] = (usage[id] || 0) + 1;
266
+ localStorage.setItem('aztomiq_usage', JSON.stringify(usage));
267
+
268
+ } catch (e) { console.error('Tracking error', e); }
269
+ }
270
+ // --- Favorite Tools Logic (Star System) ---
271
+ const FAVORITES_KEY = 'aztomiq_favorites';
272
+ let favorites = JSON.parse(localStorage.getItem(FAVORITES_KEY) || '[]');
273
+
274
+ function updateStars() {
275
+ document.querySelectorAll('.star-btn').forEach(btn => {
276
+ const id = btn.getAttribute('data-tool-id');
277
+ const isActive = favorites.includes(id);
278
+ btn.classList.toggle('active', isActive);
279
+ const icon = btn.querySelector('i');
280
+ if (icon && window.lucide) {
281
+ // We just toggle the class, CSS handles the fill/color
282
+ }
283
+ });
284
+ }
285
+
286
+ function toggleFavorite(id) {
287
+ const index = favorites.indexOf(id);
288
+ if (index === -1) {
289
+ favorites.push(id);
290
+ } else {
291
+ favorites.splice(index, 1);
292
+ }
293
+ localStorage.setItem(FAVORITES_KEY, JSON.stringify(favorites));
294
+ updateStars();
295
+ renderFavorites();
296
+ }
297
+
298
+ function renderFavorites() {
299
+ const favoritesSection = document.getElementById('favorites-section');
300
+ const favoritesGrid = document.getElementById('favorites-grid');
301
+ if (!favoritesGrid) return;
302
+
303
+ if (favorites.length === 0) {
304
+ favoritesSection.style.display = 'none';
305
+ return;
306
+ }
307
+
308
+ favoritesSection.style.display = 'block';
309
+
310
+ // Find tool objects from toolsData
311
+ const favoriteTools = favorites.map(id => toolsData.find(t => t.id === id)).filter(Boolean);
312
+
313
+ favoritesGrid.innerHTML = favoriteTools.map(tool => {
314
+ let modeClass = '';
315
+ if (tool.mode === 'standard') modeClass = 'mode-standard-only';
316
+ else if (tool.mode === 'advanced') modeClass = 'mode-advanced-only';
317
+
318
+ return `
319
+ <div class="tool-card-wrapper ${modeClass}">
320
+ <div class="tool-card">
321
+ <button class="star-btn active" data-tool-id="${tool.id}" aria-label="Remove from favorites" title="Remove from favorites">
322
+ <i data-lucide="star" style="width: 14px; height: 14px; fill: currentColor;"></i>
323
+ </button>
324
+
325
+ <div class="tool-badge-row">
326
+ ${tool.highlight ? `<span class="tool-badge hot"><i data-lucide="sparkles" style="width: 12px; height: 12px;"></i> HOT</span>` : ''}
327
+ ${tool.status === 'not-ready' ? `<span class="tool-badge beta">BETA</span>` : ''}
328
+ </div>
329
+
330
+ <a href="${tool.link}" class="tool-link">
331
+ <div class="tool-icon-wrap">
332
+ <i data-lucide="${tool.icon}" style="width: 32px; height: 32px;"></i>
333
+ </div>
334
+ <h3>${tool.title}</h3>
335
+ <p>${tool.desc}</p>
336
+ </a>
337
+ </div>
338
+ </div>
339
+ `;
340
+ }).join('');
341
+
342
+ if (window.lucide) lucide.createIcons();
343
+
344
+ // Re-attach listeners to new buttons in favorites grid
345
+ favoritesGrid.querySelectorAll('.star-btn').forEach(btn => {
346
+ btn.onclick = (e) => {
347
+ e.preventDefault();
348
+ e.stopPropagation();
349
+ toggleFavorite(btn.getAttribute('data-tool-id'));
350
+ };
351
+ });
352
+ }
353
+
354
+ // Global delegate for stars
355
+ document.addEventListener('click', (e) => {
356
+ const starBtn = e.target.closest('.star-btn');
357
+ if (starBtn && !starBtn.closest('#favorites-grid')) {
358
+ e.preventDefault();
359
+ e.stopPropagation();
360
+ toggleFavorite(starBtn.getAttribute('data-tool-id'));
361
+ }
362
+ });
363
+
364
+ // Initial render
365
+ updateStars();
366
+ renderFavorites();
367
+
368
+ // Final Lucide init (for any dynamically added or static icons missed)
369
+ if (window.lucide) lucide.createIcons();
370
+
371
+ // --- End Favorite Tools Logic ---
372
+ });
373
+