@ytspar/devbar 0.0.1 → 1.0.0-canary.e1f65d0

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,215 @@
1
+ /**
2
+ * Document Outline Extraction
3
+ *
4
+ * Functions for extracting and formatting the semantic document outline.
5
+ */
6
+ // ============================================================================
7
+ // Semantic Element Sets
8
+ // ============================================================================
9
+ const semanticElements = new Set([
10
+ 'article', 'aside', 'nav', 'section',
11
+ 'main', 'body',
12
+ 'header', 'footer', 'figure', 'figcaption',
13
+ 'details', 'summary', 'dialog', 'address', 'hgroup',
14
+ 'form', 'fieldset', 'legend',
15
+ 'ul', 'ol', 'dl', 'menu',
16
+ 'table', 'thead', 'tbody', 'tfoot', 'caption',
17
+ 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'
18
+ ]);
19
+ const headingElements = new Set(['h1', 'h2', 'h3', 'h4', 'h5', 'h6']);
20
+ // ============================================================================
21
+ // Helper Functions
22
+ // ============================================================================
23
+ /**
24
+ * Get the semantic category for an element tag
25
+ */
26
+ function getSemanticCategory(tag) {
27
+ if (headingElements.has(tag))
28
+ return 'heading';
29
+ if (['article', 'section', 'aside', 'nav'].includes(tag))
30
+ return 'sectioning';
31
+ if (['main', 'header', 'footer'].includes(tag))
32
+ return 'landmark';
33
+ if (['figure', 'figcaption', 'details', 'summary'].includes(tag))
34
+ return 'grouping';
35
+ if (['form', 'fieldset', 'legend'].includes(tag))
36
+ return 'form';
37
+ if (['table', 'thead', 'tbody', 'tfoot', 'caption'].includes(tag))
38
+ return 'table';
39
+ if (['ul', 'ol', 'dl', 'menu'].includes(tag))
40
+ return 'list';
41
+ return 'other';
42
+ }
43
+ /**
44
+ * Get trimmed text content from an element with optional max length
45
+ */
46
+ function getTextContent(el, maxLen) {
47
+ return el?.textContent?.trim().slice(0, maxLen) || '';
48
+ }
49
+ /**
50
+ * Mapping of tag names to their child element selector for text extraction
51
+ */
52
+ const childTextSelectors = {
53
+ figure: 'figcaption',
54
+ details: 'summary',
55
+ fieldset: 'legend',
56
+ table: 'caption',
57
+ };
58
+ /**
59
+ * Get descriptive text for an element
60
+ */
61
+ function getElementText(el, tagName) {
62
+ // Check ARIA attributes first
63
+ const ariaLabel = el.getAttribute('aria-label');
64
+ if (ariaLabel)
65
+ return ariaLabel;
66
+ const labelledBy = el.getAttribute('aria-labelledby');
67
+ if (labelledBy) {
68
+ const labelEl = document.getElementById(labelledBy);
69
+ if (labelEl)
70
+ return getTextContent(labelEl, 80);
71
+ }
72
+ // Headings: use direct text content
73
+ if (headingElements.has(tagName)) {
74
+ return getTextContent(el, 100);
75
+ }
76
+ // Elements with child selectors (figure, details, fieldset, table)
77
+ const childSelector = childTextSelectors[tagName];
78
+ if (childSelector) {
79
+ const childEl = el.querySelector(childSelector);
80
+ if (childEl)
81
+ return getTextContent(childEl, 80);
82
+ }
83
+ // Form: use name or id attribute
84
+ if (tagName === 'form') {
85
+ const name = el.getAttribute('name') || el.getAttribute('id');
86
+ if (name)
87
+ return name;
88
+ }
89
+ // Nav: try heading first, then first link
90
+ if (tagName === 'nav') {
91
+ const heading = el.querySelector('h1, h2, h3, h4, h5, h6');
92
+ if (heading)
93
+ return getTextContent(heading, 50);
94
+ const firstLink = el.querySelector('a');
95
+ if (firstLink)
96
+ return `Navigation (${getTextContent(firstLink, 30)}...)`;
97
+ }
98
+ // Sectioning elements: try direct child heading, then class name
99
+ if (['section', 'article', 'aside'].includes(tagName)) {
100
+ const heading = el.querySelector(':scope > h1, :scope > h2, :scope > h3, :scope > h4, :scope > h5, :scope > h6');
101
+ if (heading)
102
+ return getTextContent(heading, 80);
103
+ const className = el.className?.toString().split(' ')[0];
104
+ if (className && className.length < 30)
105
+ return className;
106
+ }
107
+ // Lists: count items
108
+ if (['ul', 'ol'].includes(tagName)) {
109
+ return `${el.querySelectorAll(':scope > li').length} items`;
110
+ }
111
+ if (tagName === 'dl') {
112
+ return `${el.querySelectorAll(':scope > dt').length} terms`;
113
+ }
114
+ // Fallback to role attribute
115
+ const role = el.getAttribute('role');
116
+ if (role)
117
+ return `[role="${role}"]`;
118
+ return '';
119
+ }
120
+ /**
121
+ * Check if an element is visible
122
+ */
123
+ function isVisible(el) {
124
+ const style = window.getComputedStyle(el);
125
+ return style.display !== 'none' && style.visibility !== 'hidden';
126
+ }
127
+ /**
128
+ * Recursively extract outline nodes from an element
129
+ */
130
+ function extractFromElement(root) {
131
+ const nodes = [];
132
+ for (const child of Array.from(root.children)) {
133
+ const tagName = child.tagName.toLowerCase();
134
+ if (!isVisible(child))
135
+ continue;
136
+ if (child.getAttribute('data-devbar'))
137
+ continue;
138
+ if (semanticElements.has(tagName)) {
139
+ const text = getElementText(child, tagName);
140
+ const isHeading = headingElements.has(tagName);
141
+ const isLandmark = ['main', 'nav', 'header', 'footer', 'article', 'section', 'aside'].includes(tagName);
142
+ const hasText = text.length > 0;
143
+ if (isHeading || isLandmark || hasText) {
144
+ const level = isHeading ? parseInt(tagName[1], 10) : 0;
145
+ const node = {
146
+ tagName,
147
+ level,
148
+ text: text || `<${tagName}>`,
149
+ id: child.id || undefined,
150
+ children: [],
151
+ category: getSemanticCategory(tagName)
152
+ };
153
+ if (!isHeading) {
154
+ node.children = extractFromElement(child);
155
+ }
156
+ nodes.push(node);
157
+ }
158
+ else {
159
+ const childNodes = extractFromElement(child);
160
+ nodes.push(...childNodes);
161
+ }
162
+ }
163
+ else {
164
+ const childNodes = extractFromElement(child);
165
+ nodes.push(...childNodes);
166
+ }
167
+ }
168
+ return nodes;
169
+ }
170
+ // ============================================================================
171
+ // Public API
172
+ // ============================================================================
173
+ /**
174
+ * Extract the document outline from the page
175
+ */
176
+ export function extractDocumentOutline() {
177
+ const body = document.body;
178
+ if (!body)
179
+ return [];
180
+ return extractFromElement(body);
181
+ }
182
+ /**
183
+ * Convert an outline to markdown format
184
+ */
185
+ export function outlineToMarkdown(outline, indent = 0) {
186
+ let md = '';
187
+ if (indent === 0) {
188
+ md += '# Document Outline\n\n';
189
+ md += '**Semantic Categories:**\n';
190
+ md += '- `heading` - h1-h6 elements\n';
191
+ md += '- `sectioning` - article, section, aside, nav\n';
192
+ md += '- `landmark` - main, header, footer\n';
193
+ md += '- `grouping` - figure, details, summary\n';
194
+ md += '- `form` - form, fieldset\n';
195
+ md += '- `table` - table elements\n';
196
+ md += '- `list` - ul, ol, dl\n\n';
197
+ md += '---\n\n';
198
+ }
199
+ for (const node of outline) {
200
+ const prefix = ' '.repeat(indent);
201
+ const tagLabel = `\`<${node.tagName}>\``;
202
+ const anchor = node.id ? ` \`#${node.id}\`` : '';
203
+ const category = node.category ? ` [${node.category}]` : '';
204
+ if (node.category === 'heading' && indent === 0) {
205
+ md += `${'#'.repeat(node.level)} ${tagLabel} ${node.text}${anchor}\n\n`;
206
+ }
207
+ else {
208
+ md += `${prefix}- ${tagLabel}${category} ${node.text}${anchor}\n`;
209
+ }
210
+ if (node.children.length > 0) {
211
+ md += outlineToMarkdown(node.children, indent + 1);
212
+ }
213
+ }
214
+ return md;
215
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Page Schema Extraction
3
+ *
4
+ * Functions for extracting and formatting structured data from pages.
5
+ */
6
+ import type { PageSchema } from './types.js';
7
+ /**
8
+ * Extract structured data (JSON-LD, meta tags, Open Graph, etc.) from the page
9
+ */
10
+ export declare function extractPageSchema(): PageSchema;
11
+ /**
12
+ * Convert a page schema to markdown format
13
+ */
14
+ export declare function schemaToMarkdown(schema: PageSchema): string;
package/dist/schema.js ADDED
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Page Schema Extraction
3
+ *
4
+ * Functions for extracting and formatting structured data from pages.
5
+ */
6
+ // ============================================================================
7
+ // Public API
8
+ // ============================================================================
9
+ /**
10
+ * Extract structured data (JSON-LD, meta tags, Open Graph, etc.) from the page
11
+ */
12
+ export function extractPageSchema() {
13
+ const schema = {
14
+ jsonLd: [],
15
+ metaTags: {},
16
+ openGraph: {},
17
+ twitter: {},
18
+ microdata: []
19
+ };
20
+ // Extract JSON-LD
21
+ const jsonLdScripts = document.querySelectorAll('script[type="application/ld+json"]');
22
+ jsonLdScripts.forEach((script) => {
23
+ try {
24
+ const data = JSON.parse(script.textContent || '');
25
+ schema.jsonLd.push(data);
26
+ }
27
+ catch {
28
+ // Invalid JSON-LD, skip
29
+ }
30
+ });
31
+ // Extract meta tags
32
+ const metaTags = document.querySelectorAll('meta[name], meta[property]');
33
+ metaTags.forEach((meta) => {
34
+ const name = meta.getAttribute('name') || meta.getAttribute('property') || '';
35
+ const content = meta.getAttribute('content') || '';
36
+ if (name.startsWith('og:')) {
37
+ schema.openGraph[name.replace('og:', '')] = content;
38
+ }
39
+ else if (name.startsWith('twitter:')) {
40
+ schema.twitter[name.replace('twitter:', '')] = content;
41
+ }
42
+ else if (name) {
43
+ schema.metaTags[name] = content;
44
+ }
45
+ });
46
+ // Extract microdata
47
+ const microdataItems = document.querySelectorAll('[itemscope]');
48
+ microdataItems.forEach((item) => {
49
+ const itemType = item.getAttribute('itemtype');
50
+ const props = {};
51
+ item.querySelectorAll('[itemprop]').forEach((prop) => {
52
+ const propName = prop.getAttribute('itemprop') || '';
53
+ const propValue = prop.getAttribute('content') ||
54
+ prop.getAttribute('href') ||
55
+ prop.textContent?.trim().slice(0, 200) || '';
56
+ if (propName)
57
+ props[propName] = propValue;
58
+ });
59
+ if (itemType || Object.keys(props).length > 0) {
60
+ schema.microdata.push({ type: itemType, properties: props });
61
+ }
62
+ });
63
+ return schema;
64
+ }
65
+ /**
66
+ * Convert a page schema to markdown format
67
+ */
68
+ export function schemaToMarkdown(schema) {
69
+ let md = '';
70
+ if (schema.jsonLd.length > 0) {
71
+ md += '## JSON-LD\n\n';
72
+ schema.jsonLd.forEach((item, i) => {
73
+ md += `### Schema ${i + 1}\n\n`;
74
+ md += '```json\n' + JSON.stringify(item, null, 2) + '\n```\n\n';
75
+ });
76
+ }
77
+ if (Object.keys(schema.openGraph).length > 0) {
78
+ md += '## Open Graph\n\n';
79
+ for (const [key, value] of Object.entries(schema.openGraph)) {
80
+ md += `- **${key}**: ${value}\n`;
81
+ }
82
+ md += '\n';
83
+ }
84
+ if (Object.keys(schema.twitter).length > 0) {
85
+ md += '## Twitter Cards\n\n';
86
+ for (const [key, value] of Object.entries(schema.twitter)) {
87
+ md += `- **${key}**: ${value}\n`;
88
+ }
89
+ md += '\n';
90
+ }
91
+ if (Object.keys(schema.metaTags).length > 0) {
92
+ md += '## Meta Tags\n\n';
93
+ for (const [key, value] of Object.entries(schema.metaTags)) {
94
+ md += `- **${key}**: ${value}\n`;
95
+ }
96
+ md += '\n';
97
+ }
98
+ if (schema.microdata.length > 0) {
99
+ md += '## Microdata\n\n';
100
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
101
+ schema.microdata.forEach((item, i) => {
102
+ md += `### Item ${i + 1}${item.type ? ` (${item.type})` : ''}\n\n`;
103
+ for (const [key, value] of Object.entries(item.properties || {})) {
104
+ md += `- **${key}**: ${value}\n`;
105
+ }
106
+ md += '\n';
107
+ });
108
+ }
109
+ if (!md) {
110
+ md = '_No structured data found on this page_\n';
111
+ }
112
+ return md;
113
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * DevBar Type Definitions
3
+ *
4
+ * Re-exports shared types from @ytspar/sweetlink and defines DevBar-specific types.
5
+ *
6
+ * NOTE: We import from the types sub-path to avoid pulling in Node.js-only modules.
7
+ */
8
+ export type { ConsoleLog, SweetlinkCommand, OutlineNode, PageSchema, } from '@ytspar/sweetlink/types';
9
+ /**
10
+ * Options for configuring the GlobalDevBar
11
+ */
12
+ export interface GlobalDevBarOptions {
13
+ /** Position of the devbar. Default: 'bottom-left' */
14
+ position?: 'bottom-left' | 'bottom-right' | 'top-left' | 'top-right' | 'bottom-center';
15
+ /** Primary accent color (CSS color). Default: '#10b981' (emerald) */
16
+ accentColor?: string;
17
+ /** Which metrics to show. Default: all enabled */
18
+ showMetrics?: {
19
+ breakpoint?: boolean;
20
+ fcp?: boolean;
21
+ lcp?: boolean;
22
+ pageSize?: boolean;
23
+ };
24
+ /** Whether to show the screenshot button. Default: true */
25
+ showScreenshot?: boolean;
26
+ /** Whether to show console error/warning badges. Default: true */
27
+ showConsoleBadges?: boolean;
28
+ /** Whether to show tooltips on hover. Default: true */
29
+ showTooltips?: boolean;
30
+ /** Size overrides for special layouts (e.g., when other dev bars are present) */
31
+ sizeOverrides?: {
32
+ /** Custom width (CSS value). Default: calc(100vw - 140px) for centered, fit-content otherwise */
33
+ width?: string;
34
+ /** Custom max-width (CSS value). Default: 600px for centered, calc(100vw - 32px) otherwise */
35
+ maxWidth?: string;
36
+ /** Custom min-width (CSS value). Optional */
37
+ minWidth?: string;
38
+ };
39
+ }
40
+ /**
41
+ * Custom control that can be registered by host applications
42
+ */
43
+ export interface DevBarControl {
44
+ id: string;
45
+ label: string;
46
+ onClick: () => void;
47
+ active?: boolean;
48
+ disabled?: boolean;
49
+ variant?: 'default' | 'warning';
50
+ }
package/dist/types.js ADDED
@@ -0,0 +1,8 @@
1
+ /**
2
+ * DevBar Type Definitions
3
+ *
4
+ * Re-exports shared types from @ytspar/sweetlink and defines DevBar-specific types.
5
+ *
6
+ * NOTE: We import from the types sub-path to avoid pulling in Node.js-only modules.
7
+ */
8
+ export {};
@@ -0,0 +1,21 @@
1
+ /**
2
+ * DevBar Buttons
3
+ *
4
+ * Button creation and styling utilities for the DevBar UI.
5
+ */
6
+ /**
7
+ * Get button styling based on active state and color
8
+ */
9
+ export declare function getButtonStyles(color: string, isActive: boolean, isDisabled: boolean): Record<string, string>;
10
+ /**
11
+ * Create a styled button with common properties
12
+ */
13
+ export declare function createStyledButton(options: {
14
+ color: string;
15
+ text: string;
16
+ padding?: string;
17
+ borderRadius?: string;
18
+ fontSize?: string;
19
+ width?: string;
20
+ height?: string;
21
+ }): HTMLButtonElement;
@@ -0,0 +1,55 @@
1
+ /**
2
+ * DevBar Buttons
3
+ *
4
+ * Button creation and styling utilities for the DevBar UI.
5
+ */
6
+ import { ACTION_BUTTON_BASE_STYLES } from '../constants.js';
7
+ /**
8
+ * Get button styling based on active state and color
9
+ */
10
+ export function getButtonStyles(color, isActive, isDisabled) {
11
+ return {
12
+ ...ACTION_BUTTON_BASE_STYLES,
13
+ borderColor: isActive ? color : `${color}80`,
14
+ backgroundColor: isActive ? `${color}33` : 'transparent',
15
+ color: isActive ? color : `${color}99`,
16
+ cursor: isDisabled ? 'not-allowed' : 'pointer',
17
+ opacity: '1',
18
+ };
19
+ }
20
+ /**
21
+ * Apply hover effects to a button element (internal helper)
22
+ */
23
+ function applyButtonHoverEffects(btn, color, isActive = false) {
24
+ btn.onmouseenter = () => {
25
+ btn.style.backgroundColor = `${color}20`;
26
+ };
27
+ btn.onmouseleave = () => {
28
+ btn.style.backgroundColor = isActive ? `${color}33` : 'transparent';
29
+ };
30
+ }
31
+ /**
32
+ * Create a styled button with common properties
33
+ */
34
+ export function createStyledButton(options) {
35
+ const { color, text, padding = '6px 12px', borderRadius = '6px', fontSize = '0.75rem', width, height, } = options;
36
+ const btn = document.createElement('button');
37
+ Object.assign(btn.style, {
38
+ padding: width ? undefined : padding,
39
+ width,
40
+ height,
41
+ display: 'flex',
42
+ alignItems: 'center',
43
+ justifyContent: 'center',
44
+ backgroundColor: 'transparent',
45
+ border: `1px solid ${color}60`,
46
+ borderRadius,
47
+ color,
48
+ fontSize,
49
+ cursor: 'pointer',
50
+ transition: 'all 150ms',
51
+ });
52
+ btn.textContent = text;
53
+ applyButtonHoverEffects(btn, color);
54
+ return btn;
55
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * DevBar Icons
3
+ *
4
+ * SVG icon creation utilities for the DevBar UI.
5
+ */
6
+ /**
7
+ * Create an SVG icon element with the given path data
8
+ */
9
+ export declare function createSvgIcon(pathData: string, options: {
10
+ viewBox?: string;
11
+ fill?: boolean;
12
+ stroke?: boolean;
13
+ }): SVGSVGElement;
@@ -0,0 +1,25 @@
1
+ /**
2
+ * DevBar Icons
3
+ *
4
+ * SVG icon creation utilities for the DevBar UI.
5
+ */
6
+ /**
7
+ * Create an SVG icon element with the given path data
8
+ */
9
+ export function createSvgIcon(pathData, options) {
10
+ const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
11
+ svg.setAttribute('width', '12');
12
+ svg.setAttribute('height', '12');
13
+ svg.setAttribute('viewBox', options.viewBox || '0 0 24 24');
14
+ if (options.fill) {
15
+ svg.style.fill = 'currentColor';
16
+ }
17
+ if (options.stroke) {
18
+ svg.style.stroke = 'currentColor';
19
+ svg.style.fill = 'none';
20
+ }
21
+ const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
22
+ path.setAttribute('d', pathData);
23
+ svg.appendChild(path);
24
+ return svg;
25
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * DevBar UI Components
3
+ *
4
+ * Re-exports all UI utilities.
5
+ */
6
+ export { createSvgIcon } from './icons.js';
7
+ export { getButtonStyles, createStyledButton } from './buttons.js';
8
+ export { createModalOverlay, createModalBox, createModalHeader, createModalContent, createEmptyMessage, createInfoBox, type ModalConfig } from './modals.js';
@@ -0,0 +1,8 @@
1
+ /**
2
+ * DevBar UI Components
3
+ *
4
+ * Re-exports all UI utilities.
5
+ */
6
+ export { createSvgIcon } from './icons.js';
7
+ export { getButtonStyles, createStyledButton } from './buttons.js';
8
+ export { createModalOverlay, createModalBox, createModalHeader, createModalContent, createEmptyMessage, createInfoBox } from './modals.js';
@@ -0,0 +1,40 @@
1
+ /**
2
+ * DevBar Modals
3
+ *
4
+ * Modal creation utilities for the DevBar UI.
5
+ */
6
+ /**
7
+ * Configuration for creating a modal
8
+ */
9
+ export interface ModalConfig {
10
+ color: string;
11
+ title: string;
12
+ onClose: () => void;
13
+ onCopyMd: () => Promise<void>;
14
+ onSave?: () => void;
15
+ sweetlinkConnected: boolean;
16
+ }
17
+ /**
18
+ * Create modal overlay with click-outside-to-close behavior
19
+ */
20
+ export declare function createModalOverlay(onClose: () => void): HTMLDivElement;
21
+ /**
22
+ * Create modal box with border and shadow
23
+ */
24
+ export declare function createModalBox(color: string): HTMLDivElement;
25
+ /**
26
+ * Create modal header with title, copy/save/close buttons
27
+ */
28
+ export declare function createModalHeader(config: ModalConfig): HTMLDivElement;
29
+ /**
30
+ * Create modal content container
31
+ */
32
+ export declare function createModalContent(): HTMLDivElement;
33
+ /**
34
+ * Create empty state message for modals
35
+ */
36
+ export declare function createEmptyMessage(text: string): HTMLDivElement;
37
+ /**
38
+ * Create a colored info box (for error states, cost estimates, etc.)
39
+ */
40
+ export declare function createInfoBox(color: string, title: string, content: string | HTMLElement[]): HTMLDivElement;