vanduo-framework 1.1.8-docs-update

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 (196) hide show
  1. package/LICENSE +35 -0
  2. package/README.md +216 -0
  3. package/css/components/alerts.css +224 -0
  4. package/css/components/avatar.css +275 -0
  5. package/css/components/badges.css +230 -0
  6. package/css/components/breadcrumbs.css +146 -0
  7. package/css/components/button-group.css +82 -0
  8. package/css/components/buttons.css +530 -0
  9. package/css/components/cards.css +304 -0
  10. package/css/components/chips.css +259 -0
  11. package/css/components/code-snippet.css +555 -0
  12. package/css/components/collapsible.css +267 -0
  13. package/css/components/collections.css +253 -0
  14. package/css/components/doc-search.css +464 -0
  15. package/css/components/doc-tabs.css +38 -0
  16. package/css/components/draggable.css +317 -0
  17. package/css/components/dropdown.css +266 -0
  18. package/css/components/footer.css +375 -0
  19. package/css/components/forms.css +1774 -0
  20. package/css/components/image-box.css +279 -0
  21. package/css/components/modals.css +285 -0
  22. package/css/components/navbar.css +530 -0
  23. package/css/components/pagination.css +186 -0
  24. package/css/components/preloader.css +340 -0
  25. package/css/components/progress.css +107 -0
  26. package/css/components/sidenav.css +301 -0
  27. package/css/components/skeleton.css +241 -0
  28. package/css/components/spinner.css +144 -0
  29. package/css/components/tabs.css +327 -0
  30. package/css/components/theme-customizer.css +835 -0
  31. package/css/components/toast.css +357 -0
  32. package/css/components/tooltips.css +270 -0
  33. package/css/core/colors.css +1017 -0
  34. package/css/core/fonts.css +266 -0
  35. package/css/core/grid.css +1699 -0
  36. package/css/core/helpers.css +2202 -0
  37. package/css/core/reset.css +128 -0
  38. package/css/core/tokens.css +213 -0
  39. package/css/core/typography.css +405 -0
  40. package/css/core/vd-aliases.css +47 -0
  41. package/css/effects/parallax.css +113 -0
  42. package/css/icons/icons-all.css +23 -0
  43. package/css/icons/icons.css +25 -0
  44. package/css/utilities/media.css +167 -0
  45. package/css/utilities/print.css +111 -0
  46. package/css/utilities/shadow.css +243 -0
  47. package/css/utilities/table.css +381 -0
  48. package/css/utilities/transforms.css +71 -0
  49. package/css/utilities/transitions.css +87 -0
  50. package/css/vanduo.css +80 -0
  51. package/dist/build-info.json +6 -0
  52. package/dist/fonts/fira-sans/fira-sans-bold.woff2 +0 -0
  53. package/dist/fonts/fira-sans/fira-sans-medium.woff2 +0 -0
  54. package/dist/fonts/fira-sans/fira-sans-regular.woff2 +0 -0
  55. package/dist/fonts/ibm-plex/ibm-plex-sans-bold.woff2 +0 -0
  56. package/dist/fonts/ibm-plex/ibm-plex-sans-medium.woff2 +0 -0
  57. package/dist/fonts/ibm-plex/ibm-plex-sans-regular.woff2 +0 -0
  58. package/dist/fonts/inter/inter-bold.woff2 +0 -0
  59. package/dist/fonts/inter/inter-medium.woff2 +0 -0
  60. package/dist/fonts/inter/inter-regular.woff2 +0 -0
  61. package/dist/fonts/inter/inter-semibold.woff2 +0 -0
  62. package/dist/fonts/jetbrains-mono/jetbrains-mono-bold.woff2 +0 -0
  63. package/dist/fonts/jetbrains-mono/jetbrains-mono-regular.woff2 +0 -0
  64. package/dist/fonts/open-sans/open-sans-bold.woff2 +0 -0
  65. package/dist/fonts/open-sans/open-sans-medium.woff2 +0 -0
  66. package/dist/fonts/open-sans/open-sans-regular.woff2 +0 -0
  67. package/dist/fonts/rubik/rubik-bold.woff2 +0 -0
  68. package/dist/fonts/rubik/rubik-medium.woff2 +0 -0
  69. package/dist/fonts/rubik/rubik-regular.woff2 +0 -0
  70. package/dist/fonts/source-sans/source-sans-bold.woff2 +0 -0
  71. package/dist/fonts/source-sans/source-sans-regular.woff2 +0 -0
  72. package/dist/fonts/source-sans/source-sans-semibold.woff2 +0 -0
  73. package/dist/fonts/titillium-web/titillium-web-bold.woff2 +0 -0
  74. package/dist/fonts/titillium-web/titillium-web-regular.woff2 +0 -0
  75. package/dist/fonts/titillium-web/titillium-web-semibold.woff2 +0 -0
  76. package/dist/fonts/ubuntu/ubuntu-bold.woff2 +0 -0
  77. package/dist/fonts/ubuntu/ubuntu-medium.woff2 +0 -0
  78. package/dist/fonts/ubuntu/ubuntu-regular.woff2 +0 -0
  79. package/dist/icons/phosphor/LICENSE +21 -0
  80. package/dist/icons/phosphor/bold/Phosphor-Bold.ttf +0 -0
  81. package/dist/icons/phosphor/bold/Phosphor-Bold.woff +0 -0
  82. package/dist/icons/phosphor/bold/Phosphor-Bold.woff2 +0 -0
  83. package/dist/icons/phosphor/bold/style.css +4627 -0
  84. package/dist/icons/phosphor/duotone/Phosphor-Duotone.ttf +0 -0
  85. package/dist/icons/phosphor/duotone/Phosphor-Duotone.woff +0 -0
  86. package/dist/icons/phosphor/duotone/Phosphor-Duotone.woff2 +0 -0
  87. package/dist/icons/phosphor/duotone/style.css +12115 -0
  88. package/dist/icons/phosphor/fill/Phosphor-Fill.ttf +0 -0
  89. package/dist/icons/phosphor/fill/Phosphor-Fill.woff +0 -0
  90. package/dist/icons/phosphor/fill/Phosphor-Fill.woff2 +0 -0
  91. package/dist/icons/phosphor/fill/style.css +4627 -0
  92. package/dist/icons/phosphor/light/Phosphor-Light.ttf +0 -0
  93. package/dist/icons/phosphor/light/Phosphor-Light.woff +0 -0
  94. package/dist/icons/phosphor/light/Phosphor-Light.woff2 +0 -0
  95. package/dist/icons/phosphor/light/style.css +4627 -0
  96. package/dist/icons/phosphor/regular/Phosphor.ttf +0 -0
  97. package/dist/icons/phosphor/regular/Phosphor.woff +0 -0
  98. package/dist/icons/phosphor/regular/Phosphor.woff2 +0 -0
  99. package/dist/icons/phosphor/regular/style.css +4627 -0
  100. package/dist/icons/phosphor/thin/Phosphor-Thin.ttf +0 -0
  101. package/dist/icons/phosphor/thin/Phosphor-Thin.woff +0 -0
  102. package/dist/icons/phosphor/thin/Phosphor-Thin.woff2 +0 -0
  103. package/dist/icons/phosphor/thin/style.css +4627 -0
  104. package/dist/vanduo.cjs.js +5569 -0
  105. package/dist/vanduo.cjs.js.map +7 -0
  106. package/dist/vanduo.cjs.min.js +48 -0
  107. package/dist/vanduo.cjs.min.js.map +7 -0
  108. package/dist/vanduo.css +60666 -0
  109. package/dist/vanduo.css.map +1 -0
  110. package/dist/vanduo.esm.js +5548 -0
  111. package/dist/vanduo.esm.js.map +7 -0
  112. package/dist/vanduo.esm.min.js +48 -0
  113. package/dist/vanduo.esm.min.js.map +7 -0
  114. package/dist/vanduo.js +5545 -0
  115. package/dist/vanduo.js.map +7 -0
  116. package/dist/vanduo.min.css +2 -0
  117. package/dist/vanduo.min.css.map +1 -0
  118. package/dist/vanduo.min.js +48 -0
  119. package/dist/vanduo.min.js.map +7 -0
  120. package/fonts/fira-sans/fira-sans-bold.woff2 +0 -0
  121. package/fonts/fira-sans/fira-sans-medium.woff2 +0 -0
  122. package/fonts/fira-sans/fira-sans-regular.woff2 +0 -0
  123. package/fonts/ibm-plex/ibm-plex-sans-bold.woff2 +0 -0
  124. package/fonts/ibm-plex/ibm-plex-sans-medium.woff2 +0 -0
  125. package/fonts/ibm-plex/ibm-plex-sans-regular.woff2 +0 -0
  126. package/fonts/inter/inter-bold.woff2 +0 -0
  127. package/fonts/inter/inter-medium.woff2 +0 -0
  128. package/fonts/inter/inter-regular.woff2 +0 -0
  129. package/fonts/inter/inter-semibold.woff2 +0 -0
  130. package/fonts/jetbrains-mono/jetbrains-mono-bold.woff2 +0 -0
  131. package/fonts/jetbrains-mono/jetbrains-mono-regular.woff2 +0 -0
  132. package/fonts/open-sans/open-sans-bold.woff2 +0 -0
  133. package/fonts/open-sans/open-sans-medium.woff2 +0 -0
  134. package/fonts/open-sans/open-sans-regular.woff2 +0 -0
  135. package/fonts/rubik/rubik-bold.woff2 +0 -0
  136. package/fonts/rubik/rubik-medium.woff2 +0 -0
  137. package/fonts/rubik/rubik-regular.woff2 +0 -0
  138. package/fonts/source-sans/source-sans-bold.woff2 +0 -0
  139. package/fonts/source-sans/source-sans-regular.woff2 +0 -0
  140. package/fonts/source-sans/source-sans-semibold.woff2 +0 -0
  141. package/fonts/titillium-web/titillium-web-bold.woff2 +0 -0
  142. package/fonts/titillium-web/titillium-web-regular.woff2 +0 -0
  143. package/fonts/titillium-web/titillium-web-semibold.woff2 +0 -0
  144. package/fonts/ubuntu/ubuntu-bold.woff2 +0 -0
  145. package/fonts/ubuntu/ubuntu-medium.woff2 +0 -0
  146. package/fonts/ubuntu/ubuntu-regular.woff2 +0 -0
  147. package/icons/phosphor/LICENSE +21 -0
  148. package/icons/phosphor/bold/Phosphor-Bold.ttf +0 -0
  149. package/icons/phosphor/bold/Phosphor-Bold.woff +0 -0
  150. package/icons/phosphor/bold/Phosphor-Bold.woff2 +0 -0
  151. package/icons/phosphor/bold/style.css +4627 -0
  152. package/icons/phosphor/duotone/Phosphor-Duotone.ttf +0 -0
  153. package/icons/phosphor/duotone/Phosphor-Duotone.woff +0 -0
  154. package/icons/phosphor/duotone/Phosphor-Duotone.woff2 +0 -0
  155. package/icons/phosphor/duotone/style.css +12115 -0
  156. package/icons/phosphor/fill/Phosphor-Fill.ttf +0 -0
  157. package/icons/phosphor/fill/Phosphor-Fill.woff +0 -0
  158. package/icons/phosphor/fill/Phosphor-Fill.woff2 +0 -0
  159. package/icons/phosphor/fill/style.css +4627 -0
  160. package/icons/phosphor/light/Phosphor-Light.ttf +0 -0
  161. package/icons/phosphor/light/Phosphor-Light.woff +0 -0
  162. package/icons/phosphor/light/Phosphor-Light.woff2 +0 -0
  163. package/icons/phosphor/light/style.css +4627 -0
  164. package/icons/phosphor/regular/Phosphor.ttf +0 -0
  165. package/icons/phosphor/regular/Phosphor.woff +0 -0
  166. package/icons/phosphor/regular/Phosphor.woff2 +0 -0
  167. package/icons/phosphor/regular/style.css +4627 -0
  168. package/icons/phosphor/thin/Phosphor-Thin.ttf +0 -0
  169. package/icons/phosphor/thin/Phosphor-Thin.woff +0 -0
  170. package/icons/phosphor/thin/Phosphor-Thin.woff2 +0 -0
  171. package/icons/phosphor/thin/style.css +4627 -0
  172. package/js/components/code-snippet.js +639 -0
  173. package/js/components/collapsible.js +226 -0
  174. package/js/components/doc-search.js +936 -0
  175. package/js/components/draggable.js +725 -0
  176. package/js/components/dropdown.js +362 -0
  177. package/js/components/font-switcher.js +253 -0
  178. package/js/components/grid.js +279 -0
  179. package/js/components/image-box.js +372 -0
  180. package/js/components/modals.js +367 -0
  181. package/js/components/navbar.js +264 -0
  182. package/js/components/pagination.js +286 -0
  183. package/js/components/parallax.js +216 -0
  184. package/js/components/preloader.js +183 -0
  185. package/js/components/select.js +444 -0
  186. package/js/components/sidenav.js +303 -0
  187. package/js/components/tabs.js +303 -0
  188. package/js/components/theme-customizer.js +784 -0
  189. package/js/components/theme-switcher.js +183 -0
  190. package/js/components/toast.js +343 -0
  191. package/js/components/tooltips.js +306 -0
  192. package/js/index.js +52 -0
  193. package/js/utils/helpers.js +306 -0
  194. package/js/utils/lifecycle.js +135 -0
  195. package/js/vanduo.js +120 -0
  196. package/package.json +78 -0
