@docmd/live 0.4.0

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,276 @@
1
+ // Source file from the docmd project — https://github.com/docmd-io/docmd
2
+
3
+ /*
4
+ * Main client-side script for docmd UI interactions
5
+ */
6
+
7
+ // --- Collapsible Navigation Logic ---
8
+ function initializeCollapsibleNav() {
9
+ const nav = document.querySelector('.sidebar-nav');
10
+ if (!nav) return;
11
+
12
+ // 1. Initial Cleanup (ensure classes match aria states)
13
+ nav.querySelectorAll('li.collapsible').forEach(item => {
14
+ // If server rendered it as expanded, ensure aria matches
15
+ if (item.classList.contains('expanded')) {
16
+ item.setAttribute('aria-expanded', 'true');
17
+ } else {
18
+ item.setAttribute('aria-expanded', 'false');
19
+ }
20
+ });
21
+
22
+ // 2. Event Delegation
23
+ nav.addEventListener('click', (e) => {
24
+ // Check if the click target is the arrow or inside the arrow wrapper
25
+ const arrow = e.target.closest('.collapse-icon-wrapper');
26
+
27
+ if (arrow) {
28
+ // STOP everything. Do not follow the link.
29
+ e.preventDefault();
30
+ e.stopPropagation();
31
+
32
+ const item = arrow.closest('li.collapsible');
33
+ if (item) {
34
+ // Toggle State
35
+ const isExpanded = item.classList.contains('expanded');
36
+
37
+ if (isExpanded) {
38
+ item.classList.remove('expanded');
39
+ item.setAttribute('aria-expanded', 'false');
40
+ } else {
41
+ item.classList.add('expanded');
42
+ item.setAttribute('aria-expanded', 'true');
43
+ }
44
+ }
45
+ return false;
46
+ }
47
+ });
48
+ }
49
+
50
+ // --- Mobile Menu Logic ---
51
+ function initializeMobileMenus() {
52
+ const sidebarBtn = document.querySelector('.sidebar-menu-button');
53
+ const sidebar = document.querySelector('.sidebar');
54
+
55
+ if (sidebarBtn && sidebar) {
56
+ sidebarBtn.addEventListener('click', (e) => {
57
+ e.stopPropagation();
58
+ sidebar.classList.toggle('mobile-expanded');
59
+ });
60
+ }
61
+
62
+ const tocBtn = document.querySelector('.toc-menu-button');
63
+ const tocContainer = document.querySelector('.toc-container');
64
+ const tocTitle = document.querySelector('.toc-title');
65
+
66
+ const toggleToc = (e) => {
67
+ if (window.getComputedStyle(tocBtn).display === 'none') return;
68
+ e.stopPropagation();
69
+ tocContainer.classList.toggle('mobile-expanded');
70
+ };
71
+
72
+ if (tocBtn && tocContainer) {
73
+ tocBtn.addEventListener('click', toggleToc);
74
+ if (tocTitle) tocTitle.addEventListener('click', toggleToc);
75
+ }
76
+ }
77
+
78
+ // --- Sidebar Scroll Preservation (Instant Center) ---
79
+ function initializeSidebarScroll() {
80
+ const sidebar = document.querySelector('.sidebar');
81
+ if (!sidebar) return;
82
+
83
+ // Wait for the layout to be stable
84
+ requestAnimationFrame(() => {
85
+ // Find the active link
86
+ const activeElement = sidebar.querySelector('a.active');
87
+
88
+ if (activeElement) {
89
+ activeElement.scrollIntoView({
90
+ behavior: 'auto', // INSTANT jump (prevents scrolling animation jitter)
91
+ block: 'center', // Center it vertically in the sidebar
92
+ inline: 'nearest'
93
+ });
94
+ }
95
+ });
96
+ }
97
+
98
+ // --- Theme Toggle Logic ---
99
+ function setupThemeToggleListener() {
100
+ const toggleButtons = document.querySelectorAll('.theme-toggle-button');
101
+
102
+ function applyTheme(theme) {
103
+ const validThemes = ['light', 'dark'];
104
+ const selectedTheme = validThemes.includes(theme) ? theme : 'light';
105
+
106
+ // 1. Update DOM & Storage
107
+ document.documentElement.setAttribute('data-theme', selectedTheme);
108
+ document.body.setAttribute('data-theme', selectedTheme);
109
+ localStorage.setItem('docmd-theme', selectedTheme);
110
+
111
+ // 2. Toggle Highlight.js Stylesheets
112
+ const lightLink = document.getElementById('hljs-light');
113
+ const darkLink = document.getElementById('hljs-dark');
114
+
115
+ if (lightLink && darkLink) {
116
+ if (selectedTheme === 'dark') {
117
+ lightLink.disabled = true;
118
+ darkLink.disabled = false;
119
+ } else {
120
+ lightLink.disabled = false;
121
+ darkLink.disabled = true;
122
+ }
123
+ }
124
+ }
125
+
126
+ toggleButtons.forEach(btn => {
127
+ // Clone button to remove old listeners if any (safety measure)
128
+ const newBtn = btn.cloneNode(true);
129
+ btn.parentNode.replaceChild(newBtn, btn);
130
+
131
+ newBtn.addEventListener('click', () => {
132
+ const currentTheme = document.documentElement.getAttribute('data-theme') || 'light';
133
+ const newTheme = currentTheme === 'light' ? 'dark' : 'light';
134
+ applyTheme(newTheme);
135
+ });
136
+ });
137
+ }
138
+
139
+ // --- Sidebar Collapse Logic ---
140
+ function initializeSidebarToggle() {
141
+ const toggleButton = document.getElementById('sidebar-toggle-button');
142
+ const body = document.body;
143
+
144
+ if (!body.classList.contains('sidebar-collapsible') || !toggleButton) return;
145
+
146
+ const defaultConfigCollapsed = body.dataset.defaultCollapsed === 'true';
147
+ let isCollapsed = localStorage.getItem('docmd-sidebar-collapsed');
148
+
149
+ if (isCollapsed === null) isCollapsed = defaultConfigCollapsed;
150
+ else isCollapsed = isCollapsed === 'true';
151
+
152
+ if (isCollapsed) body.classList.add('sidebar-collapsed');
153
+
154
+ toggleButton.addEventListener('click', () => {
155
+ body.classList.toggle('sidebar-collapsed');
156
+ const currentlyCollapsed = body.classList.contains('sidebar-collapsed');
157
+ localStorage.setItem('docmd-sidebar-collapsed', currentlyCollapsed);
158
+ });
159
+ }
160
+
161
+ // --- Tabs Container Logic ---
162
+ function initializeTabs() {
163
+ document.querySelectorAll('.docmd-tabs').forEach(tabsContainer => {
164
+ const navItems = tabsContainer.querySelectorAll('.docmd-tabs-nav-item');
165
+ const tabPanes = tabsContainer.querySelectorAll('.docmd-tab-pane');
166
+
167
+ navItems.forEach((navItem, index) => {
168
+ navItem.addEventListener('click', () => {
169
+ navItems.forEach(item => item.classList.remove('active'));
170
+ tabPanes.forEach(pane => pane.classList.remove('active'));
171
+
172
+ navItem.classList.add('active');
173
+ if (tabPanes[index]) tabPanes[index].classList.add('active');
174
+ });
175
+ });
176
+ });
177
+ }
178
+
179
+ // --- Copy Code Button Logic ---
180
+ function initializeCopyCodeButtons() {
181
+ if (document.body.dataset.copyCodeEnabled !== 'true') return;
182
+
183
+ const copyIconSvg = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-copy"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"></rect><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"></path></svg>`;
184
+ const checkIconSvg = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-check"><polyline points="20 6 9 17 4 12"></polyline></svg>`;
185
+
186
+ document.querySelectorAll('pre').forEach(preElement => {
187
+ const codeElement = preElement.querySelector('code');
188
+ if (!codeElement) return;
189
+
190
+ const wrapper = document.createElement('div');
191
+ wrapper.style.position = 'relative';
192
+ wrapper.style.display = 'block';
193
+
194
+ preElement.parentNode.insertBefore(wrapper, preElement);
195
+ wrapper.appendChild(preElement);
196
+ preElement.style.position = 'static';
197
+
198
+ const copyButton = document.createElement('button');
199
+ copyButton.className = 'copy-code-button';
200
+ copyButton.innerHTML = copyIconSvg;
201
+ copyButton.setAttribute('aria-label', 'Copy code to clipboard');
202
+ wrapper.appendChild(copyButton);
203
+
204
+ copyButton.addEventListener('click', () => {
205
+ navigator.clipboard.writeText(codeElement.innerText).then(() => {
206
+ copyButton.innerHTML = checkIconSvg;
207
+ copyButton.classList.add('copied');
208
+ setTimeout(() => {
209
+ copyButton.innerHTML = copyIconSvg;
210
+ copyButton.classList.remove('copied');
211
+ }, 2000);
212
+ }).catch(err => {
213
+ console.error('Failed to copy text: ', err);
214
+ copyButton.innerText = 'Error';
215
+ });
216
+ });
217
+ });
218
+ }
219
+
220
+ // --- Theme Sync Function ---
221
+ function syncBodyTheme() {
222
+ const currentTheme = document.documentElement.getAttribute('data-theme');
223
+ if (currentTheme && document.body) {
224
+ document.body.setAttribute('data-theme', currentTheme);
225
+ }
226
+ }
227
+
228
+ // --- Scroll Spy Logic ---
229
+ function initializeScrollSpy() {
230
+ const tocLinks = document.querySelectorAll('.toc-link');
231
+ const headings = document.querySelectorAll('.main-content h2, .main-content h3');
232
+
233
+ if (tocLinks.length === 0 || headings.length === 0) return;
234
+
235
+ const observerOptions = {
236
+ root: null,
237
+ // Trigger when heading crosses the top 10% of screen
238
+ rootMargin: '-10% 0px -80% 0px',
239
+ threshold: 0
240
+ };
241
+
242
+ const observer = new IntersectionObserver((entries) => {
243
+ entries.forEach(entry => {
244
+ if (entry.isIntersecting) {
245
+ // 1. Clear current active state
246
+ tocLinks.forEach(link => link.classList.remove('active'));
247
+
248
+ // 2. Find link corresponding to this heading
249
+ const id = entry.target.getAttribute('id');
250
+ const activeLink = document.querySelector(`.toc-link[href="#${id}"]`);
251
+
252
+ if (activeLink) {
253
+ activeLink.classList.add('active');
254
+
255
+ // Optional: Auto-scroll the TOC sidebar itself if needed
256
+ // activeLink.scrollIntoView({ block: 'nearest' });
257
+ }
258
+ }
259
+ });
260
+ }, observerOptions);
261
+
262
+ headings.forEach(heading => observer.observe(heading));
263
+ }
264
+
265
+ // --- Main Execution ---
266
+ document.addEventListener('DOMContentLoaded', () => {
267
+ syncBodyTheme();
268
+ setupThemeToggleListener();
269
+ initializeSidebarToggle();
270
+ initializeTabs();
271
+ initializeCopyCodeButtons();
272
+ initializeCollapsibleNav();
273
+ initializeMobileMenus();
274
+ initializeSidebarScroll();
275
+ initializeScrollSpy();
276
+ });
@@ -0,0 +1,275 @@
1
+ /* Source file from the docmd project — https://github.com/docmd-io/docmd */
2
+
3
+ :root {
4
+ --header-height: 50px;
5
+ --border-color: #e0e0e0;
6
+ --bg-color: #f9fafb;
7
+ --primary-color: #007bff;
8
+ --resizer-width: 8px
9
+ }
10
+
11
+ body {
12
+ margin: 0;
13
+ height: 100vh;
14
+ display: flex;
15
+ flex-direction: column;
16
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
17
+ overflow: hidden
18
+ }
19
+
20
+ .top-bar {
21
+ height: var(--header-height);
22
+ background: #fff;
23
+ border-bottom: 1px solid var(--border-color);
24
+ display: flex;
25
+ align-items: center;
26
+ justify-content: space-between;
27
+ padding: 0 1rem;
28
+ flex-shrink: 0
29
+ }
30
+
31
+ .logo {
32
+ font-weight: 700;
33
+ font-size: 1.1rem;
34
+ display: flex;
35
+ align-items: center;
36
+ gap: 12px
37
+ }
38
+
39
+ .logo span {
40
+ background: var(--primary-color);
41
+ color: #fff;
42
+ padding: 2px 6px;
43
+ border-radius: 4px;
44
+ font-size: .75rem;
45
+ text-transform: uppercase
46
+ }
47
+
48
+ .back-link {
49
+ display: flex;
50
+ width: 24px;
51
+ height: 24px;
52
+ background-color: #f0f0f0;
53
+ border-radius: 100%;
54
+ align-items: center;
55
+ justify-content: center;
56
+ color: #666;
57
+ transition: color 0.2s, transform .2s;
58
+ text-decoration: none;
59
+ padding: 4px;
60
+ }
61
+
62
+ .back-link:hover {
63
+ color: var(--primary-color);
64
+ background: #f0f0f0;
65
+ transform: translateX(-2px)
66
+ }
67
+
68
+ .logo .docmd-logo {
69
+ color: inherit;
70
+ text-decoration: none;
71
+ transition: .5s;
72
+ }
73
+
74
+ .logo .docmd-logo:hover {
75
+ color: var(--primary-color);
76
+ }
77
+
78
+ .view-switcher {
79
+ display: flex;
80
+ background: #f0f0f0;
81
+ padding: 3px;
82
+ border-radius: 8px;
83
+ gap: 0;
84
+ }
85
+
86
+ .view-btn {
87
+ border: none;
88
+ background: transparent;
89
+ padding: 6px 12px;
90
+ border-radius: 6px;
91
+ cursor: pointer;
92
+ font-size: 0.85rem;
93
+ color: #666;
94
+ font-weight: 500;
95
+ transition: background 0.15s, color 0.15s;
96
+ }
97
+
98
+ .view-btn:hover {
99
+ color: #333;
100
+ }
101
+
102
+ .view-btn.active {
103
+ background: #fff;
104
+ color: #000;
105
+ box-shadow: 0 1px 3px #0000001a;
106
+ font-weight: 600;
107
+ }
108
+
109
+ .workspace {
110
+ flex: 1;
111
+ display: flex;
112
+ position: relative;
113
+ overflow: hidden
114
+ }
115
+
116
+ .pane {
117
+ height: 100%;
118
+ display: flex;
119
+ flex-direction: column;
120
+ min-width: 300px;
121
+ background: #fff
122
+ }
123
+
124
+ .pane-header {
125
+ padding: 8px 16px;
126
+ font-size: .75rem;
127
+ font-weight: 600;
128
+ text-transform: uppercase;
129
+ color: #888;
130
+ background: var(--bg-color);
131
+ border-bottom: 1px solid var(--border-color);
132
+ flex-shrink: 0
133
+ }
134
+
135
+ .editor-pane {
136
+ width: 50%
137
+ }
138
+
139
+ textarea#input {
140
+ flex: 1;
141
+ border: none;
142
+ resize: none;
143
+ padding: 20px;
144
+ font-family: 'JetBrains Mono', 'Fira Code', Consolas, monospace;
145
+ font-size: 14px;
146
+ line-height: 1.6;
147
+ outline: none;
148
+ background: var(--bg-color)
149
+ }
150
+
151
+ .preview-pane {
152
+ flex: 1;
153
+ background: #fff
154
+ }
155
+
156
+ iframe#preview {
157
+ width: 100%;
158
+ height: 100%;
159
+ border: none;
160
+ display: block
161
+ }
162
+
163
+ .resizer {
164
+ width: var(--resizer-width);
165
+ background: var(--bg-color);
166
+ border-left: 1px solid var(--border-color);
167
+ border-right: 1px solid var(--border-color);
168
+ cursor: col-resize;
169
+ display: flex;
170
+ align-items: center;
171
+ justify-content: center;
172
+ transition: background .2s;
173
+ z-index: 10
174
+ }
175
+
176
+ .resizer:hover,
177
+ .resizer.resizing {
178
+ background: #e0e0e0
179
+ }
180
+
181
+ .resizer::after {
182
+ content: "||";
183
+ color: #aaa;
184
+ font-size: 10px;
185
+ letter-spacing: 1px
186
+ }
187
+
188
+ body.mode-single .resizer {
189
+ display: none
190
+ }
191
+
192
+ body.mode-single .pane {
193
+ width: 100% !important;
194
+ min-width: 0
195
+ }
196
+
197
+ body.mode-single .editor-pane {
198
+ display: none
199
+ }
200
+
201
+ body.mode-single .preview-pane {
202
+ display: none
203
+ }
204
+
205
+ body.mode-single.show-editor .editor-pane {
206
+ display: flex
207
+ }
208
+
209
+ body.mode-single.show-preview .preview-pane {
210
+ display: flex
211
+ }
212
+
213
+ @media (max-width: 768px) {
214
+ .desktop-only {
215
+ display: none !important
216
+ }
217
+
218
+ .resizer {
219
+ display: none !important
220
+ }
221
+
222
+ .pane {
223
+ width: 100% !important
224
+ }
225
+
226
+ .editor-pane {
227
+ display: none
228
+ }
229
+
230
+ .preview-pane {
231
+ display: none
232
+ }
233
+
234
+ body.mobile-tab-editor .editor-pane {
235
+ display: flex
236
+ }
237
+
238
+ body.mobile-tab-preview .preview-pane {
239
+ display: flex
240
+ }
241
+
242
+ .mobile-tabs {
243
+ display: flex !important;
244
+ position: fixed;
245
+ bottom: 0;
246
+ left: 0;
247
+ right: 0;
248
+ height: 50px;
249
+ background: #fff;
250
+ border-top: 1px solid var(--border-color);
251
+ z-index: 100
252
+ }
253
+
254
+ .mobile-tab-btn {
255
+ flex: 1;
256
+ border: none;
257
+ background: transparent;
258
+ font-weight: 600;
259
+ color: #888;
260
+ cursor: pointer
261
+ }
262
+
263
+ .mobile-tab-btn.active {
264
+ color: var(--primary-color);
265
+ border-top: 2px solid var(--primary-color)
266
+ }
267
+
268
+ .workspace {
269
+ padding-bottom: 50px
270
+ }
271
+ }
272
+
273
+ .mobile-tabs {
274
+ display: none
275
+ }