@pure-ds/storybook 0.1.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.
Files changed (129) hide show
  1. package/.storybook/addons/description/preview.js +15 -0
  2. package/.storybook/addons/description/register.js +60 -0
  3. package/.storybook/addons/html-preview/Panel.jsx +327 -0
  4. package/.storybook/addons/html-preview/constants.js +6 -0
  5. package/.storybook/addons/html-preview/preview.js +178 -0
  6. package/.storybook/addons/html-preview/register.js +16 -0
  7. package/.storybook/addons/pds-configurator/SearchTool.js +44 -0
  8. package/.storybook/addons/pds-configurator/Tool.js +30 -0
  9. package/.storybook/addons/pds-configurator/constants.js +9 -0
  10. package/.storybook/addons/pds-configurator/preview.js +159 -0
  11. package/.storybook/addons/pds-configurator/register.js +24 -0
  12. package/.storybook/docs.css +35 -0
  13. package/.storybook/htmlPreview.css +103 -0
  14. package/.storybook/htmlPreview.js +271 -0
  15. package/.storybook/main.js +160 -0
  16. package/.storybook/preview-body.html +48 -0
  17. package/.storybook/preview-head.html +11 -0
  18. package/.storybook/preview.js +1563 -0
  19. package/README.md +266 -0
  20. package/bin/index.js +40 -0
  21. package/dist/pds-reference.json +2101 -0
  22. package/package.json +45 -0
  23. package/pds.config.js +6 -0
  24. package/public/assets/css/app.css +1216 -0
  25. package/public/assets/data/auto-design-advanced.json +704 -0
  26. package/public/assets/data/auto-design-simple.json +123 -0
  27. package/public/assets/img/icon-512x512.png +0 -0
  28. package/public/assets/img/logo-trans.png +0 -0
  29. package/public/assets/img/logo.png +0 -0
  30. package/public/assets/js/app.js +15088 -0
  31. package/public/assets/js/app.js.map +7 -0
  32. package/public/assets/js/lit.js +1176 -0
  33. package/public/assets/js/lit.js.map +7 -0
  34. package/public/assets/js/pds.js +9801 -0
  35. package/public/assets/js/pds.js.map +7 -0
  36. package/public/assets/pds/components/pds-calendar.js +837 -0
  37. package/public/assets/pds/components/pds-drawer.js +857 -0
  38. package/public/assets/pds/components/pds-icon.js +338 -0
  39. package/public/assets/pds/components/pds-jsonform.js +1775 -0
  40. package/public/assets/pds/components/pds-richtext.js +1035 -0
  41. package/public/assets/pds/components/pds-scrollrow.js +331 -0
  42. package/public/assets/pds/components/pds-splitpanel.js +401 -0
  43. package/public/assets/pds/components/pds-tabstrip.js +251 -0
  44. package/public/assets/pds/components/pds-toaster.js +446 -0
  45. package/public/assets/pds/components/pds-upload.js +657 -0
  46. package/public/assets/pds/custom-elements.json +2003 -0
  47. package/public/assets/pds/icons/pds-icons.svg +498 -0
  48. package/public/assets/pds/pds-css-complete.json +1861 -0
  49. package/public/assets/pds/pds-runtime-config.json +11 -0
  50. package/public/assets/pds/pds.css-data.json +2152 -0
  51. package/public/assets/pds/styles/pds-components.css +1944 -0
  52. package/public/assets/pds/styles/pds-components.css.js +3895 -0
  53. package/public/assets/pds/styles/pds-primitives.css +352 -0
  54. package/public/assets/pds/styles/pds-primitives.css.js +711 -0
  55. package/public/assets/pds/styles/pds-styles.css +3761 -0
  56. package/public/assets/pds/styles/pds-styles.css.js +7529 -0
  57. package/public/assets/pds/styles/pds-tokens.css +699 -0
  58. package/public/assets/pds/styles/pds-tokens.css.js +1405 -0
  59. package/public/assets/pds/styles/pds-utilities.css +763 -0
  60. package/public/assets/pds/styles/pds-utilities.css.js +1533 -0
  61. package/public/assets/pds/vscode-custom-data.json +824 -0
  62. package/scripts/build-pds-reference.mjs +807 -0
  63. package/scripts/generate-stories.js +542 -0
  64. package/scripts/package-build.js +86 -0
  65. package/src/js/app.js +17 -0
  66. package/src/js/common/ask.js +208 -0
  67. package/src/js/common/common.js +20 -0
  68. package/src/js/common/font-loader.js +200 -0
  69. package/src/js/common/msg.js +90 -0
  70. package/src/js/lit.js +40 -0
  71. package/src/js/pds-core/pds-config.js +1162 -0
  72. package/src/js/pds-core/pds-enhancer-metadata.js +75 -0
  73. package/src/js/pds-core/pds-enhancers.js +357 -0
  74. package/src/js/pds-core/pds-enums.js +86 -0
  75. package/src/js/pds-core/pds-generator.js +5317 -0
  76. package/src/js/pds-core/pds-ontology.js +256 -0
  77. package/src/js/pds-core/pds-paths.js +109 -0
  78. package/src/js/pds-core/pds-query.js +571 -0
  79. package/src/js/pds-core/pds-registry.js +129 -0
  80. package/src/js/pds-core/pds.d.ts +129 -0
  81. package/src/js/pds.d.ts +408 -0
  82. package/src/js/pds.js +1579 -0
  83. package/src/pds-core/pds-api.js +105 -0
  84. package/stories/GettingStarted.md +96 -0
  85. package/stories/GettingStarted.stories.js +144 -0
  86. package/stories/WhatIsPDS.md +194 -0
  87. package/stories/WhatIsPDS.stories.js +144 -0
  88. package/stories/components/PdsCalendar.stories.js +263 -0
  89. package/stories/components/PdsDrawer.stories.js +623 -0
  90. package/stories/components/PdsIcon.stories.js +78 -0
  91. package/stories/components/PdsJsonform.stories.js +1444 -0
  92. package/stories/components/PdsRichtext.stories.js +367 -0
  93. package/stories/components/PdsScrollrow.stories.js +140 -0
  94. package/stories/components/PdsSplitpanel.stories.js +502 -0
  95. package/stories/components/PdsTabstrip.stories.js +442 -0
  96. package/stories/components/PdsToaster.stories.js +186 -0
  97. package/stories/components/PdsUpload.stories.js +66 -0
  98. package/stories/enhancements/Dropdowns.stories.js +185 -0
  99. package/stories/enhancements/InteractiveStates.stories.js +625 -0
  100. package/stories/enhancements/MeshGradients.stories.js +320 -0
  101. package/stories/enhancements/OpenGroups.stories.js +227 -0
  102. package/stories/enhancements/RangeSliders.stories.js +232 -0
  103. package/stories/enhancements/RequiredFields.stories.js +189 -0
  104. package/stories/enhancements/Toggles.stories.js +167 -0
  105. package/stories/foundations/Colors.stories.js +283 -0
  106. package/stories/foundations/Icons.stories.js +305 -0
  107. package/stories/foundations/SmartSurfaces.stories.js +367 -0
  108. package/stories/foundations/Spacing.stories.js +175 -0
  109. package/stories/foundations/Typography.stories.js +960 -0
  110. package/stories/foundations/ZIndex.stories.js +325 -0
  111. package/stories/patterns/BorderEffects.stories.js +72 -0
  112. package/stories/patterns/Layout.stories.js +99 -0
  113. package/stories/patterns/Utilities.stories.js +107 -0
  114. package/stories/primitives/Accordion.stories.js +359 -0
  115. package/stories/primitives/Alerts.stories.js +64 -0
  116. package/stories/primitives/Badges.stories.js +183 -0
  117. package/stories/primitives/Buttons.stories.js +229 -0
  118. package/stories/primitives/Cards.stories.js +353 -0
  119. package/stories/primitives/FormGroups.stories.js +569 -0
  120. package/stories/primitives/Forms.stories.js +131 -0
  121. package/stories/primitives/Media.stories.js +203 -0
  122. package/stories/primitives/Tables.stories.js +232 -0
  123. package/stories/reference/ReferenceCatalog.stories.js +28 -0
  124. package/stories/reference/reference-catalog.js +413 -0
  125. package/stories/reference/reference-docs.js +302 -0
  126. package/stories/reference/reference-helpers.js +310 -0
  127. package/stories/utilities/GridSystem.stories.js +208 -0
  128. package/stories/utils/PdsAsk.stories.js +420 -0
  129. package/stories/utils/toast-utils.js +148 -0
