diff --git a/.gitignore b/.gitignore index 47e893a7c..31f01c16e 100755 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ tests/test-case/dist # for my ai friends ai/meta/agent-guestbook.md +ai/meta/jisei.md # Numerous always-ignore extensions *.diff diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 438f2efcb..8df43a4dd 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -4,6 +4,15 @@ This is a pre-release version and APIs will change quickly. Before `1.0` release Please note after `1.0` Semver will be followed using normal protocols. +# Version 0.13.0 + +* **Query** - Added `clippingParent` to find closest ancestor which will clip the current element +* **Query** - `offsetParent` has been renamed to `containingParent` and now includes many other possible containing parent checks like `will-change` and `filter`. +* **Query** - `setting()` can now be used as a getter. +* **Query** - Added `.data()` for getting and setting html data +* **Query** - Added `.slice()` for returning a portion of the element collection +* **Query** - Fixed `offsetParent` to correctly return offset parent for willChange + # Version 0.12.4-1 * **Tailwind** - `@semantic-ui/tailwind` and `tailwindcss-iso` now have bundled CDN version to avoid issues importing css files in browser via esm. Modified `tailwind` package to accomodate this change. * **Tailwind** - Removed bundled `wasm` files, these are now part of the generic `tailwindcss-iso` package. diff --git a/ai/guides/html-css-style-guide.md b/ai/guides/html-css-style-guide.md index b1ea3b71b..c79cee9f3 100644 --- a/ai/guides/html-css-style-guide.md +++ b/ai/guides/html-css-style-guide.md @@ -1,6 +1,6 @@ # Semantic UI HTML & CSS Style Guide -Based on analysis of canonical examples authored by Jack Lukic, this guide captures the distinctive patterns and philosophies for writing HTML and CSS within Semantic UI web components. +This guide captures the distinctive patterns and philosophies for writing HTML and CSS within Semantic UI web components. ## Core Philosophy: Natural Language Applied to Markup @@ -467,4 +467,4 @@ Your HTML and CSS style reflects a sophisticated understanding of how natural la - **Smooth interactions** with design system transitions - **Seamless accessibility** integration -This approach creates code that leverages the full power of the design system while maintaining component-specific customization where needed. \ No newline at end of file +This approach creates code that leverages the full power of the design system while maintaining component-specific customization where needed. diff --git a/ai/specialized/query-system-guide.md b/ai/packages/query.md similarity index 75% rename from ai/specialized/query-system-guide.md rename to ai/packages/query.md index 612dc11c1..c11e59776 100644 --- a/ai/specialized/query-system-guide.md +++ b/ai/packages/query.md @@ -1,53 +1,78 @@ # Semantic UI Query System Guide -**For AI agents working with Semantic UI's `@semantic-ui/query` package** +> **For:** AI agents working with DOM querying and manipulation in Semantic UI +> **Purpose:** Comprehensive reference for the Query package and component debugging +> **Prerequisites:** Basic understanding of DOM and Shadow DOM concepts +> **Related:** [Mental Model](../foundations/mental-model.md) • [Component Guide](../guides/component-generation-instructions.md) • [Quick Reference](../foundations/quick-reference.md) +> **Back to:** [Documentation Hub](../00-START-HERE.md) -## Overview +--- -The `@semantic-ui/query` package is a standalone DOM querying and manipulation library that provides jQuery-like functionality with modern JavaScript features. It's designed specifically to handle Shadow DOM traversal and component-aware querying while remaining lightweight and focused. +## Table of Contents -## Package Structure +- [Core Query Concepts](#core-query-concepts) +- [Shadow DOM Traversal Strategy](#shadow-dom-traversal-strategy) +- [Component Instance Access](#component-instance-access) +- [Component Debugging Workflow](#component-debugging-workflow) +- [Event System Integration](#event-system-integration) +- [Performance Considerations](#performance-considerations) +- [Advanced Query Patterns](#advanced-query-patterns) +- [Troubleshooting Common Issues](#troubleshooting-common-issues) -``` -@semantic-ui/query -├── Query ← Main query class with DOM manipulation methods -├── $ function ← Standard DOM querying (respects Shadow DOM boundaries) -├── $$ function ← Deep querying (crosses Shadow DOM boundaries) -└── Utilities ← Global management and aliasing functions -``` +--- + +## Core Query Concepts + +### The Dual Query System + +Semantic UI provides two entry points for DOM querying, each with distinct capabilities: -**Main Exports**: ```javascript -import { $, $$, Query, exportGlobals, restoreGlobals, useAlias } from '@semantic-ui/query'; +import { $, $$ } from '@semantic-ui/query'; + +// Standard DOM querying (respects Shadow DOM boundaries) +$('button') // Finds buttons in light DOM only +$('ui-dropdown') // Finds dropdown components, but not their internal structure + +// Deep querying (pierces Shadow DOM boundaries) +$$('button') // Finds buttons in light AND shadow DOM +$$('ui-dropdown .item') // Finds .item elements inside dropdown's shadow DOM ``` -## Core Functions +**Mental Model**: Think of `$` as "CSS selectors" and `$$` as "CSS selectors that understand web components." + +### Query Instance Architecture -### $ - Standard DOM Querying +Every Query instance contains: -The `$` function provides standard DOM querying that respects Shadow DOM boundaries. +```javascript +const $elements = $('div'); + +// Core properties +$elements.length // Number of matched elements +$elements.selector // Original selector used +$elements.options // Query configuration (root, pierceShadow) +$elements[0], $elements[1] // Array-like access to elements + +// Chaining system +$elements.find('.child') // Returns new Query instance +$elements.chain([newEl]) // Create Query with specific elements +``` + +### Query Options and Configuration ```javascript -import { $ } from '@semantic-ui/query'; - -// Query selectors (standard CSS selectors) -$('.button') // All elements with class 'button' in light DOM -$('#main') // Element with id 'main' -$('input[type="text"]') // All text inputs -$('div > .child') // Direct child selectors - -// Create elements from HTML -$('
Content
') -$('Text') - -// Wrap existing elements -$(document.body) // Wrap body element -$(elementArray) // Wrap array of elements -$(nodeList) // Wrap NodeList - -// Global selectors -$('window') // Window object (special handling) -$('body') // Document body +// Root scoping +$('button', { root: shadowRoot }) // Query within specific root +$('input', { root: document.getElementById('form') }) + +// Shadow DOM piercing +$('button', { pierceShadow: true }) // Equivalent to $$('button') + +// Component-scoped querying +const dropdown = $('ui-dropdown').get(0); +$('button', { root: dropdown.shadowRoot }); // Query inside component +``` ``` ### $$ - Deep DOM Querying @@ -93,50 +118,52 @@ The Query class provides a comprehensive set of methods organized into logical c - `get(index)` - Get element at index - `eq(index)` - Get new Query object with element at index - `first()`, `last()` - Get first/last element as new Query -- `slice(start, end)` - Get subset of elements ### DOM Traversal - `find(selector)` - Find descendants -- `parent()`, `parents()` - Get parent elements -- `children()`, `siblings()` - Get child/sibling elements -- `next()`, `prev()` - Get adjacent siblings +- `parent(selector)` - Get parent elements +- `children(selector)`, `siblings(selector)` - Get child/sibling elements +- `next(selector)`, `prev(selector)` - Get adjacent siblings - `closest(selector)` - Find closest ancestor matching selector ### Content Manipulation - `html()`, `html(content)` - Get/set innerHTML - `text()`, `text(content)` - Get/set textContent - `val()`, `val(value)` - Get/set form element values -- `append()`, `prepend()` - Add content to elements -- `before()`, `after()` - Add content around elements +- `append(content)`, `prepend(content)` - Add content to elements +- `insertBefore(selector)`, `insertAfter(selector)` - Insert elements relative to targets ### Attribute/Property Management - `attr(name)`, `attr(name, value)` - Get/set attributes - `removeAttr(name)` - Remove attributes - `prop(name)`, `prop(name, value)` - Get/set properties -- `data(key)`, `data(key, value)` - Get/set data attributes ### CSS and Styling - `css(property)`, `css(property, value)` - Get/set CSS properties -- `addClass()`, `removeClass()`, `toggleClass()` - Manage CSS classes -- `hasClass()` - Check for CSS class -- `show()`, `hide()` - Show/hide elements +- `cssVar(variable, value)` - Get/set CSS custom properties +- `computedStyle(property)` - Get computed styles +- `addClass(classes)`, `removeClass(classes)`, `toggleClass(classes)` - Manage CSS classes +- `hasClass(className)` - Check for CSS class ### Event Handling - `on(event, selector, handler)` - Event delegation - `off(event, handler)` - Remove event listeners -- `trigger(event, data)` - Trigger custom events +- `trigger(event, data)` - Trigger events +- `dispatchEvent(event, data, settings)` - Dispatch custom events - `one(event, handler)` - One-time event listener ### Dimensions and Positioning -- `width()`, `height()` - Get/set dimensions -- `innerWidth()`, `outerWidth()` - Get calculated dimensions -- `offset()`, `position()` - Get element positioning -- `scrollTop()`, `scrollLeft()` - Get/set scroll position +- `width(value)`, `height(value)` - Get/set dimensions +- `scrollWidth(value)`, `scrollHeight(value)` - Get/set scroll dimensions +- `scrollTop(value)`, `scrollLeft(value)` - Get/set scroll position +- `offsetParent(options)` - Get offset parent for positioning +- `naturalWidth()`, `naturalHeight()` - Get natural dimensions ### Component Integration (Semantic UI specific) - `settings(newSettings)` - Configure component settings +- `setting(name, value)` - Get/set individual component settings - `initialize(settings)` - Initialize component before DOM insertion -- `component()` - Get component template instance +- `component()` - Get component instance - `dataContext()` - Get component's data context for debugging ## Component-Aware Methods @@ -321,14 +348,13 @@ $('document').on('keydown', function(event) { ### Method Chaining ```javascript -// jQuery-style chaining +// Query-style chaining $('.items') .addClass('processed') .find('.button') .on('click', handleClick) .css('color', 'blue') - .end() // Return to previous selection (.items) - .fadeIn(); + .end(); // Return to previous selection (.items) ``` ### Iteration and Filtering @@ -357,9 +383,9 @@ $('.item').filter(function(index, element) { $('.title').text('New Title'); $('.container').html('

New content