@@ -0,0 +1,639 @@
1
+ /**
2
+ * Vanduo Framework - Code Snippet Component
3
+ * Copyable code blocks with tabs, syntax highlighting, and HTML extraction
4
+ */
5
+
6
+ (function () {
7
+ 'use strict';
8
+
9
+ /**
10
+ * Code Snippet Component
11
+ */
12
+ const CodeSnippet = {
13
+ _snippetIdCounter: 0,
14
+
15
+ getSnippetInstanceId: function (snippet) {
16
+ if (snippet.dataset.codeSnippetId) {
17
+ return snippet.dataset.codeSnippetId;
18
+ }
19
+
20
+ const baseId = (snippet.id || '').trim();
21
+ if (baseId) {
22
+ snippet.dataset.codeSnippetId = `snippet-${baseId}`;
23
+ return snippet.dataset.codeSnippetId;
24
+ }
25
+
26
+ this._snippetIdCounter += 1;
27
+ snippet.dataset.codeSnippetId = `snippet-auto-${this._snippetIdCounter}`;
28
+ return snippet.dataset.codeSnippetId;
29
+ },
30
+
31
+ addListener: function (snippet, target, event, handler) {
32
+ if (!target) return;
33
+ target.addEventListener(event, handler);
34
+ if (!snippet._codeSnippetCleanup) {
35
+ snippet._codeSnippetCleanup = [];
36
+ }
37
+ snippet._codeSnippetCleanup.push(() => target.removeEventListener(event, handler));
38
+ },
39
+
40
+ /**
41
+ * Initialize all code snippet components
42
+ */
43
+ init: function () {
44
+ const snippets = document.querySelectorAll('.vd-code-snippet');
45
+
46
+ snippets.forEach(snippet => {
47
+ if (!snippet.dataset.initialized) {
48
+ this.initSnippet(snippet);
49
+ }
50
+ });
51
+ },
52
+
53
+ /**
54
+ * Initialize a single code snippet
55
+ * @param {HTMLElement} snippet - Code snippet container element
56
+ */
57
+ initSnippet: function (snippet) {
58
+ snippet.dataset.initialized = 'true';
59
+ snippet._codeSnippetCleanup = [];
60
+
61
+ // Handle collapsible toggle
62
+ const toggle = snippet.querySelector('.vd-code-snippet-toggle');
63
+ const content = snippet.querySelector('.vd-code-snippet-content');
64
+
65
+ if (toggle && content) {
66
+ this.initCollapsible(snippet, toggle, content);
67
+ }
68
+
69
+ // Handle tabs
70
+ const tabs = snippet.querySelectorAll('.vd-code-snippet-tab');
71
+ const panes = snippet.querySelectorAll('.vd-code-snippet-pane');
72
+
73
+ if (tabs.length > 0) {
74
+ this.initTabs(snippet, tabs, panes);
75
+ }
76
+
77
+ // Handle copy button
78
+ const copyBtn = snippet.querySelector('.vd-code-snippet-copy');
79
+ if (copyBtn) {
80
+ this.initCopyButton(snippet, copyBtn);
81
+ }
82
+
83
+ // Handle HTML extraction
84
+ const extractPanes = snippet.querySelectorAll('[data-extract]');
85
+ extractPanes.forEach(pane => {
86
+ this.extractHtml(pane);
87
+ });
88
+
89
+ // Handle line numbers
90
+ const lineNumberPanes = snippet.querySelectorAll('.has-line-numbers');
91
+ lineNumberPanes.forEach(pane => {
92
+ this.addLineNumbers(pane);
93
+ });
94
+ },
95
+
96
+ /**
97
+ * Initialize collapsible functionality
98
+ * @param {HTMLElement} snippet - Code snippet container
99
+ * @param {HTMLElement} toggle - Toggle button
100
+ * @param {HTMLElement} content - Collapsible content
101
+ */
102
+ initCollapsible: function (snippet, toggle, content) {
103
+ // Set initial state
104
+ const isExpanded = snippet.dataset.expanded === 'true';
105
+ toggle.setAttribute('aria-expanded', isExpanded);
106
+ content.dataset.visible = isExpanded;
107
+
108
+ this.addListener(snippet, toggle, 'click', () => {
109
+ const expanded = snippet.dataset.expanded === 'true';
110
+ snippet.dataset.expanded = !expanded;
111
+ toggle.setAttribute('aria-expanded', !expanded);
112
+ content.dataset.visible = !expanded;
113
+
114
+ // Extract HTML on first expand if needed
115
+ if (!expanded) {
116
+ const extractPanes = content.querySelectorAll('[data-extract]:not([data-extracted])');
117
+ extractPanes.forEach(pane => {
118
+ this.extractHtml(pane);
119
+ });
120
+ }
121
+
122
+ // Dispatch event
123
+ const event = new CustomEvent('codesnippet:toggle', {
124
+ bubbles: true,
125
+ detail: { snippet, expanded: !expanded }
126
+ });
127
+ snippet.dispatchEvent(event);
128
+ });
129
+ },
130
+
131
+ /**
132
+ * Initialize tab functionality
133
+ * @param {HTMLElement} snippet - Code snippet container
134
+ * @param {NodeList} tabs - Tab buttons
135
+ * @param {NodeList} panes - Code panes
136
+ */
137
+ initTabs: function (snippet, tabs, panes) {
138
+ const snippetId = this.getSnippetInstanceId(snippet);
139
+
140
+ // Set up ARIA attributes
141
+ const tabList = snippet.querySelector('.vd-code-snippet-tabs');
142
+ if (tabList) {
143
+ tabList.setAttribute('role', 'tablist');
144
+ }
145
+
146
+ tabs.forEach((tab, index) => {
147
+ const lang = tab.dataset.lang;
148
+ const isActive = tab.classList.contains('is-active');
149
+
150
+ // Set ARIA attributes
151
+ tab.setAttribute('role', 'tab');
152
+ tab.setAttribute('aria-selected', isActive);
153
+ tab.setAttribute('tabindex', isActive ? '0' : '-1');
154
+ tab.id = tab.id || `code-tab-${snippetId}-${lang || 'tab'}-${index}`;
155
+
156
+ // Find corresponding pane
157
+ const pane = snippet.querySelector(`.vd-code-snippet-pane[data-lang="${lang}"]`);
158
+ if (pane) {
159
+ pane.id = pane.id || `code-pane-${snippetId}-${lang || 'pane'}-${index}`;
160
+ pane.setAttribute('role', 'tabpanel');
161
+ tab.setAttribute('aria-controls', pane.id);
162
+ pane.setAttribute('aria-labelledby', tab.id);
163
+ }
164
+
165
+ // Click handler
166
+ this.addListener(snippet, tab, 'click', () => {
167
+ this.switchTab(snippet, tab, tabs, panes);
168
+ });
169
+
170
+ // Keyboard navigation
171
+ this.addListener(snippet, tab, 'keydown', (e) => {
172
+ this.handleTabKeydown(e, snippet, tabs, panes);
173
+ });
174
+ });
175
+ },
176
+
177
+ /**
178
+ * Switch to a specific tab
179
+ * @param {HTMLElement} snippet - Code snippet container
180
+ * @param {HTMLElement} activeTab - Tab to activate
181
+ * @param {NodeList} tabs - All tab buttons
182
+ * @param {NodeList} panes - All code panes
183
+ */
184
+ switchTab: function (snippet, activeTab, tabs, panes) {
185
+ const lang = activeTab.dataset.lang;
186
+
187
+ // Deactivate all tabs
188
+ tabs.forEach(tab => {
189
+ tab.classList.remove('is-active');
190
+ tab.setAttribute('aria-selected', 'false');
191
+ tab.setAttribute('tabindex', '-1');
192
+ });
193
+
194
+ // Hide all panes
195
+ panes.forEach(pane => {
196
+ pane.classList.remove('is-active');
197
+ });
198
+
199
+ // Activate selected tab
200
+ activeTab.classList.add('is-active');
201
+ activeTab.setAttribute('aria-selected', 'true');
202
+ activeTab.setAttribute('tabindex', '0');
203
+
204
+ // Show corresponding pane
205
+ const activePane = snippet.querySelector(`.vd-code-snippet-pane[data-lang="${lang}"]`);
206
+ if (activePane) {
207
+ activePane.classList.add('is-active');
208
+ }
209
+
210
+ // Dispatch event
211
+ const event = new CustomEvent('codesnippet:tabchange', {
212
+ bubbles: true,
213
+ detail: { snippet, tab: activeTab, lang }
214
+ });
215
+ snippet.dispatchEvent(event);
216
+ },
217
+
218
+ /**
219
+ * Handle keyboard navigation for tabs
220
+ * @param {KeyboardEvent} e - Keyboard event
221
+ * @param {HTMLElement} snippet - Code snippet container
222
+ * @param {NodeList} tabs - All tab buttons
223
+ * @param {NodeList} panes - All code panes
224
+ */
225
+ handleTabKeydown: function (e, snippet, tabs, panes) {
226
+ const tabArray = Array.from(tabs);
227
+ const currentIndex = tabArray.indexOf(e.target);
228
+ let newIndex;
229
+
230
+ switch (e.key) {
231
+ case 'ArrowLeft':
232
+ e.preventDefault();
233
+ newIndex = currentIndex > 0 ? currentIndex - 1 : tabArray.length - 1;
234
+ break;
235
+ case 'ArrowRight':
236
+ e.preventDefault();
237
+ newIndex = currentIndex < tabArray.length - 1 ? currentIndex + 1 : 0;
238
+ break;
239
+ case 'Home':
240
+ e.preventDefault();
241
+ newIndex = 0;
242
+ break;
243
+ case 'End':
244
+ e.preventDefault();
245
+ newIndex = tabArray.length - 1;
246
+ break;
247
+ default:
248
+ return;
249
+ }
250
+
251
+ if (newIndex !== currentIndex) {
252
+ tabArray[newIndex].focus();
253
+ this.switchTab(snippet, tabArray[newIndex], tabs, panes);
254
+ }
255
+ },
256
+
257
+ /**
258
+ * Initialize copy button
259
+ * @param {HTMLElement} snippet - Code snippet container
260
+ * @param {HTMLElement} copyBtn - Copy button element
261
+ */
262
+ initCopyButton: function (snippet, copyBtn) {
263
+ this.addListener(snippet, copyBtn, 'click', async () => {
264
+ await this.copyCode(snippet, copyBtn);
265
+ });
266
+ },
267
+
268
+ /**
269
+ * Copy code to clipboard
270
+ * @param {HTMLElement} snippet - Code snippet container
271
+ * @param {HTMLElement} copyBtn - Copy button element
272
+ */
273
+ copyCode: async function (snippet, copyBtn) {
274
+ const activePane = snippet.querySelector('.vd-code-snippet-pane.is-active') ||
275
+ snippet.querySelector('.vd-code-snippet-pane');
276
+
277
+ if (!activePane) {
278
+ console.warn('CodeSnippet: No code pane found');
279
+ return;
280
+ }
281
+
282
+ const codeElement = activePane.querySelector('code') || activePane;
283
+ const code = codeElement.textContent;
284
+
285
+ try {
286
+ await navigator.clipboard.writeText(code);
287
+ this.showCopyFeedback(copyBtn, true);
288
+ } catch (_err) {
289
+ // Fallback for older browsers
290
+ const success = this.fallbackCopy(code);
291
+ this.showCopyFeedback(copyBtn, success);
292
+ }
293
+
294
+ // Dispatch event
295
+ const event = new CustomEvent('codesnippet:copy', {
296
+ bubbles: true,
297
+ detail: { snippet, code, success: true }
298
+ });
299
+ snippet.dispatchEvent(event);
300
+ },
301
+
302
+ /**
303
+ * Fallback copy method for older browsers
304
+ * @param {string} text - Text to copy
305
+ * @returns {boolean} Success status
306
+ */
307
+ fallbackCopy: function (text) {
308
+ const textarea = document.createElement('textarea');
309
+ textarea.value = text;
310
+ textarea.style.position = 'fixed';
311
+ textarea.style.left = '-9999px';
312
+ textarea.style.top = '-9999px';
313
+ document.body.appendChild(textarea);
314
+ textarea.focus();
315
+ textarea.select();
316
+
317
+ let success = false;
318
+ try {
319
+ success = document.execCommand('copy');
320
+ } catch (err) {
321
+ console.warn('CodeSnippet: Fallback copy failed', err);
322
+ }
323
+
324
+ document.body.removeChild(textarea);
325
+ return success;
326
+ },
327
+
328
+ /**
329
+ * Show copy feedback
330
+ * @param {HTMLElement} copyBtn - Copy button element
331
+ * @param {boolean} success - Whether copy was successful
332
+ */
333
+ showCopyFeedback: function (copyBtn, success) {
334
+ if (success) {
335
+ copyBtn.classList.add('is-copied');
336
+
337
+ // Announce to screen readers
338
+ const announcement = document.createElement('span');
339
+ announcement.setAttribute('role', 'status');
340
+ announcement.setAttribute('aria-live', 'polite');
341
+ announcement.className = 'sr-only';
342
+ announcement.textContent = 'Code copied to clipboard';
343
+ copyBtn.appendChild(announcement);
344
+
345
+ setTimeout(() => {
346
+ copyBtn.classList.remove('is-copied');
347
+ if (announcement.parentNode) {
348
+ announcement.parentNode.removeChild(announcement);
349
+ }
350
+ }, 2000);
351
+ }
352
+ },
353
+
354
+ /**
355
+ * Extract HTML from a demo element
356
+ * @param {HTMLElement} pane - Code pane with data-extract attribute
357
+ */
358
+ extractHtml: function (pane) {
359
+ const selector = pane.dataset.extract;
360
+ if (!selector) return;
361
+
362
+ const source = document.querySelector(selector);
363
+ if (!source) {
364
+ console.warn(`CodeSnippet: Source element not found: ${selector}`);
365
+ return;
366
+ }
367
+
368
+ // Get inner HTML
369
+ let html = source.innerHTML;
370
+
371
+ // Format the HTML
372
+ html = this.formatHtml(html);
373
+
374
+ // Escape for display
375
+ html = this.escapeHtml(html);
376
+
377
+ // Apply syntax highlighting
378
+ html = this.highlightHtml(html);
379
+
380
+ // Set content
381
+ pane.innerHTML = '<code>' + html + '</code>';
382
+ pane.dataset.extracted = 'true';
383
+ },
384
+
385
+ /**
386
+ * Format HTML with proper indentation
387
+ * @param {string} html - Raw HTML string
388
+ * @returns {string} Formatted HTML
389
+ */
390
+ formatHtml: function (html) {
391
+ // Remove leading/trailing whitespace
392
+ html = html.trim();
393
+
394
+ // Simple formatting: normalize whitespace
395
+ // Split by tags, then rejoin with proper indentation
396
+ const lines = html.split('\n');
397
+ let indent = 0;
398
+ const indentSize = 2;
399
+ const formattedLines = [];
400
+
401
+ lines.forEach(line => {
402
+ line = line.trim();
403
+ if (!line) return;
404
+
405
+ // Check for closing tags at start
406
+ if (line.match(/^<\/\w/)) {
407
+ indent = Math.max(0, indent - indentSize);
408
+ }
409
+
410
+ formattedLines.push(' '.repeat(indent) + line);
411
+
412
+ // Check for opening tags (not self-closing)
413
+ // Use short fixed-length regex + indexOf to prevent ReDoS
414
+ const hasOpenTag = /<[a-zA-Z]/.test(line);
415
+ const isSelfClosing = line.includes('/>');
416
+ if (hasOpenTag && !isSelfClosing) {
417
+ // Don't indent for void elements
418
+ if (!line.match(/<(br|hr|img|input|meta|link|area|base|col|embed|param|source|track|wbr)/i)) {
419
+ // Only indent if not also closing on same line
420
+ if (!line.match(/<\/\w+>$/)) {
421
+ indent += indentSize;
422
+ }
423
+ }
424
+ }
425
+ });
426
+
427
+ return formattedLines.join('\n');
428
+ },
429
+
430
+ /**
431
+ * Escape HTML entities for display
432
+ * @param {string} html - HTML string
433
+ * @returns {string} Escaped HTML
434
+ */
435
+ escapeHtml: function (html) {
436
+ const div = document.createElement('div');
437
+ div.textContent = html;
438
+ return div.innerHTML;
439
+ },
440
+
441
+ /**
442
+ * Apply syntax highlighting to HTML
443
+ * @param {string} html - Escaped HTML string
444
+ * @returns {string} HTML with syntax highlighting spans
445
+ */
446
+ highlightHtml: function (html) {
447
+ // Highlight HTML tags
448
+ html = html.replace(/(&lt;\/?)([\w-]+)/g, '$1<span class="code-tag">$2</span>');
449
+
450
+ // Highlight attributes
451
+ html = html.replace(/([\w-]+)(=)(&quot;|&#39;)/g, '<span class="code-attr">$1</span>$2$3');
452
+
453
+ // Highlight attribute values (strings)
454
+ html = html.replace(/(&quot;|&#39;)([^&]*)(&quot;|&#39;)/g, '$1<span class="code-string">$2</span>$3');
455
+
456
+ // Highlight comments
457
+ html = html.replace(/(&lt;!--)(.*?)(--&gt;)/g, '<span class="code-comment">$1$2$3</span>');
458
+
459
+ return html;
460
+ },
461
+
462
+ /**
463
+ * Apply syntax highlighting to CSS
464
+ * @param {string} css - CSS string
465
+ * @returns {string} CSS with syntax highlighting spans
466
+ */
467
+ highlightCss: function (css) {
468
+ // Highlight selectors — use non-backtracking bounded pattern
469
+ css = css.replace(/([.#]?[a-zA-Z][a-zA-Z0-9_-]{0,200})(\s*\{)/g, '<span class="code-selector">$1</span>$2');
470
+
471
+ // Highlight properties — use non-backtracking bounded pattern
472
+ css = css.replace(/([a-zA-Z][a-zA-Z0-9_-]{0,200})(\s*:)/g, '<span class="code-property">$1</span>$2');
473
+
474
+ // Highlight values
475
+ css = css.replace(/:\s*([^;{}]+)(;)/g, ': <span class="code-value">$1</span>$2');
476
+
477
+ // Highlight units
478
+ css = css.replace(/(\d+)(px|rem|em|%|vh|vw|deg|s|ms)/g, '<span class="code-number">$1</span><span class="code-unit">$2</span>');
479
+
480
+ // Highlight comments
481
+ css = css.replace(/(\/\*[\s\S]*?\*\/)/g, '<span class="code-comment">$1</span>');
482
+
483
+ return css;
484
+ },
485
+
486
+ /**
487
+ * Apply syntax highlighting to JavaScript
488
+ * @param {string} js - JavaScript string
489
+ * @returns {string} JS with syntax highlighting spans
490
+ */
491
+ highlightJs: function (js) {
492
+ // Highlight keywords
493
+ const keywords = ['const', 'let', 'var', 'function', 'return', 'if', 'else', 'for', 'while', 'switch', 'case', 'break', 'continue', 'new', 'this', 'class', 'extends', 'import', 'export', 'default', 'async', 'await', 'try', 'catch', 'throw', 'typeof', 'instanceof'];
494
+ keywords.forEach(kw => {
495
+ const regex = new RegExp(`\\b(${kw})\\b`, 'g');
496
+ js = js.replace(regex, '<span class="code-keyword">$1</span>');
497
+ });
498
+
499
+ // Highlight strings (limit to 10 000 chars to prevent polynomial backtracking)
500
+ js = js.replace(/('(?:[^'\\]|\\.){0,10000}'|"(?:[^"\\]|\\.){0,10000}"|`(?:[^`\\]|\\.){0,10000}`)/g, '<span class="code-string">$1</span>');
501
+
502
+ // Highlight numbers
503
+ js = js.replace(/\b(\d+\.?\d*)\b/g, '<span class="code-number">$1</span>');
504
+
505
+ // Highlight function calls
506
+ js = js.replace(/\b([\w]+)(\s*\()/g, '<span class="code-function">$1</span>$2');
507
+
508
+ // Highlight comments
509
+ js = js.replace(/(\/\/.*$)/gm, '<span class="code-comment">$1</span>');
510
+ js = js.replace(/(\/\*[\s\S]*?\*\/)/g, '<span class="code-comment">$1</span>');
511
+
512
+ return js;
513
+ },
514
+
515
+ /**
516
+ * Add line numbers to a code pane
517
+ * @param {HTMLElement} pane - Code pane element
518
+ */
519
+ addLineNumbers: function (pane) {
520
+ const code = pane.querySelector('code');
521
+ if (!code) return;
522
+
523
+ const lines = code.innerHTML.split('\n');
524
+ const lineCount = lines.length;
525
+
526
+ // Create line numbers container
527
+ const lineNumbers = document.createElement('div');
528
+ lineNumbers.className = 'vd-code-snippet-line-numbers';
529
+ lineNumbers.setAttribute('aria-hidden', 'true');
530
+
531
+ for (let i = 1; i <= lineCount; i++) {
532
+ const lineNum = document.createElement('span');
533
+ lineNum.textContent = i;
534
+ lineNumbers.appendChild(lineNum);
535
+ }
536
+
537
+ // Wrap code content
538
+ const codeWrapper = document.createElement('div');
539
+ codeWrapper.className = 'vd-code-snippet-code';
540
+ codeWrapper.innerHTML = code.outerHTML;
541
+
542
+ // Replace code with new structure
543
+ code.parentNode.removeChild(code);
544
+ pane.appendChild(lineNumbers);
545
+ pane.appendChild(codeWrapper);
546
+ },
547
+
548
+ /**
549
+ * Programmatically expand a code snippet
550
+ * @param {string|HTMLElement} snippet - Snippet selector or element
551
+ */
552
+ expand: function (snippet) {
553
+ if (typeof snippet === 'string') {
554
+ snippet = document.querySelector(snippet);
555
+ }
556
+ if (!snippet) return;
557
+
558
+ snippet.dataset.expanded = 'true';
559
+ const toggle = snippet.querySelector('.vd-code-snippet-toggle');
560
+ const content = snippet.querySelector('.vd-code-snippet-content');
561
+
562
+ if (toggle) toggle.setAttribute('aria-expanded', 'true');
563
+ if (content) content.dataset.visible = 'true';
564
+ },
565
+
566
+ /**
567
+ * Programmatically collapse a code snippet
568
+ * @param {string|HTMLElement} snippet - Snippet selector or element
569
+ */
570
+ collapse: function (snippet) {
571
+ if (typeof snippet === 'string') {
572
+ snippet = document.querySelector(snippet);
573
+ }
574
+ if (!snippet) return;
575
+
576
+ snippet.dataset.expanded = 'false';
577
+ const toggle = snippet.querySelector('.vd-code-snippet-toggle');
578
+ const content = snippet.querySelector('.vd-code-snippet-content');
579
+
580
+ if (toggle) toggle.setAttribute('aria-expanded', 'false');
581
+ if (content) content.dataset.visible = 'false';
582
+ },
583
+
584
+ /**
585
+ * Programmatically switch to a specific language tab
586
+ * @param {string|HTMLElement} snippet - Snippet selector or element
587
+ * @param {string} lang - Language to switch to (html, css, js)
588
+ */
589
+ showLang: function (snippet, lang) {
590
+ if (typeof snippet === 'string') {
591
+ snippet = document.querySelector(snippet);
592
+ }
593
+ if (!snippet) return;
594
+
595
+ const tab = snippet.querySelector(`.vd-code-snippet-tab[data-lang="${lang}"]`);
596
+ const tabs = snippet.querySelectorAll('.vd-code-snippet-tab');
597
+ const panes = snippet.querySelectorAll('.vd-code-snippet-pane');
598
+
599
+ if (tab) {
600
+ this.switchTab(snippet, tab, tabs, panes);
601
+ }
602
+ },
603
+
604
+ /**
605
+ * Destroy a code snippet instance and clean up listeners
606
+ * @param {string|HTMLElement} snippet - Snippet selector or element
607
+ */
608
+ destroy: function (snippet) {
609
+ if (typeof snippet === 'string') {
610
+ snippet = document.querySelector(snippet);
611
+ }
612
+ if (!snippet) return;
613
+
614
+ if (snippet._codeSnippetCleanup) {
615
+ snippet._codeSnippetCleanup.forEach(fn => fn());
616
+ delete snippet._codeSnippetCleanup;
617
+ }
618
+
619
+ delete snippet.dataset.initialized;
620
+ },
621
+
622
+ /**
623
+ * Destroy all code snippet instances
624
+ */
625
+ destroyAll: function () {
626
+ const snippets = document.querySelectorAll('.vd-code-snippet[data-initialized="true"]');
627
+ snippets.forEach(snippet => this.destroy(snippet));
628
+ }
629
+ };
630
+
631
+ // Register with Vanduo framework if available
632
+ if (typeof window.Vanduo !== 'undefined') {
633
+ window.Vanduo.register('codeSnippet', CodeSnippet);
634
+ }
635
+
636
+ // Also expose globally for convenience
637
+ window.CodeSnippet = CodeSnippet;
638
+
639
+ })();