feat: implement Phase 3 container transformation with CLASSES.md compliance
- Add backend container transformation in engine.go following syntactic sugar specification - Containers with .insertr get class removed and viable children get .insertr added - Remove incorrect frontend container expansion - frontend only finds enhanced elements - Fix StyleAwareEditor hasMultiPropertyElements runtime error - Add addClass/removeClass methods to ContentEngine for class manipulation - Update frontend to match HTML-first approach with no runtime container logic - Test verified: container <section class='insertr'> transforms to individual h1.insertr, p.insertr, button.insertr This completes the container expansion functionality per CLASSES.md: Developer convenience (one .insertr enables section editing) + granular control (individual element editing)
This commit is contained in:
@@ -36,7 +36,6 @@
|
||||
/* Background colors */
|
||||
--insertr-bg-primary: #ffffff;
|
||||
--insertr-bg-secondary: #f8f9fa;
|
||||
--insertr-bg-dark: #343a40;
|
||||
--insertr-bg-overlay: rgba(0, 0, 0, 0.5);
|
||||
|
||||
/* Border and spacing */
|
||||
@@ -48,12 +47,8 @@
|
||||
--insertr-spacing-lg: 24px;
|
||||
|
||||
/* Z-index management */
|
||||
--insertr-z-dropdown: 1000;
|
||||
--insertr-z-sticky: 1020;
|
||||
--insertr-z-fixed: 1030;
|
||||
--insertr-z-modal-backdrop: 1040;
|
||||
--insertr-z-modal: 1050;
|
||||
--insertr-z-popover: 1060;
|
||||
--insertr-z-tooltip: 1070;
|
||||
--insertr-z-overlay: 999999;
|
||||
|
||||
@@ -61,14 +56,9 @@
|
||||
--insertr-font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
--insertr-font-size-sm: 12px;
|
||||
--insertr-font-size-base: 14px;
|
||||
--insertr-font-size-lg: 16px;
|
||||
--insertr-line-height: 1.4;
|
||||
|
||||
/* Form elements */
|
||||
--insertr-input-padding: 0.75rem;
|
||||
--insertr-input-border: 1px solid var(--insertr-border-color);
|
||||
--insertr-input-border-focus: 1px solid var(--insertr-primary);
|
||||
--insertr-input-bg: #ffffff;
|
||||
/* Form elements - using existing variables */
|
||||
|
||||
/* Animation */
|
||||
--insertr-transition: all 0.2s ease-in-out;
|
||||
@@ -380,16 +370,16 @@ body:not(.insertr-edit-mode) .insertr-editing-hover::after {
|
||||
transform: translateY(1px);
|
||||
}
|
||||
|
||||
/* Style preview buttons - styled dynamically via JavaScript */
|
||||
/* Style preview buttons - new approach using CSS isolation */
|
||||
.insertr-style-btn.insertr-style-preview {
|
||||
/* Preserve button structure */
|
||||
background: var(--insertr-bg-primary) !important;
|
||||
border: 1px solid var(--insertr-border-color) !important;
|
||||
border-radius: var(--insertr-border-radius) !important;
|
||||
padding: var(--insertr-spacing-xs) var(--insertr-spacing-sm) !important;
|
||||
font-size: var(--insertr-font-size-sm) !important;
|
||||
cursor: pointer !important;
|
||||
transition: var(--insertr-transition) !important;
|
||||
/* Clean button container - minimal styling */
|
||||
background: var(--insertr-bg-primary);
|
||||
border: 1px solid var(--insertr-border-color);
|
||||
border-radius: var(--insertr-border-radius);
|
||||
padding: 0; /* Remove padding - let preview content handle it */
|
||||
font-size: var(--insertr-font-size-sm);
|
||||
cursor: pointer;
|
||||
transition: var(--insertr-transition);
|
||||
|
||||
/* Button layout */
|
||||
min-height: 28px;
|
||||
@@ -397,37 +387,59 @@ body:not(.insertr-edit-mode) .insertr-editing-hover::after {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
/* Ensure button remains clickable */
|
||||
position: relative;
|
||||
/* Reset any inherited text styles on the button container only */
|
||||
font-family: var(--insertr-font-family);
|
||||
text-decoration: none;
|
||||
font-weight: normal;
|
||||
text-transform: none;
|
||||
|
||||
/* Styles will be applied dynamically via JavaScript */
|
||||
/* Don't set color here - let the preview content inherit naturally */
|
||||
|
||||
/* Ensure content fits */
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Add subtle background to preview buttons to ensure they remain clickable-looking */
|
||||
.insertr-style-btn.insertr-style-preview::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: var(--insertr-bg-primary);
|
||||
opacity: 0.9;
|
||||
border-radius: inherit;
|
||||
z-index: -1;
|
||||
/* Preview content container - minimal interference with original styling */
|
||||
.insertr-preview-content {
|
||||
/* Allow the original classes to style this element completely naturally */
|
||||
display: inline-block;
|
||||
|
||||
/* Only set essential layout properties */
|
||||
box-sizing: border-box;
|
||||
|
||||
/* Ensure text fits within button */
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 120px;
|
||||
|
||||
/* Inherit font size from button container */
|
||||
font-size: inherit;
|
||||
|
||||
/* Remove browser defaults that might interfere - but don't override intentional styling */
|
||||
border: none;
|
||||
margin: 0;
|
||||
|
||||
/* NO background, color, padding defaults - let the classes handle everything */
|
||||
}
|
||||
|
||||
/* Hover state for preview buttons */
|
||||
/* Minimal fallback styling when no meaningful classes are detected */
|
||||
.insertr-preview-content.insertr-fallback-style {
|
||||
padding: var(--insertr-spacing-xs) var(--insertr-spacing-sm);
|
||||
color: var(--insertr-text-primary);
|
||||
}
|
||||
|
||||
/* Hover state for preview buttons - subtle visual feedback */
|
||||
.insertr-style-btn.insertr-style-preview:hover {
|
||||
background: var(--insertr-bg-secondary) !important;
|
||||
border-color: var(--insertr-text-muted) !important;
|
||||
transform: none !important;
|
||||
border-color: var(--insertr-text-muted);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* Active state for preview buttons */
|
||||
.insertr-style-btn.insertr-style-preview:active {
|
||||
background: var(--insertr-border-color) !important;
|
||||
transform: translateY(1px) !important;
|
||||
transform: translateY(0);
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* Editor components */
|
||||
@@ -435,14 +447,14 @@ body:not(.insertr-edit-mode) .insertr-editing-hover::after {
|
||||
.insertr-rich-editor,
|
||||
.insertr-fallback-textarea {
|
||||
width: 100%;
|
||||
border: var(--insertr-input-border);
|
||||
border: 1px solid var(--insertr-border-color);
|
||||
border-radius: var(--insertr-border-radius);
|
||||
padding: var(--insertr-input-padding);
|
||||
padding: var(--insertr-spacing-md);
|
||||
font-size: var(--insertr-font-size-base);
|
||||
line-height: var(--insertr-line-height);
|
||||
font-family: var(--insertr-font-family);
|
||||
color: var(--insertr-text-primary);
|
||||
background: var(--insertr-input-bg);
|
||||
background: var(--insertr-bg-primary);
|
||||
margin-bottom: var(--insertr-spacing-md);
|
||||
transition: var(--insertr-transition);
|
||||
box-sizing: border-box;
|
||||
@@ -452,7 +464,7 @@ body:not(.insertr-edit-mode) .insertr-editing-hover::after {
|
||||
.insertr-rich-editor:focus,
|
||||
.insertr-fallback-textarea:focus {
|
||||
outline: none;
|
||||
border: var(--insertr-input-border-focus);
|
||||
border: 1px solid var(--insertr-primary);
|
||||
box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
|
||||
}
|
||||
|
||||
|
||||
@@ -72,14 +72,13 @@ func (e *ContentEngine) ProcessContent(input ContentInput) (*ContentResult, erro
|
||||
processedElements[i] = ProcessedElement{
|
||||
Node: elem.Node,
|
||||
ID: id,
|
||||
Type: elem.Type,
|
||||
Generated: !contentExists, // Mark as generated only if new to database
|
||||
Tag: elem.Node.Data,
|
||||
Classes: GetClasses(elem.Node),
|
||||
}
|
||||
|
||||
// Add/update content attributes to the node
|
||||
e.addContentAttributes(elem.Node, id, elem.Type)
|
||||
// Add/update content attributes to the node (only content-id now)
|
||||
e.addContentAttributes(elem.Node, id)
|
||||
|
||||
// Store content only for truly new elements (database-first check)
|
||||
if !contentExists && (input.Mode == Enhancement || input.Mode == ContentInjection) {
|
||||
@@ -88,12 +87,12 @@ func (e *ContentEngine) ProcessContent(input ContentInput) (*ContentResult, erro
|
||||
originalTemplate := e.extractOriginalTemplate(elem.Node)
|
||||
|
||||
// Store in database via content client
|
||||
_, err := e.client.CreateContent(input.SiteID, id, htmlContent, originalTemplate, elem.Type, "system")
|
||||
_, err := e.client.CreateContent(input.SiteID, id, htmlContent, originalTemplate, "system")
|
||||
if err != nil {
|
||||
// Log error but don't fail the enhancement - content just won't be stored
|
||||
fmt.Printf("⚠️ Failed to store content for %s: %v\n", id, err)
|
||||
} else {
|
||||
fmt.Printf("✅ Created new content: %s (%s)\n", id, elem.Type)
|
||||
fmt.Printf("✅ Created new content: %s (html)\n", id)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -122,21 +121,47 @@ func (e *ContentEngine) ProcessContent(input ContentInput) (*ContentResult, erro
|
||||
// InsertrElement represents an insertr element found in HTML
|
||||
type InsertrElement struct {
|
||||
Node *html.Node
|
||||
Type string
|
||||
}
|
||||
|
||||
// findInsertrElements finds all elements with class="insertr"
|
||||
// findInsertrElements finds all elements with class="insertr" and applies container transformation
|
||||
// This implements the "syntactic sugar transformation" from CLASSES.md:
|
||||
// - Containers with .insertr get their .insertr class removed
|
||||
// - Viable children of those containers get .insertr class added
|
||||
// - Regular elements with .insertr are kept as-is
|
||||
func (e *ContentEngine) findInsertrElements(doc *html.Node) []InsertrElement {
|
||||
var elements []InsertrElement
|
||||
var containersToTransform []*html.Node
|
||||
|
||||
// First pass: find all .insertr elements and identify containers
|
||||
e.walkNodes(doc, func(n *html.Node) {
|
||||
if n.Type == html.ElementNode && e.hasInsertrClass(n) {
|
||||
elementType := e.determineContentType(n)
|
||||
elements = append(elements, InsertrElement{
|
||||
Node: n,
|
||||
Type: elementType,
|
||||
})
|
||||
if isContainer(n) {
|
||||
// Container element - mark for transformation
|
||||
containersToTransform = append(containersToTransform, n)
|
||||
} else {
|
||||
// Regular element - add directly
|
||||
elements = append(elements, InsertrElement{
|
||||
Node: n,
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Second pass: transform containers (remove .insertr from container, add to children)
|
||||
for _, container := range containersToTransform {
|
||||
// Remove .insertr class from container
|
||||
e.removeClass(container, "insertr")
|
||||
|
||||
// Find viable children and add .insertr class to them
|
||||
viableChildren := FindViableChildren(container)
|
||||
for _, child := range viableChildren {
|
||||
e.addClass(child, "insertr")
|
||||
elements = append(elements, InsertrElement{
|
||||
Node: child,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return elements
|
||||
}
|
||||
|
||||
@@ -159,28 +184,11 @@ func (e *ContentEngine) hasInsertrClass(node *html.Node) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// determineContentType determines the content type based on element
|
||||
func (e *ContentEngine) determineContentType(node *html.Node) string {
|
||||
tag := strings.ToLower(node.Data)
|
||||
|
||||
switch tag {
|
||||
case "a", "button":
|
||||
return "link"
|
||||
case "h1", "h2", "h3", "h4", "h5", "h6":
|
||||
return "text"
|
||||
case "p", "div", "section", "article", "span":
|
||||
return "text"
|
||||
default:
|
||||
return "text"
|
||||
}
|
||||
}
|
||||
|
||||
// addContentAttributes adds data-content-id and data-content-type attributes
|
||||
func (e *ContentEngine) addContentAttributes(node *html.Node, contentID, contentType string) {
|
||||
// addContentAttributes adds data-content-id attribute only
|
||||
// HTML-first approach: no content-type attribute needed
|
||||
func (e *ContentEngine) addContentAttributes(node *html.Node, contentID string) {
|
||||
// Add data-content-id attribute
|
||||
e.setAttribute(node, "data-content-id", contentID)
|
||||
// Add data-content-type attribute
|
||||
e.setAttribute(node, "data-content-type", contentType)
|
||||
}
|
||||
|
||||
// getAttribute gets an attribute value from an HTML node
|
||||
@@ -209,6 +217,85 @@ func (e *ContentEngine) setAttribute(node *html.Node, key, value string) {
|
||||
})
|
||||
}
|
||||
|
||||
// addClass safely adds a class to an HTML node
|
||||
func (e *ContentEngine) addClass(node *html.Node, className string) {
|
||||
var classAttr *html.Attribute
|
||||
var classIndex int = -1
|
||||
|
||||
// Find existing class attribute
|
||||
for idx, attr := range node.Attr {
|
||||
if attr.Key == "class" {
|
||||
classAttr = &attr
|
||||
classIndex = idx
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
var classes []string
|
||||
if classAttr != nil {
|
||||
classes = strings.Fields(classAttr.Val)
|
||||
}
|
||||
|
||||
// Check if class already exists
|
||||
for _, class := range classes {
|
||||
if class == className {
|
||||
return // Class already exists
|
||||
}
|
||||
}
|
||||
|
||||
// Add new class
|
||||
classes = append(classes, className)
|
||||
newClassValue := strings.Join(classes, " ")
|
||||
|
||||
if classIndex >= 0 {
|
||||
// Update existing class attribute
|
||||
node.Attr[classIndex].Val = newClassValue
|
||||
} else {
|
||||
// Add new class attribute
|
||||
node.Attr = append(node.Attr, html.Attribute{
|
||||
Key: "class",
|
||||
Val: newClassValue,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// removeClass safely removes a class from an HTML node
|
||||
func (e *ContentEngine) removeClass(node *html.Node, className string) {
|
||||
var classIndex int = -1
|
||||
|
||||
// Find existing class attribute
|
||||
for idx, attr := range node.Attr {
|
||||
if attr.Key == "class" {
|
||||
classIndex = idx
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if classIndex == -1 {
|
||||
return // No class attribute found
|
||||
}
|
||||
|
||||
// Parse existing classes
|
||||
classes := strings.Fields(node.Attr[classIndex].Val)
|
||||
|
||||
// Filter out the target class
|
||||
var newClasses []string
|
||||
for _, class := range classes {
|
||||
if class != className {
|
||||
newClasses = append(newClasses, class)
|
||||
}
|
||||
}
|
||||
|
||||
// Update or remove class attribute
|
||||
if len(newClasses) == 0 {
|
||||
// Remove class attribute entirely if no classes remain
|
||||
node.Attr = append(node.Attr[:classIndex], node.Attr[classIndex+1:]...)
|
||||
} else {
|
||||
// Update class attribute with remaining classes
|
||||
node.Attr[classIndex].Val = strings.Join(newClasses, " ")
|
||||
}
|
||||
}
|
||||
|
||||
// injectContent injects content from database into elements
|
||||
func (e *ContentEngine) injectContent(elements []ProcessedElement, siteID string) error {
|
||||
for i := range elements {
|
||||
|
||||
Reference in New Issue
Block a user