'); -// Data attributes -$('.item').data('id', 123); -const itemId = $('.item').data('id'); +// HTML data attributes via attr() +$('.item').attr('data-id', '123'); +const itemId = $('.item').attr('data-id'); // Form values $('input[name="email"]').val('user@example.com'); diff --git a/ai/specialized/reactivity-system-guide.md b/ai/packages/reactivity.md similarity index 100% rename from ai/specialized/reactivity-system-guide.md rename to ai/packages/reactivity.md diff --git a/ai/specialized/templating-system-guide.md b/ai/packages/templating.md similarity index 100% rename from ai/specialized/templating-system-guide.md rename to ai/packages/templating.md diff --git a/ai/specialized/utils-package-guide.md b/ai/packages/utils.md similarity index 100% rename from ai/specialized/utils-package-guide.md rename to ai/packages/utils.md diff --git a/ai/proposals/animations.js b/ai/proposals/animations.js new file mode 100644 index 000000000..fc75a1dd5 --- /dev/null +++ b/ai/proposals/animations.js @@ -0,0 +1,287 @@ +/** + * Reusable keyframe objects. Stored as constants so they are defined only once + * and can be referenced by multiple animation definitions. This is ideal for + * both DRY principles and minification. + */ +const slideInY = { + '0%': { opacity: 0, transform: 'scaleY(0)' }, + '100%': { opacity: 1, transform: 'scaleY(1)' } +}; +const slideOutY = { + '0%': { opacity: 1, transform: 'scaleY(1)' }, + '100%': { opacity: 0, transform: 'scaleY(0)' } +}; +const slideInX = { + '0%': { opacity: 0, transform: 'scaleX(0)' }, + '100%': { opacity: 1, transform: 'scaleX(1)' } +}; +const slideOutX = { + '0%': { opacity: 1, transform: 'scaleX(1)' }, + '100%': { opacity: 0, transform: 'scaleX(0)' } +}; + +const swingInX = { + '0%': { transform: 'perspective(1000px) rotateX(90deg)', opacity: 0 }, + '40%': { transform: 'perspective(1000px) rotateX(-30deg)', opacity: 1 }, + '60%': { transform: 'perspective(1000px) rotateX(15deg)' }, + '80%': { transform: 'perspective(1000px) rotateX(-7.5deg)' }, + '100%': { transform: 'perspective(1000px) rotateX(0deg)' } +}; +const swingOutX = { + '0%': { transform: 'perspective(1000px) rotateX(0deg)' }, + '40%': { transform: 'perspective(1000px) rotateX(-7.5deg)' }, + '60%': { transform: 'perspective(1000px) rotateX(17.5deg)' }, + '80%': { transform: 'perspective(1000px) rotateX(-30deg)', opacity: 1 }, + '100%': { transform: 'perspective(1000px) rotateX(90deg)', opacity: 0 } +}; +const swingInY = { + '0%': { transform: 'perspective(1000px) rotateY(-90deg)', opacity: 0 }, + '40%': { transform: 'perspective(1000px) rotateY(30deg)', opacity: 1 }, + '60%': { transform: 'perspective(1000px) rotateY(-17.5deg)' }, + '80%': { transform: 'perspective(1000px) rotateY(7.5deg)' }, + '100%': { transform: 'perspective(1000px) rotateY(0deg)' } +}; +const swingOutY = { + '0%': { transform: 'perspective(1000px) rotateY(0deg)' }, + '40%': { transform: 'perspective(1000px) rotateY(7.5deg)' }, + '60%': { transform: 'perspective(1000px) rotateY(-10deg)' }, + '80%': { transform: 'perspective(1000px) rotateY(30deg)', opacity: 1 }, + '100%': { transform: 'perspective(1000px) rotateY(-90deg)', opacity: 0 } +}; + + +/** + * The main animation definitions, structured as JS objects for optimal minification. + * - `keyframe`: A direct reference to one of the reusable keyframe objects above. + * - `keyframeName`: A string to name the animation. This is used when the keyframe is unique. + * - `alias`: Points to another definition path within this object to avoid repetition. + */ +const animationDefinitions = { + browse: { + in: { + duration: '500ms', + keyframeName: 'browseIn', + keyframe: { + '0%': { transform: 'scale(0.8) translateZ(0px)', zIndex: -1 }, + '10%': { transform: 'scale(0.8) translateZ(0px)', zIndex: -1, opacity: 0.7 }, + '80%': { transform: 'scale(1.05) translateZ(0px)', opacity: 1, zIndex: 999 }, + '100%': { transform: 'scale(1) translateZ(0px)', zIndex: 999 } + } + }, + out: { + duration: '500ms', + keyframeName: 'browseOutLeft', + keyframe: { + '0%': { zIndex: 999, transform: 'translateX(0%) rotateY(0deg) rotateX(0deg)' }, + '50%': { zIndex: -1, transform: 'translateX(-105%) rotateY(35deg) rotateX(10deg) translateZ(-10px)' }, + '80%': { opacity: 1 }, + '100%': { zIndex: -1, transform: 'translateX(0%) rotateY(0deg) rotateX(0deg) translateZ(-10px)', opacity: 0 } + } + }, + right: { + out: { + duration: '500ms', + keyframeName: 'browseOutRight', + keyframe: { + '0%': { zIndex: 999, transform: 'translateX(0%) rotateY(0deg) rotateX(0deg)' }, + '50%': { zIndex: 1, transform: 'translateX(105%) rotateY(35deg) rotateX(10deg) translateZ(-10px)' }, + '80%': { opacity: 1 }, + '100%': { zIndex: 1, transform: 'translateX(0%) rotateY(0deg) rotateX(0deg) translateZ(-10px)', opacity: 0 } + } + } + } + }, + + drop: { + in: { + duration: '400ms', + keyframeName: 'dropIn', + keyframe: { + '0%': { opacity: 0, transform: 'scale(0)' }, + '100%': { opacity: 1, transform: 'scale(1)' } + } + }, + out: { + duration: '400ms', + keyframeName: 'dropOut', + keyframe: { + '0%': { opacity: 1, transform: 'scale(1)' }, + '100%': { opacity: 0, transform: 'scale(0)' } + } + } + }, + + fade: { + in: { duration: '300ms', keyframeName: 'fadeIn', keyframe: { '0%': { opacity: 0 }, '100%': { opacity: 1 } } }, + out: { duration: '300ms', keyframeName: 'fadeOut', keyframe: { '0%': { opacity: 1 }, '100%': { opacity: 0 } } }, + up: { + in: { keyframeName: 'fadeInUp', keyframe: { '0%': { opacity: 0, transform: 'translateY(10%)' }, '100%': { opacity: 1, transform: 'translateY(0%)' } } }, + out: { keyframeName: 'fadeOutUp', keyframe: { '0%': { opacity: 1, transform: 'translateY(0%)' }, '100%': { opacity: 0, transform: 'translateY(5%)' } } } + }, + down: { + in: { keyframeName: 'fadeInDown', keyframe: { '0%': { opacity: 0, transform: 'translateY(-10%)' }, '100%': { opacity: 1, transform: 'translateY(0%)' } } }, + out: { keyframeName: 'fadeOutDown', keyframe: { '0%': { opacity: 1, transform: 'translateY(0%)' }, '100%': { opacity: 0, transform: 'translateY(-5%)' } } } + }, + left: { + in: { keyframeName: 'fadeInLeft', keyframe: { '0%': { opacity: 0, transform: 'translateX(10%)' }, '100%': { opacity: 1, transform: 'translateX(0%)' } } }, + out: { keyframeName: 'fadeOutLeft', keyframe: { '0%': { opacity: 1, transform: 'translateX(0%)' }, '100%': { opacity: 0, transform: 'translateX(5%)' } } } + }, + right: { + in: { keyframeName: 'fadeInRight', keyframe: { '0%': { opacity: 0, transform: 'translateX(-10%)' }, '100%': { opacity: 1, transform: 'translateX(0%)' } } }, + out: { keyframeName: 'fadeOutRight', keyframe: { '0%': { opacity: 1, transform: 'translateX(0%)' }, '100%': { opacity: 0, transform: 'translateX(-5%)' } } } + } + }, + + fly: { + duration: '600ms', + in: { keyframeName: 'flyIn', keyframe: { '0%':{opacity:0,transform:'scale3d(.3,.3,.3)'},'20%':{transform:'scale3d(1.1,1.1,1.1)'},'40%':{transform:'scale3d(.9,.9,.9)'},'60%':{opacity:1,transform:'scale3d(1.03,1.03,1.03)'},'80%':{transform:'scale3d(.97,.97,.97)'},'100%':{opacity:1,transform:'scale3d(1,1,1)'} } }, + out: { keyframeName: 'flyOut', keyframe: { '20%':{transform:'scale3d(.9,.9,.9)'},'50%,55%':{opacity:1,transform:'scale3d(1.1,1.1,1.1)'},'100%':{opacity:0,transform:'scale3d(.3,.3,.3)'} } }, + up: { + in: { keyframeName: 'flyInUp', keyframe: { '0%':{opacity:0,transform:'translate3d(0,1500px,0)'},'60%':{opacity:1,transform:'translate3d(0,-20px,0)'},'75%':{transform:'translate3d(0,10px,0)'},'90%':{transform:'translate3d(0,-5px,0)'},'100%':{transform:'translate3d(0,0,0)'} } }, + out: { keyframeName: 'flyOutUp', keyframe: { '20%':{transform:'translate3d(0,10px,0)'},'40%,45%':{opacity:1,transform:'translate3d(0,-20px,0)'},'100%':{opacity:0,transform:'translate3d(0,2000px,0)'} } } + }, + down: { + in: { keyframeName: 'flyInDown', keyframe: { '0%':{opacity:0,transform:'translate3d(0,-1500px,0)'},'60%':{opacity:1,transform:'translate3d(0,25px,0)'},'75%':{transform:'translate3d(0,-10px,0)'},'90%':{transform:'translate3d(0,5px,0)'},'100%':{transform:'none'} } }, + out: { keyframeName: 'flyOutDown', keyframe: { '20%':{transform:'translate3d(0,-10px,0)'},'40%,45%':{opacity:1,transform:'translate3d(0,20px,0)'},'100%':{opacity:0,transform:'translate3d(0,-2000px,0)'} } } + }, + left: { + in: { keyframeName: 'flyInLeft', keyframe: { '0%':{opacity:0,transform:'translate3d(1500px,0,0)'},'60%':{opacity:1,transform:'translate3d(-25px,0,0)'},'75%':{transform:'translate3d(10px,0,0)'},'90%':{transform:'translate3d(-5px,0,0)'},'100%':{transform:'none'} } }, + out: { keyframeName: 'flyOutLeft', keyframe: { '20%':{opacity:1,transform:'translate3d(-20px,0,0)'},'100%':{opacity:0,transform:'translate3d(2000px,0,0)'} } } + }, + right: { + in: { keyframeName: 'flyInRight', keyframe: { '0%':{opacity:0,transform:'translate3d(-1500px,0,0)'},'60%':{opacity:1,transform:'translate3d(25px,0,0)'},'75%':{transform:'translate3d(-10px,0,0)'},'90%':{transform:'translate3d(5px,0,0)'},'100%':{transform:'none'} } }, + out: { keyframeName: 'flyOutRight', keyframe: { '20%':{opacity:1,transform:'translate3d(20px,0,0)'},'100%':{opacity:0,transform:'translate3d(-2000px,0,0)'} } } + } + }, + + flip: { + duration: '600ms', + horizontal: { + in: { keyframeName: 'horizontalFlipIn', keyframe: { '0%':{transform:'perspective(2000px) rotateY(-90deg)',opacity:0},'100%':{transform:'perspective(2000px) rotateY(0)',opacity:1} } }, + out: { keyframeName: 'horizontalFlipOut', keyframe: { '0%':{transform:'perspective(2000px) rotateY(0)',opacity:1},'100%':{transform:'perspective(2000px) rotateY(90deg)',opacity:0} } } + }, + vertical: { + in: { keyframeName: 'verticalFlipIn', keyframe: { '0%':{transform:'perspective(2000px) rotateX(-90deg)',opacity:0},'100%':{transform:'perspective(2000px) rotateX(0)',opacity:1} } }, + out: { keyframeName: 'verticalFlipOut', keyframe: { '0%':{transform:'perspective(2000px) rotateX(0)',opacity:1},'100%':{transform:'perspective(2000px) rotateX(-90deg)',opacity:0} } } + } + }, + + scale: { + in: { duration: '300ms', keyframeName: 'scaleIn', keyframe: { '0%': { opacity: 0, transform: 'scale(0.8)' }, '100%': { opacity: 1, transform: 'scale(1)' } } }, + out: { duration: '300ms', keyframeName: 'scaleOut', keyframe: { '0%': { opacity: 1, transform: 'scale(1)' }, '100%': { opacity: 0, transform: 'scale(0.9)' } } } + }, + + slide: { + down: { + in: { keyframeName: 'slideInY', keyframe: slideInY }, + out: { keyframeName: 'slideOutY', keyframe: slideOutY } + }, + up: { alias: 'slide.down' }, + left: { + in: { keyframeName: 'slideInX', keyframe: slideInX }, + out: { keyframeName: 'slideOutX', keyframe: slideOutX } + }, + right: { alias: 'slide.left' } + }, + + swing: { + duration: '800ms', + down: { + in: { keyframeName: 'swingInX', keyframe: swingInX }, + out: { keyframeName: 'swingOutX', keyframe: swingOutX } + }, + up: { alias: 'swing.down' }, + left: { + in: { keyframeName: 'swingInY', keyframe: swingInY }, + out: { keyframeName: 'swingOutY', keyframe: swingOutY } + }, + right: { alias: 'swing.left' } + }, + + zoom: { + in: { keyframeName: 'zoomIn', keyframe: { '0%':{opacity:1,transform:'scale(0)'},'100%':{opacity:1,transform:'scale(1)'} } }, + out: { keyframeName: 'zoomOut', keyframe: { '0%':{opacity:1,transform:'scale(1)'},'100%':{opacity:1,transform:'scale(0)'} } } + }, + + + // --- Static Animations (no in/out direction) --- + jiggle: { + static: { + duration: '750ms', + keyframeName: 'jiggle', + keyframe: { + '0%': { transform: 'scale3d(1, 1, 1)' }, + '30%': { transform: 'scale3d(1.25, 0.75, 1)' }, + '40%': { transform: 'scale3d(0.75, 1.25, 1)' }, + '50%': { transform: 'scale3d(1.15, 0.85, 1)' }, + '65%': { transform: 'scale3d(0.95, 1.05, 1)' }, + '75%': { transform: 'scale3d(1.05, 0.95, 1)' }, + '100%': { transform: 'scale3d(1, 1, 1)' } + } + } + }, + flash: { + static: { + duration: '750ms', + keyframeName: 'flash', + keyframe: { '0%, 50%, 100%': { opacity: 1 }, '25%, 75%': { opacity: 0 } } + } + }, + shake: { + static: { + duration: '750ms', + keyframeName: 'shake', + keyframe: { + '0%, 100%': { transform: 'translateX(0)' }, + '10%, 30%, 50%, 70%, 90%': { transform: 'translateX(-10px)' }, + '20%, 40%, 60%, 80%': { transform: 'translateX(10px)' } + } + } + }, + bounce: { + static: { + duration: '750ms', + keyframeName: 'bounce', + keyframe: { + '0%, 20%, 50%, 80%, 100%': { transform: 'translateY(0)' }, + '40%': { transform: 'translateY(-30px)' }, + '60%': { transform: 'translateY(-15px)' } + } + } + }, + tada: { + static: { + duration: '750ms', + keyframeName: 'tada', + keyframe: { + '0%': { transform: 'scale(1)' }, + '10%, 20%': { transform: 'scale(0.9) rotate(-3deg)' }, + '30%, 50%, 70%, 90%': { transform: 'scale(1.1) rotate(3deg)' }, + '40%, 60%, 80%': { transform: 'scale(1.1) rotate(-3deg)' }, + '100%': { transform: 'scale(1) rotate(0)' } + } + } + }, + pulse: { + static: { + duration: '500ms', + keyframeName: 'pulse', + keyframe: { + '0%': { transform: 'scale(1)', opacity: 1 }, + '50%': { transform: 'scale(0.9)', opacity: 0.7 }, + '100%': { transform: 'scale(1)', opacity: 1 } + } + } + }, + glow: { + static: { + duration: '2000ms', + keyframeName: 'glow', + keyframe: { + '0%': { backgroundColor: '#FCFCFD' }, + '30%': { backgroundColor: '#FFF6CD' }, + '100%': { backgroundColor: '#FCFCFD' } + } + } + } +}; diff --git a/ai/proposals/query-core-missing-methods.md b/ai/proposals/query-core-missing-methods.md new file mode 100644 index 000000000..301160984 --- /dev/null +++ b/ai/proposals/query-core-missing-methods.md @@ -0,0 +1,100 @@ +# Query Core - Missing Methods Proposal + +> **Scope:** Essential missing methods with minimal complexity +> **Status:** Draft for implementation +> **Size Impact:** Minimal - mostly aliases and simple logic reuse + +--- + +## Array-like Operations + +### `slice(start, end)` +Get subset of elements as new Query instance. + +- `start` - Number starting index +- `end` - Number ending index (optional) + +**Implementation:** Simple Array.slice() wrapper + +### `splice(start, deleteCount, ...items)` +Modify the element collection in place. + +- `start` - Number starting index +- `deleteCount` - Number of elements to remove +- `items` - Elements to insert at start position + +**Implementation:** Array.splice() with Query wrapping + +--- + +## Enhanced Traversal + +~~Enhanced traversal removed from core scope - `parents()` not commonly needed in practice~~ + +--- + +## DOM Insertion Aliases + +### `before(content)` +Alias for `insertBefore()` with more intuitive name. + +- `content` - String HTML | Element | Query | Array of content + +**Implementation:** Simple alias to existing insertBefore logic + +### `after(content)` +Alias for `insertAfter()` with more intuitive name. + +- `content` - String HTML | Element | Query | Array of content + +**Implementation:** Simple alias to existing insertAfter logic + +--- + +## Enhanced Dimensions + +### `width(options)` / `height(options)` +Extend existing methods with inclusion options. + +- `options.includeMargin` - Boolean to include margin +- `options.includePadding` - Boolean to include padding +- `options.includeBorder` - Boolean to include border + +**Implementation:** Extend existing width/height with computed style calculations + +--- + +## Data Attribute Helpers + +### `data(key, value)` +Get/set data-* attributes with type conversion. + +- `key` - String attribute name or Object of key-value pairs +- `value` - Any value to set, undefined to get + +**Implementation:** Wrapper around attr() with 'data-' prefix and JSON parsing + +### `data()` +Get all data-* attributes as object. + +**Implementation:** Filter attributes starting with 'data-', parse values + +--- + +## Shadow DOM Enhancements + +### `contains(selector)` +Check if elements contain targets. Automatically shadow DOM aware based on `this.options.pierceShadow`. + +- `selector` - String | Element | Query to check containment + +**Implementation:** Use DOM `.contains()` or deep traversal based on `this.options.pierceShadow` + +--- + +## Implementation Notes + +- **File size impact:** < 1.5KB gzipped +- **Complexity:** Very low - mostly aliases and simple extensions +- **Dependencies:** None - uses existing Query infrastructure +- **Breaking changes:** None - all additions \ No newline at end of file diff --git a/ai/proposals/query-css-animations.md b/ai/proposals/query-css-animations.md new file mode 100644 index 000000000..033f8ddb0 --- /dev/null +++ b/ai/proposals/query-css-animations.md @@ -0,0 +1,135 @@ +# Query CSS Animations Extension + +> **Scope:** CSS-based animations and transitions integration +> **Status:** Complex scope requiring design decisions +> **Size Impact:** Moderate - style injection and state management logic + +--- + +## Core Animation Methods + +### `show(options)` +Show elements with CSS animation support. + +- `options.animation` - String CSS animation class or transition property +- `options.duration` - String CSS duration ('300ms', '0.3s') +- `options.timing` - String CSS timing function ('ease', 'cubic-bezier(...)') +- `options.display` - String target display value ('block', 'flex', 'inline-block') + +### `hide(options)` +Hide elements with CSS animation support. + +- `options.animation` - String CSS animation class or transition property +- `options.duration` - String CSS duration +- `options.timing` - String CSS timing function + +### `toggle(force, options)` +Toggle visibility with animation support. + +- `force` - Boolean to force show/hide, undefined for toggle +- `options` - Same as show/hide options + +### `visible()` +Check if elements are visible. + +**Returns:** Boolean true if any element is visible + +--- + +## Design Challenges + +### Display State Resolution +**Problem:** When showing `display: none` elements, what should the final display value be? + +**Options:** +1. **Store original display** - Remember pre-hide display value +2. **Compute from CSS** - Analyze stylesheets for intended display +3. **Default + override** - Default to 'block', allow explicit override +4. **Element-based heuristics** - div=block, span=inline, etc. + +### Style Injection Strategy +**Problem:** How to inject temporary animation styles? + +**Options:** +1. **Inline styles** - Direct element.style modification +2. **Style sheet injection** - Dynamic ` + + +
+