@@ -0,0 +1,159 @@
1
+ /**
2
+ * Preview-side addon logic for PDS Configurator
3
+ * Runs in the iframe where stories are rendered
4
+ */
5
+
6
+ import { addons } from '@storybook/preview-api';
7
+ import { EVENTS } from './constants.js';
8
+
9
+ let drawerElement = null;
10
+ let configFormLoaded = false;
11
+
12
+ // Initialize configurator UI in preview
13
+ async function initializeConfigurator() {
14
+ if (drawerElement) return;
15
+
16
+ // Force-load pds-drawer by triggering AutoDefiner
17
+ // Create a temporary element to force the component to load
18
+ if (!customElements.get('pds-drawer')) {
19
+ const temp = document.createElement('pds-drawer');
20
+ temp.style.display = 'none';
21
+ document.body.appendChild(temp);
22
+ try {
23
+ await customElements.whenDefined('pds-drawer');
24
+ } finally {
25
+ temp.remove();
26
+ }
27
+ }
28
+
29
+ // Create drawer for configurator
30
+ drawerElement = document.createElement('pds-drawer');
31
+ drawerElement.id = 'storybook-pds-configurator';
32
+ drawerElement.setAttribute('position', 'bottom');
33
+ drawerElement.setAttribute('show-close', '');
34
+ drawerElement.style.zIndex = '999999';
35
+
36
+ // Create header
37
+ //const header = document.createElement('div');
38
+ //header.slot = 'drawer-header';
39
+ //header.innerHTML = `
40
+ //`;
41
+ //drawerElement.appendChild(header);
42
+
43
+ // Create configurator content container
44
+ const content = document.createElement('div');
45
+ content.slot = 'drawer-content';
46
+ content.id = 'configurator-content';
47
+
48
+ content.innerHTML = `<p style="padding: 1rem;">Loading configurator...</p>`;
49
+ drawerElement.appendChild(content);
50
+
51
+ document.body.appendChild(drawerElement);
52
+
53
+ // Load pds-config-form dynamically
54
+ loadConfigForm();
55
+ }
56
+
57
+ async function loadConfigForm() {
58
+ if (configFormLoaded) return;
59
+
60
+ try {
61
+ // Import the config form component with timeout
62
+ const importTimeout = new Promise((_, reject) =>
63
+ setTimeout(() => reject(new Error('Config form import timeout')), 3000)
64
+ );
65
+
66
+ await Promise.race([
67
+ import('../../../../pds-configurator/src/pds-config-form.js'),
68
+ importTimeout
69
+ ]);
70
+
71
+ // Wait for component to be defined
72
+ await customElements.whenDefined('pds-config-form');
73
+
74
+ const content = document.getElementById('configurator-content');
75
+ if (content) {
76
+ content.innerHTML = '<pds-config-form id="storybook-config-form"></pds-config-form>';
77
+ configFormLoaded = true;
78
+
79
+ // Listen for design updates from the form
80
+ const PDS = (await import('../../../../../src/js/pds.js')).PDS;
81
+ PDS.addEventListener('pds:design:updated', async (e) => {
82
+ console.log('Design updated in configurator:', e.detail);
83
+
84
+ try {
85
+ // The event detail contains both config and designer from pds-config-form
86
+ if (e.detail.designer) {
87
+ // Apply the styles from the designer that was already created by pds-config-form
88
+ await PDS.Generator.applyStyles(e.detail.designer);
89
+ }
90
+
91
+ // Notify manager
92
+ const channel = addons.getChannel();
93
+ channel.emit(EVENTS.DESIGN_UPDATED, e.detail);
94
+ } catch (error) {
95
+ console.error('Failed to apply design update:', error);
96
+ }
97
+ });
98
+ }
99
+ } catch (error) {
100
+ console.error('Failed to load config form:', error);
101
+ const content = document.getElementById('configurator-content');
102
+ if (content) {
103
+ content.innerHTML = `<p style="padding: 1rem; color: red;">Error loading configurator: ${error.message}</p>`;
104
+ }
105
+ }
106
+ }
107
+
108
+ // Listen for addon events from manager
109
+ if (typeof window !== 'undefined') {
110
+ const channel = addons.getChannel();
111
+
112
+ channel.on(EVENTS.OPEN_CONFIGURATOR, async () => {
113
+ console.log('🎯 OPEN_CONFIGURATOR event received in preview');
114
+ try {
115
+ // Add timeout to prevent hanging forever
116
+ const timeout = new Promise((_, reject) =>
117
+ setTimeout(() => reject(new Error('Configurator initialization timeout')), 5000)
118
+ );
119
+
120
+ await Promise.race([initializeConfigurator(), timeout]);
121
+
122
+ if (drawerElement) {
123
+ console.log('✅ Opening configurator drawer');
124
+ // Always open (no toggle)
125
+ drawerElement.open = true;
126
+ } else {
127
+ throw new Error('Drawer element failed to initialize');
128
+ }
129
+ } catch (error) {
130
+ console.error('❌ Failed to open configurator:', error);
131
+ // Show user-friendly error
132
+ alert(`Failed to open PDS Configurator: ${error.message}\n\nPlease refresh the page and try again.`);
133
+ }
134
+ });
135
+
136
+ channel.on(EVENTS.QUERY_EXECUTED, async ({ query }) => {
137
+ try {
138
+ const PDS = (await import('../../../../../src/js/pds.js')).PDS;
139
+ const results = await PDS.query(query);
140
+
141
+ // Send results back to manager
142
+ channel.emit(EVENTS.QUERY_EXECUTED + '_RESPONSE', {
143
+ query,
144
+ results: results.slice(0, 10).map(r => ({
145
+ id: r.text,
146
+ title: r.text,
147
+ value: r.value,
148
+ category: r.category,
149
+ onClick: () => {
150
+ console.log('Query result clicked:', r);
151
+ // Could copy to clipboard or navigate
152
+ }
153
+ }))
154
+ });
155
+ } catch (error) {
156
+ console.error('Query execution failed:', error);
157
+ }
158
+ });
159
+ }
@@ -0,0 +1,24 @@
1
+ import { addons, types } from '@storybook/manager-api';
2
+ import { ADDON_ID, TOOL_ID } from './constants.js';
3
+ import { Tool } from './Tool.js';
4
+ import { SearchTool } from './SearchTool.js';
5
+
6
+ const SEARCH_TOOL_ID = `${ADDON_ID}/search`;
7
+
8
+ addons.register(ADDON_ID, () => {
9
+ // Register PDS Configurator button
10
+ addons.add(TOOL_ID, {
11
+ type: types.TOOL,
12
+ title: 'PDS Configurator',
13
+ match: ({ viewMode, tabId }) => !tabId && (viewMode === 'story' || viewMode === 'docs'),
14
+ render: Tool
15
+ });
16
+
17
+ // Register Quick Search button
18
+ addons.add(SEARCH_TOOL_ID, {
19
+ type: types.TOOL,
20
+ title: 'Quick Search',
21
+ match: ({ viewMode, tabId }) => !tabId && (viewMode === 'story' || viewMode === 'docs'),
22
+ render: SearchTool
23
+ });
24
+ });
@@ -0,0 +1,35 @@
1
+ /* Custom Docs page styling - hide all examples, show only description */
2
+
3
+ /* Hide ALL story previews and interactive elements in Docs */
4
+ .docs-story,
5
+ .sbdocs-preview,
6
+ .sb-unstyled,
7
+ .docblock-canvas-wrapper,
8
+ .docblock-canvas,
9
+ .docblock-argstable-wrapper,
10
+ .docblock-argstable,
11
+ .docblock-source-wrapper,
12
+ .docblock-source,
13
+ .sbdocs-preview-wrapper,
14
+ .sb-story,
15
+ [id*="story--"],
16
+ [class*="story-wrapper"] {
17
+ display: none !important;
18
+ }
19
+
20
+ /* Show only the description block */
21
+ .sbdocs-wrapper {
22
+ background: var(--surface-bg);
23
+ color: var(--color-text-primary);
24
+ }
25
+
26
+ .sbdocs .sbdocs-title,
27
+ .sbdocs .sbdocs-subtitle {
28
+ color: var(--color-text-primary);
29
+ }
30
+
31
+ .sbdocs-description,
32
+ .sbdocs .sbdocs-p {
33
+ color: var(--color-text-secondary);
34
+ line-height: 1.6;
35
+ }
@@ -0,0 +1,103 @@
1
+ /* HTML Source Preview Styles */
2
+
3
+ .html-source-section {
4
+ border-top: 1px solid var(--color-border);
5
+ }
6
+
7
+ .html-source-details {
8
+ margin: var(--spacing-4, 1rem);
9
+ }
10
+
11
+ .html-source-summary {
12
+ padding: var(--spacing-3, 0.75rem) var(--spacing-4, 1rem);
13
+ cursor: pointer;
14
+ font-weight: 600;
15
+ user-select: none;
16
+ display: flex;
17
+ justify-content: space-between;
18
+ align-items: center;
19
+ border-radius: var(--radius-md, 4px);
20
+ }
21
+
22
+ .html-source-title {
23
+ display: flex;
24
+ align-items: center;
25
+ gap: var(--spacing-2, 0.5rem);
26
+ }
27
+
28
+ .html-source-icon {
29
+ font-size: 1.25rem;
30
+ }
31
+
32
+ .html-source-content {
33
+ padding: var(--spacing-4, 1rem);
34
+ }
35
+
36
+ .html-source-pre {
37
+ margin: 0;
38
+ padding: var(--spacing-4, 1rem);
39
+ border-radius: var(--radius-md, 4px);
40
+ overflow-x: auto;
41
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', monospace;
42
+ font-size: 0.875rem;
43
+ line-height: 1.6;
44
+ tab-size: 2;
45
+ }
46
+
47
+ /* Syntax highlighting colors - work in both light and dark modes */
48
+ :root {
49
+ --code-bg: #282c34;
50
+ --code-text: #abb2bf;
51
+ --code-tag: #e06c75;
52
+ --code-attr: #d19a66;
53
+ --code-value: #98c379;
54
+ --code-comment: #5c6370;
55
+ }
56
+
57
+ [data-theme="dark"] {
58
+ --code-bg: #1e1e1e;
59
+ --code-text: #d4d4d4;
60
+ --code-tag: #569cd6;
61
+ --code-attr: #9cdcfe;
62
+ --code-value: #ce9178;
63
+ --code-comment: #6a9955;
64
+ }
65
+
66
+ .html-source-pre {
67
+ background: var(--code-bg);
68
+ }
69
+
70
+ .html-source-code {
71
+ color: var(--code-text);
72
+ }
73
+
74
+ .html-token-tag {
75
+ color: var(--code-tag);
76
+ }
77
+
78
+ .html-token-attr {
79
+ color: var(--code-attr);
80
+ }
81
+
82
+ .html-token-value {
83
+ color: var(--code-value);
84
+ }
85
+
86
+ .html-token-comment {
87
+ color: var(--code-comment);
88
+ font-style: italic;
89
+ }
90
+
91
+ pds-config-form {
92
+ position: relative;
93
+ label:has(input[type="color"]){
94
+ display: inline-block;
95
+ min-width: 7rem;
96
+ }
97
+
98
+ .designer-toolbar {
99
+ position: absolute;
100
+ top: 0;
101
+ right: 0;
102
+ }
103
+ }
@@ -0,0 +1,271 @@
1
+ /**
2
+ * HTML Preview and Copy Utility for Storybook
3
+ * Provides HTML source viewing and copy functionality for all stories
4
+ * Uses Storybook's built-in source code display in Docs mode
5
+ */
6
+
7
+ import { render as litRender } from 'lit';
8
+
9
+ /**
10
+ * Format HTML string with proper indentation
11
+ */
12
+ export function formatHTML(html) {
13
+ if (!html) return '';
14
+
15
+ let formatted = '';
16
+ let indent = 0;
17
+ const tab = ' ';
18
+
19
+ const tokens = html.split(/(<[^>]+>)/g).filter(Boolean);
20
+
21
+ tokens.forEach((token) => {
22
+ if (token.startsWith('</')) {
23
+ // Closing tag
24
+ indent = Math.max(0, indent - 1);
25
+ formatted += '\n' + tab.repeat(indent) + token;
26
+ } else if (token.startsWith('<')) {
27
+ // Opening tag
28
+ const isSelfClosing = token.endsWith('/>') || token.match(/<(area|base|br|col|embed|hr|img|input|link|meta|param|source|track|wbr)/);
29
+ formatted += '\n' + tab.repeat(indent) + token;
30
+ if (!isSelfClosing) {
31
+ indent++;
32
+ }
33
+ } else if (token.trim()) {
34
+ // Text content
35
+ formatted += token.trim();
36
+ }
37
+ });
38
+
39
+ return formatted.trim();
40
+ }
41
+
42
+ /**
43
+ * Extract HTML from a rendered story
44
+ */
45
+ export function extractHTML(element) {
46
+ if (!element) return '';
47
+
48
+ // Clone the element to avoid modifying the original
49
+ const clone = element.cloneNode(true);
50
+
51
+ // Clean up any Storybook-specific attributes or classes
52
+ const cleanElement = (el) => {
53
+ if (el.removeAttribute) {
54
+ el.removeAttribute('data-story-id');
55
+ el.removeAttribute('data-view-mode');
56
+ }
57
+ if (el.children) {
58
+ Array.from(el.children).forEach(cleanElement);
59
+ }
60
+ };
61
+
62
+ cleanElement(clone);
63
+ return clone.innerHTML || '';
64
+ }
65
+
66
+ /**
67
+ * Transform Lit template result to HTML string
68
+ */
69
+ export async function litToHTML(templateResult) {
70
+ if (!templateResult || !templateResult._$litType$) {
71
+ return '';
72
+ }
73
+
74
+ const temp = document.createElement('div');
75
+ litRender(templateResult, temp);
76
+
77
+ // Wait for any custom elements to upgrade
78
+ await new Promise(resolve => setTimeout(resolve, 50));
79
+
80
+ return formatHTML(temp.innerHTML);
81
+ }
82
+
83
+ /**
84
+ * Global decorator that provides HTML source code display
85
+ */
86
+ export const withHTMLSource = (storyFn, context) => {
87
+ const story = storyFn();
88
+
89
+ // For docs mode, Storybook will automatically show source
90
+ // We just need to ensure the parameters are set
91
+ if (context.viewMode === 'docs') {
92
+ return story;
93
+ }
94
+
95
+ // For canvas/story mode, we'll add a source viewer below the story
96
+ const container = document.createElement('div');
97
+ container.style.cssText = 'display: flex; flex-direction: column; gap: 1rem; width: 100%; height: 100%;';
98
+
99
+ // Story container
100
+ const storyContainer = document.createElement('div');
101
+ storyContainer.id = 'story-preview';
102
+ storyContainer.style.cssText = 'flex: 1; overflow: auto;';
103
+
104
+ // Source panel with copy button
105
+ const sourceSection = document.createElement('div');
106
+ sourceSection.className = 'html-source-section';
107
+ sourceSection.innerHTML = `
108
+ <details class="html-source-details surface-subtle">
109
+ <summary class="html-source-summary surface-elevated">
110
+ <span class="html-source-title">
111
+ <span class="html-source-icon">📝</span>
112
+ <span>View HTML Source</span>
113
+ </span>
114
+ <button id="copy-source-btn" class="btn-outline">
115
+ 📋 Copy HTML
116
+ </button>
117
+ </summary>
118
+ <div class="html-source-content">
119
+ <pre class="html-source-pre"><code class="html-source-code"></code></pre>
120
+ </div>
121
+ </details>
122
+ `;
123
+
124
+ container.appendChild(storyContainer);
125
+ container.appendChild(sourceSection);
126
+
127
+ // Render the story
128
+ setTimeout(async () => {
129
+ if (story && story._$litType$) {
130
+ litRender(story, storyContainer);
131
+ } else if (story instanceof HTMLElement) {
132
+ storyContainer.appendChild(story);
133
+ } else if (typeof story === 'string') {
134
+ storyContainer.innerHTML = story;
135
+ }
136
+
137
+ // Extract and display HTML
138
+ setTimeout(async () => {
139
+ let html = '';
140
+
141
+ if (story && story._$litType$) {
142
+ html = await litToHTML(story);
143
+ } else {
144
+ html = formatHTML(extractHTML(storyContainer));
145
+ }
146
+
147
+ const codeEl = sourceSection.querySelector('.html-source-code');
148
+ if (codeEl && html) {
149
+ // Simple syntax highlighter that properly tokenizes HTML
150
+ const highlightHTML = (code) => {
151
+ let result = '';
152
+ let i = 0;
153
+
154
+ while (i < code.length) {
155
+ // Handle HTML comments
156
+ if (code.substr(i, 4) === '<!--') {
157
+ const end = code.indexOf('-->', i);
158
+ if (end !== -1) {
159
+ const comment = code.substring(i, end + 3);
160
+ result += `<span class="html-token-comment">${escapeHtml(comment)}</span>`;
161
+ i = end + 3;
162
+ continue;
163
+ }
164
+ }
165
+
166
+ // Handle tags
167
+ if (code[i] === '<') {
168
+ const tagEnd = code.indexOf('>', i);
169
+ if (tagEnd !== -1) {
170
+ const tagContent = code.substring(i + 1, tagEnd);
171
+ result += '&lt;';
172
+
173
+ // Check if it's a closing tag
174
+ if (tagContent[0] === '/') {
175
+ result += '/';
176
+ const tagName = tagContent.substring(1).split(/[\s>]/)[0];
177
+ result += `<span class="html-token-tag">${escapeHtml(tagName)}</span>`;
178
+ } else {
179
+ // Parse tag name and attributes
180
+ const parts = tagContent.match(/^([\w-]+)([\s\S]*?)(\/?)?$/);
181
+ if (parts) {
182
+ const [, tagName, attrsStr, slash] = parts;
183
+ result += `<span class="html-token-tag">${escapeHtml(tagName)}</span>`;
184
+
185
+ // Parse attributes
186
+ if (attrsStr.trim()) {
187
+ const attrRegex = /([\w-]+)(?:=(?:"([^"]*)"|'([^']*)'|([^\s>]*)))?/g;
188
+ let match;
189
+ let lastIndex = 0;
190
+
191
+ while ((match = attrRegex.exec(attrsStr)) !== null) {
192
+ // Add whitespace before attribute
193
+ result += escapeHtml(attrsStr.substring(lastIndex, match.index));
194
+
195
+ const [fullMatch, attrName, doubleQuoted, singleQuoted, unquoted] = match;
196
+ result += `<span class="html-token-attr">${escapeHtml(attrName)}</span>`;
197
+
198
+ if (doubleQuoted !== undefined) {
199
+ result += `=<span class="html-token-value">"${escapeHtml(doubleQuoted)}"</span>`;
200
+ } else if (singleQuoted !== undefined) {
201
+ result += `=<span class="html-token-value">'${escapeHtml(singleQuoted)}'</span>`;
202
+ } else if (unquoted !== undefined) {
203
+ result += `=<span class="html-token-value">${escapeHtml(unquoted)}</span>`;
204
+ }
205
+
206
+ lastIndex = match.index + fullMatch.length;
207
+ }
208
+
209
+ result += escapeHtml(attrsStr.substring(lastIndex));
210
+ }
211
+
212
+ if (slash) result += '/';
213
+ }
214
+ }
215
+
216
+ result += '&gt;';
217
+ i = tagEnd + 1;
218
+ continue;
219
+ }
220
+ }
221
+
222
+ // Regular text
223
+ result += escapeHtml(code[i]);
224
+ i++;
225
+ }
226
+
227
+ return result;
228
+ };
229
+
230
+ const escapeHtml = (text) => {
231
+ return text
232
+ .replace(/&/g, '&amp;')
233
+ .replace(/</g, '&lt;')
234
+ .replace(/>/g, '&gt;');
235
+ };
236
+
237
+ codeEl.innerHTML = highlightHTML(html);
238
+ }
239
+
240
+ // Setup copy button
241
+ const copyBtn = sourceSection.querySelector('#copy-source-btn');
242
+ if (copyBtn && html) {
243
+ copyBtn.addEventListener('click', async (e) => {
244
+ e.preventDefault();
245
+ e.stopPropagation();
246
+
247
+ try {
248
+ await navigator.clipboard.writeText(html);
249
+ const originalClass = copyBtn.className;
250
+ copyBtn.textContent = '✅ Copied!';
251
+ copyBtn.className = 'btn-primary';
252
+
253
+ setTimeout(() => {
254
+ copyBtn.textContent = '📋 Copy HTML';
255
+ copyBtn.className = originalClass;
256
+ }, 2000);
257
+ } catch (err) {
258
+ console.error('Copy failed:', err);
259
+ const originalText = copyBtn.textContent;
260
+ copyBtn.textContent = '❌ Failed';
261
+ setTimeout(() => {
262
+ copyBtn.textContent = originalText;
263
+ }, 2000);
264
+ }
265
+ });
266
+ }
267
+ }, 100);
268
+ }, 0);
269
+
270
+ return container;
271
+ };