\u{1F916} AI Workspace

+

Simple testing environment for Semantic UI components.

+ +

Getting Started

+
    +
  1. Create component files in ai/workspace/components/my-component/
  2. +
  3. Use the standard pattern: component.js, component.html, component.css
  4. +
  5. Test your component at /component.html?name=my-component
  6. +
+ +

Quick Links

+
+ + \u{1F4DD} Component Tester - Test any component + + + \u{1F3AF} Example Component - See how it works + +
+ +

Available Components

+
+

Loading components...

+
+
+ + \n\n"], + "mappings": "AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;", + "names": [] +} diff --git a/ai/workspace/README.md b/ai/workspace/README.md new file mode 100644 index 000000000..e08aabf7b --- /dev/null +++ b/ai/workspace/README.md @@ -0,0 +1,384 @@ +# AI Workspace - MCP Debugging Environment + +Complete web component debugging environment with Model Context Protocol (MCP) tools for AI agents. + +## 🎯 Features + +- **MCP WebSocket Server** - Real-time communication between AI agents and browser +- **Shadow DOM-Aware Debugging** - Pierces web component boundaries +- **Component Auto-Discovery** - Automatic registration and tracking +- **Real-Time Event Streaming** - Monitor component events as they happen +- **Code Execution in Context** - Run JavaScript in component scope +- **State Introspection** - Deep component state analysis +- **Mutation Tracking** - Track DOM changes over time +- **CSS Analysis** - Computed styles and custom properties inspection + +## 🚀 Quick Start + +### Start the Development Environment + +```bash +npm run dev:workspace +``` + +This starts: +- **Web Server**: `http://localhost:8080` +- **MCP WebSocket**: `ws://localhost:8081` + +### Access the Interface + +- **Main Dashboard**: `http://localhost:8080/public/` +- **Component Tester**: `http://localhost:8080/public/component.html` +- **Test Component Demo**: `http://localhost:8080/public/component.html?name=test-component` + +## 🛠️ MCP Tools for AI Agents + +### Core Inspection Tools + +#### `inspect_component` +Deep analysis of component structure, attributes, and shadow DOM. + +```javascript +{ + "tool": "inspect_component", + "params": { + "selector": "#my-component", + "depth": 3 + } +} +``` + +#### `execute_in_component` +Execute JavaScript code within component context. + +```javascript +{ + "tool": "execute_in_component", + "params": { + "selector": "test-component", + "code": "return { counter: host.counter, state: host.state }" + } +} +``` + +#### `monitor_events` +Real-time event monitoring with WebSocket streaming. + +```javascript +{ + "tool": "monitor_events", + "params": { + "selector": "#demo-test", + "events": ["click", "counter-changed", "section-toggled"] + } +} +``` + +### Shadow DOM Tools + +#### `query_shadow_dom` +Query elements within shadow DOM boundaries. + +```javascript +{ + "tool": "query_shadow_dom", + "params": { + "selector": "test-component", + "query": "button.increment-btn" + } +} +``` + +#### `get_computed_styles` +Analyze CSS styles and custom properties. + +```javascript +{ + "tool": "get_computed_styles", + "params": { + "selector": "test-component", + "elementPath": ".counter-display" + } +} +``` + +### State & History Tools + +#### `get_component_state` +Retrieve component's internal state and methods. + +```javascript +{ + "tool": "get_component_state", + "params": { + "selector": "#demo-test" + } +} +``` + +#### `mutation_history` +Track recent DOM changes within components. + +```javascript +{ + "tool": "mutation_history", + "params": { + "selector": "test-component", + "count": 10 + } +} +``` + +#### `list_components` +Discover all web components on the page. + +```javascript +{ + "tool": "list_components", + "params": {} +} +``` + +## 🧩 Creating Components + +### Standard Component Structure + +``` +ai/workspace/components/my-component/ +├── component.js # Main component definition +├── component.html # Template (optional) +└── component.css # Styles (optional) +``` + +### Example Component + +```javascript +// component.js +class MyComponent extends HTMLElement { + constructor() { + super(); + this.attachShadow({ mode: 'open' }); + this._counter = 0; + } + + connectedCallback() { + this.render(); + } + + render() { + this.shadowRoot.innerHTML = ` + +
+

Counter: ${this._counter}

+ +
+ `; + } + + increment() { + this._counter++; + this.render(); + this.dispatchEvent(new CustomEvent('counter-changed', { + detail: { value: this._counter }, + bubbles: true, + composed: true + })); + } + + get counter() { return this._counter; } +} + +customElements.define('my-component', MyComponent); +``` + +## 🔍 Debugging Workflow + +### 1. Component Discovery +```javascript +// Find all components +await tools.list_components({}); + +// Inspect specific component +await tools.inspect_component({ + selector: "my-component", + depth: 2 +}); +``` + +### 2. Event Monitoring +```javascript +// Monitor user interactions +await tools.monitor_events({ + selector: "my-component", + events: ["click", "counter-changed"] +}); + +// Events stream to WebSocket automatically +``` + +### 3. State Analysis +```javascript +// Check component state +await tools.get_component_state({ + selector: "my-component" +}); + +// Execute custom code +await tools.execute_in_component({ + selector: "my-component", + code: "return { counter: host.counter, methods: Object.getOwnPropertyNames(host.__proto__) }" +}); +``` + +### 4. Style Debugging +```javascript +// Analyze computed styles +await tools.get_computed_styles({ + selector: "my-component", + elementPath: "button" +}); +``` + +### 5. Change Tracking +```javascript +// View recent mutations +await tools.mutation_history({ + selector: "my-component", + count: 5 +}); +``` + +## 📡 WebSocket Communication + +### Message Types + +#### Tool Requests (Agent → Browser) +```javascript +{ + "type": "mcp-tool-request", + "tool": "inspect_component", + "params": { "selector": "my-component" }, + "id": "req-123" +} +``` + +#### Tool Responses (Browser → Agent) +```javascript +{ + "type": "mcp-tool-response", + "id": "req-123", + "result": { /* tool output */ } +} +``` + +#### Event Streams (Browser → Agent) +```javascript +{ + "type": "event-triggered", + "selector": "my-component", + "event": "counter-changed", + "detail": { + "type": "counter-changed", + "target": "MY-COMPONENT", + "detail": { "value": 5 }, + "timestamp": 1640995200000 + } +} +``` + +## 🎯 Test Components + +### Basic Test Component +- **Location**: `components/test-component/component.js` +- **Features**: Counter, themes, state management, events +- **Usage**: Perfect for learning MCP debugging + +### Example Component +- **Location**: `components/example/component.js` +- **Features**: Semantic UI integration, reactive templates +- **Usage**: Framework-specific patterns + +## 🔧 Technical Architecture + +### Debug Bridge (`debug-bridge.js`) +- Component auto-registration +- Shadow DOM traversal +- Event delegation and capture +- MCP tool request handling +- WebSocket communication + +### MCP Server (`build-ai-workspace.js`) +- WebSocket server on port 8081 +- Tool request routing +- Event broadcasting +- Connection management + +### esbuild Integration +- Debug bridge injection +- Live reloading +- Module serving +- Development server + +## 📚 Documentation References + +- **MCP Tools Schema**: `/mcp-tools.js` +- **Component Guide**: `/ai/guides/component-generation-instructions.md` +- **Query System**: `/ai/specialized/query-system-guide.md` +- **Mental Model**: `/ai/foundations/mental-model.md` + +## 🎮 Usage Examples + +### Debug a Broken Component +```javascript +// 1. Find the component +const components = await tools.list_components({}); + +// 2. Inspect its structure +const structure = await tools.inspect_component({ + selector: "broken-component" +}); + +// 3. Monitor for errors +await tools.monitor_events({ + selector: "broken-component", + events: ["error", "click", "change"] +}); + +// 4. Execute diagnostic code +const diagnostics = await tools.execute_in_component({ + selector: "broken-component", + code: ` + return { + hasErrors: !!shadow.querySelector('.error'), + eventListeners: getEventListeners ? getEventListeners(host) : 'Not available', + attributes: [...host.attributes].map(a => ({name: a.name, value: a.value})) + } + ` +}); +``` + +### Performance Analysis +```javascript +// Monitor mutations for performance issues +const mutations = await tools.mutation_history({ + selector: "heavy-component", + count: 20 +}); + +// Check computed styles for layout issues +const styles = await tools.get_computed_styles({ + selector: "heavy-component", + elementPath: ".performance-critical-element" +}); +``` + +--- + +## 🎉 Ready to Debug! + +The MCP debugging environment provides comprehensive tools for AI agents to inspect, monitor, and debug web components with full shadow DOM support and real-time event streaming. + +Start the server with `npm run dev:workspace` and begin debugging! \ No newline at end of file diff --git a/ai/workspace/components/example/component.css b/ai/workspace/components/example/component.css new file mode 100644 index 000000000..4418ce34f --- /dev/null +++ b/ai/workspace/components/example/component.css @@ -0,0 +1,182 @@ +:host { + --component-bg: var(--background-color, #ffffff); + --component-border: var(--border-color, #e1e5e9); + --component-text: var(--text-color, #333333); + --primary-color: var(--primary, #007bff); + --secondary-color: var(--secondary, #6c757d); + --success-color: var(--success, #28a745); + --spacing: var(--spacing-md, 1rem); + --border-radius: var(--radius, 6px); + + display: block; + font-family: system-ui, -apple-system, sans-serif; +} + +.example-component { + background: var(--component-bg); + border: 1px solid var(--component-border); + border-radius: var(--border-radius); + padding: var(--spacing); + color: var(--component-text); + box-shadow: 0 2px 4px rgba(0,0,0,0.1); +} + +.header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: var(--spacing); + padding-bottom: calc(var(--spacing) * 0.5); + border-bottom: 1px solid var(--component-border); +} + +.header h3 { + margin: 0; + color: var(--component-text); +} + +.content { + margin-bottom: var(--spacing); +} + +.counter { + text-align: center; + padding: var(--spacing); + background: #f8f9fa; + border-radius: var(--border-radius); + margin-bottom: var(--spacing); +} + +.counter p { + margin: 0 0 var(--spacing) 0; + font-size: 1.1rem; +} + +.increment-btn, .toggle-btn { + background: var(--primary-color); + color: white; + border: none; + padding: 0.75rem 1.5rem; + border-radius: var(--border-radius); + cursor: pointer; + font-size: 1rem; + transition: all 0.2s ease; +} + +.increment-btn:hover, .toggle-btn:hover { + transform: translateY(-1px); + box-shadow: 0 4px 8px rgba(0,0,0,0.2); +} + +.increment-btn:active, .toggle-btn:active { + transform: translateY(0); +} + +.increment-btn:disabled, .toggle-btn:disabled { + opacity: 0.6; + cursor: not-allowed; + transform: none !important; +} + +/* Theme variations */ +.example-component.primary .increment-btn { + background: var(--primary-color); +} + +.example-component.secondary .increment-btn { + background: var(--secondary-color); +} + +.example-component.success .increment-btn { + background: var(--success-color); +} + +.expanded-content { + background: #f1f3f4; + padding: var(--spacing); + border-radius: var(--border-radius); + margin-top: var(--spacing); + animation: slideDown 0.3s ease-out; +} + +@keyframes slideDown { + from { + opacity: 0; + transform: translateY(-10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.message-section { + margin-bottom: var(--spacing); +} + +.message-section label { + display: block; + margin-bottom: 0.5rem; + font-weight: 600; +} + +.message-input { + width: 100%; + padding: 0.75rem; + border: 1px solid var(--component-border); + border-radius: var(--border-radius); + font-size: 1rem; + box-sizing: border-box; +} + +.message-input:focus { + outline: none; + border-color: var(--primary-color); + box-shadow: 0 0 0 2px rgba(0,123,255,0.25); +} + +.message-display { + margin-top: 0.5rem; + font-size: 0.9rem; + color: var(--secondary-color); +} + +.debug-info { + background: white; + padding: var(--spacing); + border-radius: var(--border-radius); + border: 1px solid var(--component-border); +} + +.debug-info h4 { + margin: 0 0 0.75rem 0; + color: var(--component-text); +} + +.debug-info ul { + margin: 0; + padding-left: 1.5rem; +} + +.debug-info li { + margin-bottom: 0.25rem; +} + +.debug-info code { + background: #e9ecef; + padding: 0.125rem 0.375rem; + border-radius: 3px; + font-family: 'Courier New', monospace; + font-size: 0.9rem; +} + +.footer { + text-align: center; + padding-top: calc(var(--spacing) * 0.5); + border-top: 1px solid var(--component-border); + color: var(--secondary-color); +} + +.footer small { + font-size: 0.8rem; +} \ No newline at end of file diff --git a/ai/workspace/components/example/component.html b/ai/workspace/components/example/component.html new file mode 100644 index 000000000..353806e65 --- /dev/null +++ b/ai/workspace/components/example/component.html @@ -0,0 +1,52 @@ +
+
+

{title}

+
+ +
+
+ +
+
+

Button clicked: {clicks} times

+ +
+ + {#if isExpanded} +
+
+ + +

Current: {message}

+
+ +
+

Debug Information:

+
    +
  • Theme: {theme}
  • +
  • Disabled: {disabled}
  • +
  • Expanded: {isExpanded}
  • +
  • Total clicks: {clicks}
  • +
+
+
+ {/if} +
+ + +
\ No newline at end of file diff --git a/ai/workspace/components/example/component.js b/ai/workspace/components/example/component.js new file mode 100644 index 000000000..185cfb12a --- /dev/null +++ b/ai/workspace/components/example/component.js @@ -0,0 +1,83 @@ +import { defineComponent, getText } from '/packages/component/src/index.js'; + +const template = await getText('./component.html'); +const css = await getText('./component.css'); + +const defaultSettings = { + title: 'Example Component', + theme: 'primary', + count: 0, + disabled: false, +}; + +const defaultState = { + isExpanded: false, + clicks: 0, + message: 'Hello from AI Workspace!', +}; + +const createComponent = ({ self, state, settings, dispatchEvent }) => ({ + increment() { + state.clicks.increment(); + dispatchEvent('countChanged', { + count: state.clicks.get(), + component: 'example', + }); + }, + + toggleExpanded() { + state.isExpanded.toggle(); + console.log('Expanded state:', state.isExpanded.get()); + }, + + updateMessage(newMessage) { + state.message.set(newMessage); + }, + + getStatus() { + return { + clicks: state.clicks.get(), + expanded: state.isExpanded.get(), + theme: settings.theme, + title: settings.title, + }; + }, +}); + +const events = { + 'click .increment-btn': ({ self }) => { + self.increment(); + }, + + 'click .toggle-btn': ({ self }) => { + self.toggleExpanded(); + }, + + 'input .message-input': ({ state, value }) => { + state.message.set(value); + }, +}; + +const onCreated = ({ state, settings }) => { + console.log('Example component created with settings:', settings); + state.clicks.set(settings.count || 0); +}; + +const onRendered = ({ self, isClient }) => { + if (isClient) { + console.log('Example component rendered in browser'); + console.log('Component status:', self.getStatus()); + } +}; + +export const ExampleComponent = defineComponent({ + tagName: 'example', + template, + css, + defaultSettings, + defaultState, + createComponent, + events, + onCreated, + onRendered, +}); diff --git a/ai/workspace/components/test-component/component.js b/ai/workspace/components/test-component/component.js new file mode 100644 index 000000000..24df95241 --- /dev/null +++ b/ai/workspace/components/test-component/component.js @@ -0,0 +1,599 @@ +/** + * Test Component for MCP Debugging Environment + * Demonstrates various web component patterns and debugging scenarios + */ + +class TestComponent extends HTMLElement { + constructor() { + super(); + this.attachShadow({ mode: 'open' }); + this._counter = 0; + this._isExpanded = false; + this._theme = 'default'; + this._data = []; + this._timerId = null; + } + + static get observedAttributes() { + return ['theme', 'disabled', 'auto-increment']; + } + + connectedCallback() { + this.render(); + this.setupEventListeners(); + this.dispatchEvent( + new CustomEvent('test-component-connected', { + detail: { counter: this._counter }, + bubbles: true, + composed: true, + }), + ); + + // Auto-increment if enabled + if (this.hasAttribute('auto-increment')) { + this.startAutoIncrement(); + } + } + + disconnectedCallback() { + this.stopAutoIncrement(); + this.dispatchEvent( + new CustomEvent('test-component-disconnected', { + detail: { finalCounter: this._counter }, + bubbles: true, + composed: true, + }), + ); + } + + attributeChangedCallback(name, oldValue, newValue) { + switch (name) { + case 'theme': + this._theme = newValue || 'default'; + break; + case 'disabled': + this.disabled = newValue !== null; + break; + case 'auto-increment': + if (newValue !== null) { + this.startAutoIncrement(); + } + else { + this.stopAutoIncrement(); + } + break; + } + + if (this.shadowRoot) { + this.render(); + } + } + + render() { + this.shadowRoot.innerHTML = ` + + +
+
+ 🧪 Test Component + AUTO +
+
${this._theme}
+
+ +
+
+ + Counter: ${this._counter} +
+
+ + + + + +
+
+ +
+
+ 📊 Data & State (Click to ${this._isExpanded ? 'collapse' : 'expand'}) + ${this._isExpanded ? '▲' : '▼'} +
+
+
+ Internal State: +
+ Counter: ${this._counter} + Type: ${typeof this._counter} +
+
+ Expanded: ${this._isExpanded} + Theme: ${this._theme} +
+
+ Data Items: ${this._data.length} + Auto-increment: ${this.hasAttribute('auto-increment')} +
+
+ + + + + ${ + this._data.length > 0 + ? ` +
+ Data Array: + ${ + this._data.map((item, index) => ` +
+ ${index}: ${JSON.stringify(item)} + +
+ `).join('') + } +
+ ` + : '' + } +
+
+ +
+ 🎯 Slot content will appear here +
+ +
+Debug Info: +- Component ID: ${this.id || 'No ID set'} +- Classes: ${this.className || 'No classes'} +- Shadow DOM: ${this.shadowRoot ? 'Attached' : 'Not attached'} +- Connected: ${this.isConnected} +- Disabled: ${this.hasAttribute('disabled')} +- Render time: ${new Date().toLocaleTimeString()} +
+ `; + } + + setupEventListeners() { + const shadowRoot = this.shadowRoot; + + // Increment button + shadowRoot.getElementById('increment-btn')?.addEventListener('click', () => { + this.increment(); + }); + + // Increment by 5 + shadowRoot.getElementById('increment-5-btn')?.addEventListener('click', () => { + this.increment(5); + }); + + // Reset button + shadowRoot.getElementById('reset-btn')?.addEventListener('click', () => { + this.reset(); + }); + + // Random button + shadowRoot.getElementById('random-btn')?.addEventListener('click', () => { + this.setCounter(Math.floor(Math.random() * 100)); + }); + + // Error button (for testing error handling) + shadowRoot.getElementById('error-btn')?.addEventListener('click', () => { + this.triggerError(); + }); + + // Expand/collapse + shadowRoot.getElementById('expand-header')?.addEventListener('click', () => { + this.toggleExpanded(); + }); + + // Add data button + shadowRoot.getElementById('add-data-btn')?.addEventListener('click', () => { + this.addRandomData(); + }); + + // Clear data button + shadowRoot.getElementById('clear-data-btn')?.addEventListener('click', () => { + this.clearData(); + }); + + // Remove data buttons + shadowRoot.querySelectorAll('.remove-data').forEach(btn => { + btn.addEventListener('click', (e) => { + const index = parseInt(e.target.dataset.index); + this.removeData(index); + }); + }); + } + + // Public API methods + increment(amount = 1) { + const oldValue = this._counter; + this._counter += amount; + this.render(); + this.dispatchEvent( + new CustomEvent('counter-changed', { + detail: { + oldValue, + newValue: this._counter, + amount, + }, + bubbles: true, + composed: true, + }), + ); + } + + reset() { + const oldValue = this._counter; + this._counter = 0; + this.render(); + this.dispatchEvent( + new CustomEvent('counter-reset', { + detail: { oldValue }, + bubbles: true, + composed: true, + }), + ); + } + + setCounter(value) { + const oldValue = this._counter; + this._counter = value; + this.render(); + this.dispatchEvent( + new CustomEvent('counter-set', { + detail: { oldValue, newValue: value }, + bubbles: true, + composed: true, + }), + ); + } + + toggleExpanded() { + this._isExpanded = !this._isExpanded; + this.render(); + this.dispatchEvent( + new CustomEvent('section-toggled', { + detail: { expanded: this._isExpanded }, + bubbles: true, + composed: true, + }), + ); + } + + addRandomData() { + const newItem = { + id: Date.now(), + value: Math.floor(Math.random() * 1000), + timestamp: new Date().toISOString(), + type: ['string', 'number', 'boolean'][Math.floor(Math.random() * 3)], + }; + + this._data.push(newItem); + this.render(); + this.dispatchEvent( + new CustomEvent('data-added', { + detail: { item: newItem, total: this._data.length }, + bubbles: true, + composed: true, + }), + ); + } + + clearData() { + const count = this._data.length; + this._data = []; + this.render(); + this.dispatchEvent( + new CustomEvent('data-cleared', { + detail: { clearedCount: count }, + bubbles: true, + composed: true, + }), + ); + } + + removeData(index) { + if (index >= 0 && index < this._data.length) { + const removed = this._data.splice(index, 1)[0]; + this.render(); + this.dispatchEvent( + new CustomEvent('data-removed', { + detail: { item: removed, index, remaining: this._data.length }, + bubbles: true, + composed: true, + }), + ); + } + } + + startAutoIncrement() { + this.stopAutoIncrement(); + this._timerId = setInterval(() => { + this.increment(); + }, 2000); + this.dispatchEvent( + new CustomEvent('auto-increment-started', { + bubbles: true, + composed: true, + }), + ); + } + + stopAutoIncrement() { + if (this._timerId) { + clearInterval(this._timerId); + this._timerId = null; + this.dispatchEvent( + new CustomEvent('auto-increment-stopped', { + bubbles: true, + composed: true, + }), + ); + } + } + + triggerError() { + this.dispatchEvent( + new CustomEvent('error-triggered', { + detail: { + message: 'Intentional test error', + timestamp: Date.now(), + counter: this._counter, + }, + bubbles: true, + composed: true, + }), + ); + + // Simulate an actual error for testing + setTimeout(() => { + throw new Error('Test component intentional error for debugging'); + }, 100); + } + + // Getters for debugging + get counter() { + return this._counter; + } + + get theme() { + return this._theme; + } + + get isExpanded() { + return this._isExpanded; + } + + get data() { + return [...this._data]; // Return copy + } + + get state() { + return { + counter: this._counter, + theme: this._theme, + expanded: this._isExpanded, + dataCount: this._data.length, + autoIncrement: this.hasAttribute('auto-increment'), + disabled: this.hasAttribute('disabled'), + }; + } +} + +// Register the component +customElements.define('test-component', TestComponent); + +// Export for module usage +export { TestComponent }; diff --git a/ai/workspace/debug-bridge.js b/ai/workspace/debug-bridge.js new file mode 100644 index 000000000..fd9020d76 --- /dev/null +++ b/ai/workspace/debug-bridge.js @@ -0,0 +1,604 @@ +/** + * Debug Bridge - Browser-side debugging infrastructure for web components + * Provides MCP tool interface for component inspection and manipulation + */ +window.__debugBridge = { + ws: null, + componentRegistry: new WeakMap(), + eventListeners: new Map(), + mutationObservers: new Map(), + + connect(url) { + this.ws = new WebSocket(url); + + this.ws.onopen = () => { + console.log('🔗 Debug bridge connected to MCP server'); + }; + + this.ws.onmessage = async (event) => { + const { type, tool, params, id } = JSON.parse(event.data); + + if (type === 'mcp-tool-request') { + console.log(`🛠️ Executing tool: ${tool}`, params); + try { + const result = await this.handleToolRequest(tool, params); + this.ws.send(JSON.stringify({ + type: 'mcp-tool-response', + id, + result, + })); + } + catch (error) { + this.ws.send(JSON.stringify({ + type: 'mcp-tool-response', + id, + error: { + message: error.message, + stack: error.stack, + params, + }, + })); + } + } + }; + + this.ws.onerror = (error) => { + console.error('❌ Debug bridge WebSocket error:', error); + }; + + // Auto-register components + this.setupComponentTracking(); + }, + + setupComponentTracking() { + // Intercept customElements.define to track new component types + const originalDefine = customElements.define; + customElements.define = function(name, constructor, options) { + console.log(`📝 Registering component type: ${name}`); + originalDefine.call(this, name, constructor, options); + + // Track all existing instances + setTimeout(() => { + document.querySelectorAll(name).forEach(el => { + window.__debugBridge.registerComponent(el); + }); + }, 0); + }; + + // Monitor for new component instances + const observer = new MutationObserver((mutations) => { + mutations.forEach(mutation => { + mutation.addedNodes.forEach(node => { + if (node.nodeType === 1 && node.tagName.includes('-')) { + this.registerComponent(node); + } + // Also check for nested components + if (node.nodeType === 1 && node.querySelectorAll) { + node.querySelectorAll('*').forEach(child => { + if (child.tagName.includes('-')) { + this.registerComponent(child); + } + }); + } + }); + }); + }); + + observer.observe(document.body, { + childList: true, + subtree: true, + }); + + // Register existing components + document.querySelectorAll('*').forEach(el => { + if (el.tagName.includes('-')) { + this.registerComponent(el); + } + }); + }, + + registerComponent(element) { + if (this.componentRegistry.has(element)) { return; } + + console.log(`🧩 Registering component instance: ${element.tagName.toLowerCase()}`); + + const componentData = { + tagName: element.tagName.toLowerCase(), + shadowRoot: element.shadowRoot, + attributes: [...element.attributes].map(a => ({ + name: a.name, + value: a.value, + })), + properties: {}, + events: [], + mutations: [], + registeredAt: Date.now(), + }; + + this.componentRegistry.set(element, componentData); + + // Set up mutation observer for shadow DOM + if (element.shadowRoot) { + const observer = new MutationObserver((mutations) => { + const data = this.componentRegistry.get(element); + if (data) { + data.mutations.push(...mutations.map(m => ({ + type: m.type, + target: m.target.tagName || m.target.nodeName, + timestamp: Date.now(), + addedNodes: m.addedNodes.length, + removedNodes: m.removedNodes.length, + attributeName: m.attributeName, + oldValue: m.oldValue, + }))); + + // Keep only last 50 mutations + if (data.mutations.length > 50) { + data.mutations = data.mutations.slice(-50); + } + } + }); + + observer.observe(element.shadowRoot, { + childList: true, + attributes: true, + characterData: true, + subtree: true, + attributeOldValue: true, + characterDataOldValue: true, + }); + + this.mutationObservers.set(element, observer); + } + }, + + async handleToolRequest(tool, params) { + switch (tool) { + case 'inspect_component': + return this.inspectComponent(params.selector, params.depth); + + case 'execute_in_component': + return this.executeInComponent(params.selector, params.code); + + case 'monitor_events': + return this.monitorEvents(params.selector, params.events); + + case 'get_computed_styles': + return this.getComputedStyles(params.selector, params.elementPath); + + case 'mutation_history': + return this.getMutationHistory(params.selector, params.count); + + case 'list_components': + return this.listComponents(); + + case 'query_shadow_dom': + return this.queryShadowDOM(params.selector, params.query); + + case 'get_component_state': + return this.getComponentState(params.selector); + + case 'take_screenshot': + return this.takeScreenshot(params.selector); + + default: + throw new Error(`Unknown tool: ${tool}`); + } + }, + + // Enhanced $ function with shadow DOM piercing + $(selector, root = document) { + // If it's already an element, return it + if (selector instanceof Element) { return selector; } + + // Try normal query first + let element = root.querySelector(selector); + if (element) { return element; } + + // Deep search through shadow DOMs + const walker = document.createTreeWalker( + root, + NodeFilter.SHOW_ELEMENT, + { + acceptNode: (node) => { + if (node.shadowRoot) { + const found = node.shadowRoot.querySelector(selector); + if (found) { + element = found; + return NodeFilter.FILTER_REJECT; + } + } + return NodeFilter.FILTER_SKIP; + }, + }, + ); + + while (walker.nextNode() && !element) { + // Walker handles the traversal + } + + return element; + }, + + inspectComponent(selector, depth = 3) { + const element = this.$(selector); + if (!element) { throw new Error(`Component not found: ${selector}`); } + + const data = this.componentRegistry.get(element); + if (!data) { + // Register it now if not already registered + this.registerComponent(element); + } + + const result = { + tagName: element.tagName.toLowerCase(), + id: element.id, + classes: [...element.classList], + attributes: [...element.attributes].map(a => ({ + name: a.name, + value: a.value, + })), + properties: this.getComponentProperties(element), + shadowRoot: null, + lightDOM: this.serializeDOM(element, Math.min(depth, 2)), + boundingRect: element.getBoundingClientRect(), + registeredEvents: data ? data.events.length : 0, + mutationCount: data ? data.mutations.length : 0, + }; + + if (element.shadowRoot && depth > 0) { + result.shadowRoot = this.serializeDOM(element.shadowRoot, depth - 1); + } + + return result; + }, + + executeInComponent(selector, code) { + const element = this.$(selector); + if (!element) { throw new Error(`Component not found: ${selector}`); } + + // Create safe execution context + const context = { + host: element, + shadow: element.shadowRoot, + $: (sel) => this.$(sel, element.shadowRoot || element), + $$: (sel) => { + const results = []; + if (element.shadowRoot) { + results.push(...element.shadowRoot.querySelectorAll(sel)); + } + results.push(...element.querySelectorAll(sel)); + return results; + }, + document: element.shadowRoot || document, + getComputedStyle: window.getComputedStyle.bind(window), + console: { + log: (...args) => { + console.log('[Component Execution]', ...args); + return args; + }, + }, + }; + + // Execute with error handling + try { + const fn = new Function(...Object.keys(context), `"use strict"; return (${code})`); + return fn(...Object.values(context)); + } + catch (error) { + throw new Error(`Execution failed: ${error.message}`); + } + }, + + monitorEvents(selector, events) { + const element = this.$(selector); + if (!element) { throw new Error(`Component not found: ${selector}`); } + + const key = `${selector}:${events.join(',')}`; + + // Remove existing listeners + if (this.eventListeners.has(key)) { + const oldListeners = this.eventListeners.get(key); + oldListeners.forEach(({ event, handler }) => { + element.removeEventListener(event, handler); + }); + } + + // Add new listeners + const listeners = events.map(event => { + const handler = (e) => { + const eventData = { + type: 'event-triggered', + selector, + event, + detail: { + type: e.type, + target: e.target.tagName || e.target.nodeName, + currentTarget: e.currentTarget.tagName || e.currentTarget.nodeName, + composed: e.composed, + bubbles: e.bubbles, + detail: e.detail, + timestamp: Date.now(), + path: e.composedPath ? e.composedPath().map(n => n.tagName || n.nodeName).slice(0, 5) : [], + }, + }; + + console.log('📡 Event captured:', eventData); + + if (this.ws && this.ws.readyState === WebSocket.OPEN) { + this.ws.send(JSON.stringify(eventData)); + } + }; + + element.addEventListener(event, handler, true); + return { event, handler }; + }); + + this.eventListeners.set(key, listeners); + return { status: 'monitoring', events, selector, listenersActive: listeners.length }; + }, + + getComputedStyles(selector, elementPath) { + const component = this.$(selector); + if (!component) { throw new Error(`Component not found: ${selector}`); } + + let element = component; + + // Navigate to specific element within shadow DOM + if (elementPath) { + if (component.shadowRoot) { + element = component.shadowRoot.querySelector(elementPath); + } + else { + element = component.querySelector(elementPath); + } + if (!element) { throw new Error(`Element not found: ${elementPath}`); } + } + + const computed = window.getComputedStyle(element); + const styles = {}; + + // Get relevant computed styles (not all 300+ properties) + const importantProps = [ + 'display', + 'position', + 'width', + 'height', + 'margin', + 'padding', + 'border', + 'background', + 'color', + 'font-family', + 'font-size', + 'font-weight', + 'z-index', + 'opacity', + 'transform', + 'transition', + 'box-shadow', + 'border-radius', + 'overflow', + 'visibility', + ]; + + importantProps.forEach(prop => { + styles[prop] = computed.getPropertyValue(prop); + }); + + // Get CSS custom properties + const customProps = {}; + for (let i = 0; i < computed.length; i++) { + const prop = computed[i]; + if (prop.startsWith('--')) { + customProps[prop] = computed.getPropertyValue(prop); + } + } + + return { + selector, + elementPath, + tagName: element.tagName.toLowerCase(), + styles, + customProperties: customProps, + boundingBox: element.getBoundingClientRect(), + }; + }, + + getMutationHistory(selector, count = 10) { + const element = this.$(selector); + if (!element) { throw new Error(`Component not found: ${selector}`); } + + const data = this.componentRegistry.get(element); + if (!data) { throw new Error(`Component not registered: ${selector}`); } + + return { + selector, + mutations: data.mutations.slice(-count), + total: data.mutations.length, + registeredAt: data.registeredAt, + }; + }, + + listComponents() { + const components = []; + + document.querySelectorAll('*').forEach(el => { + if (el.tagName.includes('-')) { + const data = this.componentRegistry.get(el); + components.push({ + tagName: el.tagName.toLowerCase(), + id: el.id || null, + classes: [...el.classList], + hasShadowRoot: !!el.shadowRoot, + isRegistered: !!data, + boundingRect: el.getBoundingClientRect(), + }); + } + }); + + return { + total: components.length, + components, + }; + }, + + queryShadowDOM(selector, query) { + const component = this.$(selector); + if (!component) { throw new Error(`Component not found: ${selector}`); } + + if (!component.shadowRoot) { + throw new Error(`Component has no shadow root: ${selector}`); + } + + const elements = [...component.shadowRoot.querySelectorAll(query)]; + + return { + selector, + query, + found: elements.length, + elements: elements.map(el => ({ + tagName: el.tagName.toLowerCase(), + id: el.id || null, + classes: [...el.classList], + attributes: [...el.attributes].map(a => ({ name: a.name, value: a.value })), + textContent: el.textContent.trim().substring(0, 100), + boundingRect: el.getBoundingClientRect(), + })), + }; + }, + + getComponentState(selector) { + const element = this.$(selector); + if (!element) { throw new Error(`Component not found: ${selector}`); } + + // Try to get component state through common patterns + const state = { + properties: this.getComponentProperties(element), + attributes: [...element.attributes].map(a => ({ name: a.name, value: a.value })), + internalState: null, + methods: [], + }; + + // Look for common state patterns + if (element._state) { state.internalState = element._state; } + if (element.state) { state.internalState = element.state; } + if (element.data) { state.internalState = element.data; } + + // Get available methods + const proto = Object.getPrototypeOf(element); + Object.getOwnPropertyNames(proto).forEach(name => { + if ( + typeof element[name] === 'function' + && !name.startsWith('_') + && name !== 'constructor' + && !['addEventListener', 'removeEventListener', 'dispatchEvent'].includes(name) + ) { + state.methods.push(name); + } + }); + + return state; + }, + + takeScreenshot(selector) { + const element = this.$(selector); + if (!element) { throw new Error(`Component not found: ${selector}`); } + + // Basic implementation - would need html2canvas for actual screenshots + const rect = element.getBoundingClientRect(); + + return { + selector, + boundingBox: { + top: rect.top, + left: rect.left, + width: rect.width, + height: rect.height, + right: rect.right, + bottom: rect.bottom, + }, + viewport: { + width: window.innerWidth, + height: window.innerHeight, + }, + note: 'For actual screenshots, html2canvas library would be needed', + }; + }, + + // Helper methods + serializeDOM(root, maxDepth, currentDepth = 0) { + if (currentDepth >= maxDepth) { return { type: 'truncated', maxDepthReached: true }; } + + const children = []; + for (const child of root.children || []) { + children.push({ + tagName: child.tagName.toLowerCase(), + id: child.id || null, + classes: [...child.classList], + attributes: [...child.attributes].map(a => ({ + name: a.name, + value: a.value, + })), + textContent: child.textContent ? child.textContent.trim().substring(0, 100) : '', + children: this.serializeDOM(child, maxDepth, currentDepth + 1), + }); + } + + return { + type: root.nodeType === 11 ? 'shadow-root' : 'element', + childCount: children.length, + children, + }; + }, + + getComponentProperties(element) { + const props = {}; + const proto = Object.getPrototypeOf(element); + + // Get all property descriptors + Object.getOwnPropertyNames(proto).forEach(name => { + if (name === 'constructor') { return; } + + try { + const descriptor = Object.getOwnPropertyDescriptor(proto, name); + if (descriptor && (descriptor.get || descriptor.value !== undefined)) { + const value = element[name]; + + // Only include serializable values + if ( + typeof value === 'string' + || typeof value === 'number' + || typeof value === 'boolean' + || value === null + || value === undefined + ) { + props[name] = value; + } + else if (typeof value === 'object' && value !== element) { + props[name] = '[Object]'; + } + else if (typeof value === 'function') { + props[name] = '[Function]'; + } + } + } + catch (e) { + // Some properties might throw + props[name] = '[Error accessing property]'; + } + }); + + return props; + }, +}; + +// Auto-connect when script loads +document.addEventListener('DOMContentLoaded', () => { + console.log('🚀 Debug bridge initializing...'); + + // Make $ function globally available + window.$ = window.__debugBridge.$.bind(window.__debugBridge); + + console.log('✅ Debug bridge ready'); +}); diff --git a/ai/workspace/mcp-tools.js b/ai/workspace/mcp-tools.js new file mode 100644 index 000000000..f9fd9a07b --- /dev/null +++ b/ai/workspace/mcp-tools.js @@ -0,0 +1,375 @@ +/** + * MCP Tool Definitions for Web Component Debugging + * These schemas define the available debugging tools for AI agents + */ + +export const debugTools = { + inspect_component: { + description: "Inspect a web component's structure, attributes, shadow DOM, and current state", + parameters: { + type: 'object', + properties: { + selector: { + type: 'string', + description: + "CSS selector, component ID, or tag name to find the component. Examples: '#my-component', 'ui-dropdown', '.my-class'", + }, + depth: { + type: 'number', + description: 'How deep to traverse shadow DOM tree (default: 3, max recommended: 5)', + default: 3, + minimum: 1, + maximum: 10, + }, + }, + required: ['selector'], + }, + examples: [ + { + description: 'Inspect a component by ID', + parameters: { selector: '#main-dropdown', depth: 2 }, + }, + { + description: 'Deep inspection of component structure', + parameters: { selector: 'test-component', depth: 4 }, + }, + ], + }, + + execute_in_component: { + description: + "Execute JavaScript code within a component's context with access to shadow DOM and component internals", + parameters: { + type: 'object', + properties: { + selector: { + type: 'string', + description: 'Component selector to execute code within', + }, + code: { + type: 'string', + description: + 'JavaScript code to execute. Available variables: host (component element), shadow (shadowRoot), $ (shadow-aware query), $$ (query all), document (shadow document), console', + }, + }, + required: ['selector', 'code'], + }, + examples: [ + { + description: "Check component's internal counter value", + parameters: { + selector: '#counter-component', + code: "host.counter || host._counter || 'Counter not found'", + }, + }, + { + description: 'Find all buttons in shadow DOM', + parameters: { + selector: 'ui-modal', + code: "$$('button').length", + }, + }, + { + description: "Get component's current state", + parameters: { + selector: 'test-component', + code: '({ counter: host.counter, shadowHTML: shadow.innerHTML.length, classList: [...host.classList] })', + }, + }, + ], + }, + + monitor_events: { + description: 'Start monitoring specific events on a component. Events will be streamed in real-time via WebSocket', + parameters: { + type: 'object', + properties: { + selector: { + type: 'string', + description: 'Component selector to monitor', + }, + events: { + type: 'array', + items: { type: 'string' }, + description: 'Array of event names to monitor. Common events: click, change, input, custom events', + }, + }, + required: ['selector', 'events'], + }, + examples: [ + { + description: 'Monitor user interactions', + parameters: { + selector: '#interactive-component', + events: ['click', 'mouseover', 'focus'], + }, + }, + { + description: 'Monitor custom component events', + parameters: { + selector: 'ui-dropdown', + events: ['selection-changed', 'dropdown-opened', 'dropdown-closed'], + }, + }, + ], + }, + + get_computed_styles: { + description: 'Get computed CSS styles for a component or element within its shadow DOM', + parameters: { + type: 'object', + properties: { + selector: { + type: 'string', + description: 'Component selector', + }, + elementPath: { + type: 'string', + description: + "Optional CSS selector for element within component's shadow DOM. If not provided, styles of the component itself are returned", + }, + }, + required: ['selector'], + }, + examples: [ + { + description: "Get component's own styles", + parameters: { selector: 'test-component' }, + }, + { + description: 'Get styles of button inside component', + parameters: { + selector: 'test-component', + elementPath: 'button', + }, + }, + { + description: 'Get styles of specific element', + parameters: { + selector: 'ui-modal', + elementPath: '.modal-header .close-button', + }, + }, + ], + }, + + mutation_history: { + description: "Get recent DOM mutations within a component's shadow DOM for debugging dynamic changes", + parameters: { + type: 'object', + properties: { + selector: { + type: 'string', + description: 'Component selector', + }, + count: { + type: 'number', + description: 'Number of recent mutations to retrieve (default: 10, max: 50)', + default: 10, + minimum: 1, + maximum: 50, + }, + }, + required: ['selector'], + }, + examples: [ + { + description: 'Get last 5 DOM changes', + parameters: { selector: 'dynamic-component', count: 5 }, + }, + { + description: 'Get full mutation history', + parameters: { selector: 'test-component', count: 50 }, + }, + ], + }, + + list_components: { + description: 'List all web components currently on the page with their basic information', + parameters: { + type: 'object', + properties: {}, + additionalProperties: false, + }, + examples: [ + { + description: 'Get overview of all components', + parameters: {}, + }, + ], + }, + + query_shadow_dom: { + description: "Query for elements within a component's shadow DOM using CSS selectors", + parameters: { + type: 'object', + properties: { + selector: { + type: 'string', + description: 'Component selector', + }, + query: { + type: 'string', + description: "CSS selector to find elements within the component's shadow DOM", + }, + }, + required: ['selector', 'query'], + }, + examples: [ + { + description: 'Find all buttons in component', + parameters: { + selector: 'ui-modal', + query: 'button', + }, + }, + { + description: 'Find elements with specific class', + parameters: { + selector: 'test-component', + query: '.interactive-element', + }, + }, + { + description: 'Find form inputs', + parameters: { + selector: 'ui-form', + query: 'input, select, textarea', + }, + }, + ], + }, + + get_component_state: { + description: "Get a component's internal state, properties, and available methods for debugging", + parameters: { + type: 'object', + properties: { + selector: { + type: 'string', + description: 'Component selector', + }, + }, + required: ['selector'], + }, + examples: [ + { + description: "Get component's current state", + parameters: { selector: 'stateful-component' }, + }, + { + description: 'Check component properties and methods', + parameters: { selector: '#dynamic-widget' }, + }, + ], + }, + + take_screenshot: { + description: + 'Get component dimensions and position information (note: actual screenshot requires additional libraries)', + parameters: { + type: 'object', + properties: { + selector: { + type: 'string', + description: 'Component selector', + }, + }, + required: ['selector'], + }, + examples: [ + { + description: 'Get component layout info', + parameters: { selector: 'ui-modal' }, + }, + ], + }, +}; + +/** + * Tool usage guidelines for AI agents + */ +export const usageGuidelines = { + debugging_workflow: [ + "1. Start with 'list_components' to see all available components", + "2. Use 'inspect_component' to understand structure and current state", + "3. Use 'execute_in_component' to test specific functionality", + "4. Monitor behavior with 'monitor_events' for interactive debugging", + "5. Check styling issues with 'get_computed_styles'", + "6. Track dynamic changes with 'mutation_history'", + ], + + best_practices: { + selectors: [ + 'Use specific selectors when possible (IDs or tag names)', + "For custom components, use tag names like 'ui-dropdown' or 'test-component'", + "Fallback to class selectors '.my-component' if needed", + ], + + code_execution: [ + 'Keep executed code simple and focused', + 'Use the provided context variables (host, shadow, $, $$)', + 'Return serializable data (avoid circular references)', + 'Use console.log for debugging output', + ], + + event_monitoring: [ + 'Start with common events: click, change, input', + 'Add custom component events based on component documentation', + 'Monitor for short periods to avoid overwhelming the system', + ], + }, + + common_patterns: { + finding_components: 'list_components() → inspect_component(selector)', + debugging_interactions: "monitor_events(selector, ['click']) → execute_in_component(selector, code)", + styling_issues: 'inspect_component(selector) → get_computed_styles(selector, elementPath)', + state_debugging: "get_component_state(selector) → execute_in_component(selector, 'return host.someProperty')", + }, +}; + +/** + * Event types that components commonly emit + */ +export const commonEvents = { + user_interactions: [ + 'click', + 'dblclick', + 'mousedown', + 'mouseup', + 'mouseover', + 'mouseout', + 'focus', + 'blur', + 'keydown', + 'keyup', + 'input', + 'change', + 'submit', + ], + + semantic_ui_components: [ + 'selection-changed', + 'item-selected', + 'dropdown-opened', + 'dropdown-closed', + 'modal-opened', + 'modal-closed', + 'tab-changed', + 'accordion-expanded', + 'form-validated', + 'field-changed', + 'button-clicked', + ], + + lifecycle_events: [ + 'component-created', + 'component-rendered', + 'component-destroyed', + 'state-changed', + 'settings-updated', + 'data-loaded', + ], +}; + +export default debugTools; diff --git a/ai/workspace/public/component.html b/ai/workspace/public/component.html new file mode 100644 index 000000000..2b5ab7938 --- /dev/null +++ b/ai/workspace/public/component.html @@ -0,0 +1,447 @@ + + + + + + Component Tester - AI Workspace + + + + +
+
+
+

Component Tester

+ No component loaded +
+
+ + +
+
+ +
+ + +
+

Loading component...

+
+
+
+ +
+
+

🛠️ Debug Tools

+
+ +
+
+

Query Tester

+ +

+ + +
Run a query to see results...
+
+ +
+

Component Inspector

+ + +
Click a button to inspect components...
+
+ +
+

Shadow DOM Explorer

+ + +
Explore shadow DOM structure...
+
+ +
+

Console Log

+ +
Console output will appear here...
+
+
+
+ +
+

Build Error

+
+

+
+ + + + \ No newline at end of file diff --git a/ai/workspace/public/index.html b/ai/workspace/public/index.html new file mode 100644 index 000000000..4521baf0a --- /dev/null +++ b/ai/workspace/public/index.html @@ -0,0 +1,384 @@ + + + + + + AI Workspace - MCP Debugging Environment + + + +
+ 🔌 Connecting to MCP... +
+ +
+
+

🧪 AI Workspace

+

MCP-Enabled Web Component Debugging Environment

+
+ +
+
+
+
WebSocket
+ Connecting... +
+
+
+
Debug Bridge
+ Initializing... +
+
+
+
Components
+ Scanning... +
+
+ +
+

🛠️ Available MCP Tools

+

AI agents can use these tools to inspect and debug web components:

+
+
+ inspect_component
+ Deep component analysis +
+
+ execute_in_component
+ Run code in component context +
+
+ monitor_events
+ Real-time event streaming +
+
+ query_shadow_dom
+ Shadow DOM querying +
+
+ get_computed_styles
+ CSS inspection +
+
+ list_components
+ Component discovery +
+
+
+ +
+

🎯 Live Test Component

+

Interact with this component while an AI agent debugs it in real-time:

+ +

🎪 This is slotted content inside the test component!

+
+
+ +
+
+

🚀 Quick Start

+
    +
  1. Create components in components/my-component/
  2. +
  3. Use MCP tools to inspect and debug
  4. +
  5. Test with real-time event monitoring
  6. +
  7. Iterate based on debugging insights
  8. +
+
+ + + +
+

📊 Debug Features

+
    +
  • 🔍 Shadow DOM inspection
  • +
  • ⚡ Real-time event monitoring
  • +
  • 🎨 CSS computed styles analysis
  • +
  • 🧠 Component state introspection
  • +
  • 💾 DOM mutation tracking
  • +
  • 🎯 Code execution in component context
  • +
+
+ +
+

📚 Documentation

+

Access comprehensive component development guides:

+ +
+
+ +
+

🔍 Discovered Components

+
+

Scanning for components...

+
+
+
+ + + + + \ No newline at end of file diff --git a/docs/src/pages/api/query/attributes.mdx b/docs/src/pages/api/query/attributes.mdx index 673ca9311..5c88148b4 100755 --- a/docs/src/pages/api/query/attributes.mdx +++ b/docs/src/pages/api/query/attributes.mdx @@ -89,3 +89,91 @@ $('selector').removeAttr(attributeName) // Remove the 'disabled' attribute from all buttons $('button').removeAttr('disabled'); ``` + +## data + +Gets or sets data attributes on elements. Data attributes are HTML attributes that start with `data-` and are commonly used to store custom data. + +### Syntax + +#### Get All Data Attributes +```javascript +$('selector').data() +``` + +#### Get Specific Data Attribute +```javascript +$('selector').data(key) +``` + +#### Set Data Attribute +```javascript +$('selector').data(key, value) +``` + +### Parameters +| Name | Type | Description | +|-------|--------|---------------------------------| +| key | string | The data attribute key (without 'data-' prefix) | +| value | string | The value to set | + +### Returns + +#### Get All Data Attributes +- **Single Element** An object containing all data attributes. +- **Multiple Elements** An array of objects, one for each matched element. + +#### Get Specific Data Attribute +- **Single Element** The data attribute value. +- **Multiple Elements** An array of values, one for each matched element. + +#### Set Data Attribute +[Query object](/query/basic#the-query-object) (for chaining). + +### Usage + +#### Get All Data Attributes +```javascript +// HTML:
+const allData = $('div').data(); +console.log(allData); // { userId: "123", role: "admin" } +``` + +#### Get Specific Data Attribute +```javascript +// HTML:
+const userId = $('div').data('userId'); +console.log(userId); // "123" +``` + +#### Set Data Attribute +```javascript +// Set a data attribute +$('div').data('theme', 'dark'); +// Results in:
+``` + +#### Working with Multiple Elements +```javascript +// HTML: +//
+//
+ +// Get specific attribute from multiple elements +const ids = $('div').data('id'); +console.log(ids); // ["1", "2"] + +// Get all data from multiple elements +const allData = $('div').data(); +console.log(allData); // [{ id: "1", name: "Alice" }, { id: "2", name: "Bob" }] + +// Set data on multiple elements +$('div').data('active', 'true'); +// Both divs now have data-active="true" +``` + +### Notes + +- Data attribute keys are automatically converted from camelCase to kebab-case in the HTML (e.g., `userId` becomes `data-user-id`) +- When retrieving data, kebab-case attribute names are converted back to camelCase object keys +- Data attributes are always stored and retrieved as strings diff --git a/docs/src/pages/api/query/dimensions.mdx b/docs/src/pages/api/query/dimensions.mdx index cf2b2809d..dbc415c93 100755 --- a/docs/src/pages/api/query/dimensions.mdx +++ b/docs/src/pages/api/query/dimensions.mdx @@ -247,3 +247,128 @@ console.log(imageNaturalHeight); const divNaturalHeight = $('div.content').naturalHeight(); console.log(divNaturalHeight); ``` + +## clippingParent + +Get the element that clips (crops) each element's visual bounds based on overflow properties. + +### Syntax +```javascript +$('selector').clippingParent() +``` + +### Returns +A new [Query object](/api/query/basic#the-query-object) containing the clipping parent elements. + +### Usage + +#### Basic Clipping Detection +```javascript +// Find what element clips a positioned element +const $tooltip = $('.tooltip'); +const $clipper = $tooltip.clippingParent(); + +// Check if element is clipped by its container +if ($clipper[0] !== document.documentElement) { + console.log('Element is clipped by:', $clipper[0]); +} +``` + +#### Working with Scroll Containers +```javascript +// Find the scrollable container that clips content +const $content = $('.overflow-content'); +const $scrollContainer = $content.clippingParent(); + +// Adjust positioning to avoid clipping +$scrollContainer.css('overflow', 'visible'); +``` + +#### Multiple Elements +```javascript +// Get clipping parents for multiple elements +$('.positioned-elements').each((el, index) => { + const $el = $(el); + const $clipper = $el.clippingParent(); + console.log(`Element ${index} is clipped by:`, $clipper[0]); +}); +``` + +> **Clipping Detection** Returns `document.documentElement` when no clipping parent is found. Useful for positioning tooltips, dropdowns, and other overlay elements. + +## containingParent + +Get the element that establishes the positioning context for each element (where `offsetTop`/`offsetLeft` are relative to). + +> **Why not `offsetParent`?** `offsetParent` will often return an element that is **different** than the value `offsetTop` and `offsetLeft` are relative to. This is because values like `transform`, `will-change`, `filter` can all create new positioning contexts. + + +### Syntax + +#### Calculate Modern Positioning Context +```javascript +$('selector').containingParent() +$('selector').containingParent({ calculate: true }) +``` + +#### Use Browser's offsetParent +```javascript +$('selector').containingParent({ calculate: false }) +``` + +### Parameters +| Name | Type | Description | +|-----------|---------|------------------------------------------------------------------| +| calculate | boolean | Whether to calculate containing parent using modern CSS properties (default: true) | + +### Returns +A new [Query object](/api/query/basic#the-query-object) containing the containing parent elements. + +### Usage + +#### Modern Positioning Context Detection +```javascript +// Find the actual positioning context (includes transform, filter, etc.) +const $absoluteEl = $('.absolute-positioned'); +const $container = $absoluteEl.containingParent(); + +// Position element relative to its containing parent +const containerRect = $container[0].getBoundingClientRect(); +$absoluteEl.css({ + top: containerRect.height / 2, + left: containerRect.width / 2 +}); +``` + +#### Browser's Native offsetParent +```javascript +// Get browser's reported offsetParent (legacy behavior) +const $element = $('.positioned-element'); +const $offsetParent = $element.containingParent({ calculate: false }); + +if ($offsetParent[0]) { + console.log('Browser offsetParent:', $offsetParent[0]); +} +``` + +#### Fixed Position Elements +```javascript +// Fixed elements have no containing parent +const $fixed = $('.fixed-positioned'); +const $container = $fixed.containingParent(); + +if ($container[0] === undefined) { + console.log('Fixed element has no containing parent'); +} +``` + +#### Transform and Filter Detection +```javascript +// Elements with transforms create new positioning contexts +const $transformed = $('.transformed-element'); +const $transformContainer = $transformed.containingParent(); + +// Useful for accurately calculating positions +const rect = $transformContainer[0].getBoundingClientRect(); +console.log('Transform container bounds:', rect); +``` diff --git a/docs/src/pages/api/query/dom-manipulation.mdx b/docs/src/pages/api/query/dom-manipulation.mdx index 23a1c8186..591ab332e 100755 --- a/docs/src/pages/api/query/dom-manipulation.mdx +++ b/docs/src/pages/api/query/dom-manipulation.mdx @@ -257,3 +257,83 @@ const $template = $('#item-template').clone(); $('#container').append($template); ``` +## reverse + +Reverses the order of the elements in the current set. + +### Syntax +```javascript +$('selector').reverse() +``` + +### Returns +A new [Query object](/api/query/basic#the-query-object) containing the reversed elements. + +### Usage +```javascript +// Reverse the order of list items +const $reversed = $('ul li').reverse(); +$('ul').empty().append($reversed); +``` + +## slice + +Returns a shallow copy of a portion of the elements into a new Query object. + +### Syntax +```javascript +$('selector').slice(start, end) +``` + +### Parameters +| Name | Type | Description | +|-------|--------|-----------------------------------------------------------------------------| +| start | number | The beginning index of the specified portion of the collection (optional) | +| end | number | The end index of the specified portion of the collection (optional, exclusive) | + +### Returns +A new [Query object](/api/query/basic#the-query-object) containing the sliced elements. + +### Usage + +#### Basic Slicing +```javascript +// Get elements 2-4 from a list of items +const $items = $('.item'); +const $sliced = $items.slice(1, 4); // Gets items at index 1, 2, 3 + +// Get all elements from index 2 onwards +const $fromIndex = $items.slice(2); + +// Get the last 2 elements using negative indices +const $lastTwo = $items.slice(-2); +``` + +#### Working with Multiple Elements +```javascript +// Slice and manipulate a portion of elements +$('.card').slice(0, 3).addClass('featured'); + +// Process elements in batches +const $allItems = $('.item'); +const firstBatch = $allItems.slice(0, 5); +const secondBatch = $allItems.slice(5, 10); + +firstBatch.addClass('batch-one'); +secondBatch.addClass('batch-two'); +``` + +#### Shadow DOM +```javascript +// Slice user posts in a user-profile component +const $posts = $$('user-profile').find('.user-post'); +const $recentPosts = $posts.slice(0, 3); +$recentPosts.addClass('recent'); +``` + +### Notes +- Returns the same instance when called on an empty collection +- Supports negative indices like `Array.prototype.slice` +- Creates a new Query object, enabling method chaining +- Does not modify the original Query object + diff --git a/docs/src/pages/api/query/size-and-position.mdx b/docs/src/pages/api/query/size-and-position.mdx deleted file mode 100755 index 2012dd9a3..000000000 --- a/docs/src/pages/api/query/size-and-position.mdx +++ /dev/null @@ -1,116 +0,0 @@ ---- -layout: '@layouts/Guide.astro' -pageType: 'API Reference' -title: Query - Size & Position -icon: move -description: API reference for Query methods related to element dimensions and positioning ---- - -Size and position methods in Query allow you to get information about element dimensions, scroll positions, and their position in the DOM. - -## count - -Get the number of elements in the Query object. - -### Syntax -```javascript -$('selector').count() -``` - -### Returns -`number` - The number of elements in the Query object. - -### Usage -```javascript -const paragraphCount = $('p').count(); -console.log(`There are ${paragraphCount} paragraphs on the page.`); -``` - -> **Quick Count** `count()` is a convenient way to get the number of matched elements without converting to an array. - -## index - -Get the index of an element in the set of matched elements. - -### Syntax -```javascript -$('selector').index() -``` - -### Returns -`number` - The index of the first element within the Query object relative to its sibling elements, or -1 if not found. - -### Usage -```javascript -const index = $('li.active').index(); -console.log(`The active list item is at index ${index}`); -``` - -> **DOM Position** `index()` is useful for determining the position of an element among its siblings. - -## offsetParent - -Get the closest positioned ancestor element. - -### Syntax -```javascript -$('selector').offsetParent(options) -``` - -### Parameters -| Name | Type | Description | -|---------|--------|---------------------------------------| -| options | Object | Configuration options for the method | - -### Returns -[Query object](/api/query/basic#the-query-object) containing the offset parent element(s). - -### Usage -```javascript -const offsetParent = $('.child-element').offsetParent(); -console.log('Offset parent:', offsetParent.get(0)); -``` - -> **Positioning Context** `offsetParent()` is particularly useful when working with absolutely positioned elements. - -## naturalWidth - -Get the natural width of an element, particularly useful for images. - -### Syntax -```javascript -$('selector').naturalWidth() -``` - -### Returns -- **Single Element** The natural width of the element in pixels. -- **Multiple Elements** An array of natural widths, one for each matched element. - -### Usage -```javascript -const imgNaturalWidth = $('img').naturalWidth(); -console.log('Natural width of the image:', imgNaturalWidth); -``` - -> **Element Dimensions** This method works by creating a clone of the element, positioning it off-screen, and measuring its dimensions. While accurate, it can cause a reflow in the document. - -## naturalHeight - -Get the natural height of an element, particularly useful for images. - -### Syntax -```javascript -$('selector').naturalHeight() -``` - -### Returns -- **Single Element** The natural height of the element in pixels. -- **Multiple Elements** An array of natural heights, one for each matched element. - -### Usage -```javascript -const imgNaturalHeight = $('img').naturalHeight(); -console.log('Natural height of the image:', imgNaturalHeight); -``` - -> **Element Dimensions** Like `naturalWidth()`, this method creates an off-screen clone to measure dimensions, which may cause a reflow. diff --git a/docs/src/pages/api/query/utilities.mdx b/docs/src/pages/api/query/utilities.mdx index d6a77b740..c21158f47 100755 --- a/docs/src/pages/api/query/utilities.mdx +++ b/docs/src/pages/api/query/utilities.mdx @@ -49,31 +49,6 @@ console.log(`The active list item is at index ${index}`); ``` -## offsetParent - -Get the closest positioned ancestor element. - -> **Edge-Case Improvements** `offsetParent()` handles some [edge cases}(https://issues.chromium.org/issues/41131675) that prevent the naive use of `el.offsetParent` - -### Syntax -```javascript -$('selector').offsetParent(options) -``` - -### Parameters -| Name | Type | Description | -|---------|--------|---------------------------------------| -| options | Object | Configuration options for the method | - -### Returns -[Query object](/api/query/basic#the-query-object) containing the offset parent element(s). - -### Usage -```javascript -const offsetParent = $('.child-element').offsetParent(); -console.log('Offset parent:', offsetParent.get(0)); -``` - ## Element Access Methods These methods provide access to individual elements in a Query collection. diff --git a/internal-packages/scripts/src/build-ai-workspace.js b/internal-packages/scripts/src/build-ai-workspace.js new file mode 100644 index 000000000..479873001 --- /dev/null +++ b/internal-packages/scripts/src/build-ai-workspace.js @@ -0,0 +1,184 @@ +import * as esbuild from 'esbuild'; +import fs from 'fs/promises'; +import { resolve } from 'path'; +import { WebSocketServer } from 'ws'; +import { build } from './lib/build.js'; + +const BASE_DIR = process.env.BASE_DIR || process.cwd(); +const WORKSPACE_DIR = resolve(BASE_DIR, 'ai/workspace'); + +/* + MCP-enabled debugging environment for AI workspace + Sets up WebSocket server for MCP tools and esbuild serve with debug injection +*/ +export const buildAIWorkspace = async ({ + serve = false, + port = 8080, + wsPort = 8081, + host = 'localhost', + buildDeps = false, + watch = false, +} = {}) => { + if (serve) { + console.log('🚀 Starting AI Workspace with MCP debugging...'); + + // Set up WebSocket server for MCP tool communication + const wss = new WebSocketServer({ port: wsPort }); + const connections = new Set(); + + console.log(`🔌 MCP WebSocket server running on ws://${host}:${wsPort}`); + + wss.on('connection', (ws) => { + connections.add(ws); + console.log('🔗 Debug bridge connected'); + + ws.on('close', () => { + connections.delete(ws); + console.log('🔌 Debug bridge disconnected'); + }); + + // Handle MCP tool requests from agents + ws.on('message', async (data) => { + try { + const message = JSON.parse(data); + + if (message.type === 'mcp-tool-request') { + console.log(`🛠️ Agent tool request: ${message.tool}`); + + // Forward to browser debug bridge + for (const conn of connections) { + if (conn !== ws && conn.readyState === 1) { + conn.send(data); + } + } + } + else if (message.type === 'mcp-tool-response') { + // Forward response back to requesting agent + for (const conn of connections) { + if (conn !== ws && conn.readyState === 1) { + conn.send(data); + } + } + } + else if (message.type === 'event-triggered') { + // Broadcast events to all connected agents + console.log(`📡 Component event: ${message.event} on ${message.selector}`); + for (const conn of connections) { + if (conn !== ws && conn.readyState === 1) { + conn.send(data); + } + } + } + } + catch (error) { + console.error('❌ WebSocket message error:', error); + } + }); + }); + + // esbuild plugin to inject debug bridge + const debugBridgePlugin = { + name: 'debug-bridge-injector', + setup(build) { + build.onLoad({ filter: /\.html$/ }, async (args) => { + let content = await fs.readFile(args.path, 'utf8'); + + // Inject debug bridge script before + const injection = ` + + + `; + + content = content.replace('', injection + ''); + return { contents: content, loader: 'html' }; + }); + }, + }; + + try { + const context = await esbuild.context({ + entryPoints: [ + resolve(WORKSPACE_DIR, 'public/index.html'), + resolve(WORKSPACE_DIR, 'debug-bridge.js'), + resolve(WORKSPACE_DIR, 'mcp-tools.js'), + ], + bundle: false, + format: 'esm', + outdir: resolve(WORKSPACE_DIR, '.esbuild'), + plugins: [debugBridgePlugin], + logLevel: 'info', + loader: { + '.html': 'text', + '.css': 'text', + '.js': 'js', + '.ts': 'ts', + }, + }); + + if (watch) { + await context.watch(); + console.log('👀 File watching enabled'); + } + + const result = await context.serve({ + port, + host, + servedir: WORKSPACE_DIR, + }); + + console.log(`\n✅ AI Workspace Ready!`); + console.log(`📡 Web server: http://${result.host}:${result.port}`); + console.log(`🔌 MCP WebSocket: ws://${host}:${wsPort}`); + console.log(`📁 Serving from: ${WORKSPACE_DIR}`); + console.log(`\n🎯 Quick Links:`); + console.log(` • Main page: http://localhost:${port}/public/`); + console.log(` • Component tester: http://localhost:${port}/public/component.html`); + console.log(` • Test component: http://localhost:${port}/public/component.html?name=test-component`); + console.log(`\n💡 MCP Tools Available:`); + console.log(` • inspect_component - Deep component analysis`); + console.log(` • execute_in_component - Run code in component context`); + console.log(` • monitor_events - Real-time event streaming`); + console.log(` • list_components - See all components on page`); + console.log(` • See mcp-tools.js for complete tool reference`); + + // Keep the process alive + return new Promise(() => { + // Graceful shutdown handling + process.on('SIGINT', () => { + console.log('\n🛑 Shutting down AI workspace...'); + wss.close(); + context.dispose(); + process.exit(0); + }); + }); + } + catch (error) { + console.error('❌ Failed to start workspace:', error); + wss.close(); + return { success: false, error }; + } + } + + return { success: true }; +}; + +// Handle direct execution of this script +if (import.meta.url === `file://${process.argv[1]}`) { + (async function() { + const serve = process.argv.includes('--serve'); + const port = process.argv.includes('--port') + ? parseInt(process.argv[process.argv.indexOf('--port') + 1]) + : 8080; + + return buildAIWorkspace({ + serve, + port, + watch: serve, // Enable watch mode when serving + }); + })(); +} diff --git a/internal-packages/scripts/src/index.js b/internal-packages/scripts/src/index.js index ed4865585..9528463a8 100644 --- a/internal-packages/scripts/src/index.js +++ b/internal-packages/scripts/src/index.js @@ -2,11 +2,12 @@ export * from './lib/index.js'; /* Builders */ +export { buildAIWorkspace } from './build-ai-workspace.js'; export { buildBundle } from './build-bundle.js'; export { buildCDN } from './build-cdn.js'; export { buildESM } from './build-esm.js'; -export { buildUIDeps } from './build-ui-deps.js'; export { buildUIComponents } from './build-ui-components.js'; +export { buildUIDeps } from './build-ui-deps.js'; /* Watch */ export { watch } from './watch.js'; diff --git a/package-lock.json b/package-lock.json index 7dfc92ccb..a58a93ec1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -39,7 +39,8 @@ "simple-git-hooks": "^2.12.1", "underscore": "^1.13.7", "vitest": "^3.0.9", - "wireit": "^0.14.11" + "wireit": "^0.14.11", + "ws": "^8.18.2" } }, "internal-packages/esbuild-callback": { @@ -8029,7 +8030,9 @@ } }, "node_modules/ws": { - "version": "8.18.1", + "version": "8.18.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz", + "integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==", "dev": true, "license": "MIT", "engines": { diff --git a/package.json b/package.json index ec8a68074..444509476 100755 --- a/package.json +++ b/package.json @@ -113,6 +113,7 @@ "scripts": { "build": "wireit", "dev": "wireit", + "dev:workspace": "node ./internal-packages/scripts/src/build-ai-workspace.js --serve", "build:docs": "wireit", "build:packages": "wireit", "watch": "wireit", @@ -218,7 +219,8 @@ "simple-git-hooks": "^2.12.1", "underscore": "^1.13.7", "vitest": "^3.0.9", - "wireit": "^0.14.11" + "wireit": "^0.14.11", + "ws": "^8.18.2" }, "ciDependencies": { "axios": "^1.6.8", diff --git a/packages/query/src/query.js b/packages/query/src/query.js index 7c8be9756..f9a4c6c68 100755 --- a/packages/query/src/query.js +++ b/packages/query/src/query.js @@ -2,6 +2,7 @@ import { camelToKebab, each, findIndex, + get, inArray, isArray, isClient, @@ -10,6 +11,7 @@ import { isObject, isPlainObject, isString, + keys, } from '@semantic-ui/utils'; /* @@ -46,7 +48,7 @@ export class Query { } // this is an existing query object - if(selector instanceof Query) { + if (selector instanceof Query) { elements = selector; } @@ -99,7 +101,7 @@ export class Query { ? new Query(globalThis, this.options) : new Query(elements, { ...this.options, prevObject: this }); } - + end() { return this.prevObject || this; } @@ -116,8 +118,10 @@ export class Query { // Add root if required if (includeRoot) { - if ((domSelector && root == selector) || - (!domSelector && root.matches && root.matches(selector))) { + if ( + (domSelector && root == selector) + || (!domSelector && root.matches && root.matches(selector)) + ) { elements.add(root); } } @@ -125,7 +129,8 @@ export class Query { // Query from root if (domSelector) { queriedRoot = true; - } else if (root.querySelectorAll) { + } + else if (root.querySelectorAll) { root.querySelectorAll(selector).forEach(el => elements.add(el)); queriedRoot = true; } @@ -150,7 +155,8 @@ export class Query { elements.add(selector); domFound = true; } - } else if (node.querySelectorAll) { + } + else if (node.querySelectorAll) { // Directly add to Set without intermediate array node.querySelectorAll(selector).forEach(el => elements.add(el)); } @@ -158,7 +164,7 @@ export class Query { const findElements = (node, selector, query) => { // Early termination condition for DOM selector search - if (domSelector && domFound) return; + if (domSelector && domFound) { return; } // If root element didn't support querySelectorAll, query each child node if (query === true) { @@ -605,10 +611,10 @@ export class Query { getSlot(name) { return this.map((el) => { - if(el.tagName.toLowerCase() == 'slot' && (!name || el.name == name)) { + if (el.tagName.toLowerCase() == 'slot' && (!name || el.name == name)) { // called directly on a matching slot const nodes = el.assignedNodes({ flatten: true }); - if(nodes) { + if (nodes) { return this.chain(nodes).html(); } } @@ -617,10 +623,11 @@ export class Query { const slotSelector = name ? `slot[name="${name}"]` : 'slot:not([name])'; const slot = el.shadowRoot.querySelector(slotSelector); const nodes = slot.assignedNodes({ flatten: true }); - if(nodes) { + if (nodes) { return this.chain(nodes).html(); } - } else { + } + else { // No shadow DOM, fallback to direct DOM querying const slotSelector = name ? `[slot="${name}"]` : ':not([slot])'; return this.chain(el).find(slotSelector).html(); @@ -629,18 +636,18 @@ export class Query { } setSlot(nameOrHTML, newHTML) { - // Determine if we're dealing with a named slot or default slot based on arguments let name; if (newHTML) { name = nameOrHTML; - } else { + } + else { newHTML = nameOrHTML; } return this.each((el) => { // find host web component - if(el.tagName.toLowerCase() == 'slot') { + if (el.tagName.toLowerCase() == 'slot') { el = el.getRootNode().getRootNode()?.host; } const $el = this.chain(el); @@ -653,7 +660,8 @@ export class Query { $slottedElement = this.chain(el).find(slotSelector); } $slottedElement.html(newHTML); - } else { + } + else { // Default slot updates the entire element content $el.html(newHTML); } @@ -717,8 +725,7 @@ export class Query { || el instanceof HTMLSelectElement || el instanceof HTMLTextAreaElement // web components may store value - || customElements.get(el.tagName.toLowerCase()) - ; + || customElements.get(el.tagName.toLowerCase()); }; if (newValue !== undefined) { // Set the value for each element @@ -795,7 +802,7 @@ export class Query { return this.css(property, null, { includeComputed: true }); } - cssVar(variable, value) { + cssVar(variable, value = null) { return this.css(`--${variable}`, value, { includeComputed: true }); } @@ -1035,29 +1042,69 @@ export class Query { return height.length > 1 ? height : height[0]; } - // offsetParent does not return the true offset parent - // in cases where there is a parent node with a transform context - // so we need to get that manually where finding the true offset parent is essential - // for instance when calculating position - offsetParent({ calculate = true } = {}) { - return Array.from(this) - .map((el) => { - if (!calculate) { - return el.offsetParent; + // this is the element that clips current element + clippingParent() { + const parents = this.map((el) => { + let current = el.parentNode; + while (current) { + if (current instanceof Element) { + const style = window.getComputedStyle(current); + if (style.overflowX !== 'visible' || style.overflowY !== 'visible') { + return current; + } } - let $el, isPositioned, isTransformed, isBody; - let parentNode = el?.parentNode; - while (parentNode && !isPositioned && !isTransformed && !isBody) { - parentNode = parentNode?.parentNode; - if (parentNode) { - $el = $(parentNode); - isPositioned = $el.computedStyle('position') !== 'static'; - isTransformed = $el.computedStyle('transform') !== 'none'; - isBody = $el.is('body'); + current = current.parentNode; + } + return document.documentElement; + }); + return this.chain(parents); + } + + // this is the parent element where top/left and offsetTop/left will be relative + containingParent({ calculate = true } = {}) { + const parents = this.map((el) => { + + // return offset parent as reported by browser + if (!calculate) { + return el.offsetParent; + } + + // fixed elements have no offset parent + if (window.getComputedStyle(el).position === 'fixed') { + return undefined; + } + + let current = el.parentNode; + while (current) { + if (current instanceof Element) { + const style = window.getComputedStyle(current); + // transformed elements create new positioning context + if (style.position !== 'static') { + return current; + } + // filter creates new positioning context + if (style.filter !== 'none') { + return current; + } + // transformed elements create new positioning context + if (style.transform !== 'none') { + return current; + } + // also creates positioning context + if(['layout', 'paint', 'strict', 'content'].includes(style.contain)) { + return current; + } + // will change will trigger same context + // + if (['filter', 'contain', 'transform'].includes(style.willChange)) { + return current; } } - return parentNode; - }); + current = current.parentNode; + } + return document.body; + }); + return this.chain(parents); } // alias @@ -1084,11 +1131,67 @@ export class Query { } setting(setting, value) { + if (value === undefined) { + const settings = this.map(el => el[setting]); + return (settings.length == 1) + ? settings[0] + : settings; + } return this.each((el) => { el[setting] = value; }); } + data(key, value) { + + // Set data attribute + if (value !== undefined) { + return this.each(el => { + if (el.dataset) { + el.dataset[key] = value; + } + }); + } + + // Get single data attribute + if (key !== undefined) { + if (this.length === 0) { + return undefined; + } + const values = this.map(el => get(el, `dataset.${key}`)); + return this.length === 1 + ? values[0] + : values; + } + + // Get all data attributes + if (this.length === 0) { + return undefined; + } + const allData = this.map(el => { + const data = {}; + if (el.dataset) { + each(keys(el.dataset), k => { + data[k] = el.dataset[k]; + }); + } + return data; + }); + + // return array only if more than one el + return (this.length === 1) + ? allData[0] + : allData; + } + + slice(start, end) { + if (this.length === 0) { + return this; + } + const slicedElements = Array.from(this).slice(start, end); + return this.chain(slicedElements); + } + // special helper for SUI components component() { const components = this.map(el => el.component).filter(Boolean); diff --git a/packages/query/test/browser/query.test.js b/packages/query/test/browser/query.test.js index d8c569d67..e8c32b3f1 100644 --- a/packages/query/test/browser/query.test.js +++ b/packages/query/test/browser/query.test.js @@ -607,4 +607,278 @@ describe('query', () => { }); }); }); + + describe('clippingParent', () => { + beforeEach(() => { + document.body.innerHTML = ''; + }); + + it('should find the clipping parent with overflow hidden', () => { + document.body.innerHTML = ` +
+
Child element
+
+ `; + + const $child = $('#child'); + const $clippingParent = $child.clippingParent(); + + expect($clippingParent.length).toBe(1); + expect($clippingParent[0]).toBe(document.getElementById('container')); + }); + + it('should find the clipping parent with overflow scroll', () => { + document.body.innerHTML = ` +
+
+
Inner content
+
+
+ `; + + const $inner = $('#inner'); + const $clippingParent = $inner.clippingParent(); + + expect($clippingParent.length).toBe(1); + expect($clippingParent[0]).toBe(document.getElementById('scroller')); + }); + + it('should find the clipping parent with overflow auto', () => { + document.body.innerHTML = ` +
+
Content
+
+ `; + + const $content = $('#content'); + const $clippingParent = $content.clippingParent(); + + expect($clippingParent.length).toBe(1); + expect($clippingParent[0]).toBe(document.getElementById('container')); + }); + + it('should return document.documentElement when no clipping parent found', () => { + document.body.innerHTML = ` +
+
+
Target
+
+
+ `; + + const $target = $('#target'); + const $clippingParent = $target.clippingParent(); + + expect($clippingParent.length).toBe(1); + expect($clippingParent[0]).toBe(document.documentElement); + }); + + it('should handle multiple elements', () => { + document.body.innerHTML = ` +
+
Item 1
+
+
+
Item 2
+
+ `; + + const $items = $('.item'); + const $clippingParents = $items.clippingParent(); + + expect($clippingParents.length).toBe(2); + expect($clippingParents[0]).toBe(document.getElementById('container1')); + expect($clippingParents[1]).toBe(document.getElementById('container2')); + }); + + it('should handle empty selection', () => { + const $empty = $('.nonexistent'); + const $clippingParent = $empty.clippingParent(); + + expect($clippingParent.length).toBe(0); + }); + + it('should find nested clipping parents correctly', () => { + document.body.innerHTML = ` +
+
+
+
Target
+
+
+
+ `; + + const $target = $('#target'); + const $clippingParent = $target.clippingParent(); + + // Should find the immediate clipping parent (inner), not the outer one + expect($clippingParent.length).toBe(1); + expect($clippingParent[0]).toBe(document.getElementById('inner')); + }); + }); + + describe('containingParent', () => { + beforeEach(() => { + document.body.innerHTML = ''; + }); + + it('should find containing parent with position relative', () => { + document.body.innerHTML = ` +
+
Child
+
+ `; + + const $child = $('#child'); + const $containingParent = $child.containingParent(); + + expect($containingParent.length).toBe(1); + expect($containingParent[0]).toBe(document.getElementById('container')); + }); + + it('should find containing parent with transform', () => { + document.body.innerHTML = ` +
+
Child
+
+ `; + + const $child = $('#child'); + const $containingParent = $child.containingParent(); + + expect($containingParent.length).toBe(1); + expect($containingParent[0]).toBe(document.getElementById('transformed')); + }); + + it('should find containing parent with filter', () => { + document.body.innerHTML = ` +
+
Child
+
+ `; + + const $child = $('#child'); + const $containingParent = $child.containingParent(); + + expect($containingParent.length).toBe(1); + expect($containingParent[0]).toBe(document.getElementById('filtered')); + }); + + it('should find containing parent with contain property', () => { + document.body.innerHTML = ` +
+
Child
+
+ `; + + const $child = $('#child'); + const $containingParent = $child.containingParent(); + + expect($containingParent.length).toBe(1); + expect($containingParent[0]).toBe(document.getElementById('contained')); + }); + + it('should find containing parent with will-change', () => { + document.body.innerHTML = ` +
+
Child
+
+ `; + + const $child = $('#child'); + const $containingParent = $child.containingParent(); + + expect($containingParent.length).toBe(1); + expect($containingParent[0]).toBe(document.getElementById('willchange')); + }); + + it('should return undefined for fixed position elements', () => { + document.body.innerHTML = ` +
+
Fixed element
+
+ `; + + const $fixed = $('#fixed'); + const $containingParent = $fixed.containingParent(); + + expect($containingParent.length).toBe(1); + expect($containingParent[0]).toBe(undefined); + }); + + it('should return document.body when no containing parent found', () => { + document.body.innerHTML = ` +
+
+
Target
+
+
+ `; + + const $target = $('#target'); + const $containingParent = $target.containingParent(); + + expect($containingParent.length).toBe(1); + expect($containingParent[0]).toBe(document.body); + }); + + it('should use browser offsetParent when calculate is false', () => { + document.body.innerHTML = ` +
+
Child
+
+ `; + + const $child = $('#child'); + const $containingParent = $child.containingParent({ calculate: false }); + + expect($containingParent.length).toBe(1); + expect($containingParent[0]).toBe(document.getElementById('child').offsetParent); + }); + + it('should handle multiple elements', () => { + document.body.innerHTML = ` +
+
Item 1
+
+
+
Item 2
+
+ `; + + const $items = $('.item'); + const $containingParents = $items.containingParent(); + + expect($containingParents.length).toBe(2); + expect($containingParents[0]).toBe(document.getElementById('container1')); + expect($containingParents[1]).toBe(document.getElementById('container2')); + }); + + it('should handle empty selection', () => { + const $empty = $('.nonexistent'); + const $containingParent = $empty.containingParent(); + + expect($containingParent.length).toBe(0); + }); + + it('should find nearest containing parent in nested contexts', () => { + document.body.innerHTML = ` +
+
+
+
Target
+
+
+
+ `; + + const $target = $('#target'); + const $containingParent = $target.containingParent(); + + // Should find the immediate containing parent (inner), not the outer one + expect($containingParent.length).toBe(1); + expect($containingParent[0]).toBe(document.getElementById('inner')); + }); + }); }); diff --git a/packages/query/test/dom/query.test.js b/packages/query/test/dom/query.test.js index 29ed3ca12..ac547338e 100644 --- a/packages/query/test/dom/query.test.js +++ b/packages/query/test/dom/query.test.js @@ -1727,6 +1727,121 @@ describe('query', () => { }); }); + describe('data', () => { + beforeEach(() => { + document.body.innerHTML = ''; + }); + + it('should set and get data attributes', () => { + const div = document.createElement('div'); + document.body.appendChild(div); + + $('div').data('test', 'value'); + expect($('div').data('test')).toBe('value'); + expect(div.dataset.test).toBe('value'); + }); + + it('should get all data attributes when no key provided', () => { + const div = document.createElement('div'); + div.dataset.foo = 'bar'; + div.dataset.baz = 'qux'; + document.body.appendChild(div); + + const data = $('div').data(); + expect(data).toEqual({ foo: 'bar', baz: 'qux' }); + }); + + it('should return undefined for non-existent data attribute', () => { + const div = document.createElement('div'); + document.body.appendChild(div); + + expect($('div').data('nonexistent')).toBe(undefined); + }); + + it('should return undefined when getting data on empty selection', () => { + expect($('.nonexistent').data('test')).toBe(undefined); + expect($('.nonexistent').data()).toBe(undefined); + }); + + it('should set data attributes on multiple elements', () => { + const div1 = document.createElement('div'); + const div2 = document.createElement('div'); + document.body.appendChild(div1); + document.body.appendChild(div2); + + $('div').data('test', 'value'); + expect(div1.dataset.test).toBe('value'); + expect(div2.dataset.test).toBe('value'); + }); + + it('should get data attribute from single element', () => { + const div = document.createElement('div'); + div.dataset.test = 'value'; + document.body.appendChild(div); + + expect($('div').data('test')).toBe('value'); + }); + + it('should get data attributes from multiple elements as array', () => { + const div1 = document.createElement('div'); + const div2 = document.createElement('div'); + div1.dataset.test = 'value1'; + div2.dataset.test = 'value2'; + document.body.appendChild(div1); + document.body.appendChild(div2); + + expect($('div').data('test')).toEqual(['value1', 'value2']); + }); + + it('should get all data attributes from single element as object', () => { + const div = document.createElement('div'); + div.dataset.foo = 'bar'; + div.dataset.baz = 'qux'; + document.body.appendChild(div); + + expect($('div').data()).toEqual({ foo: 'bar', baz: 'qux' }); + }); + + it('should get all data attributes from multiple elements as array of objects', () => { + const div1 = document.createElement('div'); + const div2 = document.createElement('div'); + div1.dataset.foo = 'bar1'; + div1.dataset.baz = 'qux1'; + div2.dataset.foo = 'bar2'; + div2.dataset.test = 'value2'; + document.body.appendChild(div1); + document.body.appendChild(div2); + + const result = $('div').data(); + expect(result).toEqual([ + { foo: 'bar1', baz: 'qux1' }, + { foo: 'bar2', test: 'value2' } + ]); + }); + + it('should handle elements without dataset', () => { + const div = document.createElement('div'); + // Remove dataset to simulate older browsers or non-HTML elements + Object.defineProperty(div, 'dataset', { value: null }); + document.body.appendChild(div); + + expect($('div').data()).toEqual({}); + expect($('div').data('test')).toBe(undefined); + + // Setting should not throw + expect(() => $('div').data('test', 'value')).not.toThrow(); + }); + + it('should return the Query instance when setting data', () => { + const div = document.createElement('div'); + document.body.appendChild(div); + + const result = $('div').data('test', 'value'); + expect(result).toBeInstanceOf(Query); + expect(result[0]).toBe(div); + }); + }); + describe('end', () => { beforeEach(() => { document.body.innerHTML = ''; @@ -1865,4 +1980,143 @@ describe('query', () => { expect(result[0]).toBe(outer); }); }); + + describe('slice', () => { + beforeEach(() => { + document.body.innerHTML = ''; + }); + + it('should slice elements from the collection', () => { + const div1 = document.createElement('div'); + const div2 = document.createElement('div'); + const div3 = document.createElement('div'); + const div4 = document.createElement('div'); + + div1.className = 'item'; + div2.className = 'item'; + div3.className = 'item'; + div4.className = 'item'; + + document.body.appendChild(div1); + document.body.appendChild(div2); + document.body.appendChild(div3); + document.body.appendChild(div4); + + const $items = $('.item'); + const $sliced = $items.slice(1, 3); + + expect($sliced.length).toBe(2); + expect($sliced[0]).toBe(div2); + expect($sliced[1]).toBe(div3); + }); + + it('should slice with only start parameter', () => { + const div1 = document.createElement('div'); + const div2 = document.createElement('div'); + const div3 = document.createElement('div'); + + div1.className = 'item'; + div2.className = 'item'; + div3.className = 'item'; + + document.body.appendChild(div1); + document.body.appendChild(div2); + document.body.appendChild(div3); + + const $items = $('.item'); + const $sliced = $items.slice(1); + + expect($sliced.length).toBe(2); + expect($sliced[0]).toBe(div2); + expect($sliced[1]).toBe(div3); + }); + + it('should handle negative indices', () => { + const div1 = document.createElement('div'); + const div2 = document.createElement('div'); + const div3 = document.createElement('div'); + + div1.className = 'item'; + div2.className = 'item'; + div3.className = 'item'; + + document.body.appendChild(div1); + document.body.appendChild(div2); + document.body.appendChild(div3); + + const $items = $('.item'); + const $sliced = $items.slice(-2); + + expect($sliced.length).toBe(2); + expect($sliced[0]).toBe(div2); + expect($sliced[1]).toBe(div3); + }); + + it('should return empty collection when start is beyond collection length', () => { + const div1 = document.createElement('div'); + div1.className = 'item'; + document.body.appendChild(div1); + + const $items = $('.item'); + const $sliced = $items.slice(5); + + expect($sliced.length).toBe(0); + expect($sliced).toBeInstanceOf(Query); + }); + + it('should return same instance when called on empty collection', () => { + const $empty = $('.nonexistent'); + const $sliced = $empty.slice(0, 2); + + expect($sliced).toBe($empty); + expect($sliced.length).toBe(0); + }); + + it('should return Query instance for chaining', () => { + const div1 = document.createElement('div'); + const div2 = document.createElement('div'); + + div1.className = 'item'; + div2.className = 'item'; + + document.body.appendChild(div1); + document.body.appendChild(div2); + + const $result = $('.item').slice(0, 1).addClass('sliced'); + + expect($result).toBeInstanceOf(Query); + expect($result.length).toBe(1); + expect(div1.classList.contains('sliced')).toBe(true); + expect(div2.classList.contains('sliced')).toBe(false); + }); + + it('should work with single element', () => { + const div = document.createElement('div'); + div.className = 'item'; + document.body.appendChild(div); + + const $items = $('.item'); + const $sliced = $items.slice(0, 1); + + expect($sliced.length).toBe(1); + expect($sliced[0]).toBe(div); + }); + + it('should handle zero slice', () => { + const div1 = document.createElement('div'); + const div2 = document.createElement('div'); + + div1.className = 'item'; + div2.className = 'item'; + + document.body.appendChild(div1); + document.body.appendChild(div2); + + const $items = $('.item'); + const $sliced = $items.slice(1, 1); + + expect($sliced.length).toBe(0); + expect($sliced).toBeInstanceOf(Query); + }); + }); }); diff --git a/packages/query/types/query.d.ts b/packages/query/types/query.d.ts index fee48e747..a82648765 100644 --- a/packages/query/types/query.d.ts +++ b/packages/query/types/query.d.ts @@ -640,6 +640,15 @@ export class Query { */ reverse(): Query; + /** + * Returns a shallow copy of a portion of the elements into a new Query object. + * @see https://next.semantic-ui.com/api/query/dom-manipulation#slice + * @param start - The beginning index of the specified portion of the collection. + * @param end - The end index of the specified portion of the collection (exclusive). + * @returns A new Query instance containing the sliced elements. + */ + slice(start?: number, end?: number): Query; + /** * Inserts content at a specified position relative to a target element. * @see https://next.semantic-ui.com/api/query/internal#insertcontent @@ -707,13 +716,20 @@ export class Query { naturalHeight(): number | number[]; /** - * Gets the offset parent of each element in the current set, optionally calculating it accurately - * by considering transformed parent. - * @see https://next.semantic-ui.com/api/query/size-and-position#offsetParent - * @param options.calculate - Whether to calculate offset parent taking transform into account. - * @returns An array of the offset parent elements. + * Gets the clipping parent (overflow container) of each element in the current set. + * @see https://next.semantic-ui.com/api/query/dimensions#clippingparent + * @returns A new Query instance containing the clipping parent elements. */ - offsetParent(options?: { calculate?: boolean; }): (HTMLElement | null)[]; + clippingParent(): Query; + + /** + * Gets the containing parent (positioning context) of each element in the current set, optionally calculating it accurately + * by considering transform, filter, and other properties that create new positioning contexts. + * @see https://next.semantic-ui.com/api/query/dimensions#containingparent + * @param options.calculate - Whether to calculate containing parent taking modern CSS properties into account. + * @returns A new Query instance containing the containing parent elements. + */ + containingParent(options?: { calculate?: boolean; }): Query; /** * Gets the number of elements in the current set. Alias for `length`. @@ -759,6 +775,26 @@ export class Query { */ component(): any; + /** + * Gets or sets data attributes on elements in the current set. + * @see https://next.semantic-ui.com/api/query/data#data + * @param key - The data attribute key. + * @param value - The value to set. + * @returns If setting, the Query instance for chaining. If getting, the value(s). + */ + data(key: string, value: string): this; + /** + * Gets a data attribute from elements in the current set. + * @param key - The data attribute key to retrieve. + * @returns The value from the first element, or an array of values from all elements. + */ + data(key: string): string | string[] | undefined; + /** + * Gets all data attributes from elements in the current set. + * @returns An object of data attributes from the first element, or an array of objects from all elements. + */ + data(): PlainObject | PlainObject[] | undefined; + /** * Gets the data context (if any) associated with the *first* element in the current set. * @see https://next.semantic-ui.com/api/query/components#datacontext diff --git a/packages/templating/src/compiler/string-scanner.js b/packages/templating/src/compiler/string-scanner.js index 0d46dc955..e1b361c15 100644 --- a/packages/templating/src/compiler/string-scanner.js +++ b/packages/templating/src/compiler/string-scanner.js @@ -110,8 +110,9 @@ export class StringScanner { // Step 1: Search backwards to confirm we're inside a tag. while (i >= 0) { - if (this.input[i] === '>') { break; // Stop if we find the end of a previous tag - } + if (this.input[i] === '>') { + break; // Stop if we find the end of a previous tag + } if (this.input[i] === '<') { insideTag = true; // Confirm we're inside a tag tagPos = i; // Save the position of the tag